kritzel-stencil 0.4.4 → 0.4.6

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 (128) hide show
  1. package/dist/cjs/kritzel-active-users_47.cjs.entry.js +79 -13
  2. package/dist/cjs/loader.cjs.js +1 -1
  3. package/dist/cjs/stencil.cjs.js +1 -1
  4. package/dist/collection/classes/core/viewport.class.js +14 -1
  5. package/dist/collection/components/core/kritzel-editor/kritzel-editor.js +48 -8
  6. package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +84 -3
  7. package/dist/collection/constants/version.js +1 -1
  8. package/dist/components/index.js +1226 -1
  9. package/dist/components/kritzel-active-users.js +6 -1
  10. package/dist/components/kritzel-avatar.js +6 -1
  11. package/dist/components/kritzel-awareness-cursors.js +6 -1
  12. package/dist/components/kritzel-back-to-content.js +6 -1
  13. package/dist/components/kritzel-brush-style.js +64 -1
  14. package/dist/components/kritzel-button.js +6 -1
  15. package/dist/components/kritzel-color-palette.js +6 -1
  16. package/dist/components/kritzel-color.js +6 -1
  17. package/dist/components/kritzel-context-menu.js +6 -1
  18. package/dist/components/kritzel-controls.js +6 -1
  19. package/dist/components/kritzel-current-user-dialog.js +6 -1
  20. package/dist/components/kritzel-current-user.js +6 -1
  21. package/dist/components/kritzel-cursor-trail.js +6 -1
  22. package/dist/components/kritzel-dialog.js +6 -1
  23. package/dist/components/kritzel-dropdown.js +6 -1
  24. package/dist/components/kritzel-editor.js +4404 -1
  25. package/dist/components/kritzel-engine.js +6 -1
  26. package/dist/components/kritzel-export.js +6 -1
  27. package/dist/components/kritzel-font-family.js +6 -1
  28. package/dist/components/kritzel-font-size.js +6 -1
  29. package/dist/components/kritzel-font.js +6 -1
  30. package/dist/components/kritzel-icon.js +6 -1
  31. package/dist/components/kritzel-input.js +6 -1
  32. package/dist/components/kritzel-line-endings.js +6 -1
  33. package/dist/components/kritzel-loading-overlay.js +6 -1
  34. package/dist/components/kritzel-login-dialog.js +6 -1
  35. package/dist/components/kritzel-master-detail.js +6 -1
  36. package/dist/components/kritzel-menu-item.js +6 -1
  37. package/dist/components/kritzel-menu.js +6 -1
  38. package/dist/components/kritzel-more-menu.js +6 -1
  39. package/dist/components/kritzel-notification-card.js +6 -1
  40. package/dist/components/kritzel-numeric-input.js +6 -1
  41. package/dist/components/kritzel-opacity-slider.js +6 -1
  42. package/dist/components/kritzel-pill-tabs.js +6 -1
  43. package/dist/components/kritzel-portal.js +6 -1
  44. package/dist/components/kritzel-settings.js +6 -1
  45. package/dist/components/kritzel-shape-fill.js +6 -1
  46. package/dist/components/kritzel-share-dialog.js +6 -1
  47. package/dist/components/kritzel-slide-toggle.js +6 -1
  48. package/dist/components/kritzel-split-button.js +6 -1
  49. package/dist/components/kritzel-stroke-size.js +6 -1
  50. package/dist/components/kritzel-sync-indicator.js +6 -1
  51. package/dist/components/kritzel-tool-config.js +6 -1
  52. package/dist/components/kritzel-tooltip.js +6 -1
  53. package/dist/components/kritzel-utility-panel.js +6 -1
  54. package/dist/components/kritzel-watermark.js +6 -1
  55. package/dist/components/kritzel-workspace-manager.js +6 -1
  56. package/dist/components/kritzel-zoom-panel.js +6 -1
  57. package/dist/components/p-894OnDQV.js +39 -1
  58. package/dist/components/p-B43upypT.js +1500 -1
  59. package/dist/components/p-B5oHmPJb.js +97 -1
  60. package/dist/components/p-B6t5q-V4.js +63 -1
  61. package/dist/components/p-B8Xi4Clg.js +43 -1
  62. package/dist/components/p-BBmP8lVY.js +51726 -0
  63. package/dist/components/p-BDzlx2hB.js +135 -1
  64. package/dist/components/p-BFTU3MAI.js +7 -1
  65. package/dist/components/p-BFYtCsZu.js +56 -1
  66. package/dist/components/p-BI2ng9I_.js +62 -1
  67. package/dist/components/p-BTzlrIxe.js +194 -1
  68. package/dist/components/p-BmyTdQGy.js +1090 -1
  69. package/dist/components/p-BsANVili.js +150 -1
  70. package/dist/components/p-BxpKq94F.js +290 -1
  71. package/dist/components/p-C38P-Vd0.js +446 -1
  72. package/dist/components/p-C4NrrUfN.js +77 -1
  73. package/dist/components/p-CW-VyJgK.js +6 -1
  74. package/dist/components/p-CWRSGYOE.js +57 -1
  75. package/dist/components/p-CXpv9Rxe.js +106 -1
  76. package/dist/components/p-Ca-igU1f.js +320 -1
  77. package/dist/components/p-CdR76C4L.js +165 -1
  78. package/dist/components/p-Ch42cOeY.js +320 -0
  79. package/dist/components/p-ClFprFvz.js +209 -1
  80. package/dist/components/p-Cm33td2O.js +122 -1
  81. package/dist/components/p-CqLgB8JK.js +91 -1
  82. package/dist/components/p-Cr682JI_.js +156 -1
  83. package/dist/components/p-CsvMgk_f.js +62 -1
  84. package/dist/components/p-CwJwsRR3.js +277 -1
  85. package/dist/components/p-Cymum41a.js +88 -1
  86. package/dist/components/p-D09kgc5Q.js +94 -1
  87. package/dist/components/p-D30zs0kA.js +121 -1
  88. package/dist/components/p-D3y_Hauo.js +85 -1
  89. package/dist/components/p-D5jf8wAu.js +142 -1
  90. package/dist/components/p-D82VaHce.js +53 -1
  91. package/dist/components/p-DAwPPDMS.js +101 -1
  92. package/dist/components/p-DEBwmRTC.js +55 -1
  93. package/dist/components/p-DIazmg6w.js +52 -1
  94. package/dist/components/p-DJ88BrIW.js +70 -1
  95. package/dist/components/p-DJkKlUyZ.js +182 -1
  96. package/dist/components/p-DUYmwrWV.js +3267 -1
  97. package/dist/components/p-DX2nriaq.js +486 -1
  98. package/dist/components/p-Dd-pMvlr.js +20 -1
  99. package/dist/components/p-Df3BwVGy.js +299 -1
  100. package/dist/components/p-DgJ0ufzr.js +43 -1
  101. package/dist/components/p-DkUb2czk.js +70 -1
  102. package/dist/components/p-Dlp84thc.js +57 -1
  103. package/dist/components/p-JYj3Facr.js +431 -1
  104. package/dist/components/p-Ka7OWbnk.js +2304 -1
  105. package/dist/components/p-PSFAfAyd.js +49 -1
  106. package/dist/components/p-WUdQqYnu.js +118 -1
  107. package/dist/components/p-djJSnjAX.js +84 -1
  108. package/dist/components/p-e6Zpc9Vp.js +143 -1
  109. package/dist/components/p-jGOpkGDl.js +30 -1
  110. package/dist/components/p-pagW8YF7.js +99 -1
  111. package/dist/esm/kritzel-active-users_47.entry.js +79 -13
  112. package/dist/esm/loader.js +1 -1
  113. package/dist/esm/stencil.js +1 -1
  114. package/dist/stencil/index.esm.js +1172 -1
  115. package/dist/stencil/p-5b969a42.entry.js +36109 -0
  116. package/dist/stencil/p-DQuL1Twl.js +3 -1
  117. package/dist/stencil/p-Dhio9uis.js +1717 -2
  118. package/dist/stencil/p-fb32cd8f.entry.js +29 -1
  119. package/dist/stencil/p-uC7_X6yr.js +30737 -1
  120. package/dist/stencil/stencil.esm.js +21 -1
  121. package/dist/types/components/core/kritzel-editor/kritzel-editor.d.ts +2 -0
  122. package/dist/types/components/core/kritzel-engine/kritzel-engine.d.ts +7 -0
  123. package/dist/types/components.d.ts +10 -0
  124. package/dist/types/constants/version.d.ts +1 -1
  125. package/package.json +1 -1
  126. package/dist/components/p--Oazz-BP.js +0 -9
  127. package/dist/components/p-Xsiz1D5Q.js +0 -1
  128. package/dist/stencil/p-9abc21b9.entry.js +0 -9
