diff --git a/dist/ueblueprint.js b/dist/ueblueprint.js index f9188b3..095fd64 100644 --- a/dist/ueblueprint.js +++ b/dist/ueblueprint.js @@ -1 +1,2354 @@ -class t{constructor(){this.nodes=new Array,this.expandGridSize=400,this.additional=[0,0],this.translateValue=[0,0],this.mousePosition=[0,0]}}const e=String.raw;class n{render(t){return""}getElements(t){let e=document.createElement("div");return e.innerHTML=this.render(t),e.childNodes}}class r extends n{header(t){return e`
1:1
`}overlay(){return e`
`}viewport(t){return e`
`}render(t){return e`${this.header(t)} ${this.overlay(t)} ${this.viewport(t)}`}}class i{constructor(t,e,n){if(this.target=t,this.blueprint=e,this.options=n,n?.wantsFocusCallback){let t=this;this.blueprintfocusHandler=e=>t.blueprintFocused(),this.blueprintunfocusHandler=e=>t.blueprintUnfocused(),this.blueprint.addEventListener("blueprintfocus",this.blueprintfocusHandler),this.blueprint.addEventListener("blueprintunfocus",this.blueprintunfocusHandler)}}unlistenDOMElement(){this.blueprint.removeEventListener("blueprintfocus",this.blueprintfocusHandler),this.blueprint.removeEventListener("blueprintunfocus",this.blueprintunfocusHandler)}blueprintFocused(){}blueprintUnfocused(){}}class s{static sanitize(t){return t instanceof Object&&(t instanceof Boolean||t instanceof Number||t instanceof String)?t.valueOf():t}constructor(t,e=!0,n){void 0===n&&(n=s.sanitize(new t)),this.value=n,this.showDefault=e,this.type=t}}class o{static clamp(t,e,n){return Math.min(Math.max(t,e),n)}static getScale(t){return getComputedStyle(t).getPropertyValue("--ueb-scale")}static objectSet(t,e,n,r=!1){if(e.constructor!=Array&&console.error("Expected keys to be an array."),1==e.length){if(r||e[0]in t)return t[e[0]]=n,!0}else if(e.length>0)return o.objectSet(t[e[0]],e.slice(1),n,r);return!1}static objectGet(t,e,n=null){return e.constructor!=Array&&console.error("Expected keys to be an array."),0!=e.length&&e[0]in t?1==e.length?t[e[0]]:o.objectGet(t[e[0]],e.slice(1),n):n}static equals(t,e){return(t=s.sanitize(t))===(e=s.sanitize(e))}static FirstCapital(t){return t.charAt(0).toUpperCase()+t.substring(1)}static getType(t){let e=t?.constructor;switch(e){case s:return t.type;case Function:return t;default:return e}}}class a extends i{constructor(t,e,n){super(t,e,n),this.movementSpace=this.blueprint?.getGridDOMElement()??document.documentElement}getLocation(t){const e=1/o.getScale(this.target),n=this.movementSpace.getBoundingClientRect();return[(t.clientX-n.x)*e,(t.clientY-n.y)*e]}}class u extends a{constructor(t,e,n){super(t,e,n),this.clickButton=n?.clickButton??0,this.exitAnyButton=n?.exitAnyButton??!0,this.moveEverywhere=n?.moveEverywhere??!1,this.looseTarget=n?.looseTarget??!1,this.started=!1,this.clickedPosition=[0,0];const r=this.moveEverywhere?document.documentElement:this.movementSpace;let i=this;this.mouseDownHandler=t=>{if(this.blueprint.setFocused(!0),t.button===i.clickButton)(i.looseTarget||t.target==t.currentTarget)&&(t.stopPropagation(),i.started=!1,r.addEventListener("mousemove",i.mouseStartedMovingHandler),document.addEventListener("mouseup",i.mouseUpHandler),i.clickedPosition=i.getLocation(t),i.clicked(i.clickedPosition));else i.exitAnyButton||i.mouseUpHandler(t)},this.mouseStartedMovingHandler=t=>{t.preventDefault(),t.stopPropagation(),r.removeEventListener("mousemove",i.mouseStartedMovingHandler),r.addEventListener("mousemove",i.mouseMoveHandler),i.startDrag(),i.started=!0},this.mouseMoveHandler=t=>{t.preventDefault(),t.stopPropagation();const e=i.getLocation(t),n=[t.movementX,t.movementY];i.dragTo(e,n)},this.mouseUpHandler=t=>{i.exitAnyButton&&t.button!=i.clickButton||(r.removeEventListener("mousemove",i.mouseStartedMovingHandler),r.removeEventListener("mousemove",i.mouseMoveHandler),document.removeEventListener("mouseup",i.mouseUpHandler),i.endDrag())},this.target.addEventListener("mousedown",this.mouseDownHandler),2==this.clickButton&&this.target.addEventListener("contextmenu",this.preventDefault)}preventDefault(t){t.preventDefault()}unlistenDOMElement(){super.unlistenDOMElement(),this.target.removeEventListener("mousedown",this.mouseDownHandler),2==this.clickButton&&this.target.removeEventListener("contextmenu",this.preventDefault),blueprintunfocusHandler}clicked(t){}startDrag(){}dragTo(t,e){}endDrag(){}}class l extends u{dragTo(t,e){this.blueprint.scrollDelta([-e[0],-e[1]])}}class c extends HTMLElement{constructor(t,e){super(),this.blueprint=null,this.entity=t,this.template=e}connectedCallback(){this.blueprint=this.closest("u-blueprint"),this.append(...this.template.getElements(this.entity))}}class h{constructor(t=(t=>t),e=null){this.array=new Uint32Array(e),this.comparisonValueSupplier=t,this.length=0,this.currentPosition=0}get(t){return t>=0&&t=0&&this.currentPosition=0&&this.currentPosition0?this.get(this.currentPosition-1):null}getPrevValue(){return this.currentPosition>0?this.comparisonValueSupplier(this.get(this.currentPosition-1)):Number.MIN_SAFE_INTEGER}shiftLeft(t,e=1){this.array.set(this.array.subarray(t+e),t)}shiftRight(t,e=1){this.array.set(this.array.subarray(t,-e),t+e)}}class d{constructor(t,e,n,r){this.initialPosition=t,this.finalPosition=t,this.metadata=new Array(e.length),this.primaryOrder=new h((t=>this.metadata[t].primaryBoundary)),this.secondaryOrder=new h((t=>this.metadata[t].secondaryBoundary)),this.selectFunc=r,this.rectangles=e,this.primaryOrder.reserve(this.rectangles.length),this.secondaryOrder.reserve(this.rectangles.length),e.forEach(((t,e)=>{let i={primaryBoundary:this.initialPosition[0],secondaryBoundary:this.initialPosition[1],rectangle:e,onSecondaryAxis:!1};this.metadata[e]=i,r(t,!1);const s=n(t);this.initialPosition[1]{if(this.metadata[n].onSecondaryAxis)this.selectFunc(this.rectangles[n],r);else if(r){this.secondaryOrder.insert(n,t[1]);const r=this.metadata[n].secondaryBoundary;Math.sign(t[1]-r)==e[1]&&Math.sign(r-this.initialPosition[1])==e[1]&&this.selectFunc(this.rectangles[n],!0)}else this.selectFunc(this.rectangles[n],!1),this.secondaryOrder.remove(n);this.computeBoundaries(t),this.selectTo(t)};t[0]this.boundaries.primaryN.v&&t[0]this.boundaries.primaryP.v&&(++this.primaryOrder.currentPosition,n(this.boundaries.primaryP.i,this.initialPosition[0]{this.selectFunc(this.rectangles[e],n),this.computeBoundaries(t),this.selectTo(t)};t[1]this.boundaries.secondaryN.v&&t[1]this.boundaries.secondaryP.v&&(++this.secondaryOrder.currentPosition,r(this.boundaries.secondaryP.i,this.initialPosition[1]{r.blueprint.entity.mousePosition=r.getLocation(t)}}blueprintFocused(){this.target.addEventListener("mousemove",this.mousemoveHandler)}blueprintUnfocused(){this.target.removeEventListener("mousemove",this.mousemoveHandler)}}class m{constructor(t={}){const e=(n,r,i)=>{let a=n.concat("");const u=a.length-1;for(let n in i){if(a[u]=n,i[n]?.constructor===Object){r[n]={},e(a,r[n],i[n]);continue}const l=o.objectGet(t,a);if(null!==l){r[n]=l;continue}let c=i[n];if(c instanceof s){if(!c.showDefault)continue;c=c.value}c instanceof Array?r[n]=[]:(c instanceof Function&&(c=s.sanitize(new c)),r[n]=c)}};e([],this,this.getAttributes())}}class g extends m{static attributes={value:String};static generateGuid(t=!0){let e=new Uint32Array(4);!0===t&&crypto.getRandomValues(e);let n="";return e.forEach((t=>{n+=("0".repeat(8)+t.toString(16).toUpperCase()).slice(-8)})),new g({valud:n})}getAttributes(){return g.attributes}toString(){return this.value}}class b extends m{static attributes={namespace:String,key:String,value:String};getAttributes(){return b.attributes}}class y extends m{static attributes={type:String,path:String};getAttributes(){return y.attributes}}class v extends m{static attributes={value:String};getAttributes(){return v.attributes}toString(){return this.value}}class w extends m{static attributes={objectName:v,pinGuid:g};getAttributes(){return w.attributes}}class E extends m{static attributes={PinId:g,PinName:"",PinFriendlyName:new s(b,!1,null),PinToolTip:"",Direction:new s(String,!1,""),PinType:{PinCategory:"",PinSubCategory:"",PinSubCategoryObject:y,PinSubCategoryMemberReference:null,PinValueType:null,ContainerType:y,bIsReference:!1,bIsConst:!1,bIsWeakPointer:!1,bIsUObjectWrapper:!1},LinkedTo:[w],DefaultValue:"",AutogeneratedDefaultValue:"",PersistentGuid:g,bHidden:!1,bNotConnectable:!1,bDefaultValueIsReadOnly:!1,bDefaultValueIsIgnored:!1,bAdvancedView:!1,bOrphanedPin:!1};getAttributes(){return E.attributes}getPinDisplayName(){return this.PinName}isOutput(){if("EGPD_Output"===this.Direction)return!0}}class S extends n{header(t){return e`
${t.getNodeDisplayName()}
`}body(t){let n=t.CustomProperties.filter((t=>t instanceof E)),r=n.filter((t=>t.isOutput()));return n=n.filter((t=>!t.isOutput())),e`
${n.map(((t,r)=>e`
${t.getPinDisplayName()}
`)).join("")??""}
${r.map(((t,n)=>e`
${t.getPinDisplayName()}
`)).join("")??""}
`}render(t){return e`
${this.header(t)} ${this.body(t)}
`}}class P extends m{static attributes={MemberParent:y,MemberName:""};getAttributes(){return P.attributes}}class x extends m{static attributes={value:Number};getAttributes(){return x.attributes}constructor(t={}){super(t),this.value=Math.round(this.value)}valueOf(){return this.value}toString(){return this.value.toString()}}class O extends m{static attributes={MemberName:String,MemberGuid:g,bSelfContext:!1};getAttributes(){return O.attributes}}class L extends m{static attributes={Class:y,Name:"",bIsPureFunc:new s(Boolean,!1,!1),VariableReference:new s(O,!1,null),FunctionReference:new s(P,!1,null),EventReference:new s(P,!1,null),TargetType:new s(y,!1,null),NodePosX:x,NodePosY:x,NodeGuid:g,ErrorType:new s(x,!1),ErrorMsg:new s(String,!1,""),CustomProperties:[E]};getAttributes(){return L.attributes}getNodeDisplayName(){return this.Name}}class B extends u{constructor(t,e,n){super(t,e,n),this.stepSize=parseInt(n?.stepSize),this.mousePosition=[0,0]}snapToGrid(t){return[this.stepSize*Math.round(t[0]/this.stepSize),this.stepSize*Math.round(t[1]/this.stepSize)]}startDrag(){(isNaN(this.stepSize)||this.stepSize<=0)&&(this.stepSize=parseInt(getComputedStyle(this.target).getPropertyValue("--ueb-grid-snap")),(isNaN(this.stepSize)||this.stepSize<=0)&&(this.stepSize=1)),this.mousePosition=1!=this.stepSize?this.snapToGrid(this.clickedPosition):this.clickedPosition}dragTo(t,e){const n=1!=this.stepSize?this.snapToGrid(t):t,r=[n[0]-this.mousePosition[0],n[1]-this.mousePosition[1]];0==r[0]&&0==r[1]||(this.target.dispatchDragEvent(r),this.mousePosition=n)}}class N extends c{constructor(...t){super(...t),this.dragObject=null,this.location=[0,0],this.selected=!1;let e=this;this.dragHandler=t=>{e.addLocation(t.detail.value)}}connectedCallback(){super.connectedCallback(),this.dragObject=new B(this,this.blueprint,{looseTarget:!0})}disconnectedCallback(){this.dragObject.unlistenDOMElement()}setLocation(t=[0,0]){this.location=t,this.style.setProperty("--ueb-position-x",this.location[0]),this.style.setProperty("--ueb-position-y",this.location[1])}addLocation(t){this.setLocation([this.location[0]+t[0],this.location[1]+t[1]])}dispatchDragEvent(t){this.selected||(this.blueprint.unselectAll(),this.setSelected(!0));let e=new CustomEvent("uDragSelected",{detail:{instigator:this,value:t},bubbles:!1,cancelable:!0,composed:!1});this.blueprint.dispatchEvent(e)}setSelected(t=!0){this.selected!=t&&(this.selected=t,this.selected?(this.classList.add("ueb-selected"),this.blueprint.addEventListener("uDragSelected",this.dragHandler)):(this.classList.remove("ueb-selected"),this.blueprint.removeEventListener("uDragSelected",this.dragHandler)))}}class M{static#t=new Map;static registerSerializer(t,e){M.#t.set(t,e)}static getSerializer(t){return M.#t.get(o.getType(t))}}class j extends u{constructor(t,e,n){super(t,e,n)}startDrag(){}dragTo(t,e){}endDrag(){this.started}}class A extends N{static fromSerializedObject(t){let e=M.getSerializer(L).read(t);return new A(e)}constructor(t){super(t,new S),this.graphNodeName="n/a",this.dragLinkObjects=Array(),super.setLocation([this.entity.NodePosX,this.entity.NodePosY])}connectedCallback(){this.getAttribute("type")?.trim(),super.connectedCallback(),this.classList.add("ueb-node"),this.selected&&this.classList.add("ueb-selected"),this.style.setProperty("--ueb-position-x",this.location[0]),this.style.setProperty("--ueb-position-y",this.location[1]),this.querySelectorAll(".ueb-node-input, .ueb-node-output").forEach((t=>{this.dragLinkObjects.push(new j(t,this.blueprint,{clickButton:0,moveEverywhere:!0,exitAnyButton:!0,looseTarget:!0}))}))}setLocation(t=[0,0]){this.entity.NodePosX=t[0],this.entity.NodePosY=t[1],super.setLocation(t)}}customElements.define("u-node",A);"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;function k(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var T={exports:{}};"undefined"!=typeof self&&self;var z=k(T.exports=function(t){var e={};function n(r){if(e[r])return e[r].exports;var i=e[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:r})},n.r=function(t){Object.defineProperty(t,"__esModule",{value:!0})},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=0)}([function(t,e,n){function r(t){if(!(this instanceof r))return new r(t);this._=t}var i=r.prototype;function s(t,e){for(var n=0;n>7),buf:function(t){var e=o((function(t,e,n,r){return t.concat(n===r.length-1?Buffer.from([e,0]).readUInt16BE(0):r.readUInt16BE(n))}),[],t);return Buffer.from(a((function(t){return(t<<1&65535)>>8}),e))}(n.buf)}})),n}function l(){return"undefined"!=typeof Buffer}function c(){if(!l())throw new Error("Buffer global does not exist; please use webpack if you need to parse Buffers in the browser.")}function h(t){c();var e=o((function(t,e){return t+e}),0,t);if(e%8!=0)throw new Error("The bits ["+t.join(", ")+"] add up to "+e+" which is not an even number of bytes; the total should be divisible by 8");var n,i=e/8,s=(n=function(t){return t>48},o((function(t,e){return t||(n(e)?e:t)}),null,t));if(s)throw new Error(s+" bit range requested exceeds 48 bit (6 byte) Number max.");return new r((function(e,n){var r=i+n;return r>e.length?S(n,i.toString()+" bytes"):E(r,o((function(t,e){var n=u(e,t.buf);return{coll:t.coll.concat(n.v),buf:n.buf}}),{coll:[],buf:e.slice(n,r)},t).coll)}))}function d(t,e){return new r((function(n,r){return c(),r+e>n.length?S(r,e+" bytes for "+t):E(r+e,n.slice(r,r+e))}))}function p(t,e){if("number"!=typeof(n=e)||Math.floor(n)!==n||e<0||e>6)throw new Error(t+" requires integer length in range [0, 6].");var n}function f(t){return p("uintBE",t),d("uintBE("+t+")",t).map((function(e){return e.readUIntBE(0,t)}))}function m(t){return p("uintLE",t),d("uintLE("+t+")",t).map((function(e){return e.readUIntLE(0,t)}))}function g(t){return p("intBE",t),d("intBE("+t+")",t).map((function(e){return e.readIntBE(0,t)}))}function b(t){return p("intLE",t),d("intLE("+t+")",t).map((function(e){return e.readIntLE(0,t)}))}function y(t){return t instanceof r}function v(t){return"[object Array]"==={}.toString.call(t)}function w(t){return l()&&Buffer.isBuffer(t)}function E(t,e){return{status:!0,index:t,value:e,furthest:-1,expected:[]}}function S(t,e){return v(e)||(e=[e]),{status:!1,index:-1,value:null,furthest:t,expected:e}}function P(t,e){if(!e)return t;if(t.furthest>e.furthest)return t;var n=t.furthest===e.furthest?function(t,e){if(function(){if(void 0!==r._supportsSet)return r._supportsSet;var t="undefined"!=typeof Set;return r._supportsSet=t,t}()&&Array.from){for(var n=new Set(t),i=0;i=0;){if(o in n){r=n[o].line,0===s&&(s=n[o].lineStart);break}"\n"===t.charAt(o)&&(i++,0===s&&(s=o+1)),o--}var a=r+i,u=e-s;return n[e]={line:a,lineStart:s},{offset:e,line:a+1,column:u+1}}function L(t){if(!y(t))throw new Error("not a parser: "+t)}function B(t,e){return"string"==typeof t?t.charAt(e):t[e]}function N(t){if("number"!=typeof t)throw new Error("not a number: "+t)}function M(t){if("function"!=typeof t)throw new Error("not a function: "+t)}function j(t){if("string"!=typeof t)throw new Error("not a string: "+t)}var A=2,k=3,T=8,z=5*T,C=4*T,D=" ";function G(t,e){return new Array(e+1).join(t)}function F(t,e,n){var r=e-t.length;return r<=0?t:G(n,r)+t}function H(t,e,n,r){return{from:t-e>0?t-e:0,to:t+n>r?r:t+n}}function V(t,e){var n,r,i,s,u,l=e.index,c=l.offset,h=1;if(c===t.length)return"Got the end of the input";if(w(t)){var d=c-c%T,p=c-d,f=H(d,z,C+T,t.length),m=a((function(t){return a((function(t){return F(t.toString(16),2,"0")}),t)}),function(t,e){var n=t.length,r=[],i=0;if(n<=e)return[t.slice()];for(var s=0;s=4&&(n+=1),h=2,i=a((function(t){return t.length<=4?t.join(" "):t.slice(0,4).join(" ")+" "+t.slice(4).join(" ")}),m),(u=(8*(s.to>0?s.to-1:s.to)).toString(16).length)<2&&(u=2)}else{var g=t.split(/\r\n|[\n\r\u2028\u2029]/);n=l.column-1,r=l.line-1,s=H(r,A,k,g.length),i=g.slice(s.from,s.to),u=s.to.toString().length}var b=r-s.from;return w(t)&&(u=(8*(s.to>0?s.to-1:s.to)).toString(16).length)<2&&(u=2),o((function(e,r,i){var o,a=i===b,l=a?"> ":D;return o=w(t)?F((8*(s.from+i)).toString(16),u,"0"):F((s.from+i+1).toString(),u," "),[].concat(e,[l+o+" | "+r],a?[D+G(" ",u)+" | "+F("",n," ")+G("^",h)]:[])}),[],i).join("\n")}function W(t,e){return["\n","-- PARSING FAILED "+G("-",50),"\n\n",V(t,e),"\n\n",(n=e.expected,1===n.length?"Expected:\n\n"+n[0]:"Expected one of the following: \n\n"+n.join(", ")),"\n"].join("");var n}function _(t){return void 0!==t.flags?t.flags:[t.global?"g":"",t.ignoreCase?"i":"",t.multiline?"m":"",t.unicode?"u":"",t.sticky?"y":""].join("")}function I(){for(var t=[].slice.call(arguments),e=t.length,n=0;n=2?N(e):e=0;var n=function(t){return RegExp("^(?:"+t.source+")",_(t))}(t),i=""+t;return r((function(t,r){var s=n.exec(t.slice(r));if(s){if(0<=e&&e<=s.length){var o=s[0],a=s[e];return E(r+o.length,a)}return S(r,"valid match group (0 to "+s.length+") in "+i)}return S(r,i)}))}function Y(t){return r((function(e,n){return E(n,t)}))}function K(t){return r((function(e,n){return S(n,t)}))}function J(t){if(y(t))return r((function(e,n){var r=t._(e,n);return r.index=n,r.value="",r}));if("string"==typeof t)return J(X(t));if(t instanceof RegExp)return J(Z(t));throw new Error("not a string, regexp, or parser: "+t)}function Q(t){return L(t),r((function(e,n){var r=t._(e,n),i=e.slice(n,r.index);return r.status?S(n,'not "'+i+'"'):E(n,null)}))}function tt(t){return M(t),r((function(e,n){var r=B(e,n);return n=t.length?S(e,"any character/byte"):E(e+1,B(t,e))})),st=r((function(t,e){return E(t.length,t.slice(e))})),ot=r((function(t,e){return e=0})).desc(e)},r.optWhitespace=ht,r.Parser=r,r.range=function(t,e){return tt((function(n){return t<=n&&n<=e})).desc(t+"-"+e)},r.regex=Z,r.regexp=Z,r.sepBy=R,r.sepBy1=U,r.seq=I,r.seqMap=q,r.seqObj=function(){for(var t,e={},n=0,i=(t=arguments,Array.prototype.slice.call(t)),s=i.length,o=0;o255)throw new Error("Value specified to byte constructor ("+t+"=0x"+t.toString(16)+") is larger in value than a single byte.");var e=(t>15?"0x":"0x0")+t.toString(16);return r((function(n,r){var i=B(n,r);return i===t?E(r+1,i):S(r,e)}))},buffer:function(t){return d("buffer",t).map((function(t){return Buffer.from(t)}))},encodedString:function(t,e){return d("string",e).map((function(e){return e.toString(t)}))},uintBE:f,uint8BE:f(1),uint16BE:f(2),uint32BE:f(4),uintLE:m,uint8LE:m(1),uint16LE:m(2),uint32LE:m(4),intBE:g,int8BE:g(1),int16BE:g(2),int32BE:g(4),intLE:b,int8LE:b(1),int16LE:b(2),int32LE:b(4),floatBE:d("floatBE",4).map((function(t){return t.readFloatBE(0)})),floatLE:d("floatLE",4).map((function(t){return t.readFloatLE(0)})),doubleBE:d("doubleBE",8).map((function(t){return t.readDoubleBE(0)})),doubleLE:d("doubleLE",8).map((function(t){return t.readDoubleLE(0)}))},t.exports=r}]));let C=z;class D{InlineWhitespace=t=>C.regex(/[^\S\n]+/).desc("inline whitespace");InlineOptWhitespace=t=>C.regex(/[^\S\n]*/).desc("inline optional whitespace");WhitespaceNewline=t=>C.regex(/[^\S\n]*\n\s*/).desc("whitespace with at least a newline");Null=t=>C.seq(C.string("("),t.InlineOptWhitespace,C.string(")")).map((t=>null)).desc("null: ()");None=t=>C.string("None").map((t=>new y({type:"None",path:""}))).desc("none");Boolean=t=>C.alt(C.string("True"),C.string("False")).map((t=>"True"===t)).desc("either True or False");Number=t=>C.regex(/[\-\+]?[0-9]+(?:\.[0-9]+)?/).map(Number).desc("a number");Integer=t=>C.regex(/[\-\+]?[0-9]+/).map((t=>new x({value:t}))).desc("an integer");String=t=>C.regex(/(?:[^"\\]|\\.)*/).wrap(C.string('"'),C.string('"')).desc('string (with possibility to escape the quote using ")');Word=t=>C.regex(/[a-zA-Z]+/).desc("a word");Guid=t=>C.regex(/[0-9a-zA-Z]{32}/).map((t=>new g({value:t}))).desc("32 digit hexadecimal (accepts all the letters for safety) value");PathSymbolEntity=t=>C.regex(/[0-9a-zA-Z_]+/).map((t=>new v({value:t})));ReferencePath=t=>C.seq(C.string("/"),t.PathSymbolEntity.map((t=>t.toString())).sepBy1(C.string(".")).tieWith(".")).tie().atLeast(2).tie().desc('a path (words with possibly underscore, separated by ".", separated by "/")');Reference=t=>C.alt(t.None,t.ReferencePath.map((t=>new y({type:"",path:t}))),C.seqMap(t.Word,C.optWhitespace,C.alt(C.string('"'),C.string("'\"")).chain((e=>t.ReferencePath.skip(C.string(e.split("").reverse().join(""))))),((t,e,n)=>new y({type:t,path:n}))));AttributeName=t=>t.Word.sepBy1(C.string(".")).tieWith(".").desc('words separated by ""');AttributeAnyValue=t=>C.alt(t.Null,t.None,t.Boolean,t.Number,t.Integer,t.String,t.Guid,t.Reference,t.LocalizedText);LocalizedText=t=>C.seqMap(C.string("NSLOCTEXT").skip(C.optWhitespace).skip(C.string("(")),t.String.trim(C.optWhitespace),C.string(","),t.String.trim(C.optWhitespace),C.string(","),t.String.trim(C.optWhitespace),C.string(")"),((t,e,n,r,i,s,o)=>new b({namespace:e,key:r,value:s})));PinReference=t=>C.seqMap(t.PathSymbolEntity,C.whitespace,t.Guid,((t,e,n)=>new w({objectName:t,pinGuid:n})));static getGrammarForType(t,e,n){switch(o.getType(e)){case Boolean:return t.Boolean;case Number:return t.Number;case x:return t.Integer;case String:return t.String;case g:return t.Guid;case y:return t.Reference;case b:return t.LocalizedText;case w:return t.PinReference;case P:return t.FunctionReference;case E:return t.Pin;case Array:return C.seqMap(C.string("("),e.map((e=>D.getGrammarForType(t,o.getType(e)))).reduce(((e,n)=>n&&e!==t.AttributeAnyValue?e.or(n):t.AttributeAnyValue)).trim(C.optWhitespace).sepBy(C.string(",")).skip(C.regex(/,?\s*/)),C.string(")"),((t,e,n)=>e));default:return n}}static CreateAttributeGrammar=(t,e,n,r=C.string("=").trim(C.optWhitespace))=>e.skip(r).chain((e=>{const r=e.split("."),i=n(r);return D.getGrammarForType(t,i,t.AttributeAnyValue).map((t=>e=>o.objectSet(e,r,t,!0)))}));static CreateMultiAttributeGrammar=(t,e,n,r)=>C.seqMap(C.seq(e,C.optWhitespace,C.string("(")),D.CreateAttributeGrammar(t,t.AttributeName,r).trim(C.optWhitespace).sepBy(C.string(",")).skip(C.regex(/,?/).then(C.optWhitespace)),C.string(")"),((t,e,r)=>{let i=new n;return e.forEach((t=>t(i))),i}));FunctionReference=t=>D.CreateMultiAttributeGrammar(t,C.succeed(),P,(t=>o.objectGet(P.attributes,t)));Pin=t=>D.CreateMultiAttributeGrammar(t,C.string("Pin"),E,(t=>o.objectGet(E.attributes,t)));CustomProperties=t=>C.string("CustomProperties").then(C.whitespace).then(t.Pin).map((t=>e=>{let n=o.objectGet(e,["CustomProperties"],[]);n.push(t),o.objectSet(e,["CustomProperties"],n,!0)}));Object=t=>C.seqMap(C.seq(C.string("Begin"),C.whitespace,C.string("Object"),C.whitespace),C.alt(t.CustomProperties,D.CreateAttributeGrammar(t,t.AttributeName,(t=>o.objectGet(L.attributes,t)))).sepBy1(C.whitespace),C.seq(t.WhitespaceNewline,C.string("End"),C.whitespace,C.string("Object")),((t,e,n)=>{let r=new L;return e.forEach((t=>t(r))),r}));MultipleObject=t=>t.Object.sepBy1(C.whitespace).trim(C.optWhitespace)}class G{static grammar=z.createLanguage(new D);constructor(t,e,n,r,i,s){this.entityType=t,this.prefix=e??"",this.separator=n??",",this.trailingSeparator=r??!1,this.attributeValueConjunctionSign=i??"=",this.attributeKeyPrinter=s??(t=>t.join("."))}writeValue(t){if(null===t)return"()";const e=t=>M.getSerializer(o.getType(t)).write(t);switch(t?.constructor){case Function:return this.writeValue(t());case Boolean:return o.FirstCapital(t.toString());case Number:return t.toString();case String:return`"${t}"`}return t instanceof Array?`(${t.map((t=>e(t)+","))})`:t instanceof m?e(t):void 0}subWrite(t,e){let n="",r=t.concat("");const i=r.length-1;for(const t in e){r[i]=t;const s=e[t];e[t]?.constructor===Object?n+=(n.length?this.separator:"")+this.subWrite(r,s):this.showProperty(r,s)&&(n+=(n.length?this.separator:"")+this.prefix+this.attributeKeyPrinter(r)+this.attributeValueConjunctionSign+this.writeValue(s))}return this.trailingSeparator&&n.length&&0===r.length&&(n+=this.separator),n}showProperty(t,e){const n=this.entityType.attributes,r=o.objectGet(n,t);return!(r instanceof s)||(!o.equals(r.value,e)||r.showDefault)}}class F extends G{constructor(){super(L," ","\n",!1)}showProperty(t,e){switch(t.toString()){case"Class":case"Name":case"CustomProperties":return!1}return super.showProperty(t,e)}read(t){const e=G.grammar.Object.parse(t);return e.status?e.value:(console.error("Error when trying to parse the object."),e)}readMultiple(t){const e=G.grammar.MultipleObject.parse(t);return e.status?e.value:(console.error("Error when trying to parse the object."),e)}write(t){return`Begin Object Class=${this.writeValue(t.Class)} Name=${this.writeValue(t.Name)}\n${this.subWrite([],t)+t.CustomProperties.map((t=>this.separator+this.prefix+"CustomProperties "+M.getSerializer(E).write(t))).join("")}\nEnd Object`}}class H extends i{constructor(t,e,n={}){n.wantsFocusCallback=!0,super(t,e,n),this.serializer=new F;let r=this;this.pasteHandle=t=>r.pasted(t.clipboardData.getData("Text"))}blueprintFocused(){document.body.addEventListener("paste",this.pasteHandle)}blueprintUnfocused(){document.body.removeEventListener("paste",this.pasteHandle)}pasted(t){let e=Number.MAX_SAFE_INTEGER,n=Number.MAX_SAFE_INTEGER,r=this.serializer.readMultiple(t).map((t=>{let r=new A(t);return e=Math.min(e,r.location[1]),n=Math.min(n,r.location[0]),r}));r.length>0&&this.blueprint.unselectAll();let i=this.blueprint.entity.mousePosition;this.blueprint.addNode(...r),r.forEach((t=>{const r=[i[0]-n,i[1]-e];t.addLocation(r),t.setSelected(!0)}))}}class V extends u{constructor(t,e,n){super(t,e,n),this.selectorElement=this.blueprint.selectorElement}startDrag(){this.selectorElement.startSelecting(this.clickedPosition)}dragTo(t,e){this.selectorElement.doSelecting(t)}endDrag(){this.started?this.selectorElement.finishSelecting():this.blueprint.unselectAll()}}class W extends i{constructor(t,e,n={}){n.wantsFocusCallback=!0,super(t,e,n);let r=this;this.clickHandler=t=>r.clickedSomewhere(t),this.blueprint.focuse&&document.addEventListener("click",this.clickHandler)}clickedSomewhere(t){t.target.closest("u-blueprint")||this.blueprint.setFocused(!1)}blueprintFocused(){document.addEventListener("click",this.clickHandler)}blueprintUnfocused(){document.removeEventListener("click",this.clickHandler)}}class _ extends a{constructor(t,e,n){n.wantsFocusCallback=!0,super(t,e,n),this.looseTarget=n?.looseTarget??!0;let r=this;this.mouseWheelHandler=t=>{t.preventDefault();const e=r.getLocation(t);r.wheel(Math.sign(t.deltaY),e)},this.mouseParentWheelHandler=t=>t.preventDefault(),this.blueprint.focused&&this.movementSpace.addEventListener("wheel",this.mouseWheelHandler,!1)}blueprintFocused(){this.movementSpace.addEventListener("wheel",this.mouseWheelHandler,!1),this.movementSpace.parentElement?.addEventListener("wheel",this.mouseParentWheelHandler)}blueprintUnfocused(){this.movementSpace.removeEventListener("wheel",this.mouseWheelHandler,!1),this.movementSpace.parentElement?.removeEventListener("wheel",this.mouseParentWheelHandler)}wheel(t,e){}}class I extends _{wheel(t,e){let n=this.blueprint.getZoom();n-=t,this.blueprint.setZoom(n,e)}}class q extends i{constructor(t,e,n={}){n.wantsFocusCallback=!0,super(t,e,n),this.serializer=new F;let r=this;this.copyHandle=t=>r.copied()}blueprintFocused(){document.body.addEventListener("copy",this.copyHandle)}blueprintUnfocused(){document.body.removeEventListener("copy",this.copyHandle)}copied(){const t=this.blueprint.getNodes(!0).map((t=>this.serializer.write(t.entity))).join("\n");navigator.clipboard.writeText(t)}}class $ extends c{constructor(){super(new t,new r),this.gridElement=null,this.viewportElement=null,this.overlayElement=null,this.selectorElement=null,this.nodesContainerElement=null,this.dragObject=null,this.selectObject=null,this.zoom=0,this.headerElement=null,this.focused=!1,this.nodeBoundariesSupplier=t=>{let e=t.getBoundingClientRect(),n=this.nodesContainerElement.getBoundingClientRect();const r=1/this.getScale();return{primaryInf:(e.left-n.left)*r,primarySup:(e.right-n.right)*r,secondaryInf:(e.top-n.top)*r,secondarySup:(e.bottom-n.bottom)*r}},this.nodeSelectToggleFunction=(t,e)=>{t.setSelected(e)}}connectedCallback(){super.connectedCallback(),this.classList.add("ueb",`ueb-zoom-${this.zoom}`),this.headerElement=this.querySelector(".ueb-viewport-header"),console.assert(this.headerElement,"Header element not provided by the template."),this.overlayElement=this.querySelector(".ueb-viewport-overlay"),console.assert(this.overlayElement,"Overlay element not provided by the template."),this.viewportElement=this.querySelector(".ueb-viewport-body"),console.assert(this.viewportElement,"Viewport element not provided by the template."),this.gridElement=this.viewportElement.querySelector(".ueb-grid"),console.assert(this.gridElement,"Grid element not provided by the template."),this.selectorElement=new p,this.nodesContainerElement=this.querySelector("[data-nodes]"),console.assert(this.nodesContainerElement,"Nodes container element not provided by the template."),this.nodesContainerElement.append(this.selectorElement),this.querySelector("[data-nodes]").append(...this.entity.nodes),this.copyObject=new q(this.getGridDOMElement(),this),this.pasteObject=new H(this.getGridDOMElement(),this),this.dragObject=new l(this.getGridDOMElement(),this,{clickButton:2,moveEverywhere:!0,exitAnyButton:!1}),this.zoomObject=new I(this.getGridDOMElement(),this,{looseTarget:!0}),this.selectObject=new V(this.getGridDOMElement(),this,{clickButton:0,moveEverywhere:!0,exitAnyButton:!0}),this.unfocusObject=new W(this.getGridDOMElement(),this),this.mouseTrackingObject=new f(this.getGridDOMElement(),this)}getGridDOMElement(){return this.gridElement}disconnectedCallback(){super.disconnectedCallback(),setSelected(!1),this.dragObject.unlistenDOMElement(),this.selectObject.unlistenDOMElement(),this.pasteObject.unlistenDOMElement()}getScroll(){return[this.viewportElement.scrollLeft,this.viewportElement.scrollTop]}setScroll(t,e=!1){this.scroll=t,e?this.viewportElement.scroll({left:t[0],top:t[1],behavior:"smooth"}):this.viewportElement.scroll(t[0],t[1])}scrollDelta(t,e=!1){const n=this.getScrollMax();let r=this.getScroll(),i=[r[0]+t[0],r[1]+t[1]],s=[0,0];for(let e=0;e<2;++e)t[e]<0&&i[e]<.25*this.entity.expandGridSize?(s[e]=i[e],s[e]>0&&(s[e]=-this.entity.expandGridSize)):t[e]>0&&i[e]>n[e]-.25*this.entity.expandGridSize&&(s[e]=i[e]-n[e],s[e]<0&&(s[e]=this.entity.expandGridSize));0==s[0]&&0==s[1]||(this.seamlessExpand(this.progressiveSnapToGrid(s[0]),this.progressiveSnapToGrid(s[1])),r=this.getScroll(),i=[r[0]+t[0],r[1]+t[1]]),this.setScroll(i,e)}scrollCenter(){const t=this.getScroll(),e=[this.entity.translateValue[0]-t[0],this.entity.translateValue[1]-t[1]],n=this.getViewportSize().map((t=>t/2)),r=[e[0]-n[0],e[1]-n[1]];this.scrollDelta(r,!0)}getExpandGridSize(){return this.entity.expandGridSize}getViewportSize(){return[this.viewportElement.clientWidth,this.viewportElement.clientHeight]}getScrollMax(){return[this.viewportElement.scrollWidth-this.viewportElement.clientWidth,this.viewportElement.scrollHeight-this.viewportElement.clientHeight]}_expand(t,e){t=Math.round(Math.abs(t)),e=Math.round(Math.abs(e)),this.entity.additional=[this.entity.additional[0]+t,this.entity.additional[1]+e],this.gridElement&&(this.gridElement.style.setProperty("--ueb-additional-x",this.entity.additional[0]),this.gridElement.style.setProperty("--ueb-additional-y",this.entity.additional[1]))}_translate(t,e){t=Math.round(t),e=Math.round(e),this.entity.translateValue=[this.entity.translateValue[0]+t,this.entity.translateValue[1]+e],this.gridElement&&(this.gridElement.style.setProperty("--ueb-translate-x",this.entity.translateValue[0]),this.gridElement.style.setProperty("--ueb-translate-y",this.entity.translateValue[1]))}seamlessExpand(t,e){let n=this.getScale(),r=t/n,i=e/n;this._expand(r,i),this._translate(r<0?-r:0,i<0?-i:0),t<0&&(this.viewportElement.scrollLeft-=t),e<0&&(this.viewportElement.scrollTop-=e)}progressiveSnapToGrid(t){return this.entity.expandGridSize*Math.round(t/this.entity.expandGridSize+.5*Math.sign(t))}getZoom(){return this.zoom}setZoom(t,e){if((t=o.clamp(t,-12,0))==this.zoom)return;let n=this.getScale();if(this.classList.remove(`ueb-zoom-${this.zoom}`),this.classList.add(`ueb-zoom-${t}`),this.zoom=t,e){let t=this.getScale()/n,r=[t*e[0],t*e[1]];this.scrollDelta([(r[0]-e[0])*n,(r[1]-e[1])*n])}}getScale(){return parseFloat(getComputedStyle(this.gridElement).getPropertyValue("--ueb-scale"))}compensateTranslation(t){return t[0]-=this.entity.translateValue[0],t[1]-=this.entity.translateValue[1],t}getNodes(t=!1){return t?this.entity.nodes.filter((t=>t.selected)):this.entity.nodes}unselectAll(){this.entity.nodes.forEach((t=>this.nodeSelectToggleFunction(t,!1)))}addNode(...t){[...t].reduce(((t,e)=>(t.push(e),t)),this.entity.nodes),this.nodesContainerElement&&this.nodesContainerElement.append(...t)}setFocused(t=!0){if(this.focused==t)return;let e=new CustomEvent(t?"blueprintfocus":"blueprintunfocus");this.focused=t,this.dataset.focused=this.focused,this.focused||this.unselectAll(),this.dispatchEvent(e)}}customElements.define("u-blueprint",$);class R extends c{constructor(t,e){super(),this.source=t,this.destination=e}render(){return'\n \n \n \n '}}customElements.define("u-link",R);class U extends G{constructor(t,e,n,r,i,s,o){super(e,n,r,i,s,o),this.wrap=t??(t=>`(${t})`)}read(t){const e=D.getGrammarForType(G.grammar,this.entityType).parse(t);return e.status?e.value:(console.error("Error when trying to parse the entity "+this.entityType.prototype.constructor.name),e)}write(t){return this.wrap(this.subWrite([],t))}}class X extends U{constructor(t,e){super(void 0,e),this.objectWriter=t}write(t){return this.objectWriter(t)}}class Z extends U{constructor(t){super(void 0,t)}write(t){return t.toString()}}M.registerSerializer(L,new F),M.registerSerializer(E,new U((t=>`Pin (${t})`),E,"",",",!0)),M.registerSerializer(P,new U((t=>`(${t})`),P,"",",",!1)),M.registerSerializer(b,new U((t=>`NSLOCTEXT(${t})`),b,"",",",!1,"",(t=>""))),M.registerSerializer(w,new U((t=>t),w,""," ",!1,"",(t=>""))),M.registerSerializer(y,new X((t=>(t.type??"")+(t.path?t.type?`'"${t.path}"'`:t.path:"")))),M.registerSerializer(v,new Z(v)),M.registerSerializer(g,new Z(g)),M.registerSerializer(x,new Z(x));export{$ as Blueprint,R as GraphLink,A as GraphNode}; +const html = String.raw; + +const div = document.createElement("div"); + +function sanitizeText(value) { + div.textContent = value; + value = div.textContent; + div.innerHTML = ""; + return value +} + +/** + * @typedef {import("../graph/GraphElement").default} GraphElement + */ +class Template { + + /** + * Computes the html content of the target element. + * @param {GraphElement} entity Element of the graph + * @returns The result html + */ + render(entity) { + return "" + } + + /** + * Applies the style to the element. + * @param {GraphElement} element Element of the graph + */ + apply(element) { + // TODO replace with the safer element.setHTML(...) when it will be available + element.innerHTML = this.render(element); + } +} + +class OrderedIndexArray { + + /** + * @param {(arrayElement: number) => number} compareFunction A function that, given acouple of elements of the array telles what order are they on. + * @param {(number|array)} value Initial length or array to copy from + */ + constructor(comparisonValueSupplier = (a) => a, value = null) { + this.array = new Uint32Array(value); + this.comparisonValueSupplier = comparisonValueSupplier; + this.length = 0; + this.currentPosition = 0; + } + + /** + * + * @param {number} index The index of the value to return + * @returns The element of the array + */ + get(index) { + if (index >= 0 && index < this.length) { + return this.array[index] + } + return null + } + + /** + * Returns the array used by this object. + * @returns The array. + */ + getArray() { + return this.array + } + + /** + * Get the position that the value supplied should (or does) occupy in the aray. + * @param {number} value The value to look for (it doesn't have to be part of the array). + * @returns The position index. + */ + getPosition(value) { + let l = 0; + let r = this.length; + while (l < r) { + let m = Math.floor((l + r) / 2); + if (this.comparisonValueSupplier(this.array[m]) < value) { + l = m + 1; + } else { + r = m; + } + } + return l + } + + reserve(length) { + if (this.array.length < length) { + let newArray = new Uint32Array(length); + newArray.set(this.array); + this.array = newArray; + } + } + + /** + * Inserts the element in the array. + * @param element {number} The value to insert into the array. + * @returns {number} The position into occupied by value into the array. + */ + insert(element, comparisonValue = null) { + let position = this.getPosition(this.comparisonValueSupplier(element)); + if ( + position < this.currentPosition + || comparisonValue != null && position == this.currentPosition && this.comparisonValueSupplier(element) < comparisonValue) { + ++this.currentPosition; + } + /* + let newArray = new Uint32Array(this.array.length + 1) + newArray.set(this.array.subarray(0, position), 0) + newArray[position] = element + newArray.set(this.array.subarray(position), position + 1) + this.array = newArray + */ + this.shiftRight(position); + this.array[position] = element; + ++this.length; + return position + } + + /** + * Removes the element from the array. + * @param {number} value The value of the element to be remove. + */ + remove(element) { + let position = this.getPosition(this.comparisonValueSupplier(element)); + if (this.array[position] == element) { + this.removeAt(position); + } + } + + /** + * Removes the element into the specified position from the array. + * @param {number} position The index of the element to be remove. + */ + removeAt(position) { + if (position < this.currentPosition) { + --this.currentPosition; + } + /* + let newArray = new Uint32Array(this.array.length - 1) + newArray.set(this.array.subarray(0, position), 0) + newArray.set(this.array.subarray(position + 1), position) + this.array = newArray + */ + this.shiftLeft(position); + --this.length; + return position + } + + getNext() { + if (this.currentPosition >= 0 && this.currentPosition < this.length) { + return this.get(this.currentPosition) + } + return null + } + + getNextValue() { + if (this.currentPosition >= 0 && this.currentPosition < this.length) { + return this.comparisonValueSupplier(this.get(this.currentPosition)) + } else { + return Number.MAX_SAFE_INTEGER + } + } + + getPrev() { + if (this.currentPosition > 0) { + return this.get(this.currentPosition - 1) + } + return null + } + + getPrevValue() { + if (this.currentPosition > 0) { + return this.comparisonValueSupplier(this.get(this.currentPosition - 1)) + } else { + return Number.MIN_SAFE_INTEGER + } + } + + shiftLeft(leftLimit, steps = 1) { + this.array.set(this.array.subarray(leftLimit + steps), leftLimit); + } + + shiftRight(leftLimit, steps = 1) { + this.array.set(this.array.subarray(leftLimit, -steps), leftLimit + steps); + } +} + +class FastSelectionModel { + + /** + * @typedef {{ + * primaryInf: number, + * primarySup: number, + * secondaryInf: number, + * secondarySup: number + * }} BoundariesInfo + * @typedef {{ + * primaryBoundary: number, + * secondaryBoundary: number, + * insertionPosition: number, + * rectangle: number + * onSecondaryAxis: Boolean + * }} Metadata + * @typedef {numeric} Rectangle + * @param {number[]} initialPosition Coordinates of the starting point of selection [primaryAxisValue, secondaryAxisValue]. + * @param {Rectangle[]} rectangles Rectangles that can be selected by this object. + * @param {(rect: Rectangle) => BoundariesInfo} boundariesFunc A function that, given a rectangle, it provides the boundaries of such rectangle. + * @param {(rect: Rectangle, selected: bool) => void} selectFunc A function that selects or deselects individual rectangles. + */ + constructor(initialPosition, rectangles, boundariesFunc, selectFunc) { + this.initialPosition = initialPosition; + this.finalPosition = initialPosition; + /** @type Metadata[] */ + this.metadata = new Array(rectangles.length); + this.primaryOrder = new OrderedIndexArray((element) => this.metadata[element].primaryBoundary); + this.secondaryOrder = new OrderedIndexArray((element) => this.metadata[element].secondaryBoundary); + this.selectFunc = selectFunc; + this.rectangles = rectangles; + this.primaryOrder.reserve(this.rectangles.length); + this.secondaryOrder.reserve(this.rectangles.length); + rectangles.forEach((rect, index) => { + /** @type Metadata */ + let rectangleMetadata = { + primaryBoundary: this.initialPosition[0], + secondaryBoundary: this.initialPosition[1], + rectangle: index, // used to move both expandings inside the this.metadata array + onSecondaryAxis: false + }; + this.metadata[index] = rectangleMetadata; + selectFunc(rect, false); // Initially deselected (Eventually) + const rectangleBoundaries = boundariesFunc(rect); + + // Secondary axis first because it may be inserted in this.secondaryOrder during the primary axis check + if (this.initialPosition[1] < rectangleBoundaries.secondaryInf) { // Initial position is before the rectangle + rectangleMetadata.secondaryBoundary = rectangleBoundaries.secondaryInf; + } else if (rectangleBoundaries.secondarySup < this.initialPosition[1]) { // Initial position is after the rectangle + rectangleMetadata.secondaryBoundary = rectangleBoundaries.secondarySup; + } else { + rectangleMetadata.onSecondaryAxis = true; + } + + if (this.initialPosition[0] < rectangleBoundaries.primaryInf) { // Initial position is before the rectangle + rectangleMetadata.primaryBoundary = rectangleBoundaries.primaryInf; + this.primaryOrder.insert(index); + } else if (rectangleBoundaries.primarySup < this.initialPosition[0]) { // Initial position is after the rectangle + rectangleMetadata.primaryBoundary = rectangleBoundaries.primarySup; + this.primaryOrder.insert(index); + } else { // Initial lays inside the rectangle (considering just this axis) + // Secondary order depends on primary order, if primary boundaries are not satisfied, the element is not watched for secondary ones + if (rectangleBoundaries.secondarySup < this.initialPosition[1] || this.initialPosition[1] < rectangleBoundaries.secondaryInf) { + this.secondaryOrder.insert(index); + } else { + selectFunc(rect, true); + } + } + }); + this.primaryOrder.currentPosition = this.primaryOrder.getPosition(this.initialPosition[0]); + this.secondaryOrder.currentPosition = this.secondaryOrder.getPosition(this.initialPosition[1]); + this.computeBoundaries(this.initialPosition); + } + + computeBoundaries() { + this.boundaries = { + // Primary axis negative expanding + primaryN: { + v: this.primaryOrder.getPrevValue(), + i: this.primaryOrder.getPrev() + }, + primaryP: { + v: this.primaryOrder.getNextValue(), + i: this.primaryOrder.getNext() + }, + // Secondary axis negative expanding + secondaryN: { + v: this.secondaryOrder.getPrevValue(), + i: this.secondaryOrder.getPrev() + }, + // Secondary axis positive expanding + secondaryP: { + v: this.secondaryOrder.getNextValue(), + i: this.secondaryOrder.getNext() + } + }; + } + + selectTo(finalPosition) { + const direction = [ + Math.sign(finalPosition[0] - this.initialPosition[0]), + Math.sign(finalPosition[1] - this.initialPosition[1]) + ]; + const primaryBoundaryCrossed = (index, added) => { + if (this.metadata[index].onSecondaryAxis) { + this.selectFunc(this.rectangles[index], added); + } else { + if (added) { + this.secondaryOrder.insert(index, finalPosition[1]); + const secondaryBoundary = this.metadata[index].secondaryBoundary; + if ( + // If inserted before the current position + Math.sign(finalPosition[1] - secondaryBoundary) == direction[1] + // And after initial position + && Math.sign(secondaryBoundary - this.initialPosition[1]) == direction[1] + ) { + // Secondary axis is already satisfied then + this.selectFunc(this.rectangles[index], true); + } + } else { + this.selectFunc(this.rectangles[index], false); + this.secondaryOrder.remove(index); + } + } + this.computeBoundaries(finalPosition); + this.selectTo(finalPosition); + }; + + if (finalPosition[0] < this.boundaries.primaryN.v) { + --this.primaryOrder.currentPosition; + primaryBoundaryCrossed( + this.boundaries.primaryN.i, + this.initialPosition[0] > this.boundaries.primaryN.v && finalPosition[0] < this.initialPosition[0]); + } else if (finalPosition[0] > this.boundaries.primaryP.v) { + ++this.primaryOrder.currentPosition; + primaryBoundaryCrossed( + this.boundaries.primaryP.i, + this.initialPosition[0] < this.boundaries.primaryP.v && this.initialPosition[0] < finalPosition[0]); + } + + + const secondaryBoundaryCrossed = (index, added) => { + this.selectFunc(this.rectangles[index], added); + this.computeBoundaries(finalPosition); + this.selectTo(finalPosition); + }; + + if (finalPosition[1] < this.boundaries.secondaryN.v) { + --this.secondaryOrder.currentPosition; + secondaryBoundaryCrossed( + this.boundaries.secondaryN.i, + this.initialPosition[1] > this.boundaries.secondaryN.v && finalPosition[1] < this.initialPosition[1]); + } else if (finalPosition[1] > this.boundaries.secondaryP.v) { + ++this.secondaryOrder.currentPosition; + secondaryBoundaryCrossed( + this.boundaries.secondaryP.i, + this.initialPosition[1] < this.boundaries.secondaryP.v && this.initialPosition[1] < finalPosition[1]); + } + this.finalPosition = finalPosition; + } + +} + +class GraphElement extends HTMLElement { + + /** + * + * @param {import("../template/Template").default} template The template to render this node + */ + constructor(entity, template) { + super(); + /** @type {import("../Blueprint").default}" */ + this.blueprint = null; + /** @type {import("../entity/Entity").default}" */ + this.entity = entity; + /** @type {import("../template/Template").default}" */ + this.template = template; + } + + connectedCallback() { + this.blueprint = this.closest("u-blueprint"); + this.template.apply(this); + } +} + +/** + * @typedef {import("../graph/GraphSelector").default} GraphSelector + */ +class SelectorTemplate extends Template { + + /** + * Applies the style to the element. + * @param {GraphSelector} selector Selector element + */ + apply(selector) { + super.apply(selector); + selector.classList.add("ueb-selector"); + selector.dataset.selecting = "false"; + } + + /** + * Applies the style relative to selection beginning. + * @param {GraphSelector} selector Selector element + */ + applyStartSelecting(selector, initialPosition) { + // Set initial position + selector.style.setProperty("--ueb-select-from-x", initialPosition[0]); + selector.style.setProperty("--ueb-select-from-y", initialPosition[1]); + // Final position coincide with the initial position, at the beginning of selection + selector.style.setProperty("--ueb-select-to-x", initialPosition[0]); + selector.style.setProperty("--ueb-select-to-y", initialPosition[1]); + selector.dataset.selecting = "true"; + } + + /** + * Applies the style relative to selection. + * @param {GraphSelector} selector Selector element + */ + applyDoSelecting(selector, finalPosition) { + selector.style.setProperty("--ueb-select-to-x", finalPosition[0]); + selector.style.setProperty("--ueb-select-to-y", finalPosition[1]); + } + + /** + * Applies the style relative to selection finishing. + * @param {GraphSelector} selector Selector element + */ + applyFinishSelecting(selector, finalPosition) { + selector.dataset.selecting = "false"; + } +} + +class GraphSelector extends GraphElement { + + constructor() { + super({}, new SelectorTemplate()); + this.selectionModel = null; + /** @type {SelectorTemplate} */ + this.template; + } + + /** + * Create a selection rectangle starting from the specified position + * @param {number[]} initialPosition - Selection rectangle initial position (relative to the .ueb-grid element) + */ + startSelecting(initialPosition) { + initialPosition = this.blueprint.compensateTranslation(initialPosition); + this.template.applyStartSelecting(this, initialPosition); + this.selectionModel = new FastSelectionModel(initialPosition, this.blueprint.getNodes(), this.blueprint.nodeBoundariesSupplier, this.blueprint.nodeSelectToggleFunction); + } + + /** + * Move selection rectagle to the specified final position. The initial position was specified by startSelecting() + * @param {number[]} finalPosition - Selection rectangle final position (relative to the .ueb-grid element) + */ + doSelecting(finalPosition) { + finalPosition = this.blueprint.compensateTranslation(finalPosition); + this.template.applyDoSelecting(this, finalPosition); + this.selectionModel.selectTo(finalPosition); + } + + finishSelecting() { + this.template.applyFinishSelecting(this); + this.selectionModel = null; + } +} + +customElements.define("u-selector", GraphSelector); + +/** @typedef {import("../Blueprint").default} Blueprint */ +class BlueprintTemplate extends Template { + header(element) { + return html` +
+
1:1
+
+ ` + } + + overlay() { + return html` +
+ ` + } + + /** + * + * @param {Blueprint} element + * @returns + */ + viewport(element) { + return html` +
+
+
+
+
+ ` + } + + /** + * Computes the html content of the target element. + * @param {HTMLElement} element Target element + * @returns The computed html + */ + render(element) { + return html` + ${this.header(element)} + ${this.overlay(element)} + ${this.viewport(element)} + ` + } + + /** + * Applies the style to the element. + * @param {Blueprint} brueprint The blueprint element + */ + apply(blueprint) { + super.apply(blueprint); + blueprint.classList.add("ueb", `ueb-zoom-${blueprint.zoom}`); + blueprint.headerElement = blueprint.querySelector('.ueb-viewport-header'); + blueprint.overlayElement = blueprint.querySelector('.ueb-viewport-overlay'); + blueprint.viewportElement = blueprint.querySelector('.ueb-viewport-body'); + blueprint.gridElement = blueprint.viewportElement.querySelector(".ueb-grid"); + blueprint.nodesContainerElement = blueprint.querySelector("[data-nodes]"); + blueprint.selectorElement = new GraphSelector(); + blueprint.nodesContainerElement.append(blueprint.selectorElement, ...blueprint.nodes); + } + + /** + * Applies the style to the element. + * @param {Blueprint} brueprint The blueprint element + */ + applyZoom(blueprint, newZoom) { + blueprint.classList.remove(`ueb-zoom-${blueprint.zoom}`); + blueprint.classList.add(`ueb-zoom-${newZoom}`); + } + + /** + * Applies the style to the element. + * @param {Blueprint} brueprint The blueprint element + */ + applyExpand(blueprint) { + blueprint.gridElement.style.setProperty("--ueb-additional-x", blueprint.additional[0]); + blueprint.gridElement.style.setProperty("--ueb-additional-y", blueprint.additional[1]); + } + + /** + * Applies the style to the element. + * @param {Blueprint} brueprint The blueprint element + */ + applyTranlate(blueprint) { + blueprint.gridElement.style.setProperty("--ueb-translate-x", blueprint.translateValue[0]); + blueprint.gridElement.style.setProperty("--ueb-translate-y", blueprint.translateValue[1]); + } +} + +class Context { + + constructor(target, blueprint, options) { + /** @type {HTMLElement} */ + this.target = target; + /** @type {import("../Blueprint").default}" */ + this.blueprint = blueprint; + this.options = options; + if (options?.wantsFocusCallback ?? false) { + let self = this; + this.blueprintfocusHandler = _ => self.blueprintFocused(); + this.blueprintunfocusHandler = _ => self.blueprintUnfocused(); + this.blueprint.addEventListener("blueprintfocus", this.blueprintfocusHandler); + this.blueprint.addEventListener("blueprintunfocus", this.blueprintunfocusHandler); + } + } + + unlistenDOMElement() { + this.blueprint.removeEventListener("blueprintfocus", this.blueprintfocusHandler); + this.blueprint.removeEventListener("blueprintunfocus", this.blueprintunfocusHandler); + } + + + /* Subclasses will probabily override the following methods */ + blueprintFocused() { + } + + blueprintUnfocused() { + } +} + +class TypeInitialization { + + static sanitize(value) { + if (!(value instanceof Object)) { + return value // Is already primitive + } + if (value instanceof Boolean || value instanceof Number || value instanceof String) { + return value.valueOf() + } + return value + } + + /** + * + * @param {typeof Object} type + * @param {boolean} showDefault + * @param {*} value + */ + constructor(type, showDefault = true, value = undefined) { + if (value === undefined) { + value = TypeInitialization.sanitize(new type()); + } + this.value = value; + this.showDefault = showDefault; + this.type = type; + } +} + +class Utility { + static clamp(val, min, max) { + return Math.min(Math.max(val, min), max) + } + + static getScale(element) { + return getComputedStyle(element).getPropertyValue("--ueb-scale") + } + + /** + * Sets a value in an object + * @param {String[]} keys The chained keys to access from object in order to set the value + * @param {any} value Value to be set + * @param {Object} target Object holding the data + * @param {Boolean} create Whether to create or not the key in case it doesn't exist + * @returns {Boolean} Returns true on succes, false otherwise + */ + static objectSet(target, keys, value, create = false) { + if (keys.constructor != Array) { + console.error("Expected keys to be an array."); + } + if (keys.length == 1) { + if (create || keys[0] in target) { + target[keys[0]] = value; + return true + } + } else if (keys.length > 0) { + return Utility.objectSet(target[keys[0]], keys.slice(1), value, create) + } + return false + } + + /** + * Gets a value from an object, gives defaultValue in case of failure + * @param {Object} source Object holding the data + * @param {String[]} keys The chained keys to access from object in order to get the value + * @param {any} defaultValue Value to return in case from doesn't have it + * @returns {any} The value in from corresponding to the keys or defaultValue otherwise + */ + static objectGet(source, keys, defaultValue = null) { + if (keys.constructor != Array) { + console.error("Expected keys to be an array."); + } + if (keys.length == 0 || !(keys[0] in source)) { + return defaultValue + } + if (keys.length == 1) { + return source[keys[0]] + } + return Utility.objectGet(source[keys[0]], keys.slice(1), defaultValue) + } + + static equals(a, b) { + a = TypeInitialization.sanitize(a); + b = TypeInitialization.sanitize(b); + return a === b + } + + /** + * + * @param {String} value + */ + static FirstCapital(value) { + return value.charAt(0).toUpperCase() + value.substring(1) + } + + static getType(value) { + let constructor = value?.constructor; + switch (constructor) { + case TypeInitialization: + return value.type + case Function: + return value + default: + return constructor + } + } +} + +class Pointing extends Context { + + constructor(target, blueprint, options) { + super(target, blueprint, options); + this.movementSpace = this.blueprint?.getGridDOMElement() ?? document.documentElement; + } + + /** + * + * @param {MouseEvent} mouseEvent + * @returns + */ + getLocation(mouseEvent) { + const scaleCorrection = 1 / Utility.getScale(this.target); + const targetOffset = this.movementSpace.getBoundingClientRect(); + let location = [ + (mouseEvent.clientX - targetOffset.x) * scaleCorrection, + (mouseEvent.clientY - targetOffset.y) * scaleCorrection + ]; + return location + } +} + +/** + * This class manages the ui gesture of mouse click and drag. Tha actual operations are implemented by the subclasses. + */ +class MouseClickDrag extends Pointing { + + constructor(target, blueprint, options) { + super(target, blueprint, options); + this.clickButton = options?.clickButton ?? 0; + this.exitAnyButton = options?.exitAnyButton ?? true; + this.moveEverywhere = options?.moveEverywhere ?? false; + this.looseTarget = options?.looseTarget ?? false; + this.started = false; + this.clickedPosition = [0, 0]; + + const movementListenedElement = this.moveEverywhere ? document.documentElement : this.movementSpace; + let self = this; + + this.mouseDownHandler = e => { + this.blueprint.setFocused(true); + switch (e.button) { + case self.clickButton: + // Either doesn't matter or consider the click only when clicking on the parent, not descandants + if (self.looseTarget || e.target == e.currentTarget) { + e.stopPropagation(); + self.started = false; + // Attach the listeners + movementListenedElement.addEventListener("mousemove", self.mouseStartedMovingHandler); + document.addEventListener("mouseup", self.mouseUpHandler); + self.clickedPosition = self.getLocation(e); + self.clicked(self.clickedPosition); + } + break + default: + if (!self.exitAnyButton) { + self.mouseUpHandler(e); + } + break + } + }; + + this.mouseStartedMovingHandler = e => { + e.preventDefault(); + e.stopPropagation(); + + // Delegate from now on to self.mouseMoveHandler + movementListenedElement.removeEventListener("mousemove", self.mouseStartedMovingHandler); + movementListenedElement.addEventListener("mousemove", self.mouseMoveHandler); + + // Do actual actions + self.startDrag(); + self.started = true; + }; + + this.mouseMoveHandler = e => { + e.preventDefault(); + e.stopPropagation(); + const location = self.getLocation(e); + const movement = [e.movementX, e.movementY]; + self.dragTo(location, movement); + }; + + this.mouseUpHandler = e => { + if (!self.exitAnyButton || e.button == self.clickButton) { + // Remove the handlers of "mousemove" and "mouseup" + movementListenedElement.removeEventListener("mousemove", self.mouseStartedMovingHandler); + movementListenedElement.removeEventListener("mousemove", self.mouseMoveHandler); + document.removeEventListener("mouseup", self.mouseUpHandler); + self.endDrag(); + } + }; + + this.target.addEventListener("mousedown", this.mouseDownHandler); + if (this.clickButton == 2) { + this.target.addEventListener("contextmenu", this.preventDefault); + } + } + + preventDefault(e) { + e.preventDefault(); + } + + unlistenDOMElement() { + super.unlistenDOMElement(); + this.target.removeEventListener("mousedown", this.mouseDownHandler); + if (this.clickButton == 2) { + this.target.removeEventListener("contextmenu", this.preventDefault); + } blueprintunfocusHandler; + } + + /* Subclasses will override the following methods */ + clicked(location) { + } + + startDrag() { + } + + dragTo(location, movement) { + } + + endDrag() { + } +} + +class DragScroll extends MouseClickDrag { + + dragTo(location, movement) { + this.blueprint.scrollDelta([-movement[0], -movement[1]]); + } +} + +class MouseTracking extends Pointing { + + constructor(target, blueprint, options = {}) { + options.wantsFocusCallback = true; + super(target, blueprint, options); + + let self = this; + this.mousemoveHandler = e => { + self.blueprint.entity.mousePosition = self.getLocation(e); + }; + } + + blueprintFocused() { + this.target.addEventListener("mousemove", this.mousemoveHandler); + } + + blueprintUnfocused() { + this.target.removeEventListener("mousemove", this.mousemoveHandler); + } +} + +class Entity { + + constructor(options = {}) { + /** + * + * @param {String[]} prefix + * @param {Object} target + * @param {Object} properties + */ + const defineAllAttributes = (prefix, target, properties) => { + let fullKey = prefix.concat(""); + const last = fullKey.length - 1; + for (let property in properties) { + fullKey[last] = property; + // Not instanceof because all objects are instenceof Object, exact match needed + if (properties[property]?.constructor === Object) { + target[property] = {}; + defineAllAttributes(fullKey, target[property], properties[property]); + continue + } + /* + * The value can either be: + * - Array: can contain multiple values, its property is assigned multiple times like (X=1, X=4, X="Hello World") + * - TypeInitialization: contains the maximum amount of information about the attribute. + * - A type: the default value will be default constructed object without arguments. + * - A proper value. + */ + const value = Utility.objectGet(options, fullKey); + if (value !== null) { + target[property] = value; + continue + } + let defaultValue = properties[property]; + if (defaultValue instanceof TypeInitialization) { + if (!defaultValue.showDefault) { + continue + } + defaultValue = defaultValue.value; + } + if (defaultValue instanceof Array) { + target[property] = []; + continue + } + if (defaultValue instanceof Function) { + defaultValue = TypeInitialization.sanitize(new defaultValue()); + } + target[property] = defaultValue; + } + }; + defineAllAttributes([], this, this.getAttributes()); + } +} + +class GuidEntity extends Entity { + + static attributes = { + value: String + } + + static generateGuid(random = true) { + let values = new Uint32Array(4); + if (random === true) { + crypto.getRandomValues(values); + } + let guid = ""; + values.forEach(n => { + guid += ("0".repeat(8) + n.toString(16).toUpperCase()).slice(-8); + }); + return new GuidEntity({ valud: guid }) + } + + getAttributes() { + return GuidEntity.attributes + } + + toString() { + return this.value + } +} + +class LocalizedTextEntity extends Entity { + + static attributes = { + namespace: String, + key: String, + value: String + } + + getAttributes() { + return LocalizedTextEntity.attributes + } +} + +class ObjectReferenceEntity extends Entity { + + static attributes = { + type: String, + path: String + } + + getAttributes() { + return ObjectReferenceEntity.attributes + } +} + +class PathSymbolEntity extends Entity { + + static attributes = { + value: String + } + + getAttributes() { + return PathSymbolEntity.attributes + } + + toString() { + return this.value + } +} + +class PinReferenceEntity extends Entity { + + static attributes = { + objectName: PathSymbolEntity, + pinGuid: GuidEntity + } + + getAttributes() { + return PinReferenceEntity.attributes + } +} + +class PinEntity extends Entity { + + static attributes = { + PinId: GuidEntity, + PinName: "", + PinFriendlyName: new TypeInitialization(LocalizedTextEntity, false, null), + PinToolTip: "", + Direction: new TypeInitialization(String, false, ""), + PinType: { + PinCategory: "", + PinSubCategory: "", + PinSubCategoryObject: ObjectReferenceEntity, + PinSubCategoryMemberReference: null, + PinValueType: null, + ContainerType: ObjectReferenceEntity, + bIsReference: false, + bIsConst: false, + bIsWeakPointer: false, + bIsUObjectWrapper: false + }, + LinkedTo: [PinReferenceEntity], + DefaultValue: "", + AutogeneratedDefaultValue: "", + PersistentGuid: GuidEntity, + bHidden: false, + bNotConnectable: false, + bDefaultValueIsReadOnly: false, + bDefaultValueIsIgnored: false, + bAdvancedView: false, + bOrphanedPin: false, + } + + getAttributes() { + return PinEntity.attributes + } + + /** + * + * @returns {String} + */ + getPinDisplayName() { + return this.PinName + } + + isConnected() { + return this.LinkedTo.length > 0 + } + + getType() { + return this.PinType.PinCategory ?? "object" + } + + isInput() { + if (!this.bHidden && this.Direction !== "EGPD_Output") { + return true + } + } + + isOutput() { + if (!this.bHidden && this.Direction === "EGPD_Output") { + return true + } + } +} + +/** + * @typedef {import("../graph/SelectableDraggable").default} SelectableDraggable + */ +class SelectableDraggableTemplate extends Template { + + /** + * Returns the html elements rendered from this template. + * @param {SelectableDraggable} element Element of the graph + */ + applyLocation(element) { + element.style.setProperty("--ueb-position-x", element.location[0]); + element.style.setProperty("--ueb-position-y", element.location[1]); + } + + /** + * Returns the html elements rendered from this template. + * @param {SelectableDraggable} element Element of the graph + */ + applySelected(element) { + if (element.selected) { + element.classList.add("ueb-selected"); + } else { + element.classList.remove("ueb-selected"); + } + } +} + +/** + * @typedef {import("../graph/GraphNode").default} GraphNode + */ +class NodeTemplate extends SelectableDraggableTemplate { + + /** + * Computes the html content of the target element. + * @param {GraphNode} node Graph node element + * @returns The result html + */ + header(node) { + return html` +
+ + + ${node.entity.getNodeDisplayName()} + +
+ ` + } + + /** + * Computes the html content of the target element. + * @param {GraphNode} node Graph node element + * @returns The result html + */ + body(node) { + /** @type {PinEntity[]} */ + let inputs = node.entity.CustomProperties.filter(v => v instanceof PinEntity); + let outputs = inputs.filter(v => v.isOutput()); + inputs = inputs.filter(v => v.isInput()); + return html` +
+
+ ${inputs.map((input, index) => html` +
+ + ${input.getPinDisplayName()} +
+ `).join("") ?? ""} +
+
+ ${outputs.map((output, index) => html` +
+ ${output.getPinDisplayName()} + +
+ `).join('') ?? ''} +
+
+ ` + } + + /** + * Computes the html content of the target element. + * @param {GraphNode} node Graph node element + * @returns The result html + */ + render(node) { + return html` +
+
+ ${this.header(node)} + ${this.body(node)} +
+
+ ` + } + + /** + * Returns the html elements rendered from this template. + * @param {GraphNode} node Element of the graph + */ + apply(node) { + super.apply(node); + node.classList.add("ueb-node"); + if (node.selected) { + node.classList.add("ueb-selected"); + } + node.style.setProperty("--ueb-position-x", node.location[0]); + node.style.setProperty("--ueb-position-y", node.location[1]); + } +} + +class FunctionReferenceEntity extends Entity { + + static attributes = { + MemberParent: ObjectReferenceEntity, + MemberName: "" + } + + getAttributes() { + return FunctionReferenceEntity.attributes + } +} + +class IntegerEntity extends Entity { + + static attributes = { + value: Number + } + + getAttributes() { + return IntegerEntity.attributes + } + + constructor(options = {}) { + super(options); + this.value = Math.round(this.value); + } + + valueOf() { + return this.value + } + + toString() { + return this.value.toString() + } +} + +class VariableReferenceEntity extends Entity { + + static attributes = { + MemberName: String, + MemberGuid: GuidEntity, + bSelfContext: false + } + + getAttributes() { + return VariableReferenceEntity.attributes + } +} + +class ObjectEntity extends Entity { + + static attributes = { + Class: ObjectReferenceEntity, + Name: "", + bIsPureFunc: new TypeInitialization(Boolean, false, false), + VariableReference: new TypeInitialization(VariableReferenceEntity, false, null), + FunctionReference: new TypeInitialization(FunctionReferenceEntity, false, null,), + EventReference: new TypeInitialization(FunctionReferenceEntity, false, null,), + TargetType: new TypeInitialization(ObjectReferenceEntity, false, null), + NodePosX: IntegerEntity, + NodePosY: IntegerEntity, + NodeGuid: GuidEntity, + ErrorType: new TypeInitialization(IntegerEntity, false), + ErrorMsg: new TypeInitialization(String, false, ""), + CustomProperties: [PinEntity] + } + + getAttributes() { + return ObjectEntity.attributes + } + + /** + * + * @returns {String} The name of the node + */ + getNodeDisplayName() { + return this.Name + } +} + +class Drag extends MouseClickDrag { + + constructor(target, blueprint, options) { + super(target, blueprint, options); + this.stepSize = parseInt(options?.stepSize); + this.mousePosition = [0, 0]; + } + + snapToGrid(location) { + return [ + this.stepSize * Math.round(location[0] / this.stepSize), + this.stepSize * Math.round(location[1] / this.stepSize) + ] + } + + startDrag() { + if (isNaN(this.stepSize) || this.stepSize <= 0) { + this.stepSize = parseInt(getComputedStyle(this.target).getPropertyValue("--ueb-grid-snap")); + if (isNaN(this.stepSize) || this.stepSize <= 0) { + this.stepSize = 1; + } + } + // Get the current mouse position + this.mousePosition = this.stepSize != 1 ? this.snapToGrid(this.clickedPosition) : this.clickedPosition; + } + + dragTo(location, movement) { + const mousePosition = this.stepSize != 1 ? this.snapToGrid(location) : location; + const d = [mousePosition[0] - this.mousePosition[0], mousePosition[1] - this.mousePosition[1]]; + + if (d[0] == 0 && d[1] == 0) { + return + } + + this.target.dispatchDragEvent(d); + + // Reassign the position of mouse + this.mousePosition = mousePosition; + } +} + +class SelectableDraggable extends GraphElement { + + constructor(...args) { + super(...args); + this.dragObject = null; + this.location = [0, 0]; + this.selected = false; + /** @type {import("../template/SelectableDraggableTemplate").default} */ + this.template; + + let self = this; + this.dragHandler = (e) => { + self.addLocation(e.detail.value); + }; + } + + connectedCallback() { + super.connectedCallback(); + this.dragObject = new Drag(this, this.blueprint, { + looseTarget: true + }); + } + + disconnectedCallback() { + this.dragObject.unlistenDOMElement(); + } + + setLocation(value = [0, 0]) { + this.location = value; + this.template.applyLocation(this); + } + + addLocation(value) { + this.setLocation([this.location[0] + value[0], this.location[1] + value[1]]); + } + + dispatchDragEvent(value) { + if (!this.selected) { + this.blueprint.unselectAll(); + this.setSelected(true); + } + let dragEvent = new CustomEvent("uDragSelected", { + detail: { + instigator: this, + value: value + }, + bubbles: false, + cancelable: true, + composed: false, + }); + this.blueprint.dispatchEvent(dragEvent); + } + + setSelected(value = true) { + if (this.selected == value) { + return + } + this.selected = value; + if (this.selected) { + this.blueprint.addEventListener("uDragSelected", this.dragHandler); + } else { + this.blueprint.removeEventListener("uDragSelected", this.dragHandler); + } + this.template.applySelected(this); + } +} + +class SerializerFactory { + + static #serializers = new Map() + + static registerSerializer(entity, object) { + SerializerFactory.#serializers.set(entity, object); + } + + static getSerializer(entity) { + return SerializerFactory.#serializers.get(Utility.getType(entity)) + } +} + +class DragLink extends MouseClickDrag { + + constructor(target, blueprint, options) { + super(target, blueprint, options); + } + + startDrag() { + //this.selectorElement.startSelecting(this.clickedPosition) + } + + dragTo(location, movement) { + //this.selectorElement.doSelecting(location) + } + + endDrag() { + if (this.started) ; + } +} + +class GraphNode extends SelectableDraggable { + + /** + * + * @param {ObjectEntity} entity + */ + constructor(entity) { + super(entity, new NodeTemplate()); + /** @type {ObjectEntity} */ + this.entity; + this.graphNodeName = "n/a"; + this.dragLinkObjects = []; + super.setLocation([this.entity.NodePosX, this.entity.NodePosY]); + } + + static fromSerializedObject(str) { + let entity = SerializerFactory.getSerializer(ObjectEntity).read(str); + return new GraphNode(entity) + } + + connectedCallback() { + this.getAttribute("type")?.trim(); + super.connectedCallback(); + this.querySelectorAll(".ueb-node-input, .ueb-node-output").forEach(element => { + this.dragLinkObjects.push( + new DragLink(element, this.blueprint, { + clickButton: 0, + moveEverywhere: true, + exitAnyButton: true, + looseTarget: true + }) + ); + }); + } + + setLocation(value = [0, 0]) { + this.entity.NodePosX = value[0]; + this.entity.NodePosY = value[1]; + super.setLocation(value); + } +} + +customElements.define("u-node", GraphNode); + +var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + +function getDefaultExportFromCjs (x) { + return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; +} + +var parsimmon_umd_min = {exports: {}}; + +(function (module, exports) { +!function(n,t){module.exports=t();}("undefined"!=typeof self?self:commonjsGlobal,function(){return function(n){var t={};function r(e){if(t[e])return t[e].exports;var u=t[e]={i:e,l:!1,exports:{}};return n[e].call(u.exports,u,u.exports,r),u.l=!0,u.exports}return r.m=n,r.c=t,r.d=function(n,t,e){r.o(n,t)||Object.defineProperty(n,t,{configurable:!1,enumerable:!0,get:e});},r.r=function(n){Object.defineProperty(n,"__esModule",{value:!0});},r.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return r.d(t,"a",t),t},r.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)},r.p="",r(r.s=0)}([function(n,t,r){function e(n){if(!(this instanceof e))return new e(n);this._=n;}var u=e.prototype;function o(n,t){for(var r=0;r>7),buf:function(n){var t=i(function(n,t,r,e){return n.concat(r===e.length-1?Buffer.from([t,0]).readUInt16BE(0):e.readUInt16BE(r))},[],n);return Buffer.from(f(function(n){return (n<<1&65535)>>8},t))}(r.buf)};}),r}function c(){return "undefined"!=typeof Buffer}function s(){if(!c())throw new Error("Buffer global does not exist; please use webpack if you need to parse Buffers in the browser.")}function l(n){s();var t=i(function(n,t){return n+t},0,n);if(t%8!=0)throw new Error("The bits ["+n.join(", ")+"] add up to "+t+" which is not an even number of bytes; the total should be divisible by 8");var r,u=t/8,o=(r=function(n){return n>48},i(function(n,t){return n||(r(t)?t:n)},null,n));if(o)throw new Error(o+" bit range requested exceeds 48 bit (6 byte) Number max.");return new e(function(t,r){var e=u+r;return e>t.length?x(r,u.toString()+" bytes"):b(e,i(function(n,t){var r=a(t,n.buf);return {coll:n.coll.concat(r.v),buf:r.buf}},{coll:[],buf:t.slice(r,e)},n).coll)})}function p(n,t){return new e(function(r,e){return s(),e+t>r.length?x(e,t+" bytes for "+n):b(e+t,r.slice(e,e+t))})}function h(n,t){if("number"!=typeof(r=t)||Math.floor(r)!==r||t<0||t>6)throw new Error(n+" requires integer length in range [0, 6].");var r;}function d(n){return h("uintBE",n),p("uintBE("+n+")",n).map(function(t){return t.readUIntBE(0,n)})}function v(n){return h("uintLE",n),p("uintLE("+n+")",n).map(function(t){return t.readUIntLE(0,n)})}function g(n){return h("intBE",n),p("intBE("+n+")",n).map(function(t){return t.readIntBE(0,n)})}function m(n){return h("intLE",n),p("intLE("+n+")",n).map(function(t){return t.readIntLE(0,n)})}function y(n){return n instanceof e}function E(n){return "[object Array]"==={}.toString.call(n)}function w(n){return c()&&Buffer.isBuffer(n)}function b(n,t){return {status:!0,index:n,value:t,furthest:-1,expected:[]}}function x(n,t){return E(t)||(t=[t]),{status:!1,index:-1,value:null,furthest:n,expected:t}}function B(n,t){if(!t)return n;if(n.furthest>t.furthest)return n;var r=n.furthest===t.furthest?function(n,t){if(function(){if(void 0!==e._supportsSet)return e._supportsSet;var n="undefined"!=typeof Set;return e._supportsSet=n,n}()&&Array.from){for(var r=new Set(n),u=0;u=0;){if(i in r){e=r[i].line,0===o&&(o=r[i].lineStart);break}"\n"===n.charAt(i)&&(u++,0===o&&(o=i+1)),i--;}var f=e+u,a=t-o;return r[t]={line:f,lineStart:o},{offset:t,line:f+1,column:a+1}}function _(n){if(!y(n))throw new Error("not a parser: "+n)}function L(n,t){return "string"==typeof n?n.charAt(t):n[t]}function O(n){if("number"!=typeof n)throw new Error("not a number: "+n)}function k(n){if("function"!=typeof n)throw new Error("not a function: "+n)}function P(n){if("string"!=typeof n)throw new Error("not a string: "+n)}var q=2,A=3,I=8,F=5*I,M=4*I,z=" ";function R(n,t){return new Array(t+1).join(n)}function U(n,t,r){var e=t-n.length;return e<=0?n:R(r,e)+n}function W(n,t,r,e){return {from:n-t>0?n-t:0,to:n+r>e?e:n+r}}function D(n,t){var r,e,u,o,a,c=t.index,s=c.offset,l=1;if(s===n.length)return "Got the end of the input";if(w(n)){var p=s-s%I,h=s-p,d=W(p,F,M+I,n.length),v=f(function(n){return f(function(n){return U(n.toString(16),2,"0")},n)},function(n,t){var r=n.length,e=[],u=0;if(r<=t)return [n.slice()];for(var o=0;o=4&&(r+=1),l=2,u=f(function(n){return n.length<=4?n.join(" "):n.slice(0,4).join(" ")+" "+n.slice(4).join(" ")},v),(a=(8*(o.to>0?o.to-1:o.to)).toString(16).length)<2&&(a=2);}else {var g=n.split(/\r\n|[\n\r\u2028\u2029]/);r=c.column-1,e=c.line-1,o=W(e,q,A,g.length),u=g.slice(o.from,o.to),a=o.to.toString().length;}var m=e-o.from;return w(n)&&(a=(8*(o.to>0?o.to-1:o.to)).toString(16).length)<2&&(a=2),i(function(t,e,u){var i,f=u===m,c=f?"> ":z;return i=w(n)?U((8*(o.from+u)).toString(16),a,"0"):U((o.from+u+1).toString(),a," "),[].concat(t,[c+i+" | "+e],f?[z+R(" ",a)+" | "+U("",r," ")+R("^",l)]:[])},[],u).join("\n")}function N(n,t){return ["\n","-- PARSING FAILED "+R("-",50),"\n\n",D(n,t),"\n\n",(r=t.expected,1===r.length?"Expected:\n\n"+r[0]:"Expected one of the following: \n\n"+r.join(", ")),"\n"].join("");var r;}function G(n){return void 0!==n.flags?n.flags:[n.global?"g":"",n.ignoreCase?"i":"",n.multiline?"m":"",n.unicode?"u":"",n.sticky?"y":""].join("")}function C(){for(var n=[].slice.call(arguments),t=n.length,r=0;r=2?O(t):t=0;var r=function(n){return RegExp("^(?:"+n.source+")",G(n))}(n),u=""+n;return e(function(n,e){var o=r.exec(n.slice(e));if(o){if(0<=t&&t<=o.length){var i=o[0],f=o[t];return b(e+i.length,f)}return x(e,"valid match group (0 to "+o.length+") in "+u)}return x(e,u)})}function X(n){return e(function(t,r){return b(r,n)})}function Y(n){return e(function(t,r){return x(r,n)})}function Z(n){if(y(n))return e(function(t,r){var e=n._(t,r);return e.index=r,e.value="",e});if("string"==typeof n)return Z(K(n));if(n instanceof RegExp)return Z(Q(n));throw new Error("not a string, regexp, or parser: "+n)}function $(n){return _(n),e(function(t,r){var e=n._(t,r),u=t.slice(r,e.index);return e.status?x(r,'not "'+u+'"'):b(r,null)})}function nn(n){return k(n),e(function(t,r){var e=L(t,r);return r=n.length?x(t,"any character/byte"):b(t+1,L(n,t))}),on=e(function(n,t){return b(n.length,n.slice(t))}),fn=e(function(n,t){return t=0}).desc(t)},e.optWhitespace=pn,e.Parser=e,e.range=function(n,t){return nn(function(r){return n<=r&&r<=t}).desc(n+"-"+t)},e.regex=Q,e.regexp=Q,e.sepBy=V,e.sepBy1=H,e.seq=C,e.seqMap=J,e.seqObj=function(){for(var n,t={},r=0,u=(n=arguments,Array.prototype.slice.call(n)),o=u.length,i=0;i255)throw new Error("Value specified to byte constructor ("+n+"=0x"+n.toString(16)+") is larger in value than a single byte.");var t=(n>15?"0x":"0x0")+n.toString(16);return e(function(r,e){var u=L(r,e);return u===n?b(e+1,u):x(e,t)})},buffer:function(n){return p("buffer",n).map(function(n){return Buffer.from(n)})},encodedString:function(n,t){return p("string",t).map(function(t){return t.toString(n)})},uintBE:d,uint8BE:d(1),uint16BE:d(2),uint32BE:d(4),uintLE:v,uint8LE:v(1),uint16LE:v(2),uint32LE:v(4),intBE:g,int8BE:g(1),int16BE:g(2),int32BE:g(4),intLE:m,int8LE:m(1),int16LE:m(2),int32LE:m(4),floatBE:p("floatBE",4).map(function(n){return n.readFloatBE(0)}),floatLE:p("floatLE",4).map(function(n){return n.readFloatLE(0)}),doubleBE:p("doubleBE",8).map(function(n){return n.readDoubleBE(0)}),doubleLE:p("doubleLE",8).map(function(n){return n.readDoubleLE(0)})},n.exports=e;}])}); +}(parsimmon_umd_min)); + +var Parsimmon = /*@__PURE__*/getDefaultExportFromCjs(parsimmon_umd_min.exports); + +let P = Parsimmon; + +class Grammar { + // General + InlineWhitespace = _ => P.regex(/[^\S\n]+/).desc("inline whitespace") + InlineOptWhitespace = _ => P.regex(/[^\S\n]*/).desc("inline optional whitespace") + WhitespaceNewline = _ => P.regex(/[^\S\n]*\n\s*/).desc("whitespace with at least a newline") + Null = r => P.seq(P.string("("), r.InlineOptWhitespace, P.string(")")).map(_ => null).desc("null: ()") + None = _ => P.string("None").map(_ => new ObjectReferenceEntity({ type: "None", path: "" })).desc("none") + Boolean = _ => P.alt(P.string("True"), P.string("False")).map(v => v === "True" ? true : false).desc("either True or False") + Number = _ => P.regex(/[\-\+]?[0-9]+(?:\.[0-9]+)?/).map(Number).desc("a number") + Integer = _ => P.regex(/[\-\+]?[0-9]+/).map(v => new IntegerEntity({ value: v })).desc("an integer") + String = _ => P.regex(/(?:[^"\\]|\\.)*/).wrap(P.string('"'), P.string('"')).desc('string (with possibility to escape the quote using \")') + Word = _ => P.regex(/[a-zA-Z]+/).desc("a word") + Guid = _ => P.regex(/[0-9a-zA-Z]{32}/).map(v => new GuidEntity({ value: v })).desc("32 digit hexadecimal (accepts all the letters for safety) value") + PathSymbolEntity = _ => P.regex(/[0-9a-zA-Z_]+/).map(v => new PathSymbolEntity({ value: v })) + ReferencePath = r => P.seq(P.string("/"), r.PathSymbolEntity.map(v => v.toString()).sepBy1(P.string(".")).tieWith(".")) + .tie() + .atLeast(2) + .tie() + .desc('a path (words with possibly underscore, separated by ".", separated by "/")') + Reference = r => P.alt( + r.None, + r.ReferencePath.map(path => new ObjectReferenceEntity({ type: "", path: path })), + P.seqMap( + r.Word, + P.optWhitespace, + P.alt(P.string(`"`), P.string(`'"`)).chain( + result => r.ReferencePath.skip( + P.string(result.split("").reverse().join("")) + ) + ), + (referenceType, _, referencePath) => new ObjectReferenceEntity({ type: referenceType, path: referencePath }) + ) + ) + AttributeName = r => r.Word.sepBy1(P.string(".")).tieWith(".").desc('words separated by ""') + AttributeAnyValue = r => P.alt(r.Null, r.None, r.Boolean, r.Number, r.Integer, r.String, r.Guid, r.Reference, r.LocalizedText) + LocalizedText = r => P.seqMap( + P.string("NSLOCTEXT").skip(P.optWhitespace).skip(P.string("(")), + r.String.trim(P.optWhitespace), // namespace + P.string(","), + r.String.trim(P.optWhitespace), // key + P.string(","), + r.String.trim(P.optWhitespace), // value + P.string(")"), + (_, namespace, __, key, ___, value, ____) => new LocalizedTextEntity({ + namespace: namespace, + key: key, + value: value + }) + ) + PinReference = r => P.seqMap( + r.PathSymbolEntity, + P.whitespace, + r.Guid, + (objectName, _, pinGuid) => new PinReferenceEntity({ + objectName: objectName, + pinGuid: pinGuid + }) + ) + static getGrammarForType(r, attributeType, defaultGrammar) { + switch (Utility.getType(attributeType)) { + case Boolean: + return r.Boolean + case Number: + return r.Number + case IntegerEntity: + return r.Integer + case String: + return r.String + case GuidEntity: + return r.Guid + case ObjectReferenceEntity: + return r.Reference + case LocalizedTextEntity: + return r.LocalizedText + case PinReferenceEntity: + return r.PinReference + case FunctionReferenceEntity: + return r.FunctionReference + case PinEntity: + return r.Pin + case Array: + return P.seqMap( + P.string("("), + attributeType + .map(v => Grammar.getGrammarForType(r, Utility.getType(v))) + .reduce((accum, cur) => + !cur || accum === r.AttributeAnyValue + ? r.AttributeAnyValue + : accum.or(cur) + ) + .trim(P.optWhitespace) + .sepBy(P.string(",")) + .skip(P.regex(/,?\s*/)), + P.string(")"), + (_, grammar, __) => grammar + ) + default: + return defaultGrammar + } + } + // Meta grammar + static CreateAttributeGrammar = (r, attributeGrammar, attributeSupplier, valueSeparator = P.string("=").trim(P.optWhitespace)) => + attributeGrammar.skip(valueSeparator) + .chain(attributeName => { + const attributeKey = attributeName.split("."); + const attribute = attributeSupplier(attributeKey); + let attributeValueGrammar = Grammar.getGrammarForType(r, attribute, r.AttributeAnyValue); + return attributeValueGrammar.map(attributeValue => + entity => Utility.objectSet(entity, attributeKey, attributeValue, true) + ) // returns attributeSetter: a function called with an object as argument that will set the correct attribute value + }) + // Meta grammar + static CreateMultiAttributeGrammar = (r, keyGrammar, entityType, attributeSupplier) => + /** + * Basically this creates a parser that looks for a string like 'Key (A=False,B="Something",)' + * Then it populates an object of type EntityType with the attribute values found inside the parentheses. + */ + P.seqMap( + P.seq(keyGrammar, P.optWhitespace, P.string("(")), + Grammar.CreateAttributeGrammar(r, r.AttributeName, attributeSupplier) + .trim(P.optWhitespace) + .sepBy(P.string(",")) + .skip(P.regex(/,?/).then(P.optWhitespace)), // Optional trailing comma + P.string(')'), + (_, attributes, __) => { + let result = new entityType(); + attributes.forEach(attributeSetter => attributeSetter(result)); + return result + }) + FunctionReference = r => Grammar.CreateMultiAttributeGrammar( + r, + P.succeed(), + FunctionReferenceEntity, + attributeKey => Utility.objectGet(FunctionReferenceEntity.attributes, attributeKey) + ) + Pin = r => Grammar.CreateMultiAttributeGrammar( + r, + P.string("Pin"), + PinEntity, + attributeKey => Utility.objectGet(PinEntity.attributes, attributeKey) + ) + CustomProperties = r => + P.string("CustomProperties") + .then(P.whitespace) + .then(r.Pin) + .map(pin => entity => { + /** @type {Array} */ + let properties = Utility.objectGet(entity, ["CustomProperties"], []); + properties.push(pin); + Utility.objectSet(entity, ["CustomProperties"], properties, true); + }) + + Object = r => P.seqMap( + P.seq(P.string("Begin"), P.whitespace, P.string("Object"), P.whitespace), + P + .alt( + r.CustomProperties, + Grammar.CreateAttributeGrammar(r, r.AttributeName, attributeKey => Utility.objectGet(ObjectEntity.attributes, attributeKey)) + ) + .sepBy1(P.whitespace), + P.seq(r.WhitespaceNewline, P.string("End"), P.whitespace, P.string("Object")), + (_, attributes, __) => { + let result = new ObjectEntity(); + attributes.forEach(attributeSetter => attributeSetter(result)); + return result + } + ) + MultipleObject = r => r.Object.sepBy1(P.whitespace).trim(P.optWhitespace) +} + +class Serializer { + + static grammar = Parsimmon.createLanguage(new Grammar()) + + constructor(entityType, prefix, separator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter) { + this.entityType = entityType; + this.prefix = prefix ?? ""; + this.separator = separator ?? ","; + this.trailingSeparator = trailingSeparator ?? false; + this.attributeValueConjunctionSign = attributeValueConjunctionSign ?? "="; + this.attributeKeyPrinter = attributeKeyPrinter ?? (k => k.join(".")); + } + + writeValue(value) { + if (value === null) { + return "()" + } + const serialize = v => SerializerFactory.getSerializer(Utility.getType(v)).write(v); + // This is an exact match (and not instanceof) to hit also primitive types (by accessing value.constructor they are converted to objects automatically) + switch (value?.constructor) { + case Function: + return this.writeValue(value()) + case Boolean: + return Utility.FirstCapital(value.toString()) + case Number: + return value.toString() + case String: + return `"${value}"` + } + if (value instanceof Array) { + return `(${value.map(v => serialize(v) + ",")})` + } + if (value instanceof Entity) { + return serialize(value) + } + } + + subWrite(key, object) { + let result = ""; + let fullKey = key.concat(""); + const last = fullKey.length - 1; + for (const property in object) { + fullKey[last] = property; + const value = object[property]; + if (object[property]?.constructor === Object) { + // Recursive call when finding an object + result += (result.length ? this.separator : "") + + this.subWrite(fullKey, value); + } else if (this.showProperty(fullKey, value)) { + result += (result.length ? this.separator : "") + + this.prefix + + this.attributeKeyPrinter(fullKey) + + this.attributeValueConjunctionSign + + this.writeValue(value); + } + } + if (this.trailingSeparator && result.length && fullKey.length === 0) { + // append separator at the end if asked and there was printed content + result += this.separator; + } + return result + } + + showProperty(attributeKey, attributeValue) { + const attributes = this.entityType.attributes; + const attribute = Utility.objectGet(attributes, attributeKey); + if (attribute instanceof TypeInitialization) { + return !Utility.equals(attribute.value, attributeValue) || attribute.showDefault + } + return true + } +} + +class ObjectSerializer extends Serializer { + + constructor() { + super(ObjectEntity, " ", "\n", false); + } + + showProperty(attributeKey, attributeValue) { + switch (attributeKey.toString()) { + case "Class": + case "Name": + case "CustomProperties": + // Serielized separately + return false + } + return super.showProperty(attributeKey, attributeValue) + } + + read(value) { + const parseResult = Serializer.grammar.Object.parse(value); + if (!parseResult.status) { + console.error("Error when trying to parse the object."); + return parseResult + } + return parseResult.value + } + + /** + * + * @param {String} value + * @returns {ObjectEntity[]} + */ + readMultiple(value) { + const parseResult = Serializer.grammar.MultipleObject.parse(value); + if (!parseResult.status) { + console.error("Error when trying to parse the object."); + return parseResult + } + return parseResult.value + } + + /** + * + * @param {ObjectEntity} object + * @returns + */ + write(object) { + let result = `Begin Object Class=${this.writeValue(object.Class)} Name=${this.writeValue(object.Name)} +${this.subWrite([], object) + + object + .CustomProperties.map(pin => this.separator + this.prefix + "CustomProperties " + SerializerFactory.getSerializer(PinEntity).write(pin)) + .join("")} +End Object`; + return result + } +} + +class Paste extends Context { + + constructor(target, blueprint, options = {}) { + options.wantsFocusCallback = true; + super(target, blueprint, options); + this.serializer = new ObjectSerializer(); + let self = this; + this.pasteHandle = e => self.pasted(e.clipboardData.getData("Text")); + } + + blueprintFocused() { + document.body.addEventListener("paste", this.pasteHandle); + } + + blueprintUnfocused() { + document.body.removeEventListener("paste", this.pasteHandle); + } + + pasted(value) { + let top = Number.MAX_SAFE_INTEGER; + let left = Number.MAX_SAFE_INTEGER; + let nodes = this.serializer.readMultiple(value).map(entity => { + let node = new GraphNode(entity); + top = Math.min(top, node.location[1]); + left = Math.min(left, node.location[0]); + return node + }); + if (nodes.length > 0) { + this.blueprint.unselectAll(); + } + let mousePosition = this.blueprint.entity.mousePosition; + this.blueprint.addNode(...nodes); + nodes.forEach(node => { + const locationOffset = [ + mousePosition[0] - left, + mousePosition[1] - top + ]; + node.addLocation(locationOffset); + node.setSelected(true); + }); + } +} + +class Select extends MouseClickDrag { + + constructor(target, blueprint, options) { + super(target, blueprint, options); + this.selectorElement = this.blueprint.selectorElement; + } + + startDrag() { + this.selectorElement.startSelecting(this.clickedPosition); + } + + dragTo(location, movement) { + this.selectorElement.doSelecting(location); + } + + endDrag() { + if (this.started) { + this.selectorElement.finishSelecting(); + } else { + this.blueprint.unselectAll(); + } + } +} + +class Unfocus extends Context { + + constructor(target, blueprint, options = {}) { + options.wantsFocusCallback = true; + super(target, blueprint, options); + + let self = this; + this.clickHandler = e => self.clickedSomewhere(e); + if (this.blueprint.focuse) { + document.addEventListener("click", this.clickHandler); + } + } + + /** + * + * @param {MouseEvent} e + */ + clickedSomewhere(e) { + // If target is inside the blueprint grid + if (e.target.closest("u-blueprint")) { + return + } + this.blueprint.setFocused(false); + } + + blueprintFocused() { + document.addEventListener("click", this.clickHandler); + } + + blueprintUnfocused() { + document.removeEventListener("click", this.clickHandler); + } +} + +class MouseWheel extends Pointing { + + /** + * + * @param {HTMLElement} target + * @param {import("../Blueprint").default} blueprint + * @param {Object} options + */ + constructor(target, blueprint, options) { + options.wantsFocusCallback = true; + super(target, blueprint, options); + this.looseTarget = options?.looseTarget ?? true; + let self = this; + + this.mouseWheelHandler = e => { + e.preventDefault(); + const location = self.getLocation(e); + self.wheel(Math.sign(e.deltaY), location); + }; + this.mouseParentWheelHandler = e => e.preventDefault(); + + if (this.blueprint.focused) { + this.movementSpace.addEventListener("wheel", this.mouseWheelHandler, false); + } + } + + blueprintFocused() { + this.movementSpace.addEventListener("wheel", this.mouseWheelHandler, false); + this.movementSpace.parentElement?.addEventListener("wheel", this.mouseParentWheelHandler); + } + + blueprintUnfocused() { + this.movementSpace.removeEventListener("wheel", this.mouseWheelHandler, false); + this.movementSpace.parentElement?.removeEventListener("wheel", this.mouseParentWheelHandler); + } + + /* Subclasses will override the following method */ + wheel(variation, location) { + } +} + +class Zoom extends MouseWheel { + + wheel(variation, location) { + let zoomLevel = this.blueprint.getZoom(); + zoomLevel -= variation; + this.blueprint.setZoom(zoomLevel, location); + } +} + +class Copy extends Context { + + constructor(target, blueprint, options = {}) { + options.wantsFocusCallback = true; + super(target, blueprint, options); + this.serializer = new ObjectSerializer(); + let self = this; + this.copyHandle = _ => self.copied(); + } + + blueprintFocused() { + document.body.addEventListener("copy", this.copyHandle); + } + + blueprintUnfocused() { + document.body.removeEventListener("copy", this.copyHandle); + } + + copied() { + const value = this.blueprint.getNodes(true).map(node => this.serializer.write(node.entity)).join("\n"); + navigator.clipboard.writeText(value); + } +} + +/** @typedef {import("./graph/GraphNode").default} GraphNode */ +class Blueprint extends GraphElement { + + constructor() { + super({}, new BlueprintTemplate()); + /** @type {BlueprintTemplate} */ + this.template; + /** @type {GraphNode[]}" */ + this.nodes = new Array(); + this.expandGridSize = 400; + /** @type {number[]} */ + this.additional = /*[2 * this.expandGridSize, 2 * this.expandGridSize]*/[0, 0]; + /** @type {number[]} */ + this.translateValue = /*[this.expandGridSize, this.expandGridSize]*/[0, 0]; + /** @type {number[]} */ + this.mousePosition = [0, 0]; + /** @type {HTMLElement} */ + this.gridElement = null; + /** @type {HTMLElement} */ + this.viewportElement = null; + /** @type {HTMLElement} */ + this.overlayElement = null; + /** @type {GraphSelector} */ + this.selectorElement = null; + /** @type {HTMLElement} */ + this.nodesContainerElement = null; + this.dragObject = null; + this.selectObject = null; + /** @type {number} */ + this.zoom = 0; + /** @type {HTMLElement} */ + this.headerElement = null; + this.focused = false; + /** @type {(node: GraphNode) => BoundariesInfo} */ + this.nodeBoundariesSupplier = node => { + let rect = node.getBoundingClientRect(); + let gridRect = this.nodesContainerElement.getBoundingClientRect(); + const scaleCorrection = 1 / this.getScale(); + return { + primaryInf: (rect.left - gridRect.left) * scaleCorrection, + primarySup: (rect.right - gridRect.right) * scaleCorrection, + // Counter intuitive here: the y (secondary axis is positive towards the bottom, therefore upper bound "sup" is bottom) + secondaryInf: (rect.top - gridRect.top) * scaleCorrection, + secondarySup: (rect.bottom - gridRect.bottom) * scaleCorrection + } + }; + /** @type {(node: GraphNode, selected: bool) => void}} */ + this.nodeSelectToggleFunction = (node, selected) => { + node.setSelected(selected); + }; + } + + connectedCallback() { + super.connectedCallback(); + + this.copyObject = new Copy(this.getGridDOMElement(), this); + this.pasteObject = new Paste(this.getGridDOMElement(), this); + + this.dragObject = new DragScroll(this.getGridDOMElement(), this, { + clickButton: 2, + moveEverywhere: true, + exitAnyButton: false + }); + + this.zoomObject = new Zoom(this.getGridDOMElement(), this, { + looseTarget: true + }); + + this.selectObject = new Select(this.getGridDOMElement(), this, { + clickButton: 0, + moveEverywhere: true, + exitAnyButton: true + }); + + this.unfocusObject = new Unfocus(this.getGridDOMElement(), this); + this.mouseTrackingObject = new MouseTracking(this.getGridDOMElement(), this); + } + + getGridDOMElement() { + return this.gridElement + } + + disconnectedCallback() { + super.disconnectedCallback(); + setSelected(false); + this.dragObject.unlistenDOMElement(); + this.selectObject.unlistenDOMElement(); + this.pasteObject.unlistenDOMElement(); + } + + getScroll() { + return [this.viewportElement.scrollLeft, this.viewportElement.scrollTop] + } + + setScroll(value, smooth = false) { + this.scroll = value; + if (!smooth) { + this.viewportElement.scroll(value[0], value[1]); + } else { + this.viewportElement.scroll({ + left: value[0], + top: value[1], + behavior: "smooth" + }); + } + } + + scrollDelta(delta, smooth = false) { + const scrollMax = this.getScrollMax(); + let currentScroll = this.getScroll(); + let finalScroll = [ + currentScroll[0] + delta[0], + currentScroll[1] + delta[1] + ]; + let expand = [0, 0]; + for (let i = 0; i < 2; ++i) { + if (delta[i] < 0 && finalScroll[i] < 0.25 * this.expandGridSize) { + // Expand if scrolling is diminishing and the remainig space is less that a quarter of an expansion step + expand[i] = finalScroll[i]; + if (expand[i] > 0) { + // Final scroll is still in rage (more than zero) but we want to expand to negative (left or top) + expand[i] = -this.expandGridSize; + } + } else if (delta[i] > 0 && finalScroll[i] > scrollMax[i] - 0.25 * this.expandGridSize) { + // Expand if scrolling is increasing and the remainig space is less that a quarter of an expansion step + expand[i] = finalScroll[i] - scrollMax[i]; + if (expand[i] < 0) { + // Final scroll is still in rage (less than the maximum scroll) but we want to expand to positive (right or bottom) + expand[i] = this.expandGridSize; + } + } + } + if (expand[0] != 0 || expand[1] != 0) { + this.seamlessExpand(this.progressiveSnapToGrid(expand[0]), this.progressiveSnapToGrid(expand[1])); + currentScroll = this.getScroll(); + finalScroll = [ + currentScroll[0] + delta[0], + currentScroll[1] + delta[1] + ]; + } + this.setScroll(finalScroll, smooth); + } + + scrollCenter() { + const scroll = this.getScroll(); + const offset = [ + this.translateValue[0] - scroll[0], + this.translateValue[1] - scroll[1] + ]; + const targetOffset = this.getViewportSize().map(size => size / 2); + const deltaOffset = [ + offset[0] - targetOffset[0], + offset[1] - targetOffset[1] + ]; + this.scrollDelta(deltaOffset, true); + } + + getExpandGridSize() { + return this.expandGridSize + } + + getViewportSize() { + return [ + this.viewportElement.clientWidth, + this.viewportElement.clientHeight + ] + } + + /** + * Get the scroll limits + * @return {array} The horizonal and vertical maximum scroll limits + */ + getScrollMax() { + return [ + this.viewportElement.scrollWidth - this.viewportElement.clientWidth, + this.viewportElement.scrollHeight - this.viewportElement.clientHeight + ] + } + + /** + * Expand the grid, considers the absolute value of params + * @param {number} x - Horizontal expansion value + * @param {number} y - Vertical expansion value + */ + _expand(x, y) { + x = Math.round(Math.abs(x)); + y = Math.round(Math.abs(y)); + this.additional = [this.additional[0] + x, this.additional[1] + y]; + this.template.applyExpand(this); + } + + /** + * Moves the content of the grid according to the coordinates + * @param {number} x - Horizontal translation value + * @param {number} y - Vertical translation value + */ + _translate(x, y) { + x = Math.round(x); + y = Math.round(y); + this.translateValue = [this.translateValue[0] + x, this.translateValue[1] + y]; + this.template.applyTranlate(this); + } + + /** + * Expand the grind indefinitely, the content will remain into position + * @param {number} x - Horizontal expand value (negative means left, positive means right) + * @param {number} y - Vertical expand value (negative means top, positive means bottom) + */ + seamlessExpand(x, y) { + let scale = this.getScale(); + let scaledX = x / scale; + let scaledY = y / scale; + // First expand the grid to contain the additional space + this._expand(scaledX, scaledY); + // If the expansion is towards the left or top, then scroll back to give the illusion that the content is in the same position and translate it accordingly + this._translate(scaledX < 0 ? -scaledX : 0, scaledY < 0 ? -scaledY : 0); + if (x < 0) { + this.viewportElement.scrollLeft -= x; + } + if (y < 0) { + this.viewportElement.scrollTop -= y; + } + } + + progressiveSnapToGrid(x) { + return this.expandGridSize * Math.round(x / this.expandGridSize + 0.5 * Math.sign(x)) + } + + getZoom() { + return this.zoom + } + + setZoom(zoom, center) { + zoom = Utility.clamp(zoom, -12, 0); + if (zoom == this.zoom) { + return + } + let initialScale = this.getScale(); + this.template.applyZoom(this, zoom); + this.zoom = zoom; + + + if (center) { + let relativeScale = this.getScale() / initialScale; + let newCenter = [ + relativeScale * center[0], + relativeScale * center[1] + ]; + this.scrollDelta([ + (newCenter[0] - center[0]) * initialScale, + (newCenter[1] - center[1]) * initialScale + ]); + } + } + + getScale() { + return parseFloat(getComputedStyle(this.gridElement).getPropertyValue("--ueb-scale")) + } + + compensateTranslation(position) { + position[0] -= this.translateValue[0]; + position[1] -= this.translateValue[1]; + return position + } + + /** + * + * @returns {GraphNode[]} Nodes + */ + getNodes(selected = false) { + if (selected) { + return this.nodes.filter( + /** + * + * @param {GraphNode} node + */ + node => node.selected + ) + } else { + return this.nodes + } + } + + /** + * Unselect all nodes + */ + unselectAll() { + this.nodes.forEach(node => this.nodeSelectToggleFunction(node, false)); + } + + /** + * + * @param {...GraphNode} graphNodes + */ + addNode(...graphNodes) { + [...graphNodes].reduce( + (s, e) => { + s.push(e); + return s + }, + this.nodes); + if (this.nodesContainerElement) { + this.nodesContainerElement.append(...graphNodes); + } + } + + setFocused(value = true) { + if (this.focused == value) { + return; + } + let event = new CustomEvent(value ? "blueprintfocus" : "blueprintunfocus"); + this.focused = value; + this.dataset.focused = this.focused; + if (!this.focused) { + this.unselectAll(); + } + this.dispatchEvent(event); + } +} + +customElements.define("u-blueprint", Blueprint); + +/** + * @typedef {import("../graph/GraphLink").default} GraphLink + */ +class LinkTemplate extends Template { + + /** + * Computes the html content of the target element. + * @param {GraphLink} link Link connecting two graph nodes + * @returns The result html + */ + render(link) { + return html` + + + + ` + } +} + +class GraphLink extends GraphElement { + + /** + * + * @typedef {{ + * node: String, + * pin: String + * }} PinReference + * @param {?PinReference} source + * @param {?PinReference} destination + */ + constructor(source, destination) { + super(this, new LinkTemplate()); + this.source = source; + this.destination = destination; + } +} + +customElements.define("u-link", GraphLink); + +class GeneralSerializer extends Serializer { + + constructor(wrap, entityType, prefix, separator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter) { + super(entityType, prefix, separator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter); + this.wrap = wrap ?? (v => `(${v})`); + } + + read(value) { + let grammar = Grammar.getGrammarForType(Serializer.grammar, this.entityType); + const parseResult = grammar.parse(value); + if (!parseResult.status) { + console.error("Error when trying to parse the entity " + this.entityType.prototype.constructor.name); + return parseResult + } + return parseResult.value + } + + write(object) { + let result = this.wrap(this.subWrite([], object)); + return result + } +} + +class CustomSerializer extends GeneralSerializer { + + constructor(objectWriter, entityType) { + super(undefined, entityType); + this.objectWriter = objectWriter; + } + + write(object) { + let result = this.objectWriter(object); + return result + } +} + +class ToStringSerializer extends GeneralSerializer { + + constructor(entityType) { + super(undefined, entityType); + } + + write(object) { + let result = object.toString(); + return result + } +} + +function initializeSerializerFactory() { + SerializerFactory.registerSerializer( + ObjectEntity, + new ObjectSerializer() + ); + SerializerFactory.registerSerializer( + PinEntity, + new GeneralSerializer(v => `Pin (${v})`, PinEntity, "", ",", true) + ); + SerializerFactory.registerSerializer( + FunctionReferenceEntity, + new GeneralSerializer(v => `(${v})`, FunctionReferenceEntity, "", ",", false) + ); + SerializerFactory.registerSerializer( + LocalizedTextEntity, + new GeneralSerializer(v => `NSLOCTEXT(${v})`, LocalizedTextEntity, "", ",", false, "", _ => "") + ); + SerializerFactory.registerSerializer( + PinReferenceEntity, + new GeneralSerializer(v => v, PinReferenceEntity, "", " ", false, "", _ => "") + ); + SerializerFactory.registerSerializer( + ObjectReferenceEntity, + new CustomSerializer( + /** @param {ObjectReferenceEntity} objectReference */ + objectReference => (objectReference.type ?? "") + ( + objectReference.path + ? objectReference.type ? `'"${objectReference.path}"'` : objectReference.path + : "" + )) + ); + SerializerFactory.registerSerializer(PathSymbolEntity, new ToStringSerializer(PathSymbolEntity)); + SerializerFactory.registerSerializer(GuidEntity, new ToStringSerializer(GuidEntity)); + SerializerFactory.registerSerializer(IntegerEntity, new ToStringSerializer(IntegerEntity)); +} + +initializeSerializerFactory(); + +export { Blueprint, GraphLink, GraphNode }; diff --git a/js/Blueprint.js b/js/Blueprint.js index 5d025f7..dc43312 100755 --- a/js/Blueprint.js +++ b/js/Blueprint.js @@ -1,4 +1,3 @@ -import BlueprintData from "./BlueprintData" import BlueprintTemplate from "./template/BlueprintTemplate" import DragScroll from "./input/DragScroll" import GraphElement from "./graph/GraphElement" @@ -15,7 +14,18 @@ import Copy from "./input/Copy" export default class Blueprint extends GraphElement { constructor() { - super(new BlueprintData(), new BlueprintTemplate()) + super({}, new BlueprintTemplate()) + /** @type {BlueprintTemplate} */ + this.template + /** @type {GraphNode[]}" */ + this.nodes = new Array() + this.expandGridSize = 400 + /** @type {number[]} */ + this.additional = /*[2 * this.expandGridSize, 2 * this.expandGridSize]*/[0, 0] + /** @type {number[]} */ + this.translateValue = /*[this.expandGridSize, this.expandGridSize]*/[0, 0] + /** @type {number[]} */ + this.mousePosition = [0, 0] /** @type {HTMLElement} */ this.gridElement = null /** @type {HTMLElement} */ @@ -54,22 +64,6 @@ export default class Blueprint extends GraphElement { connectedCallback() { super.connectedCallback() - this.classList.add("ueb", `ueb-zoom-${this.zoom}`) - - this.headerElement = this.querySelector('.ueb-viewport-header') - console.assert(this.headerElement, "Header element not provided by the template.") - this.overlayElement = this.querySelector('.ueb-viewport-overlay') - console.assert(this.overlayElement, "Overlay element not provided by the template.") - this.viewportElement = this.querySelector('.ueb-viewport-body') - console.assert(this.viewportElement, "Viewport element not provided by the template.") - this.gridElement = this.viewportElement.querySelector(".ueb-grid") - console.assert(this.gridElement, "Grid element not provided by the template.") - this.selectorElement = new GraphSelector() - this.nodesContainerElement = this.querySelector("[data-nodes]") - console.assert(this.nodesContainerElement, "Nodes container element not provided by the template.") - this.nodesContainerElement.append(this.selectorElement) - this.querySelector("[data-nodes]").append(...this.entity.nodes) - this.copyObject = new Copy(this.getGridDOMElement(), this) this.pasteObject = new Paste(this.getGridDOMElement(), this) @@ -132,19 +126,19 @@ export default class Blueprint extends GraphElement { ] let expand = [0, 0] for (let i = 0; i < 2; ++i) { - if (delta[i] < 0 && finalScroll[i] < 0.25 * this.entity.expandGridSize) { + if (delta[i] < 0 && finalScroll[i] < 0.25 * this.expandGridSize) { // Expand if scrolling is diminishing and the remainig space is less that a quarter of an expansion step expand[i] = finalScroll[i] if (expand[i] > 0) { // Final scroll is still in rage (more than zero) but we want to expand to negative (left or top) - expand[i] = -this.entity.expandGridSize + expand[i] = -this.expandGridSize } - } else if (delta[i] > 0 && finalScroll[i] > scrollMax[i] - 0.25 * this.entity.expandGridSize) { + } else if (delta[i] > 0 && finalScroll[i] > scrollMax[i] - 0.25 * this.expandGridSize) { // Expand if scrolling is increasing and the remainig space is less that a quarter of an expansion step expand[i] = finalScroll[i] - scrollMax[i] if (expand[i] < 0) { // Final scroll is still in rage (less than the maximum scroll) but we want to expand to positive (right or bottom) - expand[i] = this.entity.expandGridSize + expand[i] = this.expandGridSize } } } @@ -162,8 +156,8 @@ export default class Blueprint extends GraphElement { scrollCenter() { const scroll = this.getScroll() const offset = [ - this.entity.translateValue[0] - scroll[0], - this.entity.translateValue[1] - scroll[1] + this.translateValue[0] - scroll[0], + this.translateValue[1] - scroll[1] ] const targetOffset = this.getViewportSize().map(size => size / 2) const deltaOffset = [ @@ -174,7 +168,7 @@ export default class Blueprint extends GraphElement { } getExpandGridSize() { - return this.entity.expandGridSize + return this.expandGridSize } getViewportSize() { @@ -203,11 +197,8 @@ export default class Blueprint extends GraphElement { _expand(x, y) { x = Math.round(Math.abs(x)) y = Math.round(Math.abs(y)) - this.entity.additional = [this.entity.additional[0] + x, this.entity.additional[1] + y] - if (this.gridElement) { - this.gridElement.style.setProperty("--ueb-additional-x", this.entity.additional[0]) - this.gridElement.style.setProperty("--ueb-additional-y", this.entity.additional[1]) - } + this.additional = [this.additional[0] + x, this.additional[1] + y] + this.template.applyExpand(this) } /** @@ -218,11 +209,8 @@ export default class Blueprint extends GraphElement { _translate(x, y) { x = Math.round(x) y = Math.round(y) - this.entity.translateValue = [this.entity.translateValue[0] + x, this.entity.translateValue[1] + y] - if (this.gridElement) { - this.gridElement.style.setProperty("--ueb-translate-x", this.entity.translateValue[0]) - this.gridElement.style.setProperty("--ueb-translate-y", this.entity.translateValue[1]) - } + this.translateValue = [this.translateValue[0] + x, this.translateValue[1] + y] + this.template.applyTranlate(this) } /** @@ -247,7 +235,7 @@ export default class Blueprint extends GraphElement { } progressiveSnapToGrid(x) { - return this.entity.expandGridSize * Math.round(x / this.entity.expandGridSize + 0.5 * Math.sign(x)) + return this.expandGridSize * Math.round(x / this.expandGridSize + 0.5 * Math.sign(x)) } getZoom() { @@ -260,8 +248,7 @@ export default class Blueprint extends GraphElement { return } let initialScale = this.getScale() - this.classList.remove(`ueb-zoom-${this.zoom}`) - this.classList.add(`ueb-zoom-${zoom}`) + this.template.applyZoom(this, zoom) this.zoom = zoom @@ -283,8 +270,8 @@ export default class Blueprint extends GraphElement { } compensateTranslation(position) { - position[0] -= this.entity.translateValue[0] - position[1] -= this.entity.translateValue[1] + position[0] -= this.translateValue[0] + position[1] -= this.translateValue[1] return position } @@ -294,7 +281,7 @@ export default class Blueprint extends GraphElement { */ getNodes(selected = false) { if (selected) { - return this.entity.nodes.filter( + return this.nodes.filter( /** * * @param {GraphNode} node @@ -302,7 +289,7 @@ export default class Blueprint extends GraphElement { node => node.selected ) } else { - return this.entity.nodes + return this.nodes } } @@ -310,7 +297,7 @@ export default class Blueprint extends GraphElement { * Unselect all nodes */ unselectAll() { - this.entity.nodes.forEach(node => this.nodeSelectToggleFunction(node, false)) + this.nodes.forEach(node => this.nodeSelectToggleFunction(node, false)) } /** @@ -323,7 +310,7 @@ export default class Blueprint extends GraphElement { s.push(e) return s }, - this.entity.nodes) + this.nodes) if (this.nodesContainerElement) { this.nodesContainerElement.append(...graphNodes) } diff --git a/js/BlueprintData.js b/js/BlueprintData.js index c63709c..e69de29 100644 --- a/js/BlueprintData.js +++ b/js/BlueprintData.js @@ -1,15 +0,0 @@ -/** @typedef {import("./graph/GraphNode").default} GraphNode */ -export default class BlueprintData { - - constructor() { - /** @type {GraphNode[]}" */ - this.nodes = new Array() - this.expandGridSize = 400 - /** @type {number[]} */ - this.additional = /*[2 * this.expandGridSize, 2 * this.expandGridSize]*/[0, 0] - /** @type {number[]} */ - this.translateValue = /*[this.expandGridSize, this.expandGridSize]*/[0, 0] - /** @type {number[]} */ - this.mousePosition = [0, 0] - } -} diff --git a/js/entity/PinEntity.js b/js/entity/PinEntity.js index bc46a65..8ed11d1 100755 --- a/js/entity/PinEntity.js +++ b/js/entity/PinEntity.js @@ -49,8 +49,22 @@ export default class PinEntity extends Entity { return this.PinName } + isConnected() { + return this.LinkedTo.length > 0 + } + + getType() { + return this.PinType.PinCategory ?? "object" + } + + isInput() { + if (!this.bHidden && this.Direction !== "EGPD_Output") { + return true + } + } + isOutput() { - if (this.Direction === "EGPD_Output") { + if (!this.bHidden && this.Direction === "EGPD_Output") { return true } } diff --git a/js/graph/GraphElement.js b/js/graph/GraphElement.js index 09adba2..5d31920 100755 --- a/js/graph/GraphElement.js +++ b/js/graph/GraphElement.js @@ -8,12 +8,14 @@ export default class GraphElement extends HTMLElement { super() /** @type {import("../Blueprint").default}" */ this.blueprint = null + /** @type {import("../entity/Entity").default}" */ this.entity = entity + /** @type {import("../template/Template").default}" */ this.template = template } connectedCallback() { this.blueprint = this.closest("u-blueprint") - this.append(...this.template.getElements(this.entity)) + this.template.apply(this) } } diff --git a/js/graph/GraphLink.js b/js/graph/GraphLink.js index 2210c04..f0e4a6a 100755 --- a/js/graph/GraphLink.js +++ b/js/graph/GraphLink.js @@ -1,4 +1,5 @@ import GraphElement from "./GraphElement" +import LinkTemplate from "../template/LinkTemplate" export default class GraphLink extends GraphElement { @@ -12,18 +13,10 @@ export default class GraphLink extends GraphElement { * @param {?PinReference} destination */ constructor(source, destination) { - super() + super(this, new LinkTemplate()) this.source = source this.destination = destination } - - render() { - return ` - - - - ` - } } customElements.define("u-link", GraphLink) diff --git a/js/graph/GraphNode.js b/js/graph/GraphNode.js index 7542d0c..10ace76 100755 --- a/js/graph/GraphNode.js +++ b/js/graph/GraphNode.js @@ -6,31 +6,27 @@ import DragLink from "../input/DragLink" export default class GraphNode extends SelectableDraggable { - static fromSerializedObject(str) { - let entity = SerializerFactory.getSerializer(ObjectEntity).read(str) - return new GraphNode(entity) - } - /** * * @param {ObjectEntity} entity */ constructor(entity) { super(entity, new NodeTemplate()) + /** @type {ObjectEntity} */ + this.entity this.graphNodeName = "n/a" - this.dragLinkObjects = Array() + this.dragLinkObjects = [] super.setLocation([this.entity.NodePosX, this.entity.NodePosY]) } + static fromSerializedObject(str) { + let entity = SerializerFactory.getSerializer(ObjectEntity).read(str) + return new GraphNode(entity) + } + connectedCallback() { const type = this.getAttribute("type")?.trim() super.connectedCallback() - this.classList.add("ueb-node") - if (this.selected) { - this.classList.add("ueb-selected") - } - this.style.setProperty("--ueb-position-x", this.location[0]) - this.style.setProperty("--ueb-position-y", this.location[1]) this.querySelectorAll(".ueb-node-input, .ueb-node-output").forEach(element => { this.dragLinkObjects.push( new DragLink(element, this.blueprint, { diff --git a/js/graph/GraphPin.js b/js/graph/GraphPin.js new file mode 100644 index 0000000..269339b --- /dev/null +++ b/js/graph/GraphPin.js @@ -0,0 +1,15 @@ +import GraphElement from "./GraphElement" +import PinTemplate from "../template/PinTemplate" + +export default class GraphPin extends GraphElement { + + constructor() { + super({}, new PinTemplate()) + } + + /*connectedCallback() { + super.connectedCallback() + }*/ +} + +customElements.define("u-pin", GraphPin) diff --git a/js/graph/GraphSelector.js b/js/graph/GraphSelector.js index 3c8a4d1..08f659a 100755 --- a/js/graph/GraphSelector.js +++ b/js/graph/GraphSelector.js @@ -1,21 +1,14 @@ import FastSelectionModel from "../selection/FastSelectionModel" import GraphElement from "./GraphElement" -import Template from "../template/Template" +import SelectorTemplate from "../template/SelectorTemplate" export default class GraphSelector extends GraphElement { constructor() { - super({}, new Template()) - /** - * @type {import("./GraphSelector").default} - */ + super({}, new SelectorTemplate()) this.selectionModel = null - } - - connectedCallback() { - super.connectedCallback() - this.classList.add("ueb-selector") - this.dataset.selecting = "false" + /** @type {SelectorTemplate} */ + this.template } /** @@ -24,13 +17,7 @@ export default class GraphSelector extends GraphElement { */ startSelecting(initialPosition) { initialPosition = this.blueprint.compensateTranslation(initialPosition) - // Set initial position - this.style.setProperty("--ueb-select-from-x", initialPosition[0]) - this.style.setProperty("--ueb-select-from-y", initialPosition[1]) - // Final position coincide with the initial position, at the beginning of selection - this.style.setProperty("--ueb-select-to-x", initialPosition[0]) - this.style.setProperty("--ueb-select-to-y", initialPosition[1]) - this.dataset.selecting = "true" + this.template.applyStartSelecting(this, initialPosition) this.selectionModel = new FastSelectionModel(initialPosition, this.blueprint.getNodes(), this.blueprint.nodeBoundariesSupplier, this.blueprint.nodeSelectToggleFunction) } @@ -40,13 +27,12 @@ export default class GraphSelector extends GraphElement { */ doSelecting(finalPosition) { finalPosition = this.blueprint.compensateTranslation(finalPosition) - this.style.setProperty("--ueb-select-to-x", finalPosition[0]) - this.style.setProperty("--ueb-select-to-y", finalPosition[1]) + this.template.applyDoSelecting(this, finalPosition) this.selectionModel.selectTo(finalPosition) } finishSelecting() { - this.dataset.selecting = "false" + this.template.applyFinishSelecting(this) this.selectionModel = null } } diff --git a/js/graph/SelectableDraggable.js b/js/graph/SelectableDraggable.js index 529611e..185c612 100755 --- a/js/graph/SelectableDraggable.js +++ b/js/graph/SelectableDraggable.js @@ -8,6 +8,8 @@ export default class SelectableDraggable extends GraphElement { this.dragObject = null this.location = [0, 0] this.selected = false + /** @type {import("../template/SelectableDraggableTemplate").default} */ + this.template let self = this this.dragHandler = (e) => { @@ -28,8 +30,7 @@ export default class SelectableDraggable extends GraphElement { setLocation(value = [0, 0]) { this.location = value - this.style.setProperty("--ueb-position-x", this.location[0]) - this.style.setProperty("--ueb-position-y", this.location[1]) + this.template.applyLocation(this) } addLocation(value) { @@ -59,11 +60,10 @@ export default class SelectableDraggable extends GraphElement { } this.selected = value if (this.selected) { - this.classList.add("ueb-selected") this.blueprint.addEventListener("uDragSelected", this.dragHandler) } else { - this.classList.remove("ueb-selected") this.blueprint.removeEventListener("uDragSelected", this.dragHandler) } + this.template.applySelected(this) } } diff --git a/js/template/BlueprintTemplate.js b/js/template/BlueprintTemplate.js index 737927b..591ee12 100755 --- a/js/template/BlueprintTemplate.js +++ b/js/template/BlueprintTemplate.js @@ -1,6 +1,10 @@ +import Blueprint from "../Blueprint" import html from "./html" +import sanitizeText from "./sanitizeText" import Template from "./Template" +import GraphSelector from "../graph/GraphSelector" +/** @typedef {import("../Blueprint").default} Blueprint */ export default class BlueprintTemplate extends Template { header(element) { return html` @@ -18,14 +22,19 @@ export default class BlueprintTemplate extends Template { /** * - * @param {import("../Blueprint").Blueprint} element + * @param {Blueprint} element * @returns */ viewport(element) { return html`
+ style=" + --ueb-additional-x:${sanitizeText(element.additional[0])}; + --ueb-additional-y:${sanitizeText(element.additional[1])}; + --ueb-translate-x:${sanitizeText(element.translateValue[0])}; + --ueb-translate-y:${sanitizeText(element.translateValue[1])}; + ">
@@ -44,4 +53,47 @@ export default class BlueprintTemplate extends Template { ${this.viewport(element)} ` } + + /** + * Applies the style to the element. + * @param {Blueprint} brueprint The blueprint element + */ + apply(blueprint) { + super.apply(blueprint) + blueprint.classList.add("ueb", `ueb-zoom-${blueprint.zoom}`) + blueprint.headerElement = blueprint.querySelector('.ueb-viewport-header') + blueprint.overlayElement = blueprint.querySelector('.ueb-viewport-overlay') + blueprint.viewportElement = blueprint.querySelector('.ueb-viewport-body') + blueprint.gridElement = blueprint.viewportElement.querySelector(".ueb-grid") + blueprint.nodesContainerElement = blueprint.querySelector("[data-nodes]") + blueprint.selectorElement = new GraphSelector() + blueprint.nodesContainerElement.append(blueprint.selectorElement, ...blueprint.nodes) + } + + /** + * Applies the style to the element. + * @param {Blueprint} brueprint The blueprint element + */ + applyZoom(blueprint, newZoom) { + blueprint.classList.remove(`ueb-zoom-${blueprint.zoom}`) + blueprint.classList.add(`ueb-zoom-${newZoom}`) + } + + /** + * Applies the style to the element. + * @param {Blueprint} brueprint The blueprint element + */ + applyExpand(blueprint) { + blueprint.gridElement.style.setProperty("--ueb-additional-x", blueprint.additional[0]) + blueprint.gridElement.style.setProperty("--ueb-additional-y", blueprint.additional[1]) + } + + /** + * Applies the style to the element. + * @param {Blueprint} brueprint The blueprint element + */ + applyTranlate(blueprint) { + blueprint.gridElement.style.setProperty("--ueb-translate-x", blueprint.translateValue[0]) + blueprint.gridElement.style.setProperty("--ueb-translate-y", blueprint.translateValue[1]) + } } diff --git a/js/template/LinkTemplate.js b/js/template/LinkTemplate.js new file mode 100644 index 0000000..695a345 --- /dev/null +++ b/js/template/LinkTemplate.js @@ -0,0 +1,21 @@ +import html from "./html" +import Template from "./Template" + +/** + * @typedef {import("../graph/GraphLink").default} GraphLink + */ +export default class LinkTemplate extends Template { + + /** + * Computes the html content of the target element. + * @param {GraphLink} link Link connecting two graph nodes + * @returns The result html + */ + render(link) { + return html` + + + + ` + } +} diff --git a/js/template/NodeTemplate.js b/js/template/NodeTemplate.js index f4671fd..e541e7f 100755 --- a/js/template/NodeTemplate.js +++ b/js/template/NodeTemplate.js @@ -1,23 +1,23 @@ import html from "./html" import PinEntity from "../entity/PinEntity" -import Template from "./Template" +import SelectableDraggableTemplate from "./SelectableDraggableTemplate" /** - * @typedef {import("../entity/ObjectEntity").default} ObjectEntity + * @typedef {import("../graph/GraphNode").default} GraphNode */ -export default class NodeTemplate extends Template { +export default class NodeTemplate extends SelectableDraggableTemplate { /** * Computes the html content of the target element. - * @param {ObjectEntity} entity Entity representing the element - * @returns The computed html + * @param {GraphNode} node Graph node element + * @returns The result html */ - header(entity) { + header(node) { return html`
- ${entity.getNodeDisplayName()} + ${node.entity.getNodeDisplayName()}
` @@ -25,14 +25,14 @@ export default class NodeTemplate extends Template { /** * Computes the html content of the target element. - * @param {ObjectEntity} entity Entity representing the element - * @returns The computed html + * @param {GraphNode} node Graph node element + * @returns The result html */ - body(entity) { + body(node) { /** @type {PinEntity[]} */ - let inputs = entity.CustomProperties.filter(v => v instanceof PinEntity) + let inputs = node.entity.CustomProperties.filter(v => v instanceof PinEntity) let outputs = inputs.filter(v => v.isOutput()) - inputs = inputs.filter(v => !v.isOutput()) + inputs = inputs.filter(v => v.isInput()) return html`
@@ -57,17 +57,31 @@ export default class NodeTemplate extends Template { /** * Computes the html content of the target element. - * @param {ObjectEntity} entity Entity representing the element - * @returns The computed html + * @param {GraphNode} node Graph node element + * @returns The result html */ - render(entity) { + render(node) { return html`
- ${this.header(entity)} - ${this.body(entity)} + ${this.header(node)} + ${this.body(node)}
` } + + /** + * Returns the html elements rendered from this template. + * @param {GraphNode} node Element of the graph + */ + apply(node) { + super.apply(node) + node.classList.add("ueb-node") + if (node.selected) { + node.classList.add("ueb-selected") + } + node.style.setProperty("--ueb-position-x", node.location[0]) + node.style.setProperty("--ueb-position-y", node.location[1]) + } } diff --git a/js/template/PinTemplate.js b/js/template/PinTemplate.js new file mode 100644 index 0000000..3a3f73f --- /dev/null +++ b/js/template/PinTemplate.js @@ -0,0 +1,51 @@ +import html from "./html" +import Template from "./Template" + +/** + * @typedef {import("../entity/PinEntity").default} PinEntity + */ +export default class PinTemplate extends Template { + + /** + * Computes the template for a pin in case it is an input. + * @param {PinEntity} pin Pin entity + * @returns The result html + */ + renderInputPin(pin) { + return html` +
+ + ${pin.getPinDisplayName()} +
+ ` + } + + /** + * Computes the template for a pin in case it is an output. + * @param {PinEntity} pin Pin entity + * @returns The result html + */ + renderOutputPin(pin) { + return html` +
+ ${output.getPinDisplayName()} + +
+ ` + } + + /** + * Computes the html content of the pin. + * @param {PinEntity} pin Pin entity + * @returns The result html + */ + render(pin) { + if (pin.isInput()) { + return this.renderInputPin(pin) + } + if (pin.isOutput()) { + return this.renderOutputPin(pin) + } + return "" + } +} diff --git a/js/template/SelectableDraggableTemplate.js b/js/template/SelectableDraggableTemplate.js new file mode 100644 index 0000000..b3fdc82 --- /dev/null +++ b/js/template/SelectableDraggableTemplate.js @@ -0,0 +1,29 @@ +import html from "./html" +import Template from "./Template" + +/** + * @typedef {import("../graph/SelectableDraggable").default} SelectableDraggable + */ +export default class SelectableDraggableTemplate extends Template { + + /** + * Returns the html elements rendered from this template. + * @param {SelectableDraggable} element Element of the graph + */ + applyLocation(element) { + element.style.setProperty("--ueb-position-x", element.location[0]) + element.style.setProperty("--ueb-position-y", element.location[1]) + } + + /** + * Returns the html elements rendered from this template. + * @param {SelectableDraggable} element Element of the graph + */ + applySelected(element) { + if (element.selected) { + element.classList.add("ueb-selected") + } else { + element.classList.remove("ueb-selected") + } + } +} diff --git a/js/template/SelectorTemplate.js b/js/template/SelectorTemplate.js new file mode 100644 index 0000000..ce740d5 --- /dev/null +++ b/js/template/SelectorTemplate.js @@ -0,0 +1,48 @@ +import Template from "./Template" + +/** + * @typedef {import("../graph/GraphSelector").default} GraphSelector + */ +export default class SelectorTemplate extends Template { + + /** + * Applies the style to the element. + * @param {GraphSelector} selector Selector element + */ + apply(selector) { + super.apply(selector) + selector.classList.add("ueb-selector") + selector.dataset.selecting = "false" + } + + /** + * Applies the style relative to selection beginning. + * @param {GraphSelector} selector Selector element + */ + applyStartSelecting(selector, initialPosition) { + // Set initial position + selector.style.setProperty("--ueb-select-from-x", initialPosition[0]) + selector.style.setProperty("--ueb-select-from-y", initialPosition[1]) + // Final position coincide with the initial position, at the beginning of selection + selector.style.setProperty("--ueb-select-to-x", initialPosition[0]) + selector.style.setProperty("--ueb-select-to-y", initialPosition[1]) + selector.dataset.selecting = "true" + } + + /** + * Applies the style relative to selection. + * @param {GraphSelector} selector Selector element + */ + applyDoSelecting(selector, finalPosition) { + selector.style.setProperty("--ueb-select-to-x", finalPosition[0]) + selector.style.setProperty("--ueb-select-to-y", finalPosition[1]) + } + + /** + * Applies the style relative to selection finishing. + * @param {GraphSelector} selector Selector element + */ + applyFinishSelecting(selector, finalPosition) { + selector.dataset.selecting = "false" + } +} diff --git a/js/template/Template.js b/js/template/Template.js index 4c93772..f6becf0 100755 --- a/js/template/Template.js +++ b/js/template/Template.js @@ -1,25 +1,23 @@ /** - * @typedef {import(""../entity/Entity"").default} Entity + * @typedef {import("../graph/GraphElement").default} GraphElement */ export default class Template { /** * Computes the html content of the target element. - * @param {Entity} entity Entity representing the element - * @returns The computed html + * @param {GraphElement} entity Element of the graph + * @returns The result html */ render(entity) { return "" } /** - * Returns the html elements rendered by this template. - * @param {Entity} entity Entity representing the element - * @returns The rendered elements + * Applies the style to the element. + * @param {GraphElement} element Element of the graph */ - getElements(entity) { - let aDiv = document.createElement("div") - aDiv.innerHTML = this.render(entity) - return aDiv.childNodes + apply(element) { + // TODO replace with the safer element.setHTML(...) when it will be available + element.innerHTML = this.render(element) } } diff --git a/js/template/sanitizeText.js b/js/template/sanitizeText.js new file mode 100644 index 0000000..727dcc8 --- /dev/null +++ b/js/template/sanitizeText.js @@ -0,0 +1,10 @@ +const div = document.createElement("div") + +function sanitizeText(value) { + div.textContent = value + value = div.textContent + div.innerHTML = "" + return value +} + +export default sanitizeText \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js index f4848e2..091aa48 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -11,8 +11,8 @@ export default { }, plugins: [ nodeResolve({ browser: true }), - minifyHTML(), + //minifyHTML(), commonjs(), - terser() + //terser() ] } \ No newline at end of file