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.
- package/dist/cjs/kritzel-active-users_47.cjs.entry.js +79 -13
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/stencil.cjs.js +1 -1
- package/dist/collection/classes/core/viewport.class.js +14 -1
- package/dist/collection/components/core/kritzel-editor/kritzel-editor.js +48 -8
- package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +84 -3
- package/dist/collection/constants/version.js +1 -1
- package/dist/components/index.js +1226 -1
- package/dist/components/kritzel-active-users.js +6 -1
- package/dist/components/kritzel-avatar.js +6 -1
- package/dist/components/kritzel-awareness-cursors.js +6 -1
- package/dist/components/kritzel-back-to-content.js +6 -1
- package/dist/components/kritzel-brush-style.js +64 -1
- package/dist/components/kritzel-button.js +6 -1
- package/dist/components/kritzel-color-palette.js +6 -1
- package/dist/components/kritzel-color.js +6 -1
- package/dist/components/kritzel-context-menu.js +6 -1
- package/dist/components/kritzel-controls.js +6 -1
- package/dist/components/kritzel-current-user-dialog.js +6 -1
- package/dist/components/kritzel-current-user.js +6 -1
- package/dist/components/kritzel-cursor-trail.js +6 -1
- package/dist/components/kritzel-dialog.js +6 -1
- package/dist/components/kritzel-dropdown.js +6 -1
- package/dist/components/kritzel-editor.js +4404 -1
- package/dist/components/kritzel-engine.js +6 -1
- package/dist/components/kritzel-export.js +6 -1
- package/dist/components/kritzel-font-family.js +6 -1
- package/dist/components/kritzel-font-size.js +6 -1
- package/dist/components/kritzel-font.js +6 -1
- package/dist/components/kritzel-icon.js +6 -1
- package/dist/components/kritzel-input.js +6 -1
- package/dist/components/kritzel-line-endings.js +6 -1
- package/dist/components/kritzel-loading-overlay.js +6 -1
- package/dist/components/kritzel-login-dialog.js +6 -1
- package/dist/components/kritzel-master-detail.js +6 -1
- package/dist/components/kritzel-menu-item.js +6 -1
- package/dist/components/kritzel-menu.js +6 -1
- package/dist/components/kritzel-more-menu.js +6 -1
- package/dist/components/kritzel-notification-card.js +6 -1
- package/dist/components/kritzel-numeric-input.js +6 -1
- package/dist/components/kritzel-opacity-slider.js +6 -1
- package/dist/components/kritzel-pill-tabs.js +6 -1
- package/dist/components/kritzel-portal.js +6 -1
- package/dist/components/kritzel-settings.js +6 -1
- package/dist/components/kritzel-shape-fill.js +6 -1
- package/dist/components/kritzel-share-dialog.js +6 -1
- package/dist/components/kritzel-slide-toggle.js +6 -1
- package/dist/components/kritzel-split-button.js +6 -1
- package/dist/components/kritzel-stroke-size.js +6 -1
- package/dist/components/kritzel-sync-indicator.js +6 -1
- package/dist/components/kritzel-tool-config.js +6 -1
- package/dist/components/kritzel-tooltip.js +6 -1
- package/dist/components/kritzel-utility-panel.js +6 -1
- package/dist/components/kritzel-watermark.js +6 -1
- package/dist/components/kritzel-workspace-manager.js +6 -1
- package/dist/components/kritzel-zoom-panel.js +6 -1
- package/dist/components/p-894OnDQV.js +39 -1
- package/dist/components/p-B43upypT.js +1500 -1
- package/dist/components/p-B5oHmPJb.js +97 -1
- package/dist/components/p-B6t5q-V4.js +63 -1
- package/dist/components/p-B8Xi4Clg.js +43 -1
- package/dist/components/p-BBmP8lVY.js +51726 -0
- package/dist/components/p-BDzlx2hB.js +135 -1
- package/dist/components/p-BFTU3MAI.js +7 -1
- package/dist/components/p-BFYtCsZu.js +56 -1
- package/dist/components/p-BI2ng9I_.js +62 -1
- package/dist/components/p-BTzlrIxe.js +194 -1
- package/dist/components/p-BmyTdQGy.js +1090 -1
- package/dist/components/p-BsANVili.js +150 -1
- package/dist/components/p-BxpKq94F.js +290 -1
- package/dist/components/p-C38P-Vd0.js +446 -1
- package/dist/components/p-C4NrrUfN.js +77 -1
- package/dist/components/p-CW-VyJgK.js +6 -1
- package/dist/components/p-CWRSGYOE.js +57 -1
- package/dist/components/p-CXpv9Rxe.js +106 -1
- package/dist/components/p-Ca-igU1f.js +320 -1
- package/dist/components/p-CdR76C4L.js +165 -1
- package/dist/components/p-Ch42cOeY.js +320 -0
- package/dist/components/p-ClFprFvz.js +209 -1
- package/dist/components/p-Cm33td2O.js +122 -1
- package/dist/components/p-CqLgB8JK.js +91 -1
- package/dist/components/p-Cr682JI_.js +156 -1
- package/dist/components/p-CsvMgk_f.js +62 -1
- package/dist/components/p-CwJwsRR3.js +277 -1
- package/dist/components/p-Cymum41a.js +88 -1
- package/dist/components/p-D09kgc5Q.js +94 -1
- package/dist/components/p-D30zs0kA.js +121 -1
- package/dist/components/p-D3y_Hauo.js +85 -1
- package/dist/components/p-D5jf8wAu.js +142 -1
- package/dist/components/p-D82VaHce.js +53 -1
- package/dist/components/p-DAwPPDMS.js +101 -1
- package/dist/components/p-DEBwmRTC.js +55 -1
- package/dist/components/p-DIazmg6w.js +52 -1
- package/dist/components/p-DJ88BrIW.js +70 -1
- package/dist/components/p-DJkKlUyZ.js +182 -1
- package/dist/components/p-DUYmwrWV.js +3267 -1
- package/dist/components/p-DX2nriaq.js +486 -1
- package/dist/components/p-Dd-pMvlr.js +20 -1
- package/dist/components/p-Df3BwVGy.js +299 -1
- package/dist/components/p-DgJ0ufzr.js +43 -1
- package/dist/components/p-DkUb2czk.js +70 -1
- package/dist/components/p-Dlp84thc.js +57 -1
- package/dist/components/p-JYj3Facr.js +431 -1
- package/dist/components/p-Ka7OWbnk.js +2304 -1
- package/dist/components/p-PSFAfAyd.js +49 -1
- package/dist/components/p-WUdQqYnu.js +118 -1
- package/dist/components/p-djJSnjAX.js +84 -1
- package/dist/components/p-e6Zpc9Vp.js +143 -1
- package/dist/components/p-jGOpkGDl.js +30 -1
- package/dist/components/p-pagW8YF7.js +99 -1
- package/dist/esm/kritzel-active-users_47.entry.js +79 -13
- package/dist/esm/loader.js +1 -1
- package/dist/esm/stencil.js +1 -1
- package/dist/stencil/index.esm.js +1172 -1
- package/dist/stencil/p-5b969a42.entry.js +36109 -0
- package/dist/stencil/p-DQuL1Twl.js +3 -1
- package/dist/stencil/p-Dhio9uis.js +1717 -2
- package/dist/stencil/p-fb32cd8f.entry.js +29 -1
- package/dist/stencil/p-uC7_X6yr.js +30737 -1
- package/dist/stencil/stencil.esm.js +21 -1
- package/dist/types/components/core/kritzel-editor/kritzel-editor.d.ts +2 -0
- package/dist/types/components/core/kritzel-engine/kritzel-engine.d.ts +7 -0
- package/dist/types/components.d.ts +10 -0
- package/dist/types/constants/version.d.ts +1 -1
- package/package.json +1 -1
- package/dist/components/p--Oazz-BP.js +0 -9
- package/dist/components/p-Xsiz1D5Q.js +0 -1
- 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 };
|