@@ -1 +1,2304 @@
1
- import{p as t,H as s,h as i,d as e,t as h}from"./p-B43upypT.js";import{O as n}from"./p-Dd-pMvlr.js";import{a as r}from"./p-BmyTdQGy.js";class o{static doPolygonsIntersect(t,s){const i=[t.bottomLeft,t.bottomRight,t.topRight,t.topLeft],e=[s.bottomLeft,s.bottomRight,s.topRight,s.topLeft];for(const t of i)if(this.isPointInPolygon(t,e))return!0;for(const t of e)if(this.isPointInPolygon(t,i))return!0;for(let t=0;t<i.length;t++){const s=i[t],h=i[(t+1)%i.length];for(let t=0;t<e.length;t++)if(this.intersectLines(s,h,e[t],e[(t+1)%e.length]))return!0}return!1}static isPointInPolygon(t,s){let i=!1;for(let e=0,h=s.length-1;e<s.length;h=e++){const n=s[e].x,r=s[e].y,o=s[h].y;r>t.y!=o>t.y&&t.x<(s[h].x-n)*(t.y-r)/(o-r)+n&&(i=!i)}return i}static intersectLines(t,s,i,e){const h=(s.x-t.x)*(e.y-i.y)-(s.y-t.y)*(e.x-i.x);if(0===h)return!1;const n=((i.x-t.x)*(e.y-i.y)-(i.y-t.y)*(e.x-i.x))/h,r=-((t.x-i.x)*(s.y-t.y)-(t.y-i.y)*(s.x-t.x))/h;return n>=0&&n<=1&&r>=0&&r<=1}static getLineIntersectionPoint(t,s,i,e){const h=(s.x-t.x)*(e.y-i.y)-(s.y-t.y)*(e.x-i.x);if(0===h)return null;const n=((i.x-t.x)*(e.y-i.y)-(i.y-t.y)*(e.x-i.x))/h,r=-((t.x-i.x)*(s.y-t.y)-(t.y-i.y)*(s.x-t.x))/h;return n>=0&&n<=1&&r>=0&&r<=1?{x:t.x+n*(s.x-t.x),y:t.y+n*(s.y-t.y)}:null}static getLinePolygonIntersection(t,s,i){const e=[i.topLeft,i.topRight,i.bottomRight,i.bottomLeft];let h=null,n=1/0;for(let i=0;i<e.length;i++){const r=this.getLineIntersectionPoint(t,s,e[i],e[(i+1)%e.length]);if(r){const s=Math.sqrt(Math.pow(r.x-t.x,2)+Math.pow(r.y-t.y,2));s<n&&(n=s,h=r)}}return h}static getEllipsePolygonApproximation(t,s,i,e,h=32,n=0){const r=[],o=Math.cos(n),a=Math.sin(n);for(let n=0;n<h;n++){const l=2*Math.PI*n/h,c=i*Math.cos(l),u=e*Math.sin(l);r.push({x:t+c*o-u*a,y:s+c*a+u*o})}return r}static getLinePointsArrayIntersection(t,s,i){let e=null,h=1/0;for(let n=0;n<i.length;n++){const r=this.getLineIntersectionPoint(t,s,i[n],i[(n+1)%i.length]);if(r){const s=Math.sqrt(Math.pow(r.x-t.x,2)+Math.pow(r.y-t.y,2));s<h&&(h=s,e=r)}}return e}static isPointInPolygonPoints(t,s){return this.isPointInPolygon(t,s)}}class a{__class__="KritzelBaseObject";_core;_elementRef;id;workspaceId;x;y;translateX;translateY;height;width;backgroundColor;borderColor;borderWidth=0;opacity=1;padding=0;scale;resizing=!1;rotation=0;markedForRemoval=!1;zIndex=0;userId;isVisible=!0;isSelected=!1;isHovered=!1;isMounted=!1;isEditable=!1;isInteractive=!1;isResizable=!0;isRotatable=!0;get totalWidth(){return this.width+2*this.padding}get totalHeight(){return this.height+2*this.padding}set elementRef(t){this._elementRef=t}get elementRef(){return this._elementRef}get boundingBox(){return{x:this.translateX,y:this.translateY,z:this.scale,width:this.totalWidth/this.scale,height:this.totalHeight/this.scale}}get rotatedBoundingBox(){const t=this.rotatedPolygon,s=[t.topLeft.x,t.topRight.x,t.bottomRight.x,t.bottomLeft.x],i=[t.topLeft.y,t.topRight.y,t.bottomRight.y,t.bottomLeft.y],e=Math.min(...s),h=Math.max(...s),n=Math.min(...i),r=Math.max(...i);return{x:e,y:n,z:this.scale,width:h-e,height:r-n}}get rotatedPolygon(){const t=this.translateX+this.totalWidth/2/this.scale,s=this.translateY+this.totalHeight/2/this.scale,i=this.rotation,e=Math.cos(i),h=Math.sin(i),n=this.translateX,r=this.translateY,o=n+this.totalWidth/this.scale,a=r+this.totalHeight/this.scale,l=(i,n)=>({x:e*(i-t)-h*(n-s)+t,y:h*(i-t)+e*(n-s)+s});return{topLeft:l(n,r),topRight:l(o,r),bottomRight:l(o,a),bottomLeft:l(n,a)}}get minXRotated(){return Math.min(this.rotatedPolygon.topLeft.x,this.rotatedPolygon.topRight.x,this.rotatedPolygon.bottomRight.x,this.rotatedPolygon.bottomLeft.x)}get minYRotated(){return Math.min(this.rotatedPolygon.topLeft.y,this.rotatedPolygon.topRight.y,this.rotatedPolygon.bottomRight.y,this.rotatedPolygon.bottomLeft.y)}get maxXRotated(){return Math.max(this.rotatedPolygon.topLeft.x,this.rotatedPolygon.topRight.x,this.rotatedPolygon.bottomRight.x,this.rotatedPolygon.bottomLeft.x)}get maxYRotated(){return Math.max(this.rotatedPolygon.topLeft.y,this.rotatedPolygon.topRight.y,this.rotatedPolygon.bottomRight.y,this.rotatedPolygon.bottomLeft.y)}get transformationMatrix(){const t=1/this.scale;return`matrix(${t}, 0, 0, ${t}, ${this.translateX}, ${this.translateY})`}get rotationDegrees(){return this.rotation*(180/Math.PI)}get centerX(){return this.translateX+this.totalWidth/2/this.scale}get centerY(){return this.translateY+this.totalHeight/2/this.scale}constructor(){this.id=this.generateId()}static create(t){const s=new a;return s._core=t,s.zIndex=t.store.currentZIndex,s.workspaceId=t.store.state.activeWorkspace.id,s.userId=t.user?.id,s}mount(t){this.isMounted||(this.elementRef=t,this.isMounted=!0)}generateId(){return n.generateUUID()}isInViewport(){const t=this._core.store.state.scale;if(this.boundingBox.width*t*(this.boundingBox.height*t)<.5)return!1;const s=-this._core.store.state.translateX/this._core.store.state.scale,i=-this._core.store.state.translateY/this._core.store.state.scale;return this.rotatedBoundingBox.x<s+this._core.store.state.viewportWidth/this._core.store.state.scale&&this.rotatedBoundingBox.x+this.rotatedBoundingBox.width>s&&this.rotatedBoundingBox.y<i+this._core.store.state.viewportHeight/this._core.store.state.scale&&this.rotatedBoundingBox.y+this.rotatedBoundingBox.height>i}centerInViewport(){const{viewportWidth:t,viewportHeight:s,translateX:i,translateY:e,scale:h}=this._core.store.state,{x:n,y:r,width:o,height:a}=this.rotatedBoundingBox;this.updatePosition(this.translateX+((t/2-i)/h-(n+o/2)),this.translateY+((s/2-e)/h-(r+a/2)))}update(){this._core.store.objects.update(this)}move(t,s,i,e){const h=(s-e)/this._core.store.state.scale;this.translateX+=(t-i)/this._core.store.state.scale,this.translateY+=h,this._core.store.objects.update(this)}resize(t,s,i,e){i<=1||e<=1||(this.width=i,this.height=e,this.translateX=t,this.translateY=s,this._core.store.objects.update(this),this._core.anchorManager.updateAnchorsForObject(this.id))}rotate(t){this.rotation=t,this._core.store.objects.update(this)}clone(){const t=Object.create(Object.getPrototypeOf(this));return Object.assign(t,this),t.id=this.id,t}copy(){const t=Object.create(Object.getPrototypeOf(this));return Object.assign(t,this),t.id=this.generateId(),t.isMounted=!1,t.isSelected=!1,t.isHovered=!1,t.isInteractive=!1,t.resizing=!1,t.markedForRemoval=!1,t.isVisible=!0,t}serialize(){const{_core:t,_elementRef:s,element:i,totalWidth:e,totalHeight:h,isSelected:n,isHovered:r,isMounted:o,isInteractive:a,resizing:l,markedForRemoval:c,isVisible:u,...f}=this;return structuredClone(f)}deserialize(t){return Object.assign(this,t),this.isSelected=!1,this.isHovered=!1,this.isMounted=!1,this.isInteractive=!1,this.resizing=!1,this.markedForRemoval=!1,this.isVisible=!0,"_adjustedPoints"in this&&(this._adjustedPoints=null),"_clipInfo"in this&&(this._clipInfo=null),this}adoptTransientStateFrom(t){}isClass(t){return this.__class__===t}edit(t){}onAfterUpdate(t){}hitTest(t,s){return!0}hitTestPolygon(t){return o.doPolygonsIntersect(this.rotatedPolygon,t)}updatePosition(t,s){this.translateX=t,this.translateY=s,this._core.store.objects.update(this)}}class l{static average(t,s){return(t+s)/2}static degreesToRadians(t){return t*(Math.PI/180)}}var c,u={},f=(c||(c=1,function(t){Object.defineProperties(t,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const{PI:s}=Math,i=s+1e-4,e=[1,1];function h(t,s,i,e=t=>t){return t*e(.5-s*(.5-i))}const{min:n}=Math;function r(t,s,i){let e=n(1,s/i);return n(1,t+(n(1,1-e)-t)*(.275*e))}function o(t,s){return[t[0]+s[0],t[1]+s[1]]}function a(t,s,i){return t[0]=s[0]+i[0],t[1]=s[1]+i[1],t}function l(t,s){return[t[0]-s[0],t[1]-s[1]]}function c(t,s,i){return t[0]=s[0]-i[0],t[1]=s[1]-i[1],t}function u(t,s){return[t[0]*s,t[1]*s]}function f(t,s,i){return t[0]=s[0]*i,t[1]=s[1]*i,t}function d(t){return[t[1],-t[0]]}function g(t,s){let i=s[0];return t[0]=s[1],t[1]=-i,t}function p(t,s){return t[0]*s[0]+t[1]*s[1]}function m(t,s){return t[0]===s[0]&&t[1]===s[1]}function M(t,s){let i=t[0]-s[0],e=t[1]-s[1];return i*i+e*e}function x(t){return function(t,s){return[t[0]/s,t[1]/s]}(t,function(t){return Math.hypot(t[0],t[1])}(t))}function y(t,s){return Math.hypot(t[1]-s[1],t[0]-s[0])}function w(t,s,i){let e=Math.sin(i),h=Math.cos(i),n=t[0]-s[0],r=t[1]-s[1];return[n*h-r*e+s[0],n*e+r*h+s[1]]}function b(t,s,i,e){let h=Math.sin(e),n=Math.cos(e),r=s[0]-i[0],o=s[1]-i[1],a=r*h+o*n;return t[0]=r*n-o*h+i[0],t[1]=a+i[1],t}function v(t,s,i){return o(t,u(l(s,t),i))}function k(t,s,i,e){let h=i[1]-s[1];return t[0]=s[0]+(i[0]-s[0])*e,t[1]=s[1]+h*e,t}function $(t,s,i){return o(t,u(s,i))}const z=[0,0],I=[0,0],P=[0,0];function j(t,s){let e=$(t,x(d(l(t,o(t,[1,1])))),-s),h=[],n=1/13;for(let s=n;s<=1;s+=n)h.push(w(e,t,2*i*s));return h}function L(t,s,e){let h=[],n=1/e;for(let e=n;e<=1;e+=n)h.push(w(s,t,i*e));return h}function C(t,s,i){let e=l(s,i),h=u(e,.5),n=u(e,.51);return[l(t,h),l(t,n),o(t,n),o(t,h)]}function A(t,s,e,h){let n=[],r=$(t,s,e),o=1/h;for(let s=o;s<1;s+=o)n.push(w(r,t,3*i*s));return n}function B(t,s,i){return[o(t,u(s,i)),o(t,u(s,.99*i)),l(t,u(s,.99*i)),l(t,u(s,i))]}function _(t,s,i){return!1===t||void 0===t?0:!0===t?Math.max(s,i):t}function R(t,s={}){let{size:e=16,smoothing:n=.5,thinning:u=.5,simulatePressure:m=!0,easing:x=t=>t,start:y={},end:w={},last:v=!1}=s,{cap:$=!0,easing:R=t=>t*(2-t)}=y,{cap:S=!0,easing:T=t=>--t*t*t+1}=w;if(0===t.length||e<=0)return[];let X,Y=t[t.length-1].runningLength,D=_(y.taper,e,Y),O=_(w.taper,e,Y),E=(e*n)**2,V=[],F=[],W=function(t,s,i){return t.slice(0,10).reduce(((t,e)=>{let h=e.pressure;return s&&(h=r(t,e.distance,i)),(t+h)/2}),t[0].pressure)}(t,m,e),H=h(e,u,t[t.length-1].pressure,x),U=t[0].vector,K=t[0].point,Q=K,Z=K,q=Q,G=!1;for(let s=0;s<t.length;s++){let{pressure:n}=t[s],{point:d,vector:y,distance:w,runningLength:v}=t[s],$=s===t.length-1;if(!$&&Y-v<3)continue;u?(m&&(n=r(W,w,e)),H=h(e,u,n,x)):H=e/2,void 0===X&&(X=H);let j=v<D?R(v/D):1,L=Y-v<O?T((Y-v)/O):1;H=Math.max(.01,H*Math.min(j,L));let C=($?t[s]:t[s+1]).vector,A=$?1:p(y,C),B=null!==A&&A<0;if(p(y,U)<0&&!G||B){g(z,U),f(z,z,H);for(let t=0;t<=1;t+=.07692307692307693)c(I,d,z),b(I,I,d,i*t),Z=[I[0],I[1]],V.push(Z),a(P,d,z),b(P,P,d,i*-t),q=[P[0],P[1]],F.push(q);K=Z,Q=q,B&&(G=!0)}else G=!1,$?(g(z,y),f(z,z,H),V.push(l(d,z)),F.push(o(d,z))):(k(z,C,y,A),g(z,z),f(z,z,H),c(I,d,z),Z=[I[0],I[1]],(s<=1||M(K,Z)>E)&&(V.push(Z),K=Z),a(P,d,z),q=[P[0],P[1]],(s<=1||M(Q,q)>E)&&(F.push(q),Q=q),W=n,U=y)}let N=[t[0].point[0],t[0].point[1]],J=t.length>1?[t[t.length-1].point[0],t[t.length-1].point[1]]:o(t[0].point,[1,1]),tt=[],st=[];if(1===t.length){if(!D&&!O||v)return j(N,X||H)}else{D||O&&1===t.length||($?tt.push(...L(N,F[0],13)):tt.push(...C(N,V[0],F[0])));let s=d(function(t){return[-t[0],-t[1]]}(t[t.length-1].vector));O||D&&1===t.length?st.push(J):S?st.push(...A(J,s,H,29)):st.push(...B(J,s,H))}return V.concat(st,F.reverse(),tt)}const S=[0,0];function T(t){return null!=t&&t>=0}function X(t,s={}){let{streamline:i=.5,size:h=16,last:n=!1}=s;if(0===t.length)return[];let r=.15+.85*(1-i),a=Array.isArray(t[0])?t:t.map((({x:t,y:s,pressure:i=.5})=>[t,s,i]));if(2===a.length){let t=a[1];a=a.slice(0,-1);for(let s=1;s<5;s++)a.push(v(a[0],t,s/4))}1===a.length&&(a=[...a,[...o(a[0],e),...a[0].slice(2)]]);let l=[{point:[a[0][0],a[0][1]],pressure:T(a[0][2])?a[0][2]:.25,vector:[...e],distance:0,runningLength:0}],u=!1,f=0,d=l[0],g=a.length-1;for(let t=1;t<a.length;t++){let s=n&&t===g?[a[t][0],a[t][1]]:v(d.point,a[t],r);if(m(d.point,s))continue;let i=y(s,d.point);if(f+=i,t<g&&!u){if(f<h)continue;u=!0}c(S,d.point,s),d={point:s,pressure:T(a[t][2])?a[t][2]:.5,vector:x(S),distance:i,runningLength:f},l.push(d)}return l[0].vector=l[1]?.vector||[0,0],l}function Y(t,s={}){return R(X(t,s),s)}t.default=Y,t.getStroke=Y,t.getStrokeOutlinePoints=R,t.getStrokePoints=X}(u)),u);class d extends a{__class__="KritzelPath";points;d;stroke={light:"none",dark:"none"};strokeWidth;lineSlack=.5;fill;x=0;y=0;height=0;width=0;scale=1;options;isVisible=!0;isDebugInfoVisible=!0;isCompleted=!1;_adjustedPoints=null;get viewBox(){return`0 0 ${this.width} ${this.height}`}constructor(t){super(),this.options=t,this.points=t?.points??[],this.translateX=t?.translateX??0,this.translateY=t?.translateY??0,this.rotation=l.degreesToRadians(t?.rotation??0),this.scale=t?.scale??1,this.strokeWidth=t?.strokeWidth??8,this.fill=t?.fill??{light:"#000000",dark:"#ffffff"},this.d=this.generateSvgPath(),this.updateDimensions()}static create(t,s){const i=new d;return i._core=t,i.id=i.generateId(),i.workspaceId=t.store.state.activeWorkspace.id,i.userId=t.user?.id,i.options=s,i.points=s?.points??[],i.translateX=s?.translateX??0,i.translateY=s?.translateY??0,i.rotation=l.degreesToRadians(s?.rotation??0),i.scale=s?.scale??1,i.strokeWidth=s?.strokeWidth??8,i.fill=s?.fill??{light:"#000000",dark:"#ffffff"},i.opacity=s?.opacity??1,i.zIndex=t.store.currentZIndex,i.d=i.generateSvgPath(),i.updateDimensions(),i}copy(){const t=super.copy();return this.points&&(t.points=this.points.map((t=>[...t]))),t}deserialize(t){const s=super.deserialize(t);return this.d=this.generateSvgPath(),s}resize(t,s,i,e){if(i<=1||e<=1)return;const h=i/this.width,n=e/this.height;if(this.width=i,this.height=e,1===this.points.length){const t=this.points[0],s=Math.max(0,i-this.strokeWidth)/h,r=Math.max(0,e-this.strokeWidth)/n;this.points.push([t[0]+s,t[1]+r])}this.points=this.points.map((([t,s])=>[t*h,s*n])),this.d=this.generateSvgPath(),this.width=Math.max(...this.points.map((t=>t[0])))-Math.min(...this.points.map((t=>t[0])))+this.strokeWidth,this.height=Math.max(...this.points.map((t=>t[1])))-Math.min(...this.points.map((t=>t[1])))+this.strokeWidth,this.x=Math.min(...this.points.map((t=>t[0])))-this.strokeWidth/2,this.y=Math.min(...this.points.map((t=>t[1])))-this.strokeWidth/2,null!==t&&(this.translateX=t),null!==s&&(this.translateY=s),this._adjustedPoints=null,this._core.store.objects.update(this)}rotate(t){this.rotation=t,this._adjustedPoints=null,this._core.store.objects.update(this)}move(t,s,i,e){const h=(s-e)/this._core.store.state.scale;this.translateX+=(t-i)/this._core.store.state.scale,this.translateY+=h,this._adjustedPoints=null,this._core.store.objects.update(this)}hitTest(t,s){const i=this.strokeWidth/this.scale/2;if(null===this._adjustedPoints&&(this._adjustedPoints=this.computeAdjustedPoints()),1===this._adjustedPoints.length){const e=this._adjustedPoints[0];return this.pointToLineSegmentDistance(t,s,e[0],e[1],e[0],e[1])<=i}for(let e=0;e<this._adjustedPoints.length-1;e++){const h=this._adjustedPoints[e],n=this._adjustedPoints[e+1];if(this.pointToLineSegmentDistance(t,s,h[0],h[1],n[0],n[1])<=i)return!0}return!1}hitTestPolygon(t){const s=this.strokeWidth/this.scale/2;null===this._adjustedPoints&&(this._adjustedPoints=this.computeAdjustedPoints());const i=[{x:t.bottomLeft.x,y:t.bottomLeft.y},{x:t.bottomRight.x,y:t.bottomRight.y},{x:t.topRight.x,y:t.topRight.y},{x:t.topLeft.x,y:t.topLeft.y}];for(const[t,s]of this._adjustedPoints)if(o.isPointInPolygon({x:t,y:s},i))return!0;for(const t of i)if(this.hitTest(t.x,t.y))return!0;for(let t=0;t<this._adjustedPoints.length-1;t++){const s={x:this._adjustedPoints[t][0],y:this._adjustedPoints[t][1]},e={x:this._adjustedPoints[t+1][0],y:this._adjustedPoints[t+1][1]};for(let t=0;t<i.length;t++)if(o.intersectLines(s,e,i[t],i[(t+1)%i.length]))return!0}for(let t=0;t<this._adjustedPoints.length-1;t++){const e=this._adjustedPoints[t],h=this._adjustedPoints[t+1];for(let t=0;t<i.length;t++){const n=i[t],r=i[(t+1)%i.length],o=this.pointToLineSegmentDistance(n.x,n.y,e[0],e[1],h[0],h[1]),a=this.pointToLineSegmentDistance(r.x,r.y,e[0],e[1],h[0],h[1]),l=this.pointToLineSegmentDistance(e[0],e[1],n.x,n.y,r.x,r.y),c=this.pointToLineSegmentDistance(h[0],h[1],n.x,n.y,r.x,r.y);if(Math.min(o,a,l,c)<=s)return!0}}return!1}updatePosition(t,s){this.translateX=t,this.translateY=s,this._adjustedPoints=null,this._core.store.objects.update(this)}onAfterUpdate(t){t.includes("strokeWidth")&&(this.d=this.generateSvgPath(),this.updateBoundingBox(),this._adjustedPoints=null)}computeAdjustedPoints(){if(!this.points?.length)return[];const t=this.rotation,s=Math.cos(t),i=Math.sin(t),e=this.points.map((t=>t[0])),h=this.points.map((t=>t[1])),n={x:(Math.min(...e)+Math.max(...e))/2,y:(Math.min(...h)+Math.max(...h))/2},{x:r,y:o}=n;return this.points.map((([t,e])=>{const h=t-r,n=e-o;return[r+h*s-n*i,o+h*i+n*s]})).map((([t,s])=>[Math.abs(t-this.x)/this.scale+this.translateX,Math.abs(s-this.y)/this.scale+this.translateY]))}pointToLineSegmentDistance(t,s,i,e,h,n){const r=h-i,o=n-e,a=r*r+o*o;let l,c,u=-1;0!==a&&(u=((t-i)*r+(s-e)*o)/a),u<0?(l=i,c=e):u>1?(l=h,c=n):(l=i+u*r,c=e+u*o);const f=t-l,d=s-c;return Math.sqrt(f*f+d*d)}updateDimensions(){const t=Math.min(...this.points.map((t=>t[0])))-this.strokeWidth/2,s=Math.min(...this.points.map((t=>t[1])))-this.strokeWidth/2,i=Math.max(...this.points.map((t=>t[0])))+this.strokeWidth/2,e=Math.max(...this.points.map((t=>t[1])))+this.strokeWidth/2;this.width=i-t+this.lineSlack,this.height=e-s+this.lineSlack,this.x=t,this.y=s,this.translateX=(this.x+this.translateX)/this.scale,this.translateY=(this.y+this.translateY)/this.scale}updateBoundingBox(){const t=Math.min(...this.points.map((t=>t[0])))-this.strokeWidth/2,s=Math.min(...this.points.map((t=>t[1])))-this.strokeWidth/2,i=Math.max(...this.points.map((t=>t[0])))+this.strokeWidth/2,e=Math.max(...this.points.map((t=>t[1])))+this.strokeWidth/2;this.width=i-t+this.lineSlack,this.height=e-s+this.lineSlack,this.x=t,this.y=s}generateSvgPath(){const t=this.getStrokeFromPoints(this.points,this.strokeWidth),s=this.points.length?Math.min(...this.points.map((t=>t[0])))-this.strokeWidth/2:0,i=this.points.length?Math.min(...this.points.map((t=>t[1])))-this.strokeWidth/2:0;return this.getSvgPathFromStroke(t,s,i)}getStrokeFromPoints(t,s){return f.getStroke(t,{size:s,thinning:.5,smoothing:.5,streamline:.5,easing:t=>t,simulatePressure:!0,last:!0,start:{cap:!0,taper:0,easing:t=>t},end:{cap:!0,taper:0,easing:t=>t}})}getSvgPathFromStroke(t,s=0,i=0,e=!0){const h=t.length;if(h<4)return"";const n=t[0],r=t[1],o=t[2],a=n[1]-i,c=r[0]-s,u=r[1]-i,f=o[0]-s,d=o[1]-i;let g=`M${(n[0]-s).toFixed(2)},${a.toFixed(2)} Q${c.toFixed(2)},${u.toFixed(2)} ${l.average(c,f).toFixed(2)},${l.average(u,d).toFixed(2)} T`;for(let e=2,n=h-1;e<n;e++){const h=t[e],n=t[e+1],r=h[1]-i,o=n[1]-i;g+=`${l.average(h[0]-s,n[0]-s).toFixed(2)},${l.average(r,o).toFixed(2)} `}return e&&(g+="Z"),g}isLowRes(){if(!this._core)return!1;const t=this._core.store.state.scale;return this.boundingBox.width*t*(this.boundingBox.height*t)<500}getClipPoint(t){const s=this.centerX,i=this.centerY,e=this.strokeWidth/this.scale/2;if(null===this._adjustedPoints&&(this._adjustedPoints=this.computeAdjustedPoints()),this._adjustedPoints.length<1)return null;let h=!0;for(let n=1;n<=32;n++){const r=n/32,o=t.x+(s-t.x)*r,a=t.y+(i-t.y)*r;let l=1/0;if(1===this._adjustedPoints.length){const t=this._adjustedPoints[0],s=o-t[0],i=a-t[1];l=Math.sqrt(s*s+i*i)}else for(let t=0;t<this._adjustedPoints.length-1;t++){const s=this._adjustedPoints[t],i=this._adjustedPoints[t+1],e=this.pointToLineSegmentDistance(o,a,s[0],s[1],i[0],i[1]);e<l&&(l=e)}const c=l<=e;if(h&&c){let h=(n-1)/32,o=r;for(let n=0;n<8;n++){const n=(h+o)/2,r=t.x+(s-t.x)*n,a=t.y+(i-t.y)*n;let l=1/0;if(1===this._adjustedPoints.length){const t=this._adjustedPoints[0],s=r-t[0],i=a-t[1];l=Math.sqrt(s*s+i*i)}else for(let t=0;t<this._adjustedPoints.length-1;t++){const s=this._adjustedPoints[t],i=this._adjustedPoints[t+1],e=this.pointToLineSegmentDistance(r,a,s[0],s[1],i[0],i[1]);e<l&&(l=e)}l<=e?o=n:h=n}const a=(h+o)/2;return{x:t.x+(s-t.x)*a,y:t.y+(i-t.y)*a}}h=!c}return null}}class g extends a{__class__="KritzelLine";startX;startY;endX;endY;controlX;controlY;stroke;strokeWidth;scale=1;options;startAnchor;endAnchor;arrows;isVisible=!0;isDebugInfoVisible=!0;isCompleted=!1;_adjustedPoints=null;_clipInfo=null;get d(){return void 0!==this.controlX&&void 0!==this.controlY?`M ${this.startX} ${this.startY} Q ${this.controlX} ${this.controlY} ${this.endX} ${this.endY}`:`M ${this.startX} ${this.startY} L ${this.endX} ${this.endY}`}get viewBox(){return`${this.x} ${this.y} ${this.width} ${this.height}`}constructor(t){super(),this.options=t,this.startX=t?.startX??0,this.startY=t?.startY??0,this.endX=t?.endX??0,this.endY=t?.endY??0,this.controlX=t?.controlX,this.controlY=t?.controlY,this.translateX=t?.translateX??0,this.translateY=t?.translateY??0,this.rotation=l.degreesToRadians(t?.rotation??0),this.scale=t?.scale??1,this.strokeWidth=t?.strokeWidth??4,this.stroke=t?.stroke??{light:"#000000",dark:"#ffffff"},this.startAnchor=t?.startAnchor,this.endAnchor=t?.endAnchor,this.arrows=t?.arrows,this.updateDimensions()}static create(t,s){const i=new g;return i._core=t,i.id=i.generateId(),i.workspaceId=t.store.state.activeWorkspace.id,i.userId=t.user?.id,i.options=s,i.startX=s?.startX??0,i.startY=s?.startY??0,i.endX=s?.endX??0,i.endY=s?.endY??0,i.controlX=s?.controlX,i.controlY=s?.controlY,i.translateX=s?.translateX??0,i.translateY=s?.translateY??0,i.rotation=l.degreesToRadians(s?.rotation??0),i.scale=s?.scale??1,i.strokeWidth=s?.strokeWidth??4,i.stroke=s?.stroke??{light:"#000000",dark:"#ffffff"},i.opacity=s?.opacity??1,i.startAnchor=s?.startAnchor,i.endAnchor=s?.endAnchor,i.arrows=s?.arrows,i.zIndex=t.store.currentZIndex,i.updateDimensions(),i}resize(t,s,i,e){if(i<=1||e<=1)return;const h=i/this.width,n=e/this.height;this.startX=this.startX*h,this.startY=this.startY*n,this.endX=this.endX*h,this.endY=this.endY*n,void 0!==this.controlX&&void 0!==this.controlY&&(this.controlX=this.controlX*h,this.controlY=this.controlY*n);const{minX:r,minY:o,maxX:a,maxY:l}=this.calculateBoundingBox();this.width=a-r,this.height=l-o,this.x=r,this.y=o,this.translateX=t,this.translateY=s,this._adjustedPoints=null,this._clipInfo=null,this._core.store.objects.update(this),this._core.anchorManager.updateAnchorsForObject(this.id),this.startAnchor&&this._core.anchorManager.updateAnchorsForObject(this.startAnchor.objectId),this.endAnchor&&this._core.anchorManager.updateAnchorsForObject(this.endAnchor.objectId)}rotate(t){this.rotation=t,this._adjustedPoints=null,this._clipInfo=null,this._core.store.objects.update(this)}move(t,s,i,e){const h=(s-e)/this._core.store.state.scale;this.translateX+=(t-i)/this._core.store.state.scale,this.translateY+=h,this.startAnchor&&this._core.anchorManager.updateAnchorsForObject(this.startAnchor.objectId),this.endAnchor&&this._core.anchorManager.updateAnchorsForObject(this.endAnchor.objectId),this._adjustedPoints=null,this._clipInfo=null,this._core.store.objects.update(this)}hitTest(t,s){const i=Math.max(this.strokeWidth,20)/this.scale/2;if(null===this._adjustedPoints&&(this._adjustedPoints=this.computeAdjustedPoints()),void 0!==this.controlX&&void 0!==this.controlY){const e=this.getClipInfo();return this.pointToBezierDistance(t,s,e.start?.t??0,e.end?.t??1)<=i}const e=this.getClipInfo(),h=e.start?[e.start.x,e.start.y]:this._adjustedPoints[0],n=e.end?[e.end.x,e.end.y]:this._adjustedPoints[1];return this.pointToLineSegmentDistance(t,s,h[0],h[1],n[0],n[1])<=i}hitTestPolygon(t){const s=this.strokeWidth/this.scale/2;null===this._adjustedPoints&&(this._adjustedPoints=this.computeAdjustedPoints());const i=this.getClipInfo(),e=[{x:t.bottomLeft.x,y:t.bottomLeft.y},{x:t.bottomRight.x,y:t.bottomRight.y},{x:t.topRight.x,y:t.topRight.y},{x:t.topLeft.x,y:t.topLeft.y}],h=i.start?[i.start.x,i.start.y]:this._adjustedPoints[0],n=i.end?[i.end.x,i.end.y]:this._adjustedPoints[1],r=[h,n];for(const[t,s]of r)if(o.isPointInPolygon({x:t,y:s},e))return!0;for(const t of e)if(this.hitTest(t.x,t.y))return!0;if(void 0!==this.controlX&&void 0!==this.controlY){const t=this._adjustedPoints[0],h=this._adjustedPoints[1],n=this.computeAdjustedControlPoint(),r=i.start?.t??0,a=i.end?.t??1,l=20;for(let i=0;i<=l;i++){const c=r+i/l*(a-r),u=1-c,f=u*u*t[0]+2*u*c*n[0]+c*c*h[0],d=u*u*t[1]+2*u*c*n[1]+c*c*h[1];if(o.isPointInPolygon({x:f,y:d},e))return!0;for(let t=0;t<e.length;t++){const i=e[t],h=e[(t+1)%e.length];if(this.pointToLineSegmentDistance(f,d,i.x,i.y,h.x,h.y)<=s)return!0}}return!1}const a={x:h[0],y:h[1]},l={x:n[0],y:n[1]};for(let t=0;t<e.length;t++){const i=e[t],h=e[(t+1)%e.length];if(o.intersectLines(a,l,i,h))return!0;const n=this.pointToLineSegmentDistance(i.x,i.y,a.x,a.y,l.x,l.y),r=this.pointToLineSegmentDistance(h.x,h.y,a.x,a.y,l.x,l.y),c=this.pointToLineSegmentDistance(a.x,a.y,i.x,i.y,h.x,h.y),u=this.pointToLineSegmentDistance(l.x,l.y,i.x,i.y,h.x,h.y);if(Math.min(n,r,c,u)<=s)return!0}return!1}updatePosition(t,s){this.translateX=t,this.translateY=s,this._adjustedPoints=null,this._clipInfo=null,this._core.store.objects.update(this)}updateEndpoint(t,s,i){"start"===t?(this.startX=s,this.startY=i):(this.endX=s,this.endY=i);const e=this.width,h=this.height,{minX:n,minY:r,maxX:o,maxY:a}=this.calculateBoundingBox(),l=n-this.x,c=r-this.y,u=o-n,f=a-r,d=(u-e)/2,g=(f-h)/2;this.x=n,this.y=r,this.width=u,this.height=f;const p=Math.cos(this.rotation),m=Math.sin(this.rotation),M=l+d,x=c+g,y=M*m+x*p-g;this.translateX+=(M*p-x*m-d)/this.scale,this.translateY+=y/this.scale,this._adjustedPoints=null,this._clipInfo=null,this._core.store.objects.update(this)}updateControlPoint(t,s){this.controlX=t,this.controlY=s;const i=this.width,e=this.height,{minX:h,minY:n,maxX:r,maxY:o}=this.calculateBoundingBox(),a=h-this.x,l=n-this.y,c=r-h,u=o-n,f=(c-i)/2,d=(u-e)/2;this.x=h,this.y=n,this.width=c,this.height=u;const g=Math.cos(this.rotation),p=Math.sin(this.rotation),m=a+f,M=l+d,x=m*p+M*g-d;this.translateX+=(m*g-M*p-f)/this.scale,this.translateY+=x/this.scale,this._adjustedPoints=null,this._clipInfo=null,this._core.store.objects.update(this)}computeAdjustedPoints(){const t=[[this.startX,this.startY],[this.endX,this.endY]],s=this.rotation,i=Math.cos(s),e=Math.sin(s),h=t.map((t=>t[0])),n=t.map((t=>t[1])),r={x:(Math.min(...h)+Math.max(...h))/2,y:(Math.min(...n)+Math.max(...n))/2},{x:o,y:a}=r;return t.map((([t,s])=>{const h=t-o,n=s-a;return[o+h*i-n*e,a+h*e+n*i]})).map((([t,s])=>[(t-this.x)/this.scale+this.translateX,(s-this.y)/this.scale+this.translateY]))}pointToLineSegmentDistance(t,s,i,e,h,n){const r=h-i,o=n-e,a=r*r+o*o;let l,c,u=-1;0!==a&&(u=((t-i)*r+(s-e)*o)/a),u<0?(l=i,c=e):u>1?(l=h,c=n):(l=i+u*r,c=e+u*o);const f=t-l,d=s-c;return Math.sqrt(f*f+d*d)}pointToBezierDistance(t,s,i=0,e=1){null===this._adjustedPoints&&(this._adjustedPoints=this.computeAdjustedPoints());const h=this._adjustedPoints[0],n=this._adjustedPoints[1],r=this.computeAdjustedControlPoint();let o=1/0;for(let a=0;a<=20;a++){const l=i+a/20*(e-i),c=1-l,u=t-(c*c*h[0]+2*c*l*r[0]+l*l*n[0]),f=s-(c*c*h[1]+2*c*l*r[1]+l*l*n[1]),d=Math.sqrt(u*u+f*f);d<o&&(o=d)}return o}getClipInfo(){if(this._clipInfo)return this._clipInfo;const t=this.endAnchor,s={};if(this.startAnchor){const t=this._core.anchorManager.findAnchorTarget(this,"start");if(t){const i=this._core.anchorManager.computeAnchorClipInfo(this,"start",t);i&&(s.start={x:i.worldX,y:i.worldY,t:i.t})}}if(t){const t=this._core.anchorManager.findAnchorTarget(this,"end");if(t){const i=this._core.anchorManager.computeAnchorClipInfo(this,"end",t);i&&(s.end={x:i.worldX,y:i.worldY,t:i.t})}}return this._clipInfo=s,s}computeAdjustedControlPoint(){if(void 0===this.controlX||void 0===this.controlY)return[(this.startX+this.endX)/2,(this.startY+this.endY)/2];const t=this.rotation,s=Math.cos(t),i=Math.sin(t),e=[this.startX,this.endX],h=[this.startY,this.endY],n={x:(Math.min(...e)+Math.max(...e))/2,y:(Math.min(...h)+Math.max(...h))/2},{x:r,y:o}=n,a=this.controlX-r,l=this.controlY-o;return[(r+a*s-l*i-this.x)/this.scale+this.translateX,(o+a*i+l*s-this.y)/this.scale+this.translateY]}get rotatedPolygon(){const t=this.padding,{minX:s,minY:i,maxX:e,maxY:h}=g.calculateBoundingBox(this.startX,this.startY,this.endX,this.endY,this.controlX,this.controlY,this.strokeWidth),n=s-this.x+t,r=i-this.y+t,o=e-this.x+t,a=h-this.y+t,l={x:n,y:r},c={x:o,y:r},u={x:o,y:a},f={x:n,y:a},d=this.totalWidth/2,p=this.totalHeight/2,m=this.rotation,M=Math.cos(m),x=Math.sin(m),y=t=>({x:((t.x-d)*M-(t.y-p)*x+d)/this.scale+this.translateX,y:((t.x-d)*x+(t.y-p)*M+p)/this.scale+this.translateY});return{topLeft:y(l),topRight:y(c),bottomRight:y(u),bottomLeft:y(f)}}static calculateBoundingBox(t,s,i,e,h,n,r){let o=Math.min(t,i),a=Math.min(s,e),l=Math.max(t,i),c=Math.max(s,e);if(void 0!==h&&void 0!==n){const r=t-2*h+i;if(0!==r){const s=(t-h)/r;if(s>0&&s<1){const e=(1-s)*(1-s)*t+2*(1-s)*s*h+s*s*i;o=Math.min(o,e),l=Math.max(l,e)}}const u=s-2*n+e;if(0!==u){const t=(s-n)/u;if(t>0&&t<1){const i=(1-t)*(1-t)*s+2*(1-t)*t*n+t*t*e;a=Math.min(a,i),c=Math.max(c,i)}}}const u=r/2;return{minX:o-u,minY:a-u,maxX:l+u,maxY:c+u}}calculateBoundingBox(){return g.calculateBoundingBox(this.startX,this.startY,this.endX,this.endY,this.controlX,this.controlY,this.strokeWidth)}serialize(){const t=super.serialize();return delete t._adjustedPoints,delete t._clipInfo,t}deserialize(t){const s=super.deserialize(t);return this._adjustedPoints=null,this._clipInfo=null,s}updateDimensions(){const{minX:t,minY:s,maxX:i,maxY:e}=this.calculateBoundingBox();this.width=i-t,this.height=e-s,this.x=t,this.y=s,this.translateX=(this.x+this.translateX)/this.scale,this.translateY=(this.y+this.translateY)/this.scale}get startMarkerId(){return`arrow-start-${this.id}`}get endMarkerId(){return`arrow-end-${this.id}`}getArrowSize(t){const s="start"===t?this.arrows?.start:this.arrows?.end;return s?.size??3*this.strokeWidth}getArrowFill(t){const s="start"===t?this.arrows?.start:this.arrows?.end;return r.resolveThemeColor(s?.fill??this.stroke)}getArrowPath(t="triangle"){switch(t){case"triangle":default:return"M 0 0 L 10 5 L 0 10 z";case"open":return"M 0 0 L 10 5 L 0 10";case"diamond":return"M 0 5 L 5 0 L 10 5 L 5 10 z";case"circle":return"M 5,0 A 5,5 0 1,1 5,10 A 5,5 0 1,1 5,0"}}get hasStartArrow(){return!0===this.arrows?.start?.enabled}get hasEndArrow(){return!0===this.arrows?.end?.enabled}}const p=t(class extends s{constructor(t){super(),!1!==t&&this.__registerHost(),this.__attachShadow()}core;showEdgeIndicators=!0;edgeIndicatorPadding=8;remoteCursors=new Map;objectVersion=0;cleanupIntervalId;objectChangeRafId=null;removeAwarenessChangeListener;removeObjectsChangeListener;componentDidLoad(){this.removeAwarenessChangeListener=this.core.store.objects?.onAwarenessChange((t=>{this.handleAwarenessChange(t)})),this.removeObjectsChangeListener=this.core.store.objects?.onObjectsChange((()=>{this.handleRemoteObjectChange()})),this.cleanupIntervalId=setInterval((()=>{this.cleanupStaleCursors()}),3e3),this.cleanupIntervalId&&"function"==typeof this.cleanupIntervalId.unref&&this.cleanupIntervalId.unref()}disconnectedCallback(){this.removeAwarenessChangeListener&&(this.removeAwarenessChangeListener(),this.removeAwarenessChangeListener=void 0),this.removeObjectsChangeListener&&(this.removeObjectsChangeListener(),this.removeObjectsChangeListener=void 0),this.cleanupIntervalId&&"function"==typeof globalThis.clearInterval&&(globalThis.clearInterval(this.cleanupIntervalId),this.cleanupIntervalId=void 0),null!==this.objectChangeRafId&&"function"==typeof globalThis.cancelAnimationFrame&&(globalThis.cancelAnimationFrame(this.objectChangeRafId),this.objectChangeRafId=null)}handleAwarenessChange(t){const s=this.core.store.objects?.localClientId,i=Date.now(),e=new Map(this.remoteCursors),h=new Set;t.forEach(((t,n)=>{if(n===s)return;if(!t.user)return;h.add(n);const r=t.user,o=t.cursor,a=t.activeObjectId||null,l=t.selectionBox||null,c=e.get(n);e.set(n,{clientId:n,user:r,cursor:o,activeObjectId:a,selectionBox:l,lastUpdated:i,lastCursorMove:!c||!c.cursor!=!o||o&&c.cursor&&(o.x!==c.cursor.x||o.y!==c.cursor.y)?i:c?.lastCursorMove??i})}));for(const t of e.keys())h.has(t)||e.delete(t);this.remoteCursors=e}cleanupStaleCursors(){const t=Date.now();let s=!1;const i=new Map(this.remoteCursors);for(const[e,h]of i)t-h.lastUpdated>3e4?(i.delete(e),s=!0):!s&&t-h.lastCursorMove>1e4&&(s=!0);s&&(this.remoteCursors=i)}isStale(t){return Date.now()-t.lastCursorMove>1e4}hasActiveDrawingCursors(){for(const t of this.remoteCursors.values())if(t.activeObjectId)return!0;return!1}handleRemoteObjectChange(){this.hasActiveDrawingCursors()&&null===this.objectChangeRafId&&(this.objectChangeRafId=requestAnimationFrame((()=>{this.objectChangeRafId=null,this.objectVersion++})))}getActiveObjectTip(t){const s=this.core.store.objects?.findById(t);if(!s)return null;if(s instanceof d&&!s.isCompleted){const t=s.points[s.points.length-1];return t?{x:(t[0]-s.x)/s.scale+s.translateX,y:(t[1]-s.y)/s.scale+s.translateY}:null}return s instanceof g&&!s.isCompleted?{x:(s.endX-s.x)/s.scale+s.translateX,y:(s.endY-s.y)/s.scale+s.translateY}:null}worldToScreen(t,s){const{scale:i,translateX:e,translateY:h}=this.core.store.state;return{x:t*i+e,y:s*i+h}}isInViewport(t,s){const{viewportWidth:i,viewportHeight:e}=this.core.store.state;return t>=0&&t<=i&&s>=0&&s<=e}clampToEdge(t,s){const{viewportWidth:i,viewportHeight:e}=this.core.store.state,h=this.edgeIndicatorPadding,n=Math.max(h,Math.min(i-h,t)),r=Math.max(h,Math.min(e-h,s)),o=n-h,a=i-h-n,l=r-h;let c="top";const u=Math.min(o,a,l,e-h-r);return c=u===o?"left":u===a?"right":u===l?"top":"bottom",{x:n,y:r,angle:Math.atan2(s-r,t-n),edge:c}}getUserDisplayName(t){return t.displayName?t.displayName:t.firstName||t.lastName?[t.firstName,t.lastName].filter(Boolean).join(" "):"Unknown"}getInitials(t){const s=t.trim().split(/\s+/);return 0===s.length||1===s.length&&s[0].length<=1?t:1===s.length?s[0][0].toUpperCase():(s[0][0]+s[s.length-1][0]).toUpperCase()}render(){const t=Array.from(this.remoteCursors.values());return i(e,{key:"9be6694250bafe011be27301d1c7a53fc7957657"},t.map((t=>{if(!t.cursor)return null;let s,i=!1;if(t.activeObjectId){const e=this.getActiveObjectTip(t.activeObjectId);e?(i=!0,s=this.worldToScreen(e.x,e.y)):s=this.worldToScreen(t.cursor.x,t.cursor.y)}else s=this.worldToScreen(t.cursor.x,t.cursor.y);const e=this.isInViewport(s.x,s.y),h=this.isStale(t),n=t.user.color||"#6B7280";return e?this.renderCursor(t,s.x,s.y,n,h,i):this.showEdgeIndicators?this.renderEdgeIndicator(t,s.x,s.y,n,h,i):null})),t.map((t=>{if(!t.selectionBox)return null;const s=t.user.color||"#6B7280",e=t.selectionBox,h=this.worldToScreen(e.x,e.y),{scale:n}=this.core.store.state;return i("div",{key:`selection-box-${t.clientId}`,class:"remote-selection-box",style:{transform:`translate(${h.x}px, ${h.y}px)`,width:e.width*n+"px",height:e.height*n+"px",backgroundColor:`color-mix(in srgb, ${s} 20%, transparent)`,borderColor:`color-mix(in srgb, ${s} 50%, transparent)`}})})))}renderCursor(t,s,e,h,n,r){return i("div",{key:`cursor-${t.clientId}`,class:{"awareness-cursor":!0,stale:n,"tracking-object":r},style:{transform:`translate(${s}px, ${e}px)`}},i("svg",{class:"cursor-arrow",width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg"},i("path",{d:"M5 3L19 12L12 13L9 20L5 3Z",fill:h,stroke:"#ffffff","stroke-width":"1.5","stroke-linejoin":"round"})),i("span",{class:"cursor-label",style:{backgroundColor:h}},this.getUserDisplayName(t.user)))}renderEdgeIndicator(t,s,e,h,n,r){const o=this.clampToEdge(s,e),a=180*o.angle/Math.PI+90;let l=0,c=0;"left"===o.edge?l=20:"right"===o.edge?l=-20:"top"===o.edge?c=20:"bottom"===o.edge&&(c=-20);const u=this.getUserDisplayName(t.user);return i("div",{key:`edge-${t.clientId}`,class:{"edge-indicator":!0,stale:n,"tracking-object":r},style:{transform:`translate(${o.x}px, ${o.y}px)`}},i("svg",{class:{"edge-arrow":!0,stale:n},width:"16",height:"16",viewBox:"0 0 16 16",style:{transform:`rotate(${a}deg)`}},i("path",{d:"M8 1L14 13H2L8 1Z",fill:h,stroke:"#ffffff","stroke-width":"1.5","stroke-linejoin":"round"})),i("span",{class:"edge-label",style:{backgroundColor:h,transform:`translate(${l}px, ${c}px)`}},this.getInitials(u)))}static get style(){return":host{display:block;position:fixed;top:0;left:0;width:100vw;height:100vh;pointer-events:none;z-index:1}.awareness-cursor{position:absolute;top:0;left:0;transition:transform var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, opacity 300ms ease;will-change:transform}.awareness-cursor.stale{opacity:0}.awareness-cursor.tracking-object{transition-duration:0ms}.cursor-arrow{filter:drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))}.cursor-label{position:absolute;left:16px;top:16px;white-space:nowrap;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:var(--kritzel-awareness-cursor-label-font-size, 12px);color:var(--kritzel-awareness-cursor-label-text-color, #ffffff);padding:2px 8px;border-radius:4px;line-height:1.4;font-weight:500;pointer-events:none;user-select:none}.edge-indicator{position:absolute;top:-12px;left:-12px;width:24px;height:24px;display:flex;align-items:center;justify-content:center;transition:transform var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, opacity 300ms ease;will-change:transform;pointer-events:auto;user-select:none;cursor:pointer}.edge-indicator.stale{opacity:0}.edge-indicator.tracking-object{transition-duration:0ms}.edge-arrow{position:absolute;filter:drop-shadow(0 1px 3px rgba(0, 0, 0, 0.3));transition:opacity 300ms ease}.edge-arrow.stale{opacity:0}.edge-label{position:absolute;white-space:nowrap;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:var(--kritzel-awareness-cursor-label-font-size, 12px);color:var(--kritzel-awareness-cursor-label-text-color, #ffffff);padding:2px 8px;border-radius:4px;line-height:1.4;font-weight:500;pointer-events:none;opacity:0;transform-origin:center;transition:opacity 150ms ease}.edge-indicator:hover .edge-label{opacity:1}.remote-selection-box{position:absolute;top:0;left:0;border-width:2px;border-style:solid;pointer-events:none;will-change:transform, width, height;transition:transform var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, width var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, height var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out}"}},[513,"kritzel-awareness-cursors",{core:[16],showEdgeIndicators:[4,"show-edge-indicators"],edgeIndicatorPadding:[2,"edge-indicator-padding"],remoteCursors:[32],objectVersion:[32]}]);function m(){"undefined"!=typeof customElements&&["kritzel-awareness-cursors"].forEach((t=>{"kritzel-awareness-cursors"===t&&(customElements.get(h(t))||customElements.define(h(t),p))}))}export{a as K,d as a,g as b,p as c,m as d,l as e,o as f}
1
+ import { p as proxyCustomElement, H, h, d as Host, t as transformTag } from './p-B43upypT.js';
2
+ import { O as ObjectHelper } from './p-Dd-pMvlr.js';
3
+ import { a as KritzelColorHelper } from './p-BmyTdQGy.js';
4
+
5
+ class KritzelGeometryHelper {
6
+ static doPolygonsIntersect(polygon1, polygon2) {
7
+ // 1. Convert polygons to array of points for easier processing
8
+ const points1 = [polygon1.bottomLeft, polygon1.bottomRight, polygon1.topRight, polygon1.topLeft];
9
+ const points2 = [polygon2.bottomLeft, polygon2.bottomRight, polygon2.topRight, polygon2.topLeft];
10
+ // 2. Check if any point of polygon1 is inside polygon2
11
+ for (const point of points1) {
12
+ if (this.isPointInPolygon(point, points2)) {
13
+ return true;
14
+ }
15
+ }
16
+ // 3. Check if any point of polygon2 is inside polygon1
17
+ for (const point of points2) {
18
+ if (this.isPointInPolygon(point, points1)) {
19
+ return true;
20
+ }
21
+ }
22
+ // 4. Check for edge intersections (more complex)
23
+ for (let i = 0; i < points1.length; i++) {
24
+ const p1a = points1[i];
25
+ const p1b = points1[(i + 1) % points1.length]; // Wrap around to the first point
26
+ for (let j = 0; j < points2.length; j++) {
27
+ const p2a = points2[j];
28
+ const p2b = points2[(j + 1) % points2.length];
29
+ if (this.intersectLines(p1a, p1b, p2a, p2b)) {
30
+ return true;
31
+ }
32
+ }
33
+ }
34
+ return false; // No intersection found
35
+ }
36
+ static isPointInPolygon(point, polygon) {
37
+ let inside = false;
38
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
39
+ const xi = polygon[i].x, yi = polygon[i].y;
40
+ const xj = polygon[j].x, yj = polygon[j].y;
41
+ const intersect = yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;
42
+ if (intersect)
43
+ inside = !inside;
44
+ }
45
+ return inside;
46
+ }
47
+ static intersectLines(p1a, p1b, p2a, p2b) {
48
+ const det = (p1b.x - p1a.x) * (p2b.y - p2a.y) - (p1b.y - p1a.y) * (p2b.x - p2a.x);
49
+ if (det === 0) {
50
+ return false; // Lines are parallel
51
+ }
52
+ const t = ((p2a.x - p1a.x) * (p2b.y - p2a.y) - (p2a.y - p1a.y) * (p2b.x - p2a.x)) / det;
53
+ const u = -((p1a.x - p2a.x) * (p1b.y - p1a.y) - (p1a.y - p2a.y) * (p1b.x - p1a.x)) / det;
54
+ return t >= 0 && t <= 1 && u >= 0 && u <= 1;
55
+ }
56
+ /**
57
+ * Finds the intersection point between a line segment and a line segment.
58
+ * Returns the intersection point or null if no intersection.
59
+ */
60
+ static getLineIntersectionPoint(p1a, p1b, p2a, p2b) {
61
+ const det = (p1b.x - p1a.x) * (p2b.y - p2a.y) - (p1b.y - p1a.y) * (p2b.x - p2a.x);
62
+ if (det === 0) {
63
+ return null; // Lines are parallel
64
+ }
65
+ const t = ((p2a.x - p1a.x) * (p2b.y - p2a.y) - (p2a.y - p1a.y) * (p2b.x - p2a.x)) / det;
66
+ const u = -((p1a.x - p2a.x) * (p1b.y - p1a.y) - (p1a.y - p2a.y) * (p1b.x - p1a.x)) / det;
67
+ if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
68
+ return {
69
+ x: p1a.x + t * (p1b.x - p1a.x),
70
+ y: p1a.y + t * (p1b.y - p1a.y)
71
+ };
72
+ }
73
+ return null;
74
+ }
75
+ /**
76
+ * Finds the closest intersection point between a line segment (from lineStart to lineEnd)
77
+ * and a polygon. Returns the intersection point closest to lineStart, or null if no intersection.
78
+ */
79
+ static getLinePolygonIntersection(lineStart, lineEnd, polygon) {
80
+ const points = [polygon.topLeft, polygon.topRight, polygon.bottomRight, polygon.bottomLeft];
81
+ let closestIntersection = null;
82
+ let closestDistance = Infinity;
83
+ for (let i = 0; i < points.length; i++) {
84
+ const edgeStart = points[i];
85
+ const edgeEnd = points[(i + 1) % points.length];
86
+ const intersection = this.getLineIntersectionPoint(lineStart, lineEnd, edgeStart, edgeEnd);
87
+ if (intersection) {
88
+ const distance = Math.sqrt(Math.pow(intersection.x - lineStart.x, 2) +
89
+ Math.pow(intersection.y - lineStart.y, 2));
90
+ if (distance < closestDistance) {
91
+ closestDistance = distance;
92
+ closestIntersection = intersection;
93
+ }
94
+ }
95
+ }
96
+ return closestIntersection;
97
+ }
98
+ /**
99
+ * Generates a polygon approximation of an ellipse.
100
+ * @param centerX - X coordinate of ellipse center
101
+ * @param centerY - Y coordinate of ellipse center
102
+ * @param rx - Horizontal radius
103
+ * @param ry - Vertical radius
104
+ * @param segments - Number of segments (more = smoother approximation)
105
+ * @param rotation - Optional rotation angle in radians
106
+ */
107
+ static getEllipsePolygonApproximation(centerX, centerY, rx, ry, segments = 32, rotation = 0) {
108
+ const points = [];
109
+ const cos = Math.cos(rotation);
110
+ const sin = Math.sin(rotation);
111
+ for (let i = 0; i < segments; i++) {
112
+ const angle = (2 * Math.PI * i) / segments;
113
+ // Point on unrotated ellipse
114
+ const px = rx * Math.cos(angle);
115
+ const py = ry * Math.sin(angle);
116
+ // Apply rotation around center
117
+ const rotatedX = centerX + px * cos - py * sin;
118
+ const rotatedY = centerY + px * sin + py * cos;
119
+ points.push({ x: rotatedX, y: rotatedY });
120
+ }
121
+ return points;
122
+ }
123
+ /**
124
+ * Finds the closest intersection point between a line segment and a polygon
125
+ * defined as an array of points. Returns the intersection closest to lineStart,
126
+ * or null if no intersection.
127
+ */
128
+ static getLinePointsArrayIntersection(lineStart, lineEnd, polygonPoints) {
129
+ let closestIntersection = null;
130
+ let closestDistance = Infinity;
131
+ for (let i = 0; i < polygonPoints.length; i++) {
132
+ const edgeStart = polygonPoints[i];
133
+ const edgeEnd = polygonPoints[(i + 1) % polygonPoints.length];
134
+ const intersection = this.getLineIntersectionPoint(lineStart, lineEnd, edgeStart, edgeEnd);
135
+ if (intersection) {
136
+ const distance = Math.sqrt(Math.pow(intersection.x - lineStart.x, 2) +
137
+ Math.pow(intersection.y - lineStart.y, 2));
138
+ if (distance < closestDistance) {
139
+ closestDistance = distance;
140
+ closestIntersection = intersection;
141
+ }
142
+ }
143
+ }
144
+ return closestIntersection;
145
+ }
146
+ /**
147
+ * Checks if a point is inside a polygon defined as an array of points.
148
+ * This is a convenience wrapper that works with arbitrary polygon point arrays.
149
+ */
150
+ static isPointInPolygonPoints(point, polygonPoints) {
151
+ return this.isPointInPolygon(point, polygonPoints);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Base class for all objects that can be rendered on the Kritzel canvas.
157
+ * Provides common functionality for positioning, transformation, hit testing,
158
+ * serialization, and lifecycle management.
159
+ * @template T - The type of the underlying DOM element (HTMLElement or SVGElement)
160
+ */
161
+ class KritzelBaseObject {
162
+ __class__ = 'KritzelBaseObject';
163
+ _core;
164
+ _elementRef;
165
+ id;
166
+ workspaceId;
167
+ x;
168
+ y;
169
+ translateX;
170
+ translateY;
171
+ height;
172
+ width;
173
+ backgroundColor;
174
+ borderColor;
175
+ borderWidth = 0;
176
+ opacity = 1;
177
+ padding = 0;
178
+ scale;
179
+ resizing = false;
180
+ rotation = 0;
181
+ markedForRemoval = false;
182
+ zIndex = 0;
183
+ userId;
184
+ isVisible = true;
185
+ isSelected = false;
186
+ isHovered = false;
187
+ isMounted = false;
188
+ isEditable = false;
189
+ isInteractive = false;
190
+ isResizable = true;
191
+ isRotatable = true;
192
+ /**
193
+ * Gets the total width of the object including padding.
194
+ * @returns The width plus double the padding value
195
+ */
196
+ get totalWidth() {
197
+ return this.width + this.padding * 2;
198
+ }
199
+ /**
200
+ * Gets the total height of the object including padding.
201
+ * @returns The height plus double the padding value
202
+ */
203
+ get totalHeight() {
204
+ return this.height + this.padding * 2;
205
+ }
206
+ /**
207
+ * Sets the underlying DOM element reference for this object.
208
+ * @param element - The DOM element to associate with this object
209
+ */
210
+ set elementRef(element) {
211
+ this._elementRef = element;
212
+ }
213
+ /**
214
+ * Gets the underlying DOM element reference for this object.
215
+ * @returns The associated DOM element
216
+ */
217
+ get elementRef() {
218
+ return this._elementRef;
219
+ }
220
+ /**
221
+ * Gets the axis-aligned bounding box of the object without rotation.
222
+ * @returns The bounding box with position, scale, and dimensions
223
+ */
224
+ get boundingBox() {
225
+ return {
226
+ x: this.translateX,
227
+ y: this.translateY,
228
+ z: this.scale,
229
+ width: this.totalWidth / this.scale,
230
+ height: this.totalHeight / this.scale,
231
+ };
232
+ }
233
+ /**
234
+ * Gets the axis-aligned bounding box that encompasses the rotated object.
235
+ * Calculates the smallest rectangle that contains all four rotated corners.
236
+ * @returns The bounding box accounting for rotation
237
+ */
238
+ get rotatedBoundingBox() {
239
+ const polygon = this.rotatedPolygon;
240
+ const xValues = [polygon.topLeft.x, polygon.topRight.x, polygon.bottomRight.x, polygon.bottomLeft.x];
241
+ const yValues = [polygon.topLeft.y, polygon.topRight.y, polygon.bottomRight.y, polygon.bottomLeft.y];
242
+ const minX = Math.min(...xValues);
243
+ const maxX = Math.max(...xValues);
244
+ const minY = Math.min(...yValues);
245
+ const maxY = Math.max(...yValues);
246
+ return {
247
+ x: minX,
248
+ y: minY,
249
+ z: this.scale,
250
+ width: maxX - minX,
251
+ height: maxY - minY,
252
+ };
253
+ }
254
+ /**
255
+ * Gets the four corners of the object as a polygon, accounting for rotation.
256
+ * Uses optimized trigonometric calculations with precomputed cos/sin values.
257
+ * @returns A polygon with topLeft, topRight, bottomRight, and bottomLeft coordinates
258
+ */
259
+ get rotatedPolygon() {
260
+ const cx = this.translateX + this.totalWidth / 2 / this.scale;
261
+ const cy = this.translateY + this.totalHeight / 2 / this.scale;
262
+ const angle = this.rotation;
263
+ // Optimization: Precomputes cos/sin once
264
+ const cos = Math.cos(angle);
265
+ const sin = Math.sin(angle);
266
+ const adjustedWidth = this.totalWidth / this.scale;
267
+ const adjustedHeight = this.totalHeight / this.scale;
268
+ const x1 = this.translateX;
269
+ const y1 = this.translateY;
270
+ const x2 = x1 + adjustedWidth;
271
+ const y2 = y1 + adjustedHeight;
272
+ // Helper to rotate a point around center
273
+ const rotate = (x, y) => ({
274
+ x: cos * (x - cx) - sin * (y - cy) + cx,
275
+ y: sin * (x - cx) + cos * (y - cy) + cy,
276
+ });
277
+ return {
278
+ topLeft: rotate(x1, y1),
279
+ topRight: rotate(x2, y1),
280
+ bottomRight: rotate(x2, y2),
281
+ bottomLeft: rotate(x1, y2),
282
+ };
283
+ }
284
+ /**
285
+ * Gets the minimum X coordinate among all rotated corners.
286
+ * @returns The leftmost X coordinate of the rotated object
287
+ */
288
+ get minXRotated() {
289
+ const corners = [this.rotatedPolygon.topLeft.x, this.rotatedPolygon.topRight.x, this.rotatedPolygon.bottomRight.x, this.rotatedPolygon.bottomLeft.x];
290
+ return Math.min(...corners);
291
+ }
292
+ /**
293
+ * Gets the minimum Y coordinate among all rotated corners.
294
+ * @returns The topmost Y coordinate of the rotated object
295
+ */
296
+ get minYRotated() {
297
+ const corners = [this.rotatedPolygon.topLeft.y, this.rotatedPolygon.topRight.y, this.rotatedPolygon.bottomRight.y, this.rotatedPolygon.bottomLeft.y];
298
+ return Math.min(...corners);
299
+ }
300
+ /**
301
+ * Gets the maximum X coordinate among all rotated corners.
302
+ * @returns The rightmost X coordinate of the rotated object
303
+ */
304
+ get maxXRotated() {
305
+ const corners = [this.rotatedPolygon.topLeft.x, this.rotatedPolygon.topRight.x, this.rotatedPolygon.bottomRight.x, this.rotatedPolygon.bottomLeft.x];
306
+ return Math.max(...corners);
307
+ }
308
+ /**
309
+ * Gets the maximum Y coordinate among all rotated corners.
310
+ * @returns The bottommost Y coordinate of the rotated object
311
+ */
312
+ get maxYRotated() {
313
+ const corners = [this.rotatedPolygon.topLeft.y, this.rotatedPolygon.topRight.y, this.rotatedPolygon.bottomRight.y, this.rotatedPolygon.bottomLeft.y];
314
+ return Math.max(...corners);
315
+ }
316
+ /**
317
+ * Gets the CSS transformation matrix string for positioning and scaling.
318
+ * @returns A CSS matrix() function string combining scale and translation
319
+ */
320
+ get transformationMatrix() {
321
+ const scale = 1 / this.scale;
322
+ const translateX = this.translateX;
323
+ const translateY = this.translateY;
324
+ return `matrix(${scale}, 0, 0, ${scale}, ${translateX}, ${translateY})`;
325
+ }
326
+ /**
327
+ * Gets the rotation angle converted from radians to degrees.
328
+ * @returns The rotation angle in degrees
329
+ */
330
+ get rotationDegrees() {
331
+ return this.rotation * (180 / Math.PI);
332
+ }
333
+ /**
334
+ * Gets the X coordinate of the object's center point.
335
+ * @returns The center X position in world coordinates
336
+ */
337
+ get centerX() {
338
+ return this.translateX + this.totalWidth / 2 / this.scale;
339
+ }
340
+ /**
341
+ * Gets the Y coordinate of the object's center point.
342
+ * @returns The center Y position in world coordinates
343
+ */
344
+ get centerY() {
345
+ return this.translateY + this.totalHeight / 2 / this.scale;
346
+ }
347
+ /**
348
+ * Creates a new KritzelBaseObject instance with a generated unique ID.
349
+ */
350
+ constructor() {
351
+ this.id = this.generateId();
352
+ }
353
+ /**
354
+ * Factory method to create a new KritzelBaseObject with core context.
355
+ * Automatically assigns the current z-index and active workspace ID.
356
+ * @param core - The KritzelCore instance providing context and state
357
+ * @returns A new KritzelBaseObject instance configured with core context
358
+ */
359
+ static create(core) {
360
+ const object = new KritzelBaseObject();
361
+ object._core = core;
362
+ object.zIndex = core.store.currentZIndex;
363
+ object.workspaceId = core.store.state.activeWorkspace.id;
364
+ object.userId = core.user?.id;
365
+ return object;
366
+ }
367
+ /**
368
+ * Mounts the object to a DOM element. Only executes once; subsequent calls are ignored.
369
+ * Sets the element reference and marks the object as mounted.
370
+ * @param element - The DOM element to mount this object to
371
+ */
372
+ mount(element) {
373
+ if (this.isMounted) {
374
+ return;
375
+ }
376
+ this.elementRef = element;
377
+ this.isMounted = true;
378
+ }
379
+ /**
380
+ * Generates a unique identifier for the object.
381
+ * @returns A UUID string
382
+ */
383
+ generateId() {
384
+ return ObjectHelper.generateUUID();
385
+ }
386
+ /**
387
+ * Checks whether the object is currently visible within the viewport.
388
+ * Objects smaller than 0.5 square pixels (when scaled) are considered invisible.
389
+ * Uses the rotated bounding box for accurate intersection testing.
390
+ * @returns True if the object intersects with the visible viewport area
391
+ */
392
+ isInViewport() {
393
+ const viewportScale = this._core.store.state.scale;
394
+ const scaledWidth = this.boundingBox.width * viewportScale;
395
+ const scaledHeight = this.boundingBox.height * viewportScale;
396
+ if (scaledWidth * scaledHeight < 0.5) {
397
+ return false;
398
+ }
399
+ const viewportBounds = {
400
+ x: -this._core.store.state.translateX / this._core.store.state.scale,
401
+ y: -this._core.store.state.translateY / this._core.store.state.scale,
402
+ width: this._core.store.state.viewportWidth / this._core.store.state.scale,
403
+ height: this._core.store.state.viewportHeight / this._core.store.state.scale,
404
+ };
405
+ return (this.rotatedBoundingBox.x < viewportBounds.x + viewportBounds.width &&
406
+ this.rotatedBoundingBox.x + this.rotatedBoundingBox.width > viewportBounds.x &&
407
+ this.rotatedBoundingBox.y < viewportBounds.y + viewportBounds.height &&
408
+ this.rotatedBoundingBox.y + this.rotatedBoundingBox.height > viewportBounds.y);
409
+ }
410
+ /**
411
+ * Moves the object to the center of the current viewport.
412
+ * Calculates the offset needed to align the object's center with the viewport's center.
413
+ */
414
+ centerInViewport() {
415
+ const { viewportWidth, viewportHeight, translateX: viewportTranslateX, translateY: viewportTranslateY, scale: viewportScale } = this._core.store.state;
416
+ const { x, y, width, height } = this.rotatedBoundingBox;
417
+ const objectCenterX = x + width / 2;
418
+ const objectCenterY = y + height / 2;
419
+ const targetCenterX = (viewportWidth / 2 - viewportTranslateX) / viewportScale;
420
+ const targetCenterY = (viewportHeight / 2 - viewportTranslateY) / viewportScale;
421
+ const deltaX = targetCenterX - objectCenterX;
422
+ const deltaY = targetCenterY - objectCenterY;
423
+ this.updatePosition(this.translateX + deltaX, this.translateY + deltaY);
424
+ }
425
+ /**
426
+ * Notifies the store that this object has been updated.
427
+ * Triggers a re-render of the object in the canvas.
428
+ */
429
+ update() {
430
+ this._core.store.objects.update(this);
431
+ }
432
+ /**
433
+ * Moves the object based on the delta between start and end coordinates.
434
+ * The movement is scaled according to the current viewport scale.
435
+ * @param startX - The starting X coordinate in screen space
436
+ * @param startY - The starting Y coordinate in screen space
437
+ * @param endX - The ending X coordinate in screen space
438
+ * @param endY - The ending Y coordinate in screen space
439
+ */
440
+ move(startX, startY, endX, endY) {
441
+ const deltaX = (startX - endX) / this._core.store.state.scale;
442
+ const deltaY = (startY - endY) / this._core.store.state.scale;
443
+ this.translateX += deltaX;
444
+ this.translateY += deltaY;
445
+ this._core.store.objects.update(this);
446
+ }
447
+ /**
448
+ * Resizes the object to new dimensions and position.
449
+ * Ignores resize requests that would make the object smaller than 1x1 pixels.
450
+ * Also updates any lines anchored to this object after resizing.
451
+ * @param x - The new X position (translateX)
452
+ * @param y - The new Y position (translateY)
453
+ * @param width - The new width (must be > 1)
454
+ * @param height - The new height (must be > 1)
455
+ */
456
+ resize(x, y, width, height) {
457
+ if (width <= 1 || height <= 1) {
458
+ return;
459
+ }
460
+ this.width = width;
461
+ this.height = height;
462
+ this.translateX = x;
463
+ this.translateY = y;
464
+ this._core.store.objects.update(this);
465
+ // Update any lines that are anchored to this object (after position is updated)
466
+ this._core.anchorManager.updateAnchorsForObject(this.id);
467
+ }
468
+ /**
469
+ * Sets the rotation angle of the object.
470
+ * @param value - The rotation angle in radians
471
+ */
472
+ rotate(value) {
473
+ this.rotation = value;
474
+ this._core.store.objects.update(this);
475
+ }
476
+ /**
477
+ * Creates a shallow clone of this object with the same ID.
478
+ * The clone shares the same prototype and copies all properties.
479
+ * @returns A new instance with identical properties including the same ID
480
+ */
481
+ clone() {
482
+ const clone = Object.create(Object.getPrototypeOf(this));
483
+ Object.assign(clone, this);
484
+ clone.id = this.id;
485
+ return clone;
486
+ }
487
+ /**
488
+ * Creates a copy of this object with a new unique ID.
489
+ * Unlike clone(), this creates an independent object suitable for duplication.
490
+ * The copied object is not mounted by default.
491
+ * @returns A new instance with copied properties and a new unique ID
492
+ */
493
+ copy() {
494
+ const copiedObject = Object.create(Object.getPrototypeOf(this));
495
+ Object.assign(copiedObject, this);
496
+ copiedObject.id = this.generateId();
497
+ copiedObject.isMounted = false;
498
+ copiedObject.isSelected = false;
499
+ copiedObject.isHovered = false;
500
+ copiedObject.isInteractive = false;
501
+ copiedObject.resizing = false;
502
+ copiedObject.markedForRemoval = false;
503
+ copiedObject.isVisible = true;
504
+ return copiedObject;
505
+ }
506
+ /**
507
+ * Serializes the object to a plain JavaScript object for persistence.
508
+ * Excludes internal references (_core, _elementRef) and computed properties.
509
+ * Subclasses should override to handle domain-specific serialization (e.g., HTML elements).
510
+ * @returns A serializable object containing all persistent properties
511
+ */
512
+ serialize() {
513
+ const { _core, _elementRef, element, totalWidth, totalHeight, isSelected, isHovered, isMounted, isInteractive, resizing, markedForRemoval, isVisible, ...remainingProps } = this;
514
+ // No longer clone raw elements—let subclasses handle serializable payloads
515
+ return structuredClone(remainingProps);
516
+ }
517
+ /**
518
+ * Restores the object's state from a serialized plain object.
519
+ * Merges all properties from the input object into this instance.
520
+ * @template T - The expected return type
521
+ * @param object - The serialized object data to restore from
522
+ * @returns This instance cast to type T
523
+ */
524
+ deserialize(object) {
525
+ Object.assign(this, object);
526
+ // Selection/interactivity state is transient UI state and must not survive revival.
527
+ this.isSelected = false;
528
+ this.isHovered = false;
529
+ this.isMounted = false;
530
+ this.isInteractive = false;
531
+ this.resizing = false;
532
+ this.markedForRemoval = false;
533
+ this.isVisible = true;
534
+ // Reset known transient geometry caches if present on subclasses.
535
+ if ('_adjustedPoints' in this) {
536
+ this._adjustedPoints = null;
537
+ }
538
+ if ('_clipInfo' in this) {
539
+ this._clipInfo = null;
540
+ }
541
+ return this;
542
+ }
543
+ /**
544
+ * Copies transient (non-persisted) state from a previous local instance
545
+ * onto this freshly-revived instance. Called when a remote Yjs update
546
+ * replaces an existing object so that local-only fields (e.g. asset
547
+ * load state, resolved blob URLs) survive across remote position
548
+ * updates and don't visually flicker.
549
+ *
550
+ * Default implementation is a no-op; subclasses with transient fields
551
+ * should override.
552
+ * @param previous - The previous local instance being replaced.
553
+ */
554
+ adoptTransientStateFrom(_previous) {
555
+ // no-op by default
556
+ }
557
+ /**
558
+ * Type guard to check if this object is of a specific class type.
559
+ * Compares against the __class__ property.
560
+ * @template T - The expected class type extending KritzelBaseObject
561
+ * @param className - The class name to check against
562
+ * @returns True if this object's __class__ matches the given class name
563
+ */
564
+ isClass(className) {
565
+ return this.__class__ === className;
566
+ }
567
+ /**
568
+ * Handles edit actions on the object. Override in subclasses to implement
569
+ * custom edit behavior (e.g., entering text edit mode).
570
+ * @param _event - Optional pointer event that triggered the edit
571
+ */
572
+ edit(_event) {
573
+ // This method can be overridden by subclasses to handle edit actions.
574
+ }
575
+ /**
576
+ * Lifecycle hook called after properties are updated via updateObject.
577
+ * Override in subclasses to perform custom logic when specific properties change.
578
+ * @param _changedProperties - Array of property names that were changed
579
+ */
580
+ onAfterUpdate(_changedProperties) {
581
+ // Default implementation does nothing
582
+ }
583
+ /**
584
+ * Tests if a point intersects with this object.
585
+ * Override in subclasses for precise hit detection (e.g., path-based objects).
586
+ * @param _x - The X coordinate to test in world space
587
+ * @param _y - The Y coordinate to test in world space
588
+ * @returns True if the point hits the object (default always returns true)
589
+ */
590
+ hitTest(_x, _y) {
591
+ return true; // Default implementation, can be overridden by subclasses
592
+ }
593
+ /**
594
+ * Tests if this object's rotated polygon intersects with another polygon.
595
+ * Useful for lasso selection and collision detection.
596
+ * @param polygon - The polygon to test intersection against
597
+ * @returns True if the polygons intersect
598
+ */
599
+ hitTestPolygon(polygon) {
600
+ const objectPolygon = this.rotatedPolygon;
601
+ return KritzelGeometryHelper.doPolygonsIntersect(objectPolygon, polygon);
602
+ }
603
+ /**
604
+ * Updates the object's position and notifies the store.
605
+ * @param x - The new X position (translateX)
606
+ * @param y - The new Y position (translateY)
607
+ */
608
+ updatePosition(x, y) {
609
+ this.translateX = x;
610
+ this.translateY = y;
611
+ this._core.store.objects.update(this);
612
+ }
613
+ }
614
+
615
+ class KritzelMathHelper {
616
+ static average(a, b) {
617
+ return (a + b) / 2;
618
+ }
619
+ static degreesToRadians(degrees) {
620
+ return degrees * (Math.PI / 180);
621
+ }
622
+ }
623
+
624
+ var cjs = {};
625
+
626
+ var hasRequiredCjs;
627
+
628
+ function requireCjs () {
629
+ if (hasRequiredCjs) return cjs;
630
+ hasRequiredCjs = 1;
631
+ (function (exports) {
632
+ Object.defineProperties(exports,{__esModule:{value:true},[Symbol.toStringTag]:{value:`Module`}});const{PI:e}=Math,t=e+1e-4,n=.5,r=[1,1];function i(e,t,n,r=e=>e){return e*r(.5-t*(.5-n))}const{min:a}=Math;function o(e,t,n){let r=a(1,t/n);return a(1,e+(a(1,1-r)-e)*(r*.275))}function s(e){return [-e[0],-e[1]]}function c(e,t){return [e[0]+t[0],e[1]+t[1]]}function l(e,t,n){return e[0]=t[0]+n[0],e[1]=t[1]+n[1],e}function u(e,t){return [e[0]-t[0],e[1]-t[1]]}function d(e,t,n){return e[0]=t[0]-n[0],e[1]=t[1]-n[1],e}function f(e,t){return [e[0]*t,e[1]*t]}function p(e,t,n){return e[0]=t[0]*n,e[1]=t[1]*n,e}function m(e,t){return [e[0]/t,e[1]/t]}function h(e){return [e[1],-e[0]]}function g(e,t){let n=t[0];return e[0]=t[1],e[1]=-n,e}function ee(e,t){return e[0]*t[0]+e[1]*t[1]}function _(e,t){return e[0]===t[0]&&e[1]===t[1]}function v(e){return Math.hypot(e[0],e[1])}function y(e,t){let n=e[0]-t[0],r=e[1]-t[1];return n*n+r*r}function b(e){return m(e,v(e))}function x(e,t){return Math.hypot(e[1]-t[1],e[0]-t[0])}function S(e,t,n){let r=Math.sin(n),i=Math.cos(n),a=e[0]-t[0],o=e[1]-t[1],s=a*i-o*r,c=a*r+o*i;return [s+t[0],c+t[1]]}function C(e,t,n,r){let i=Math.sin(r),a=Math.cos(r),o=t[0]-n[0],s=t[1]-n[1],c=o*a-s*i,l=o*i+s*a;return e[0]=c+n[0],e[1]=l+n[1],e}function w(e,t,n){return c(e,f(u(t,e),n))}function te(e,t,n,r){let i=n[0]-t[0],a=n[1]-t[1];return e[0]=t[0]+i*r,e[1]=t[1]+a*r,e}function T(e,t,n){return c(e,f(t,n))}const E=[0,0],D=[0,0],O=[0,0];function k(e,n){let r=T(e,b(h(u(e,c(e,[1,1])))),-n),i=[],a=1/13;for(let n=a;n<=1;n+=a)i.push(S(r,e,t*2*n));return i}function A(e,n,r){let i=[],a=1/r;for(let r=a;r<=1;r+=a)i.push(S(n,e,t*r));return i}function j(e,t,n){let r=u(t,n),i=f(r,.5),a=f(r,.51);return [u(e,i),u(e,a),c(e,a),c(e,i)]}function M(e,n,r,i){let a=[],o=T(e,n,r),s=1/i;for(let n=s;n<1;n+=s)a.push(S(o,e,t*3*n));return a}function ne(e,t,n){return [c(e,f(t,n)),c(e,f(t,n*.99)),u(e,f(t,n*.99)),u(e,f(t,n))]}function N(e,t,n){return e===false||e===void 0?0:e===true?Math.max(t,n):e}function re(e,t,n){return e.slice(0,10).reduce((e,r)=>{let i=r.pressure;return t&&(i=o(e,r.distance,n)),(e+i)/2},e[0].pressure)}function P(e,n={}){let{size:r=16,smoothing:a=.5,thinning:f=.5,simulatePressure:m=true,easing:_=e=>e,start:v={},end:b={},last:x=false}=n,{cap:S=true,easing:w=e=>e*(2-e)}=v,{cap:T=true,easing:P=e=>--e*e*e+1}=b;if(e.length===0||r<=0)return [];let F=e[e.length-1].runningLength,I=N(v.taper,r,F),L=N(b.taper,r,F),R=(r*a)**2,z=[],B=[],V=re(e,m,r),H=i(r,f,e[e.length-1].pressure,_),U,W=e[0].vector,G=e[0].point,K=G,q=G,J=K,Y=false;for(let n=0;n<e.length;n++){let{pressure:a}=e[n],{point:s,vector:h,distance:v,runningLength:b}=e[n],x=n===e.length-1;if(!x&&F-b<3)continue;f?(m&&(a=o(V,v,r)),H=i(r,f,a,_)):H=r/2,U===void 0&&(U=H);let S=b<I?w(b/I):1,T=F-b<L?P((F-b)/L):1;H=Math.max(.01,H*Math.min(S,T));let k=(x?e[n]:e[n+1]).vector,A=x?1:ee(h,k),j=ee(h,W)<0&&!Y,M=A!==null&&A<0;if(j||M){g(E,W),p(E,E,H);for(let e=0;e<=1;e+=.07692307692307693)d(D,s,E),C(D,D,s,t*e),q=[D[0],D[1]],z.push(q),l(O,s,E),C(O,O,s,t*-e),J=[O[0],O[1]],B.push(J);G=q,K=J,M&&(Y=true);continue}if(Y=false,x){g(E,h),p(E,E,H),z.push(u(s,E)),B.push(c(s,E));continue}te(E,k,h,A),g(E,E),p(E,E,H),d(D,s,E),q=[D[0],D[1]],(n<=1||y(G,q)>R)&&(z.push(q),G=q),l(O,s,E),J=[O[0],O[1]],(n<=1||y(K,J)>R)&&(B.push(J),K=J),V=a,W=h;}let X=[e[0].point[0],e[0].point[1]],Z=e.length>1?[e[e.length-1].point[0],e[e.length-1].point[1]]:c(e[0].point,[1,1]),Q=[],$=[];if(e.length===1){if(!(I||L)||x)return k(X,U||H)}else {I||L&&e.length===1||(S?Q.push(...A(X,B[0],13)):Q.push(...j(X,z[0],B[0])));let t=h(s(e[e.length-1].vector));L||I&&e.length===1?$.push(Z):T?$.push(...M(Z,t,H,29)):$.push(...ne(Z,t,H));}return z.concat($,B.reverse(),Q)}const F=[0,0];function I(e){return e!=null&&e>=0}function L(e,t={}){let{streamline:i=.5,size:a=16,last:o=false}=t;if(e.length===0)return [];let s=.15+(1-i)*.85,l=Array.isArray(e[0])?e:e.map(({x:e,y:t,pressure:r=n})=>[e,t,r]);if(l.length===2){let e=l[1];l=l.slice(0,-1);for(let t=1;t<5;t++)l.push(w(l[0],e,t/4));}l.length===1&&(l=[...l,[...c(l[0],r),...l[0].slice(2)]]);let u=[{point:[l[0][0],l[0][1]],pressure:I(l[0][2])?l[0][2]:.25,vector:[...r],distance:0,runningLength:0}],f=false,p=0,m=u[0],h=l.length-1;for(let e=1;e<l.length;e++){let t=o&&e===h?[l[e][0],l[e][1]]:w(m.point,l[e],s);if(_(m.point,t))continue;let r=x(t,m.point);if(p+=r,e<h&&!f){if(p<a)continue;f=true;}d(F,m.point,t),m={point:t,pressure:I(l[e][2])?l[e][2]:n,vector:b(F),distance:r,runningLength:p},u.push(m);}return u[0].vector=u[1]?.vector||[0,0],u}function R(e,t={}){return P(L(e,t),t)}var z=R;exports.default=z,exports.getStroke=R,exports.getStrokeOutlinePoints=P,exports.getStrokePoints=L;
633
+
634
+ } (cjs));
635
+ return cjs;
636
+ }
637
+
638
+ var cjsExports = requireCjs();
639
+
640
+ class KritzelPath extends KritzelBaseObject {
641
+ __class__ = 'KritzelPath';
642
+ points;
643
+ d;
644
+ stroke = { light: 'none', dark: 'none' };
645
+ strokeWidth;
646
+ lineSlack = 0.5;
647
+ fill;
648
+ x = 0;
649
+ y = 0;
650
+ height = 0;
651
+ width = 0;
652
+ scale = 1;
653
+ options;
654
+ isVisible = true;
655
+ isDebugInfoVisible = true;
656
+ isCompleted = false;
657
+ _adjustedPoints = null;
658
+ /**
659
+ * Gets the SVG viewBox string for local (object-relative) rendering.
660
+ *
661
+ * The path's `d` string is generated in local coordinates (offset so the
662
+ * object's top-left origin is at 0,0), so the viewBox always starts at 0,0.
663
+ * Keeping the SVG's internal coordinates small — independent of the object's
664
+ * world position — avoids float32 precision loss in the browser's SVG
665
+ * rasterizer (Skia), which otherwise fails to paint paths whose absolute
666
+ * world coordinates exceed ~2^24 (e.g. far from the origin at extreme zoom).
667
+ * @returns The viewBox attribute value in the format "0 0 width height"
668
+ */
669
+ get viewBox() {
670
+ return `0 0 ${this.width} ${this.height}`;
671
+ }
672
+ /**
673
+ * Creates a new KritzelPath instance.
674
+ * @param config - Optional configuration object for the path
675
+ * @param config.points - Array of [x, y] coordinate pairs defining the path
676
+ * @param config.translateX - Horizontal translation offset (default: 0)
677
+ * @param config.translateY - Vertical translation offset (default: 0)
678
+ * @param config.scale - Scale factor for the path (default: 1)
679
+ * @param config.strokeWidth - Width of the stroke in pixels (default: 8)
680
+ * @param config.fill - Theme-aware fill color with light and dark variants
681
+ * @param config.lineSlack - Additional padding for bounding box calculations (default: 0.5)
682
+ */
683
+ constructor(config) {
684
+ super();
685
+ this.options = config;
686
+ this.points = config?.points ?? [];
687
+ this.translateX = config?.translateX ?? 0;
688
+ this.translateY = config?.translateY ?? 0;
689
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
690
+ this.scale = config?.scale ?? 1;
691
+ this.strokeWidth = config?.strokeWidth ?? 8;
692
+ this.fill = config?.fill ?? { light: '#000000', dark: '#ffffff' };
693
+ this.d = this.generateSvgPath();
694
+ this.updateDimensions();
695
+ }
696
+ /**
697
+ * Factory method to create a new KritzelPath instance with a core reference.
698
+ * This is the preferred way to create paths that will be managed by the canvas.
699
+ * @param core - The KritzelCore instance that will manage this path
700
+ * @param options - Optional configuration options for the path
701
+ * @returns A new KritzelPath instance configured with the provided options
702
+ */
703
+ static create(core, options) {
704
+ const object = new KritzelPath();
705
+ object._core = core;
706
+ object.id = object.generateId();
707
+ object.workspaceId = core.store.state.activeWorkspace.id;
708
+ object.userId = core.user?.id;
709
+ object.options = options;
710
+ object.points = options?.points ?? [];
711
+ object.translateX = options?.translateX ?? 0;
712
+ object.translateY = options?.translateY ?? 0;
713
+ object.rotation = KritzelMathHelper.degreesToRadians(options?.rotation ?? 0);
714
+ object.scale = options?.scale ?? 1;
715
+ object.strokeWidth = options?.strokeWidth ?? 8;
716
+ object.fill = options?.fill ?? { light: '#000000', dark: '#ffffff' };
717
+ object.opacity = options?.opacity ?? 1;
718
+ object.zIndex = core.store.currentZIndex;
719
+ object.d = object.generateSvgPath();
720
+ object.updateDimensions();
721
+ return object;
722
+ }
723
+ /**
724
+ * Creates a deep copy of this path object.
725
+ * The points array is cloned to ensure the copy is independent.
726
+ * @returns A new KritzelPath instance with copied properties
727
+ */
728
+ copy() {
729
+ const copiedObject = super.copy();
730
+ if (this.points) {
731
+ copiedObject.points = this.points.map(p => [...p]);
732
+ }
733
+ return copiedObject;
734
+ }
735
+ /**
736
+ * Restores the path from a serialized object, then regenerates the SVG `d`
737
+ * string from the restored points.
738
+ *
739
+ * Regeneration is required because the `d` string is now emitted in local
740
+ * (object-relative) coordinates paired with a "0 0 width height" viewBox.
741
+ * Older persisted paths stored `d` in absolute world coordinates, which
742
+ * would be inconsistent with the local-coordinate viewBox and fail to
743
+ * rasterize far from the origin at extreme zoom. Recomputing `d` here makes
744
+ * both freshly created and previously persisted paths render identically.
745
+ * @param object - The serialized path data to restore from
746
+ * @returns This instance cast to type T
747
+ */
748
+ deserialize(object) {
749
+ const result = super.deserialize(object);
750
+ this.d = this.generateSvgPath();
751
+ return result;
752
+ }
753
+ /**
754
+ * Resizes the path to fit within the specified dimensions.
755
+ * Scales all points proportionally and updates the SVG path.
756
+ * @param x - New X position for the path, or null to keep current
757
+ * @param y - New Y position for the path, or null to keep current
758
+ * @param width - New width for the path (must be > 1)
759
+ * @param height - New height for the path (must be > 1)
760
+ */
761
+ resize(x, y, width, height) {
762
+ if (width <= 1 || height <= 1) {
763
+ return;
764
+ }
765
+ const scaleX = width / this.width;
766
+ const scaleY = height / this.height;
767
+ this.width = width;
768
+ this.height = height;
769
+ if (this.points.length === 1) {
770
+ const p = this.points[0];
771
+ const spreadX = Math.max(0, width - this.strokeWidth) / scaleX;
772
+ const spreadY = Math.max(0, height - this.strokeWidth) / scaleY;
773
+ this.points.push([p[0] + spreadX, p[1] + spreadY]);
774
+ }
775
+ this.points = this.points.map(([x, y]) => [x * scaleX, y * scaleY]);
776
+ this.d = this.generateSvgPath();
777
+ this.width = Math.max(...this.points.map(p => p[0])) - Math.min(...this.points.map(p => p[0])) + this.strokeWidth;
778
+ this.height = Math.max(...this.points.map(p => p[1])) - Math.min(...this.points.map(p => p[1])) + this.strokeWidth;
779
+ this.x = Math.min(...this.points.map(p => p[0])) - this.strokeWidth / 2;
780
+ this.y = Math.min(...this.points.map(p => p[1])) - this.strokeWidth / 2;
781
+ if (x !== null) {
782
+ this.translateX = x;
783
+ }
784
+ if (y !== null) {
785
+ this.translateY = y;
786
+ }
787
+ this._adjustedPoints = null;
788
+ this._core.store.objects.update(this);
789
+ }
790
+ /**
791
+ * Rotates the path by the specified angle.
792
+ * Invalidates cached adjusted points to force recalculation.
793
+ * @param value - Rotation angle in radians
794
+ */
795
+ rotate(value) {
796
+ this.rotation = value;
797
+ this._adjustedPoints = null;
798
+ this._core.store.objects.update(this);
799
+ }
800
+ /**
801
+ * Moves the path by calculating the delta between start and end positions.
802
+ * The delta is adjusted for the current viewport scale.
803
+ * @param startX - Starting X coordinate in screen space
804
+ * @param startY - Starting Y coordinate in screen space
805
+ * @param endX - Ending X coordinate in screen space
806
+ * @param endY - Ending Y coordinate in screen space
807
+ */
808
+ move(startX, startY, endX, endY) {
809
+ const deltaX = (startX - endX) / this._core.store.state.scale;
810
+ const deltaY = (startY - endY) / this._core.store.state.scale;
811
+ this.translateX += deltaX;
812
+ this.translateY += deltaY;
813
+ this._adjustedPoints = null;
814
+ this._core.store.objects.update(this);
815
+ }
816
+ /**
817
+ * Tests whether a point intersects with the path's stroke.
818
+ * Uses distance-to-line-segment calculations for accurate hit detection.
819
+ * @param x - X coordinate to test in world space
820
+ * @param y - Y coordinate to test in world space
821
+ * @returns True if the point is within the stroke width of the path
822
+ */
823
+ hitTest(x, y) {
824
+ const halfStroke = this.strokeWidth / this.scale / 2;
825
+ if (this._adjustedPoints === null) {
826
+ this._adjustedPoints = this.computeAdjustedPoints();
827
+ }
828
+ if (this._adjustedPoints.length === 1) {
829
+ const p1 = this._adjustedPoints[0];
830
+ const distance = this.pointToLineSegmentDistance(x, y, p1[0], p1[1], p1[0], p1[1]);
831
+ return distance <= halfStroke;
832
+ }
833
+ for (let i = 0; i < this._adjustedPoints.length - 1; i++) {
834
+ const p1 = this._adjustedPoints[i];
835
+ const p2 = this._adjustedPoints[i + 1];
836
+ const distance = this.pointToLineSegmentDistance(x, y, p1[0], p1[1], p2[0], p2[1]);
837
+ if (distance <= halfStroke) {
838
+ return true;
839
+ }
840
+ }
841
+ return false;
842
+ }
843
+ /**
844
+ * Tests whether a polygon intersects with the path.
845
+ * Checks for point containment, edge intersections, and proximity to stroke.
846
+ * @param polygon - The polygon to test against, defined by its corner points
847
+ * @returns True if any part of the polygon overlaps with the path's stroke
848
+ */
849
+ hitTestPolygon(polygon) {
850
+ const halfStroke = this.strokeWidth / this.scale / 2;
851
+ if (this._adjustedPoints === null) {
852
+ this._adjustedPoints = this.computeAdjustedPoints();
853
+ }
854
+ const polyPoints = [
855
+ { x: polygon.bottomLeft.x, y: polygon.bottomLeft.y },
856
+ { x: polygon.bottomRight.x, y: polygon.bottomRight.y },
857
+ { x: polygon.topRight.x, y: polygon.topRight.y },
858
+ { x: polygon.topLeft.x, y: polygon.topLeft.y },
859
+ ];
860
+ for (const [px, py] of this._adjustedPoints) {
861
+ if (KritzelGeometryHelper.isPointInPolygon({ x: px, y: py }, polyPoints)) {
862
+ return true;
863
+ }
864
+ }
865
+ for (const pt of polyPoints) {
866
+ if (this.hitTest(pt.x, pt.y)) {
867
+ return true;
868
+ }
869
+ }
870
+ for (let i = 0; i < this._adjustedPoints.length - 1; i++) {
871
+ const p1 = { x: this._adjustedPoints[i][0], y: this._adjustedPoints[i][1] };
872
+ const p2 = { x: this._adjustedPoints[i + 1][0], y: this._adjustedPoints[i + 1][1] };
873
+ for (let j = 0; j < polyPoints.length; j++) {
874
+ const q1 = polyPoints[j];
875
+ const q2 = polyPoints[(j + 1) % polyPoints.length];
876
+ if (KritzelGeometryHelper.intersectLines(p1, p2, q1, q2)) {
877
+ return true;
878
+ }
879
+ }
880
+ }
881
+ for (let i = 0; i < this._adjustedPoints.length - 1; i++) {
882
+ const p1 = this._adjustedPoints[i];
883
+ const p2 = this._adjustedPoints[i + 1];
884
+ for (let j = 0; j < polyPoints.length; j++) {
885
+ const q1 = polyPoints[j];
886
+ const q2 = polyPoints[(j + 1) % polyPoints.length];
887
+ const d1 = this.pointToLineSegmentDistance(q1.x, q1.y, p1[0], p1[1], p2[0], p2[1]);
888
+ const d2 = this.pointToLineSegmentDistance(q2.x, q2.y, p1[0], p1[1], p2[0], p2[1]);
889
+ const d3 = this.pointToLineSegmentDistance(p1[0], p1[1], q1.x, q1.y, q2.x, q2.y);
890
+ const d4 = this.pointToLineSegmentDistance(p2[0], p2[1], q1.x, q1.y, q2.x, q2.y);
891
+ const minD = Math.min(d1, d2, d3, d4);
892
+ if (minD <= halfStroke) {
893
+ return true;
894
+ }
895
+ }
896
+ }
897
+ return false;
898
+ }
899
+ /**
900
+ * Updates the path's position to the specified coordinates.
901
+ * Invalidates cached adjusted points and triggers a store update.
902
+ * @param x - New X position in world space
903
+ * @param y - New Y position in world space
904
+ */
905
+ updatePosition(x, y) {
906
+ this.translateX = x;
907
+ this.translateY = y;
908
+ this._adjustedPoints = null;
909
+ this._core.store.objects.update(this);
910
+ }
911
+ /**
912
+ * Lifecycle hook called after properties have been updated.
913
+ * Regenerates the SVG path and bounding box when strokeWidth changes.
914
+ * @param changedProperties - Array of property names that were modified
915
+ */
916
+ onAfterUpdate(changedProperties) {
917
+ if (changedProperties.includes('strokeWidth')) {
918
+ this.d = this.generateSvgPath();
919
+ this.updateBoundingBox();
920
+ this._adjustedPoints = null;
921
+ }
922
+ }
923
+ /**
924
+ * Computes the world-space coordinates of all path points.
925
+ * Applies rotation around the path's center and adjusts for translation and scale.
926
+ * Results are cached in _adjustedPoints for performance.
927
+ * @returns Array of [x, y] coordinate pairs in world space
928
+ */
929
+ computeAdjustedPoints() {
930
+ if (!this.points?.length) {
931
+ return [];
932
+ }
933
+ const angle = this.rotation;
934
+ const cos = Math.cos(angle);
935
+ const sin = Math.sin(angle);
936
+ const xs = this.points.map(p => p[0]);
937
+ const ys = this.points.map(p => p[1]);
938
+ const pivot = {
939
+ x: (Math.min(...xs) + Math.max(...xs)) / 2,
940
+ y: (Math.min(...ys) + Math.max(...ys)) / 2,
941
+ };
942
+ const { x: cx, y: cy } = pivot;
943
+ const rotatedPoints = this.points.map(([x, y]) => {
944
+ const dx = x - cx;
945
+ const dy = y - cy;
946
+ return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
947
+ });
948
+ return rotatedPoints.map(([px, py]) => [Math.abs(px - this.x) / this.scale + this.translateX, Math.abs(py - this.y) / this.scale + this.translateY]);
949
+ }
950
+ /**
951
+ * Calculates the shortest distance from a point to a line segment.
952
+ * Uses projection to find the closest point on the segment.
953
+ * @param x - X coordinate of the test point
954
+ * @param y - Y coordinate of the test point
955
+ * @param x1 - X coordinate of the line segment start
956
+ * @param y1 - Y coordinate of the line segment start
957
+ * @param x2 - X coordinate of the line segment end
958
+ * @param y2 - Y coordinate of the line segment end
959
+ * @returns The shortest distance from the point to the line segment
960
+ */
961
+ pointToLineSegmentDistance(x, y, x1, y1, x2, y2) {
962
+ const A = x - x1;
963
+ const B = y - y1;
964
+ const C = x2 - x1;
965
+ const D = y2 - y1;
966
+ const dot = A * C + B * D;
967
+ const len_sq = C * C + D * D;
968
+ let param = -1;
969
+ if (len_sq !== 0) {
970
+ // in case of 0 length line
971
+ param = dot / len_sq;
972
+ }
973
+ let xx, yy;
974
+ if (param < 0) {
975
+ xx = x1;
976
+ yy = y1;
977
+ }
978
+ else if (param > 1) {
979
+ xx = x2;
980
+ yy = y2;
981
+ }
982
+ else {
983
+ xx = x1 + param * C;
984
+ yy = y1 + param * D;
985
+ }
986
+ const dx = x - xx;
987
+ const dy = y - yy;
988
+ return Math.sqrt(dx * dx + dy * dy);
989
+ }
990
+ /**
991
+ * Updates width, height, x, y, translateX, and translateY based on current points.
992
+ * Uses the unrotated local points and stroke width for base dimensions.
993
+ * Called during initial setup to establish the path's dimensions and position.
994
+ */
995
+ updateDimensions() {
996
+ const minX = Math.min(...this.points.map(p => p[0])) - this.strokeWidth / 2;
997
+ const minY = Math.min(...this.points.map(p => p[1])) - this.strokeWidth / 2;
998
+ const maxX = Math.max(...this.points.map(p => p[0])) + this.strokeWidth / 2;
999
+ const maxY = Math.max(...this.points.map(p => p[1])) + this.strokeWidth / 2;
1000
+ this.width = maxX - minX + this.lineSlack;
1001
+ this.height = maxY - minY + this.lineSlack;
1002
+ this.x = minX;
1003
+ this.y = minY;
1004
+ this.translateX = (this.x + this.translateX) / this.scale;
1005
+ this.translateY = (this.y + this.translateY) / this.scale;
1006
+ }
1007
+ /**
1008
+ * Updates width, height, x, y based on current points and strokeWidth.
1009
+ * Does NOT modify translateX/translateY (use updateDimensions for initial setup).
1010
+ */
1011
+ updateBoundingBox() {
1012
+ const minX = Math.min(...this.points.map(p => p[0])) - this.strokeWidth / 2;
1013
+ const minY = Math.min(...this.points.map(p => p[1])) - this.strokeWidth / 2;
1014
+ const maxX = Math.max(...this.points.map(p => p[0])) + this.strokeWidth / 2;
1015
+ const maxY = Math.max(...this.points.map(p => p[1])) + this.strokeWidth / 2;
1016
+ this.width = maxX - minX + this.lineSlack;
1017
+ this.height = maxY - minY + this.lineSlack;
1018
+ this.x = minX;
1019
+ this.y = minY;
1020
+ }
1021
+ /**
1022
+ * Generates the SVG path data string from the current points.
1023
+ * Uses perfect-freehand to create a smooth, pressure-sensitive stroke.
1024
+ * @returns SVG path data string (d attribute value)
1025
+ */
1026
+ generateSvgPath() {
1027
+ const stroke = this.getStrokeFromPoints(this.points, this.strokeWidth);
1028
+ const offsetX = this.points.length ? Math.min(...this.points.map(p => p[0])) - this.strokeWidth / 2 : 0;
1029
+ const offsetY = this.points.length ? Math.min(...this.points.map(p => p[1])) - this.strokeWidth / 2 : 0;
1030
+ return this.getSvgPathFromStroke(stroke, offsetX, offsetY);
1031
+ }
1032
+ /**
1033
+ * Converts raw input points into a smooth stroke outline using perfect-freehand.
1034
+ * Applies pressure simulation and smoothing for a natural pen-like appearance.
1035
+ * @param points - Array of [x, y] coordinate pairs representing the input path
1036
+ * @param strokeWidth - The desired stroke width in pixels
1037
+ * @returns Array of [x, y] coordinate pairs forming the stroke outline polygon
1038
+ */
1039
+ getStrokeFromPoints(points, strokeWidth) {
1040
+ return cjsExports.getStroke(points, {
1041
+ size: strokeWidth,
1042
+ thinning: 0.5,
1043
+ smoothing: 0.5,
1044
+ streamline: 0.5,
1045
+ easing: t => t,
1046
+ simulatePressure: true,
1047
+ last: true,
1048
+ start: {
1049
+ cap: true,
1050
+ taper: 0,
1051
+ easing: t => t,
1052
+ },
1053
+ end: {
1054
+ cap: true,
1055
+ taper: 0,
1056
+ easing: t => t,
1057
+ },
1058
+ });
1059
+ }
1060
+ /**
1061
+ * Converts stroke outline points into an SVG path data string.
1062
+ * Uses quadratic bezier curves for smooth rendering.
1063
+ * Coordinates are emitted relative to (offsetX, offsetY) so the resulting
1064
+ * path lives in the object's local coordinate space (paired with a
1065
+ * "0 0 width height" viewBox). This keeps SVG-internal coordinates small
1066
+ * regardless of the object's world position, avoiding float32 rasterizer
1067
+ * precision failures far from the origin.
1068
+ * @param points - Array of [x, y] coordinate pairs forming the stroke outline
1069
+ * @param offsetX - X origin to subtract from every coordinate (default 0)
1070
+ * @param offsetY - Y origin to subtract from every coordinate (default 0)
1071
+ * @param closed - Whether to close the path with 'Z' command (default: true)
1072
+ * @returns SVG path data string with M, Q, and T commands
1073
+ */
1074
+ getSvgPathFromStroke(points, offsetX = 0, offsetY = 0, closed = true) {
1075
+ const len = points.length;
1076
+ if (len < 4) {
1077
+ return ``;
1078
+ }
1079
+ const a = points[0];
1080
+ const b = points[1];
1081
+ const c = points[2];
1082
+ const ax = a[0] - offsetX;
1083
+ const ay = a[1] - offsetY;
1084
+ const bx = b[0] - offsetX;
1085
+ const by = b[1] - offsetY;
1086
+ const cx = c[0] - offsetX;
1087
+ const cy = c[1] - offsetY;
1088
+ let result = `M${ax.toFixed(2)},${ay.toFixed(2)} Q${bx.toFixed(2)},${by.toFixed(2)} ${KritzelMathHelper.average(bx, cx).toFixed(2)},${KritzelMathHelper.average(by, cy).toFixed(2)} T`;
1089
+ for (let i = 2, max = len - 1; i < max; i++) {
1090
+ const p = points[i];
1091
+ const q = points[i + 1];
1092
+ const px = p[0] - offsetX;
1093
+ const py = p[1] - offsetY;
1094
+ const qx = q[0] - offsetX;
1095
+ const qy = q[1] - offsetY;
1096
+ result += `${KritzelMathHelper.average(px, qx).toFixed(2)},${KritzelMathHelper.average(py, qy).toFixed(2)} `;
1097
+ }
1098
+ if (closed) {
1099
+ result += 'Z';
1100
+ }
1101
+ return result;
1102
+ }
1103
+ /**
1104
+ * Determines if the path appears too small on screen for detailed rendering.
1105
+ * Used for level-of-detail optimizations.
1106
+ * @returns True if the path's screen area is less than 500 square pixels
1107
+ */
1108
+ isLowRes() {
1109
+ if (!this._core) {
1110
+ return false;
1111
+ }
1112
+ const viewportScale = this._core.store.state.scale;
1113
+ const scaledWidth = this.boundingBox.width * viewportScale;
1114
+ const scaledHeight = this.boundingBox.height * viewportScale;
1115
+ return (scaledWidth * scaledHeight) < 500;
1116
+ }
1117
+ /**
1118
+ * Finds the point where a line from outsidePoint to the path's center
1119
+ * intersects the stroke edge. Used for arrow clipping at the actual stroke edge.
1120
+ * Uses binary search refinement for accurate intersection detection.
1121
+ * @param outsidePoint - A point outside the path to trace from
1122
+ * @returns The intersection point on the stroke edge, or null if no intersection found
1123
+ */
1124
+ getClipPoint(outsidePoint) {
1125
+ const centerX = this.centerX;
1126
+ const centerY = this.centerY;
1127
+ const halfStroke = this.strokeWidth / this.scale / 2;
1128
+ // Ensure adjusted points are computed
1129
+ if (this._adjustedPoints === null) {
1130
+ this._adjustedPoints = this.computeAdjustedPoints();
1131
+ }
1132
+ if (this._adjustedPoints.length < 1) {
1133
+ return null;
1134
+ }
1135
+ // Sample along the line from outsidePoint to center to find stroke edge
1136
+ const steps = 32;
1137
+ let prevOutside = true;
1138
+ for (let i = 1; i <= steps; i++) {
1139
+ const t = i / steps;
1140
+ const sampleX = outsidePoint.x + (centerX - outsidePoint.x) * t;
1141
+ const sampleY = outsidePoint.y + (centerY - outsidePoint.y) * t;
1142
+ // Check distance to path stroke
1143
+ let minDistance = Infinity;
1144
+ if (this._adjustedPoints.length === 1) {
1145
+ const p = this._adjustedPoints[0];
1146
+ const dx = sampleX - p[0];
1147
+ const dy = sampleY - p[1];
1148
+ minDistance = Math.sqrt(dx * dx + dy * dy);
1149
+ }
1150
+ else {
1151
+ for (let j = 0; j < this._adjustedPoints.length - 1; j++) {
1152
+ const p1 = this._adjustedPoints[j];
1153
+ const p2 = this._adjustedPoints[j + 1];
1154
+ const dist = this.pointToLineSegmentDistance(sampleX, sampleY, p1[0], p1[1], p2[0], p2[1]);
1155
+ if (dist < minDistance) {
1156
+ minDistance = dist;
1157
+ }
1158
+ }
1159
+ }
1160
+ const isInside = minDistance <= halfStroke;
1161
+ // Found the edge: transition from outside to inside
1162
+ if (prevOutside && isInside) {
1163
+ // Refine the intersection point using binary search
1164
+ let tLow = (i - 1) / steps;
1165
+ let tHigh = t;
1166
+ for (let k = 0; k < 8; k++) {
1167
+ const tMid = (tLow + tHigh) / 2;
1168
+ const midX = outsidePoint.x + (centerX - outsidePoint.x) * tMid;
1169
+ const midY = outsidePoint.y + (centerY - outsidePoint.y) * tMid;
1170
+ let midDist = Infinity;
1171
+ if (this._adjustedPoints.length === 1) {
1172
+ const p = this._adjustedPoints[0];
1173
+ const dx = midX - p[0];
1174
+ const dy = midY - p[1];
1175
+ midDist = Math.sqrt(dx * dx + dy * dy);
1176
+ }
1177
+ else {
1178
+ for (let j = 0; j < this._adjustedPoints.length - 1; j++) {
1179
+ const p1 = this._adjustedPoints[j];
1180
+ const p2 = this._adjustedPoints[j + 1];
1181
+ const dist = this.pointToLineSegmentDistance(midX, midY, p1[0], p1[1], p2[0], p2[1]);
1182
+ if (dist < midDist) {
1183
+ midDist = dist;
1184
+ }
1185
+ }
1186
+ }
1187
+ if (midDist <= halfStroke) {
1188
+ tHigh = tMid;
1189
+ }
1190
+ else {
1191
+ tLow = tMid;
1192
+ }
1193
+ }
1194
+ const finalT = (tLow + tHigh) / 2;
1195
+ return {
1196
+ x: outsidePoint.x + (centerX - outsidePoint.x) * finalT,
1197
+ y: outsidePoint.y + (centerY - outsidePoint.y) * finalT,
1198
+ };
1199
+ }
1200
+ prevOutside = !isInside;
1201
+ }
1202
+ return null;
1203
+ }
1204
+ }
1205
+
1206
+ class KritzelLine extends KritzelBaseObject {
1207
+ __class__ = 'KritzelLine';
1208
+ startX;
1209
+ startY;
1210
+ endX;
1211
+ endY;
1212
+ controlX;
1213
+ controlY;
1214
+ stroke;
1215
+ strokeWidth;
1216
+ scale = 1;
1217
+ options;
1218
+ /** Anchor connection for the start point of the line */
1219
+ startAnchor;
1220
+ /** Anchor connection for the end point of the line */
1221
+ endAnchor;
1222
+ /** Arrow head configuration for start and end of line */
1223
+ arrows;
1224
+ isVisible = true;
1225
+ isDebugInfoVisible = true;
1226
+ isCompleted = false;
1227
+ /** Cached adjusted points for hit testing, accounting for rotation and translation */
1228
+ _adjustedPoints = null;
1229
+ /** Cached clip information for anchored line endpoints */
1230
+ _clipInfo = null;
1231
+ /**
1232
+ * Generates the SVG path data string for the line.
1233
+ * Returns a quadratic Bezier curve path if control point exists, otherwise a straight line.
1234
+ * @returns SVG path data string in the format "M x y Q cx cy ex ey" or "M x y L ex ey"
1235
+ */
1236
+ get d() {
1237
+ if (this.controlX !== undefined && this.controlY !== undefined) {
1238
+ return `M ${this.startX} ${this.startY} Q ${this.controlX} ${this.controlY} ${this.endX} ${this.endY}`;
1239
+ }
1240
+ return `M ${this.startX} ${this.startY} L ${this.endX} ${this.endY}`;
1241
+ }
1242
+ /**
1243
+ * Generates the SVG viewBox attribute string for rendering the line.
1244
+ * @returns ViewBox string in the format "x y width height"
1245
+ */
1246
+ get viewBox() {
1247
+ return `${this.x} ${this.y} ${this.width} ${this.height}`;
1248
+ }
1249
+ /**
1250
+ * Creates a new KritzelLine instance.
1251
+ * @param config - Optional configuration options for the line including start/end points,
1252
+ * control point for curves, stroke settings, anchors, and arrow configuration
1253
+ */
1254
+ constructor(config) {
1255
+ super();
1256
+ this.options = config;
1257
+ this.startX = config?.startX ?? 0;
1258
+ this.startY = config?.startY ?? 0;
1259
+ this.endX = config?.endX ?? 0;
1260
+ this.endY = config?.endY ?? 0;
1261
+ this.controlX = config?.controlX;
1262
+ this.controlY = config?.controlY;
1263
+ this.translateX = config?.translateX ?? 0;
1264
+ this.translateY = config?.translateY ?? 0;
1265
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
1266
+ this.scale = config?.scale ?? 1;
1267
+ this.strokeWidth = config?.strokeWidth ?? 4;
1268
+ this.stroke = config?.stroke ?? { light: '#000000', dark: '#ffffff' };
1269
+ this.startAnchor = config?.startAnchor;
1270
+ this.endAnchor = config?.endAnchor;
1271
+ this.arrows = config?.arrows;
1272
+ this.updateDimensions();
1273
+ }
1274
+ /**
1275
+ * Factory method to create a new KritzelLine with a core reference and unique ID.
1276
+ * This is the preferred way to create lines that will be managed by the core.
1277
+ * @param core - The KritzelCore instance that will manage this line
1278
+ * @param options - Optional configuration options for the line
1279
+ * @returns A fully initialized KritzelLine instance with generated ID and workspace assignment
1280
+ */
1281
+ static create(core, options) {
1282
+ const object = new KritzelLine();
1283
+ object._core = core;
1284
+ object.id = object.generateId();
1285
+ object.workspaceId = core.store.state.activeWorkspace.id;
1286
+ object.userId = core.user?.id;
1287
+ object.options = options;
1288
+ object.startX = options?.startX ?? 0;
1289
+ object.startY = options?.startY ?? 0;
1290
+ object.endX = options?.endX ?? 0;
1291
+ object.endY = options?.endY ?? 0;
1292
+ object.controlX = options?.controlX;
1293
+ object.controlY = options?.controlY;
1294
+ object.translateX = options?.translateX ?? 0;
1295
+ object.translateY = options?.translateY ?? 0;
1296
+ object.rotation = KritzelMathHelper.degreesToRadians(options?.rotation ?? 0);
1297
+ object.scale = options?.scale ?? 1;
1298
+ object.strokeWidth = options?.strokeWidth ?? 4;
1299
+ object.stroke = options?.stroke ?? { light: '#000000', dark: '#ffffff' };
1300
+ object.opacity = options?.opacity ?? 1;
1301
+ object.startAnchor = options?.startAnchor;
1302
+ object.endAnchor = options?.endAnchor;
1303
+ object.arrows = options?.arrows;
1304
+ object.zIndex = core.store.currentZIndex;
1305
+ object.updateDimensions();
1306
+ return object;
1307
+ }
1308
+ /**
1309
+ * Resizes the line to fit within the specified dimensions.
1310
+ * Scales both endpoints and control point proportionally, then repositions the line.
1311
+ * @param x - New X position for the line (or null to keep current)
1312
+ * @param y - New Y position for the line (or null to keep current)
1313
+ * @param width - New width to scale the line to (must be > 1)
1314
+ * @param height - New height to scale the line to (must be > 1)
1315
+ */
1316
+ resize(x, y, width, height) {
1317
+ if (width <= 1 || height <= 1) {
1318
+ return;
1319
+ }
1320
+ const scaleX = width / this.width;
1321
+ const scaleY = height / this.height;
1322
+ // Scale the line endpoints
1323
+ this.startX = this.startX * scaleX;
1324
+ this.startY = this.startY * scaleY;
1325
+ this.endX = this.endX * scaleX;
1326
+ this.endY = this.endY * scaleY;
1327
+ if (this.controlX !== undefined && this.controlY !== undefined) {
1328
+ this.controlX = this.controlX * scaleX;
1329
+ this.controlY = this.controlY * scaleY;
1330
+ }
1331
+ // Recalculate dimensions from scaled endpoints
1332
+ const { minX, minY, maxX, maxY } = this.calculateBoundingBox();
1333
+ this.width = maxX - minX;
1334
+ this.height = maxY - minY;
1335
+ this.x = minX;
1336
+ this.y = minY;
1337
+ // Set the new position
1338
+ this.translateX = x;
1339
+ this.translateY = y;
1340
+ this._adjustedPoints = null;
1341
+ this._clipInfo = null;
1342
+ this._core.store.objects.update(this);
1343
+ // Update anchors after the line is updated
1344
+ this._core.anchorManager.updateAnchorsForObject(this.id);
1345
+ if (this.startAnchor) {
1346
+ this._core.anchorManager.updateAnchorsForObject(this.startAnchor.objectId);
1347
+ }
1348
+ if (this.endAnchor) {
1349
+ this._core.anchorManager.updateAnchorsForObject(this.endAnchor.objectId);
1350
+ }
1351
+ }
1352
+ /**
1353
+ * Sets the rotation angle of the line around its center.
1354
+ * Invalidates cached adjusted points and clip info.
1355
+ * @param value - Rotation angle in radians
1356
+ */
1357
+ rotate(value) {
1358
+ this.rotation = value;
1359
+ this._adjustedPoints = null;
1360
+ this._clipInfo = null;
1361
+ this._core.store.objects.update(this);
1362
+ }
1363
+ /**
1364
+ * Moves the line by calculating the delta between start and end positions.
1365
+ * Updates anchored objects if the line has anchor connections.
1366
+ * @param startX - Starting X position of the drag in screen coordinates
1367
+ * @param startY - Starting Y position of the drag in screen coordinates
1368
+ * @param endX - Ending X position of the drag in screen coordinates
1369
+ * @param endY - Ending Y position of the drag in screen coordinates
1370
+ */
1371
+ move(startX, startY, endX, endY) {
1372
+ const deltaX = (startX - endX) / this._core.store.state.scale;
1373
+ const deltaY = (startY - endY) / this._core.store.state.scale;
1374
+ this.translateX += deltaX;
1375
+ this.translateY += deltaY;
1376
+ // If the line is anchored, we need to ensure the anchored endpoints stay at the target
1377
+ if (this.startAnchor) {
1378
+ this._core.anchorManager.updateAnchorsForObject(this.startAnchor.objectId);
1379
+ }
1380
+ if (this.endAnchor) {
1381
+ this._core.anchorManager.updateAnchorsForObject(this.endAnchor.objectId);
1382
+ }
1383
+ this._adjustedPoints = null;
1384
+ this._clipInfo = null;
1385
+ this._core.store.objects.update(this);
1386
+ }
1387
+ /**
1388
+ * Tests if a point intersects with the line within the stroke tolerance.
1389
+ * Uses distance to line segment for straight lines, or distance to Bezier curve for curved lines.
1390
+ * @param x - X coordinate of the point to test in world coordinates
1391
+ * @param y - Y coordinate of the point to test in world coordinates
1392
+ * @returns True if the point is within the stroke width tolerance of the line
1393
+ */
1394
+ hitTest(x, y) {
1395
+ const minHitThreshold = 20;
1396
+ const strokeWidth = Math.max(this.strokeWidth, minHitThreshold);
1397
+ const halfStroke = strokeWidth / this.scale / 2;
1398
+ if (this._adjustedPoints === null) {
1399
+ this._adjustedPoints = this.computeAdjustedPoints();
1400
+ }
1401
+ // For curved lines, use distance to the Bezier curve
1402
+ if (this.controlX !== undefined && this.controlY !== undefined) {
1403
+ const clip = this.getClipInfo();
1404
+ const startT = clip.start?.t ?? 0;
1405
+ const endT = clip.end?.t ?? 1;
1406
+ const distance = this.pointToBezierDistance(x, y, startT, endT);
1407
+ return distance <= halfStroke;
1408
+ }
1409
+ // For straight lines, use distance to line segment
1410
+ const clip = this.getClipInfo();
1411
+ const p1 = clip.start ? [clip.start.x, clip.start.y] : this._adjustedPoints[0];
1412
+ const p2 = clip.end ? [clip.end.x, clip.end.y] : this._adjustedPoints[1];
1413
+ const distance = this.pointToLineSegmentDistance(x, y, p1[0], p1[1], p2[0], p2[1]);
1414
+ return distance <= halfStroke;
1415
+ }
1416
+ /**
1417
+ * Tests if the line intersects with or is contained within a polygon.
1418
+ * Checks endpoint containment, polygon vertex proximity, curve sampling for Bezier curves,
1419
+ * and line-edge intersections for straight lines.
1420
+ * @param polygon - The polygon to test intersection with, defined by four corner points
1421
+ * @returns True if any part of the line intersects with the polygon
1422
+ */
1423
+ hitTestPolygon(polygon) {
1424
+ const halfStroke = this.strokeWidth / this.scale / 2;
1425
+ if (this._adjustedPoints === null) {
1426
+ this._adjustedPoints = this.computeAdjustedPoints();
1427
+ }
1428
+ const clip = this.getClipInfo();
1429
+ const polyPoints = [
1430
+ { x: polygon.bottomLeft.x, y: polygon.bottomLeft.y },
1431
+ { x: polygon.bottomRight.x, y: polygon.bottomRight.y },
1432
+ { x: polygon.topRight.x, y: polygon.topRight.y },
1433
+ { x: polygon.topLeft.x, y: polygon.topLeft.y },
1434
+ ];
1435
+ // Check if any endpoint is inside the polygon
1436
+ // Use clipped endpoints
1437
+ const p1 = clip.start ? [clip.start.x, clip.start.y] : this._adjustedPoints[0];
1438
+ const p2 = clip.end ? [clip.end.x, clip.end.y] : this._adjustedPoints[1];
1439
+ const endpoints = [p1, p2];
1440
+ for (const [px, py] of endpoints) {
1441
+ if (KritzelGeometryHelper.isPointInPolygon({ x: px, y: py }, polyPoints)) {
1442
+ return true;
1443
+ }
1444
+ }
1445
+ // Check if any polygon vertex is on the line/curve
1446
+ for (const pt of polyPoints) {
1447
+ if (this.hitTest(pt.x, pt.y)) {
1448
+ return true;
1449
+ }
1450
+ }
1451
+ // For curved lines, sample points along the curve and check if any are inside the polygon
1452
+ if (this.controlX !== undefined && this.controlY !== undefined) {
1453
+ const p0 = this._adjustedPoints[0];
1454
+ const p2 = this._adjustedPoints[1];
1455
+ const controlAdjusted = this.computeAdjustedControlPoint();
1456
+ const startT = clip.start?.t ?? 0;
1457
+ const endT = clip.end?.t ?? 1;
1458
+ const samples = 20;
1459
+ for (let i = 0; i <= samples; i++) {
1460
+ const fraction = i / samples;
1461
+ const t = startT + fraction * (endT - startT);
1462
+ const oneMinusT = 1 - t;
1463
+ const bx = oneMinusT * oneMinusT * p0[0] + 2 * oneMinusT * t * controlAdjusted[0] + t * t * p2[0];
1464
+ const by = oneMinusT * oneMinusT * p0[1] + 2 * oneMinusT * t * controlAdjusted[1] + t * t * p2[1];
1465
+ if (KritzelGeometryHelper.isPointInPolygon({ x: bx, y: by }, polyPoints)) {
1466
+ return true;
1467
+ }
1468
+ // Also check if curve point is within stroke distance of polygon edges
1469
+ for (let j = 0; j < polyPoints.length; j++) {
1470
+ const q1 = polyPoints[j];
1471
+ const q2 = polyPoints[(j + 1) % polyPoints.length];
1472
+ const d = this.pointToLineSegmentDistance(bx, by, q1.x, q1.y, q2.x, q2.y);
1473
+ if (d <= halfStroke) {
1474
+ return true;
1475
+ }
1476
+ }
1477
+ }
1478
+ return false;
1479
+ }
1480
+ // For straight lines, check if line intersects any polygon edge
1481
+ const checkP1 = { x: p1[0], y: p1[1] };
1482
+ const checkP2 = { x: p2[0], y: p2[1] };
1483
+ for (let j = 0; j < polyPoints.length; j++) {
1484
+ const q1 = polyPoints[j];
1485
+ const q2 = polyPoints[(j + 1) % polyPoints.length];
1486
+ if (KritzelGeometryHelper.intersectLines(checkP1, checkP2, q1, q2)) {
1487
+ return true;
1488
+ }
1489
+ // Check distance from polygon edges to line segment
1490
+ const d1 = this.pointToLineSegmentDistance(q1.x, q1.y, checkP1.x, checkP1.y, checkP2.x, checkP2.y);
1491
+ const d2 = this.pointToLineSegmentDistance(q2.x, q2.y, checkP1.x, checkP1.y, checkP2.x, checkP2.y);
1492
+ const d3 = this.pointToLineSegmentDistance(checkP1.x, checkP1.y, q1.x, q1.y, q2.x, q2.y);
1493
+ const d4 = this.pointToLineSegmentDistance(checkP2.x, checkP2.y, q1.x, q1.y, q2.x, q2.y);
1494
+ const minD = Math.min(d1, d2, d3, d4);
1495
+ if (minD <= halfStroke) {
1496
+ return true;
1497
+ }
1498
+ }
1499
+ return false;
1500
+ }
1501
+ /**
1502
+ * Updates the translation position of the line without recalculating dimensions.
1503
+ * Invalidates cached adjusted points and clip info.
1504
+ * @param x - New X translation in world coordinates
1505
+ * @param y - New Y translation in world coordinates
1506
+ */
1507
+ updatePosition(x, y) {
1508
+ this.translateX = x;
1509
+ this.translateY = y;
1510
+ this._adjustedPoints = null;
1511
+ this._clipInfo = null;
1512
+ this._core.store.objects.update(this);
1513
+ }
1514
+ /**
1515
+ * Updates a specific endpoint of the line (start or end).
1516
+ * Recalculates the bounding box dimensions and adjusts translation to maintain
1517
+ * the visual position after the origin shift, accounting for rotation.
1518
+ * @param handleType - Which endpoint to update: 'start' or 'end'
1519
+ * @param newX - New X coordinate in viewBox-local space (relative to x, y origin)
1520
+ * @param newY - New Y coordinate in viewBox-local space (relative to x, y origin)
1521
+ */
1522
+ updateEndpoint(handleType, newX, newY) {
1523
+ // Update the appropriate endpoint
1524
+ if (handleType === 'start') {
1525
+ this.startX = newX;
1526
+ this.startY = newY;
1527
+ }
1528
+ else {
1529
+ this.endX = newX;
1530
+ this.endY = newY;
1531
+ }
1532
+ const oldWidth = this.width;
1533
+ const oldHeight = this.height;
1534
+ // Recalculate the bounding box (viewBox dimensions)
1535
+ const { minX, minY, maxX, maxY } = this.calculateBoundingBox();
1536
+ // Calculate the change in origin (in local coordinates)
1537
+ const deltaX = minX - this.x;
1538
+ const deltaY = minY - this.y;
1539
+ const newWidth = maxX - minX;
1540
+ const newHeight = maxY - minY;
1541
+ const deltaWidth = newWidth - oldWidth;
1542
+ const deltaHeight = newHeight - oldHeight;
1543
+ const deltaCx = deltaWidth / 2;
1544
+ const deltaCy = deltaHeight / 2;
1545
+ // Update viewBox dimensions
1546
+ this.x = minX;
1547
+ this.y = minY;
1548
+ this.width = newWidth;
1549
+ this.height = newHeight;
1550
+ // Rotate the delta to world coordinates before applying to translate
1551
+ const cos = Math.cos(this.rotation);
1552
+ const sin = Math.sin(this.rotation);
1553
+ // We need to compensate for:
1554
+ // 1. The shift of the top-left corner (deltaX, deltaY)
1555
+ // 2. The shift of the center point (deltaCx, deltaCy) which affects rotation pivot
1556
+ // Formula: DeltaT = R(DeltaPos + DeltaCenter) - DeltaCenter
1557
+ const vx = deltaX + deltaCx;
1558
+ const vy = deltaY + deltaCy;
1559
+ const rotatedVx = vx * cos - vy * sin;
1560
+ const rotatedVy = vx * sin + vy * cos;
1561
+ const correctionX = rotatedVx - deltaCx;
1562
+ const correctionY = rotatedVy - deltaCy;
1563
+ // Adjust translateX/Y to compensate for the origin shift
1564
+ // so the line stays visually in the same position
1565
+ this.translateX += correctionX / this.scale;
1566
+ this.translateY += correctionY / this.scale;
1567
+ // Clear cached adjusted points
1568
+ this._adjustedPoints = null;
1569
+ this._clipInfo = null;
1570
+ this._core.store.objects.update(this);
1571
+ }
1572
+ /**
1573
+ * Updates the control point for a quadratic Bezier curve.
1574
+ * Recalculates bounding box and adjusts translation to maintain visual position.
1575
+ * Pass undefined for both parameters to convert the line back to a straight line.
1576
+ * @param newX - New X coordinate for the control point, or undefined to remove
1577
+ * @param newY - New Y coordinate for the control point, or undefined to remove
1578
+ */
1579
+ updateControlPoint(newX, newY) {
1580
+ this.controlX = newX;
1581
+ this.controlY = newY;
1582
+ const oldWidth = this.width;
1583
+ const oldHeight = this.height;
1584
+ // Recalculate the bounding box (viewBox dimensions)
1585
+ const { minX, minY, maxX, maxY } = this.calculateBoundingBox();
1586
+ // Calculate the change in origin (in local coordinates)
1587
+ const deltaX = minX - this.x;
1588
+ const deltaY = minY - this.y;
1589
+ const newWidth = maxX - minX;
1590
+ const newHeight = maxY - minY;
1591
+ const deltaWidth = newWidth - oldWidth;
1592
+ const deltaHeight = newHeight - oldHeight;
1593
+ const deltaCx = deltaWidth / 2;
1594
+ const deltaCy = deltaHeight / 2;
1595
+ // Update viewBox dimensions
1596
+ this.x = minX;
1597
+ this.y = minY;
1598
+ this.width = newWidth;
1599
+ this.height = newHeight;
1600
+ // Rotate the delta to world coordinates before applying to translate
1601
+ const cos = Math.cos(this.rotation);
1602
+ const sin = Math.sin(this.rotation);
1603
+ // We need to compensate for:
1604
+ // 1. The shift of the top-left corner (deltaX, deltaY)
1605
+ // 2. The shift of the center point (deltaCx, deltaCy) which affects rotation pivot
1606
+ // Formula: DeltaT = R(DeltaPos + DeltaCenter) - DeltaCenter
1607
+ const vx = deltaX + deltaCx;
1608
+ const vy = deltaY + deltaCy;
1609
+ const rotatedVx = vx * cos - vy * sin;
1610
+ const rotatedVy = vx * sin + vy * cos;
1611
+ const correctionX = rotatedVx - deltaCx;
1612
+ const correctionY = rotatedVy - deltaCy;
1613
+ // Adjust translateX/Y to compensate for the origin shift
1614
+ // so the line stays visually in the same position
1615
+ this.translateX += correctionX / this.scale;
1616
+ this.translateY += correctionY / this.scale;
1617
+ // Clear cached adjusted points
1618
+ this._adjustedPoints = null;
1619
+ this._clipInfo = null;
1620
+ this._core.store.objects.update(this);
1621
+ }
1622
+ /**
1623
+ * Computes the line endpoints in world coordinates, accounting for rotation,
1624
+ * scale, and translation transformations.
1625
+ * @returns Array of two points [[startX, startY], [endX, endY]] in world coordinates
1626
+ */
1627
+ computeAdjustedPoints() {
1628
+ const points = [
1629
+ [this.startX, this.startY],
1630
+ [this.endX, this.endY],
1631
+ ];
1632
+ const angle = this.rotation;
1633
+ const cos = Math.cos(angle);
1634
+ const sin = Math.sin(angle);
1635
+ const xs = points.map(p => p[0]);
1636
+ const ys = points.map(p => p[1]);
1637
+ const pivot = {
1638
+ x: (Math.min(...xs) + Math.max(...xs)) / 2,
1639
+ y: (Math.min(...ys) + Math.max(...ys)) / 2,
1640
+ };
1641
+ const { x: cx, y: cy } = pivot;
1642
+ const rotatedPoints = points.map(([x, y]) => {
1643
+ const dx = x - cx;
1644
+ const dy = y - cy;
1645
+ return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
1646
+ });
1647
+ return rotatedPoints.map(([px, py]) => [(px - this.x) / this.scale + this.translateX, (py - this.y) / this.scale + this.translateY]);
1648
+ }
1649
+ /**
1650
+ * Calculates the minimum distance from a point to a line segment.
1651
+ * Uses parametric projection to find the closest point on the segment.
1652
+ * @param x - X coordinate of the test point
1653
+ * @param y - Y coordinate of the test point
1654
+ * @param x1 - X coordinate of the segment start
1655
+ * @param y1 - Y coordinate of the segment start
1656
+ * @param x2 - X coordinate of the segment end
1657
+ * @param y2 - Y coordinate of the segment end
1658
+ * @returns The minimum distance from the point to the line segment
1659
+ */
1660
+ pointToLineSegmentDistance(x, y, x1, y1, x2, y2) {
1661
+ const A = x - x1;
1662
+ const B = y - y1;
1663
+ const C = x2 - x1;
1664
+ const D = y2 - y1;
1665
+ const dot = A * C + B * D;
1666
+ const len_sq = C * C + D * D;
1667
+ let param = -1;
1668
+ if (len_sq !== 0) {
1669
+ param = dot / len_sq;
1670
+ }
1671
+ let xx, yy;
1672
+ if (param < 0) {
1673
+ xx = x1;
1674
+ yy = y1;
1675
+ }
1676
+ else if (param > 1) {
1677
+ xx = x2;
1678
+ yy = y2;
1679
+ }
1680
+ else {
1681
+ xx = x1 + param * C;
1682
+ yy = y1 + param * D;
1683
+ }
1684
+ const dx = x - xx;
1685
+ const dy = y - yy;
1686
+ return Math.sqrt(dx * dx + dy * dy);
1687
+ }
1688
+ /**
1689
+ * Calculates the minimum distance from a point to a quadratic Bezier curve.
1690
+ * Uses sampling along the curve (20 samples) to approximate the closest point.
1691
+ * @param x - X coordinate of the test point in world coordinates
1692
+ * @param y - Y coordinate of the test point in world coordinates
1693
+ * @param startT - Start parameter for curve sampling (0-1), used for clipped curves
1694
+ * @param endT - End parameter for curve sampling (0-1), used for clipped curves
1695
+ * @returns The minimum distance from the point to the sampled curve
1696
+ */
1697
+ pointToBezierDistance(x, y, startT = 0, endT = 1) {
1698
+ if (this._adjustedPoints === null) {
1699
+ this._adjustedPoints = this.computeAdjustedPoints();
1700
+ }
1701
+ const p0 = this._adjustedPoints[0];
1702
+ const p2 = this._adjustedPoints[1];
1703
+ // Calculate the adjusted control point
1704
+ const controlAdjusted = this.computeAdjustedControlPoint();
1705
+ let minDistance = Infinity;
1706
+ const samples = 20; // Number of samples along the curve
1707
+ for (let i = 0; i <= samples; i++) {
1708
+ const fraction = i / samples;
1709
+ const t = startT + fraction * (endT - startT);
1710
+ const oneMinusT = 1 - t;
1711
+ // Quadratic Bezier: B(t) = (1-t)²P₀ + 2(1-t)tP₁ + t²P₂
1712
+ const bx = oneMinusT * oneMinusT * p0[0] + 2 * oneMinusT * t * controlAdjusted[0] + t * t * p2[0];
1713
+ const by = oneMinusT * oneMinusT * p0[1] + 2 * oneMinusT * t * controlAdjusted[1] + t * t * p2[1];
1714
+ const dx = x - bx;
1715
+ const dy = y - by;
1716
+ const distance = Math.sqrt(dx * dx + dy * dy);
1717
+ if (distance < minDistance) {
1718
+ minDistance = distance;
1719
+ }
1720
+ }
1721
+ return minDistance;
1722
+ }
1723
+ /**
1724
+ * Retrieves or computes the clip information for anchored line endpoints.
1725
+ * Caches the result to avoid recalculation on subsequent calls.
1726
+ * @returns Object containing optional start and end clip points with world coordinates
1727
+ * and parametric t values for curve clipping
1728
+ */
1729
+ getClipInfo() {
1730
+ if (this._clipInfo)
1731
+ return this._clipInfo;
1732
+ const startAnchor = this.startAnchor;
1733
+ const endAnchor = this.endAnchor;
1734
+ const result = {};
1735
+ if (startAnchor) {
1736
+ const target = this._core.anchorManager.findAnchorTarget(this, 'start');
1737
+ if (target) {
1738
+ const clip = this._core.anchorManager.computeAnchorClipInfo(this, 'start', target);
1739
+ if (clip) {
1740
+ result.start = { x: clip.worldX, y: clip.worldY, t: clip.t };
1741
+ }
1742
+ }
1743
+ }
1744
+ if (endAnchor) {
1745
+ const target = this._core.anchorManager.findAnchorTarget(this, 'end');
1746
+ if (target) {
1747
+ const clip = this._core.anchorManager.computeAnchorClipInfo(this, 'end', target);
1748
+ if (clip) {
1749
+ result.end = { x: clip.worldX, y: clip.worldY, t: clip.t };
1750
+ }
1751
+ }
1752
+ }
1753
+ this._clipInfo = result;
1754
+ return result;
1755
+ }
1756
+ /**
1757
+ * Computes the adjusted control point in world coordinates.
1758
+ * Applies rotation around the bounding box center, then scale and translation transforms.
1759
+ * @returns Array [x, y] representing the control point in world coordinates,
1760
+ * or the midpoint of the line if no control point is defined
1761
+ */
1762
+ computeAdjustedControlPoint() {
1763
+ if (this.controlX === undefined || this.controlY === undefined) {
1764
+ // Return midpoint if no control point
1765
+ return [(this.startX + this.endX) / 2, (this.startY + this.endY) / 2];
1766
+ }
1767
+ const angle = this.rotation;
1768
+ const cos = Math.cos(angle);
1769
+ const sin = Math.sin(angle);
1770
+ // Calculate pivot (center of bounding box for endpoints)
1771
+ const xs = [this.startX, this.endX];
1772
+ const ys = [this.startY, this.endY];
1773
+ const pivot = {
1774
+ x: (Math.min(...xs) + Math.max(...xs)) / 2,
1775
+ y: (Math.min(...ys) + Math.max(...ys)) / 2,
1776
+ };
1777
+ const { x: cx, y: cy } = pivot;
1778
+ // Rotate control point around pivot
1779
+ const dx = this.controlX - cx;
1780
+ const dy = this.controlY - cy;
1781
+ const rotatedX = cx + dx * cos - dy * sin;
1782
+ const rotatedY = cy + dx * sin + dy * cos;
1783
+ // Transform to world coordinates
1784
+ return [(rotatedX - this.x) / this.scale + this.translateX, (rotatedY - this.y) / this.scale + this.translateY];
1785
+ }
1786
+ /**
1787
+ * Gets the bounding polygon of the line in world coordinates, accounting for rotation.
1788
+ * Used for selection bounds and intersection testing.
1789
+ * @returns A polygon with four corner points (topLeft, topRight, bottomRight, bottomLeft)
1790
+ * transformed to world coordinates with rotation applied
1791
+ */
1792
+ get rotatedPolygon() {
1793
+ const padding = this.padding;
1794
+ // Use the bounding box which accounts for curve extrema
1795
+ const { minX, minY, maxX, maxY } = KritzelLine.calculateBoundingBox(this.startX, this.startY, this.endX, this.endY, this.controlX, this.controlY, this.strokeWidth);
1796
+ // Convert to local coordinates (relative to this.x, this.y)
1797
+ const localMinX = minX - this.x + padding;
1798
+ const localMinY = minY - this.y + padding;
1799
+ const localMaxX = maxX - this.x + padding;
1800
+ const localMaxY = maxY - this.y + padding;
1801
+ const c1 = { x: localMinX, y: localMinY }; // top-left
1802
+ const c2 = { x: localMaxX, y: localMinY }; // top-right
1803
+ const c3 = { x: localMaxX, y: localMaxY }; // bottom-right
1804
+ const c4 = { x: localMinX, y: localMaxY }; // bottom-left
1805
+ const cx = this.totalWidth / 2;
1806
+ const cy = this.totalHeight / 2;
1807
+ const angle = this.rotation;
1808
+ const cos = Math.cos(angle);
1809
+ const sin = Math.sin(angle);
1810
+ const transform = (p) => {
1811
+ const rx = (p.x - cx) * cos - (p.y - cy) * sin + cx;
1812
+ const ry = (p.x - cx) * sin + (p.y - cy) * cos + cy;
1813
+ return {
1814
+ x: rx / this.scale + this.translateX,
1815
+ y: ry / this.scale + this.translateY,
1816
+ };
1817
+ };
1818
+ return {
1819
+ topLeft: transform(c1),
1820
+ topRight: transform(c2),
1821
+ bottomRight: transform(c3),
1822
+ bottomLeft: transform(c4),
1823
+ };
1824
+ }
1825
+ /**
1826
+ * Calculates the tight bounding box for a quadratic Bezier curve or straight line.
1827
+ * For curves, finds extrema points where the derivative equals zero to ensure
1828
+ * the bounding box encompasses the entire curve arc.
1829
+ * @param startX - X coordinate of the line start point
1830
+ * @param startY - Y coordinate of the line start point
1831
+ * @param endX - X coordinate of the line end point
1832
+ * @param endY - Y coordinate of the line end point
1833
+ * @param controlX - X coordinate of the control point (undefined for straight lines)
1834
+ * @param controlY - Y coordinate of the control point (undefined for straight lines)
1835
+ * @param strokeWidth - Width of the line stroke for padding calculation
1836
+ * @returns Object with minX, minY, maxX, maxY including stroke width padding
1837
+ */
1838
+ static calculateBoundingBox(startX, startY, endX, endY, controlX, controlY, strokeWidth) {
1839
+ let minX = Math.min(startX, endX);
1840
+ let minY = Math.min(startY, endY);
1841
+ let maxX = Math.max(startX, endX);
1842
+ let maxY = Math.max(startY, endY);
1843
+ if (controlX !== undefined && controlY !== undefined) {
1844
+ // For a quadratic Bezier: B(t) = (1-t)²P₀ + 2(1-t)tP₁ + t²P₂
1845
+ // Derivative: B'(t) = 2(1-t)(P₁-P₀) + 2t(P₂-P₁) = 0
1846
+ // Solving for t: t = (P₀ - P₁) / (P₀ - 2P₁ + P₂)
1847
+ // Find extrema for X
1848
+ const aX = startX - 2 * controlX + endX;
1849
+ if (aX !== 0) {
1850
+ const tX = (startX - controlX) / aX;
1851
+ if (tX > 0 && tX < 1) {
1852
+ const extremaX = (1 - tX) * (1 - tX) * startX + 2 * (1 - tX) * tX * controlX + tX * tX * endX;
1853
+ minX = Math.min(minX, extremaX);
1854
+ maxX = Math.max(maxX, extremaX);
1855
+ }
1856
+ }
1857
+ // Find extrema for Y
1858
+ const aY = startY - 2 * controlY + endY;
1859
+ if (aY !== 0) {
1860
+ const tY = (startY - controlY) / aY;
1861
+ if (tY > 0 && tY < 1) {
1862
+ const extremaY = (1 - tY) * (1 - tY) * startY + 2 * (1 - tY) * tY * controlY + tY * tY * endY;
1863
+ minY = Math.min(minY, extremaY);
1864
+ maxY = Math.max(maxY, extremaY);
1865
+ }
1866
+ }
1867
+ }
1868
+ const halfStroke = strokeWidth / 2;
1869
+ return {
1870
+ minX: minX - halfStroke,
1871
+ minY: minY - halfStroke,
1872
+ maxX: maxX + halfStroke,
1873
+ maxY: maxY + halfStroke,
1874
+ };
1875
+ }
1876
+ /**
1877
+ * Calculates the bounding box for this line instance using the current endpoint,
1878
+ * control point, and stroke width values.
1879
+ * @returns Object with minX, minY, maxX, maxY bounds including stroke width padding
1880
+ */
1881
+ calculateBoundingBox() {
1882
+ return KritzelLine.calculateBoundingBox(this.startX, this.startY, this.endX, this.endY, this.controlX, this.controlY, this.strokeWidth);
1883
+ }
1884
+ /**
1885
+ * Serializes the line while excluding transient cache fields.
1886
+ * @returns A serializable line object without runtime caches
1887
+ */
1888
+ serialize() {
1889
+ const serialized = super.serialize();
1890
+ delete serialized._adjustedPoints;
1891
+ delete serialized._clipInfo;
1892
+ return serialized;
1893
+ }
1894
+ /**
1895
+ * Deserializes and resets transient caches so geometry is recomputed lazily.
1896
+ * @param object - The serialized line object
1897
+ * @returns This instance cast to type T
1898
+ */
1899
+ deserialize(object) {
1900
+ const result = super.deserialize(object);
1901
+ this._adjustedPoints = null;
1902
+ this._clipInfo = null;
1903
+ return result;
1904
+ }
1905
+ /**
1906
+ * Recalculates and updates the width, height, x, y, and translation properties
1907
+ * based on the current endpoint positions and stroke width.
1908
+ * Called during construction and after endpoint changes.
1909
+ */
1910
+ updateDimensions() {
1911
+ const { minX, minY, maxX, maxY } = this.calculateBoundingBox();
1912
+ this.width = maxX - minX;
1913
+ this.height = maxY - minY;
1914
+ this.x = minX;
1915
+ this.y = minY;
1916
+ this.translateX = (this.x + this.translateX) / this.scale;
1917
+ this.translateY = (this.y + this.translateY) / this.scale;
1918
+ }
1919
+ /**
1920
+ * Gets the unique SVG marker ID for the arrow head at the start of the line.
1921
+ * Used in SVG defs for marker-start attribute.
1922
+ * @returns A unique marker ID string in the format "arrow-start-{lineId}"
1923
+ */
1924
+ get startMarkerId() {
1925
+ return `arrow-start-${this.id}`;
1926
+ }
1927
+ /**
1928
+ * Gets the unique SVG marker ID for the arrow head at the end of the line.
1929
+ * Used in SVG defs for marker-end attribute.
1930
+ * @returns A unique marker ID string in the format "arrow-end-{lineId}"
1931
+ */
1932
+ get endMarkerId() {
1933
+ return `arrow-end-${this.id}`;
1934
+ }
1935
+ /**
1936
+ * Gets the arrow head size for the specified end of the line.
1937
+ * @param end - Which end to get the arrow size for: 'start' or 'end'
1938
+ * @returns The configured arrow size, or strokeWidth * 3 as default
1939
+ */
1940
+ getArrowSize(end) {
1941
+ const config = end === 'start' ? this.arrows?.start : this.arrows?.end;
1942
+ return config?.size ?? this.strokeWidth * 3;
1943
+ }
1944
+ /**
1945
+ * Gets the resolved fill color for the arrow head at the specified end.
1946
+ * Resolves theme-aware colors to the appropriate value for the current theme.
1947
+ * @param end - Which end to get the arrow fill for: 'start' or 'end'
1948
+ * @returns The resolved CSS color string for the arrow fill
1949
+ */
1950
+ getArrowFill(end) {
1951
+ const config = end === 'start' ? this.arrows?.start : this.arrows?.end;
1952
+ const color = config?.fill ?? this.stroke;
1953
+ return KritzelColorHelper.resolveThemeColor(color);
1954
+ }
1955
+ /**
1956
+ * Generates SVG path data for an arrow head based on the given style.
1957
+ * All paths are designed to fit in a 10x10 viewBox with the tip at (10, 5).
1958
+ * @param style - The arrow head style: 'triangle' (filled), 'open' (outline),
1959
+ * 'diamond' (rhombus), or 'circle'. Defaults to 'triangle'.
1960
+ * @returns SVG path data string for use in a marker element
1961
+ */
1962
+ getArrowPath(style = 'triangle') {
1963
+ switch (style) {
1964
+ case 'triangle':
1965
+ return 'M 0 0 L 10 5 L 0 10 z';
1966
+ case 'open':
1967
+ return 'M 0 0 L 10 5 L 0 10';
1968
+ case 'diamond':
1969
+ return 'M 0 5 L 5 0 L 10 5 L 5 10 z';
1970
+ case 'circle':
1971
+ return 'M 5,0 A 5,5 0 1,1 5,10 A 5,5 0 1,1 5,0';
1972
+ default:
1973
+ return 'M 0 0 L 10 5 L 0 10 z';
1974
+ }
1975
+ }
1976
+ /**
1977
+ * Checks if an arrow head is enabled at the start of the line.
1978
+ * @returns True if the start arrow configuration exists and is enabled
1979
+ */
1980
+ get hasStartArrow() {
1981
+ return this.arrows?.start?.enabled === true;
1982
+ }
1983
+ /**
1984
+ * Checks if an arrow head is enabled at the end of the line.
1985
+ * @returns True if the end arrow configuration exists and is enabled
1986
+ */
1987
+ get hasEndArrow() {
1988
+ return this.arrows?.end?.enabled === true;
1989
+ }
1990
+ }
1991
+
1992
+ const kritzelAwarenessCursorsCss = () => `:host{display:block;position:fixed;top:0;left:0;width:100vw;height:100vh;pointer-events:none;z-index:1}.awareness-cursor{position:absolute;top:0;left:0;transition:transform var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, opacity 300ms ease;will-change:transform}.awareness-cursor.stale{opacity:0}.awareness-cursor.tracking-object{transition-duration:0ms}.cursor-arrow{filter:drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))}.cursor-label{position:absolute;left:16px;top:16px;white-space:nowrap;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:var(--kritzel-awareness-cursor-label-font-size, 12px);color:var(--kritzel-awareness-cursor-label-text-color, #ffffff);padding:2px 8px;border-radius:4px;line-height:1.4;font-weight:500;pointer-events:none;user-select:none}.edge-indicator{position:absolute;top:-12px;left:-12px;width:24px;height:24px;display:flex;align-items:center;justify-content:center;transition:transform var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, opacity 300ms ease;will-change:transform;pointer-events:auto;user-select:none;cursor:pointer}.edge-indicator.stale{opacity:0}.edge-indicator.tracking-object{transition-duration:0ms}.edge-arrow{position:absolute;filter:drop-shadow(0 1px 3px rgba(0, 0, 0, 0.3));transition:opacity 300ms ease}.edge-arrow.stale{opacity:0}.edge-label{position:absolute;white-space:nowrap;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:var(--kritzel-awareness-cursor-label-font-size, 12px);color:var(--kritzel-awareness-cursor-label-text-color, #ffffff);padding:2px 8px;border-radius:4px;line-height:1.4;font-weight:500;pointer-events:none;opacity:0;transform-origin:center;transition:opacity 150ms ease}.edge-indicator:hover .edge-label{opacity:1}.remote-selection-box{position:absolute;top:0;left:0;border-width:2px;border-style:solid;pointer-events:none;will-change:transform, width, height;transition:transform var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, width var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, height var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out}`;
1993
+
1994
+ const STALE_THRESHOLD_MS = 10_000;
1995
+ const REMOVE_THRESHOLD_MS = 30_000;
1996
+ const CLEANUP_INTERVAL_MS = 3_000;
1997
+ const KritzelAwarenessCursors = /*@__PURE__*/ proxyCustomElement(class KritzelAwarenessCursors extends H {
1998
+ constructor(registerHost) {
1999
+ super();
2000
+ if (registerHost !== false) {
2001
+ this.__registerHost();
2002
+ }
2003
+ this.__attachShadow();
2004
+ }
2005
+ core;
2006
+ showEdgeIndicators = true;
2007
+ edgeIndicatorPadding = 8;
2008
+ remoteCursors = new Map();
2009
+ objectVersion = 0;
2010
+ cleanupIntervalId;
2011
+ objectChangeRafId = null;
2012
+ removeAwarenessChangeListener;
2013
+ removeObjectsChangeListener;
2014
+ componentDidLoad() {
2015
+ this.removeAwarenessChangeListener = this.core.store.objects?.onAwarenessChange(states => {
2016
+ this.handleAwarenessChange(states);
2017
+ });
2018
+ this.removeObjectsChangeListener = this.core.store.objects?.onObjectsChange(() => {
2019
+ this.handleRemoteObjectChange();
2020
+ });
2021
+ this.cleanupIntervalId = setInterval(() => {
2022
+ this.cleanupStaleCursors();
2023
+ }, CLEANUP_INTERVAL_MS);
2024
+ if (this.cleanupIntervalId && typeof this.cleanupIntervalId.unref === 'function') {
2025
+ this.cleanupIntervalId.unref();
2026
+ }
2027
+ }
2028
+ disconnectedCallback() {
2029
+ if (this.removeAwarenessChangeListener) {
2030
+ this.removeAwarenessChangeListener();
2031
+ this.removeAwarenessChangeListener = undefined;
2032
+ }
2033
+ if (this.removeObjectsChangeListener) {
2034
+ this.removeObjectsChangeListener();
2035
+ this.removeObjectsChangeListener = undefined;
2036
+ }
2037
+ if (this.cleanupIntervalId && typeof globalThis.clearInterval === 'function') {
2038
+ globalThis.clearInterval(this.cleanupIntervalId);
2039
+ this.cleanupIntervalId = undefined;
2040
+ }
2041
+ if (this.objectChangeRafId !== null && typeof globalThis.cancelAnimationFrame === 'function') {
2042
+ globalThis.cancelAnimationFrame(this.objectChangeRafId);
2043
+ this.objectChangeRafId = null;
2044
+ }
2045
+ }
2046
+ handleAwarenessChange(states) {
2047
+ const localClientId = this.core.store.objects?.localClientId;
2048
+ const now = Date.now();
2049
+ const updated = new Map(this.remoteCursors);
2050
+ // Track which clientIds are still present
2051
+ const activeClientIds = new Set();
2052
+ states.forEach((state, clientId) => {
2053
+ if (clientId === localClientId)
2054
+ return;
2055
+ if (!state.user)
2056
+ return;
2057
+ activeClientIds.add(clientId);
2058
+ const user = state.user;
2059
+ const cursor = state.cursor;
2060
+ const activeObjectId = state.activeObjectId || null;
2061
+ const selectionBox = state.selectionBox || null;
2062
+ const existing = updated.get(clientId);
2063
+ const cursorMoved = !existing ||
2064
+ !existing.cursor !== !cursor ||
2065
+ (cursor && existing.cursor && (cursor.x !== existing.cursor.x || cursor.y !== existing.cursor.y));
2066
+ updated.set(clientId, {
2067
+ clientId,
2068
+ user,
2069
+ cursor,
2070
+ activeObjectId,
2071
+ selectionBox,
2072
+ lastUpdated: now,
2073
+ lastCursorMove: cursorMoved ? now : (existing?.lastCursorMove ?? now),
2074
+ });
2075
+ });
2076
+ // Remove cursors for disconnected clients
2077
+ for (const clientId of updated.keys()) {
2078
+ if (!activeClientIds.has(clientId)) {
2079
+ updated.delete(clientId);
2080
+ }
2081
+ }
2082
+ this.remoteCursors = updated;
2083
+ }
2084
+ cleanupStaleCursors() {
2085
+ const now = Date.now();
2086
+ let changed = false;
2087
+ const updated = new Map(this.remoteCursors);
2088
+ for (const [clientId, cursor] of updated) {
2089
+ if (now - cursor.lastUpdated > REMOVE_THRESHOLD_MS) {
2090
+ updated.delete(clientId);
2091
+ changed = true;
2092
+ }
2093
+ else if (!changed && now - cursor.lastCursorMove > STALE_THRESHOLD_MS) {
2094
+ changed = true;
2095
+ }
2096
+ }
2097
+ if (changed) {
2098
+ this.remoteCursors = updated;
2099
+ }
2100
+ }
2101
+ isStale(cursor) {
2102
+ return Date.now() - cursor.lastCursorMove > STALE_THRESHOLD_MS;
2103
+ }
2104
+ hasActiveDrawingCursors() {
2105
+ for (const cursor of this.remoteCursors.values()) {
2106
+ if (cursor.activeObjectId)
2107
+ return true;
2108
+ }
2109
+ return false;
2110
+ }
2111
+ handleRemoteObjectChange() {
2112
+ if (!this.hasActiveDrawingCursors())
2113
+ return;
2114
+ // Debounce via rAF to batch multiple rapid Yjs updates into a single re-render
2115
+ if (this.objectChangeRafId !== null)
2116
+ return;
2117
+ this.objectChangeRafId = requestAnimationFrame(() => {
2118
+ this.objectChangeRafId = null;
2119
+ this.objectVersion++;
2120
+ });
2121
+ }
2122
+ getActiveObjectTip(objectId) {
2123
+ const obj = this.core.store.objects?.findById(objectId);
2124
+ if (!obj)
2125
+ return null;
2126
+ if (obj instanceof KritzelPath && !obj.isCompleted) {
2127
+ const lastPoint = obj.points[obj.points.length - 1];
2128
+ if (!lastPoint)
2129
+ return null;
2130
+ return {
2131
+ x: (lastPoint[0] - obj.x) / obj.scale + obj.translateX,
2132
+ y: (lastPoint[1] - obj.y) / obj.scale + obj.translateY,
2133
+ };
2134
+ }
2135
+ if (obj instanceof KritzelLine && !obj.isCompleted) {
2136
+ return {
2137
+ x: (obj.endX - obj.x) / obj.scale + obj.translateX,
2138
+ y: (obj.endY - obj.y) / obj.scale + obj.translateY,
2139
+ };
2140
+ }
2141
+ // Shapes normalize their bounding box (x/y always top-left, width/height
2142
+ // always positive), so we can't determine which corner the user is actively
2143
+ // dragging. Fall back to the throttled awareness cursor position instead.
2144
+ return null;
2145
+ }
2146
+ worldToScreen(worldX, worldY) {
2147
+ const { scale, translateX, translateY } = this.core.store.state;
2148
+ return {
2149
+ x: worldX * scale + translateX,
2150
+ y: worldY * scale + translateY,
2151
+ };
2152
+ }
2153
+ isInViewport(screenX, screenY) {
2154
+ const { viewportWidth, viewportHeight } = this.core.store.state;
2155
+ return screenX >= 0 && screenX <= viewportWidth && screenY >= 0 && screenY <= viewportHeight;
2156
+ }
2157
+ clampToEdge(screenX, screenY) {
2158
+ const { viewportWidth, viewportHeight } = this.core.store.state;
2159
+ const padding = this.edgeIndicatorPadding; // 8px
2160
+ // Clamp symmetrically
2161
+ const clampedX = Math.max(padding, Math.min(viewportWidth - padding, screenX));
2162
+ const clampedY = Math.max(padding, Math.min(viewportHeight - padding, screenY));
2163
+ // Determine nearest edge to position avatar inside
2164
+ const distLeft = clampedX - padding;
2165
+ const distRight = (viewportWidth - padding) - clampedX;
2166
+ const distTop = clampedY - padding;
2167
+ const distBottom = (viewportHeight - padding) - clampedY;
2168
+ let edge = 'top';
2169
+ const minDist = Math.min(distLeft, distRight, distTop, distBottom);
2170
+ if (minDist === distLeft)
2171
+ edge = 'left';
2172
+ else if (minDist === distRight)
2173
+ edge = 'right';
2174
+ else if (minDist === distTop)
2175
+ edge = 'top';
2176
+ else
2177
+ edge = 'bottom';
2178
+ const angle = Math.atan2(screenY - clampedY, screenX - clampedX);
2179
+ return { x: clampedX, y: clampedY, angle, edge };
2180
+ }
2181
+ getUserDisplayName(user) {
2182
+ if (user.displayName)
2183
+ return user.displayName;
2184
+ if (user.firstName || user.lastName) {
2185
+ return [user.firstName, user.lastName].filter(Boolean).join(' ');
2186
+ }
2187
+ return 'Unknown';
2188
+ }
2189
+ getInitials(name) {
2190
+ const parts = name.trim().split(/\s+/);
2191
+ if (parts.length === 0 || (parts.length === 1 && parts[0].length <= 1)) {
2192
+ return name;
2193
+ }
2194
+ if (parts.length === 1) {
2195
+ return parts[0][0].toUpperCase();
2196
+ }
2197
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
2198
+ }
2199
+ render() {
2200
+ const cursors = Array.from(this.remoteCursors.values());
2201
+ return (h(Host, { key: '9be6694250bafe011be27301d1c7a53fc7957657' }, cursors.map(remoteCursor => {
2202
+ if (!remoteCursor.cursor)
2203
+ return null;
2204
+ // When a remote user is actively drawing, derive cursor position from
2205
+ // the object's latest coordinates (synced via Yjs) instead of the
2206
+ // throttled awareness cursor position
2207
+ let screen;
2208
+ let trackingObject = false;
2209
+ if (remoteCursor.activeObjectId) {
2210
+ const tip = this.getActiveObjectTip(remoteCursor.activeObjectId);
2211
+ if (tip) {
2212
+ trackingObject = true;
2213
+ screen = this.worldToScreen(tip.x, tip.y);
2214
+ }
2215
+ else {
2216
+ screen = this.worldToScreen(remoteCursor.cursor.x, remoteCursor.cursor.y);
2217
+ }
2218
+ }
2219
+ else {
2220
+ screen = this.worldToScreen(remoteCursor.cursor.x, remoteCursor.cursor.y);
2221
+ }
2222
+ const inViewport = this.isInViewport(screen.x, screen.y);
2223
+ const stale = this.isStale(remoteCursor);
2224
+ const color = remoteCursor.user.color || '#6B7280';
2225
+ if (inViewport) {
2226
+ return this.renderCursor(remoteCursor, screen.x, screen.y, color, stale, trackingObject);
2227
+ }
2228
+ if (this.showEdgeIndicators) {
2229
+ return this.renderEdgeIndicator(remoteCursor, screen.x, screen.y, color, stale, trackingObject);
2230
+ }
2231
+ return null;
2232
+ }), cursors.map(remoteCursor => {
2233
+ if (!remoteCursor.selectionBox)
2234
+ return null;
2235
+ const color = remoteCursor.user.color || '#6B7280';
2236
+ const box = remoteCursor.selectionBox;
2237
+ const topLeft = this.worldToScreen(box.x, box.y);
2238
+ const { scale } = this.core.store.state;
2239
+ const screenWidth = box.width * scale;
2240
+ const screenHeight = box.height * scale;
2241
+ return (h("div", { key: `selection-box-${remoteCursor.clientId}`, class: "remote-selection-box", style: {
2242
+ transform: `translate(${topLeft.x}px, ${topLeft.y}px)`,
2243
+ width: `${screenWidth}px`,
2244
+ height: `${screenHeight}px`,
2245
+ backgroundColor: `color-mix(in srgb, ${color} 20%, transparent)`,
2246
+ borderColor: `color-mix(in srgb, ${color} 50%, transparent)`,
2247
+ } }));
2248
+ })));
2249
+ }
2250
+ renderCursor(cursor, screenX, screenY, color, stale, trackingObject) {
2251
+ return (h("div", { key: `cursor-${cursor.clientId}`, class: { 'awareness-cursor': true, stale, 'tracking-object': trackingObject }, style: { transform: `translate(${screenX}px, ${screenY}px)` } }, h("svg", { class: "cursor-arrow", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, h("path", { d: "M5 3L19 12L12 13L9 20L5 3Z", fill: color, stroke: "#ffffff", "stroke-width": "1.5", "stroke-linejoin": "round" })), h("span", { class: "cursor-label", style: { backgroundColor: color } }, this.getUserDisplayName(cursor.user))));
2252
+ }
2253
+ renderEdgeIndicator(cursor, screenX, screenY, color, stale, trackingObject) {
2254
+ const clamped = this.clampToEdge(screenX, screenY);
2255
+ const arrowDeg = (clamped.angle * 180) / Math.PI + 90;
2256
+ // Position the username label opposite to the edge
2257
+ const labelOffset = 20;
2258
+ let labelX = 0;
2259
+ let labelY = 0;
2260
+ // Default transform origin is somewhat arbitrary without explicit layout
2261
+ // We'll calculate simple translations relative to center point
2262
+ if (clamped.edge === 'left') {
2263
+ labelX = labelOffset;
2264
+ }
2265
+ else if (clamped.edge === 'right') {
2266
+ labelX = -labelOffset;
2267
+ }
2268
+ else if (clamped.edge === 'top') {
2269
+ labelY = labelOffset;
2270
+ }
2271
+ else if (clamped.edge === 'bottom') {
2272
+ labelY = -labelOffset;
2273
+ }
2274
+ const displayName = this.getUserDisplayName(cursor.user);
2275
+ return (h("div", { key: `edge-${cursor.clientId}`, class: { 'edge-indicator': true, stale, 'tracking-object': trackingObject }, style: {
2276
+ transform: `translate(${clamped.x}px, ${clamped.y}px)`,
2277
+ } }, h("svg", { class: { 'edge-arrow': true, stale }, width: "16", height: "16", viewBox: "0 0 16 16", style: { transform: `rotate(${arrowDeg}deg)` } }, h("path", { d: "M8 1L14 13H2L8 1Z", fill: color, stroke: "#ffffff", "stroke-width": "1.5", "stroke-linejoin": "round" })), h("span", { class: "edge-label", style: {
2278
+ backgroundColor: color,
2279
+ transform: `translate(${labelX}px, ${labelY}px)`,
2280
+ } }, this.getInitials(displayName))));
2281
+ }
2282
+ static get style() { return kritzelAwarenessCursorsCss(); }
2283
+ }, [513, "kritzel-awareness-cursors", {
2284
+ "core": [16],
2285
+ "showEdgeIndicators": [4, "show-edge-indicators"],
2286
+ "edgeIndicatorPadding": [2, "edge-indicator-padding"],
2287
+ "remoteCursors": [32],
2288
+ "objectVersion": [32]
2289
+ }]);
2290
+ function defineCustomElement() {
2291
+ if (typeof customElements === "undefined") {
2292
+ return;
2293
+ }
2294
+ const components = ["kritzel-awareness-cursors"];
2295
+ components.forEach(tagName => { switch (tagName) {
2296
+ case "kritzel-awareness-cursors":
2297
+ if (!customElements.get(transformTag(tagName))) {
2298
+ customElements.define(transformTag(tagName), KritzelAwarenessCursors);
2299
+ }
2300
+ break;
2301
+ } });
2302
+ }
2303
+
2304
+ export { KritzelBaseObject as K, KritzelPath as a, KritzelLine as b, KritzelAwarenessCursors as c, defineCustomElement as d, KritzelMathHelper as e, KritzelGeometryHelper as f };