/** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ const t$2=window,e$3=t$2.ShadowRoot&&(void 0===t$2.ShadyCSS||t$2.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s$3=Symbol(),n$3=new WeakMap;class o$3{constructor(t,e,n){if(this._$cssResult$=!0,n!==s$3)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e;}get styleSheet(){let t=this.o;const s=this.t;if(e$3&&void 0===t){const e=void 0!==s&&1===s.length;e&&(t=n$3.get(s)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&n$3.set(s,t));}return t}toString(){return this.cssText}}const r$2=t=>new o$3("string"==typeof t?t:t+"",void 0,s$3),i$3=(t,...e)=>{const n=1===t.length?t[0]:e.reduce(((e,s,n)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(s)+t[n+1]),t[0]);return new o$3(n,t,s$3)},S$1=(s,n)=>{e$3?s.adoptedStyleSheets=n.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):n.forEach((e=>{const n=document.createElement("style"),o=t$2.litNonce;void 0!==o&&n.setAttribute("nonce",o),n.textContent=e.cssText,s.appendChild(n);}));},c$1=e$3?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const s of t.cssRules)e+=s.cssText;return r$2(e)})(t):t; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */var s$2;const e$2=window,r$1=e$2.trustedTypes,h$1=r$1?r$1.emptyScript:"",o$2=e$2.reactiveElementPolyfillSupport,n$2={toAttribute(t,i){switch(i){case Boolean:t=t?h$1:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t);}return t},fromAttribute(t,i){let s=t;switch(i){case Boolean:s=null!==t;break;case Number:s=null===t?null:Number(t);break;case Object:case Array:try{s=JSON.parse(t);}catch(t){s=null;}}return s}},a$1=(t,i)=>i!==t&&(i==i||t==t),l$2={attribute:!0,type:String,converter:n$2,reflect:!1,hasChanged:a$1};class d$1 extends HTMLElement{constructor(){super(),this._$Ei=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$El=null,this.u();}static addInitializer(t){var i;this.finalize(),(null!==(i=this.h)&&void 0!==i?i:this.h=[]).push(t);}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((i,s)=>{const e=this._$Ep(s,i);void 0!==e&&(this._$Ev.set(e,s),t.push(e));})),t}static createProperty(t,i=l$2){if(i.state&&(i.attribute=!1),this.finalize(),this.elementProperties.set(t,i),!i.noAccessor&&!this.prototype.hasOwnProperty(t)){const s="symbol"==typeof t?Symbol():"__"+t,e=this.getPropertyDescriptor(t,s,i);void 0!==e&&Object.defineProperty(this.prototype,t,e);}}static getPropertyDescriptor(t,i,s){return {get(){return this[i]},set(e){const r=this[t];this[i]=e,this.requestUpdate(t,r,s);},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||l$2}static finalize(){if(this.hasOwnProperty("finalized"))return !1;this.finalized=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),void 0!==t.h&&(this.h=[...t.h]),this.elementProperties=new Map(t.elementProperties),this._$Ev=new Map,this.hasOwnProperty("properties")){const t=this.properties,i=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const s of i)this.createProperty(s,t[s]);}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(i){const s=[];if(Array.isArray(i)){const e=new Set(i.flat(1/0).reverse());for(const i of e)s.unshift(c$1(i));}else void 0!==i&&s.push(c$1(i));return s}static _$Ep(t,i){const s=i.attribute;return !1===s?void 0:"string"==typeof s?s:"string"==typeof t?t.toLowerCase():void 0}u(){var t;this._$E_=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$Eg(),this.requestUpdate(),null===(t=this.constructor.h)||void 0===t||t.forEach((t=>t(this)));}addController(t){var i,s;(null!==(i=this._$ES)&&void 0!==i?i:this._$ES=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(s=t.hostConnected)||void 0===s||s.call(t));}removeController(t){var i;null===(i=this._$ES)||void 0===i||i.splice(this._$ES.indexOf(t)>>>0,1);}_$Eg(){this.constructor.elementProperties.forEach(((t,i)=>{this.hasOwnProperty(i)&&(this._$Ei.set(i,this[i]),delete this[i]);}));}createRenderRoot(){var t;const s=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return S$1(s,this.constructor.elementStyles),s}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this._$ES)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostConnected)||void 0===i?void 0:i.call(t)}));}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this._$ES)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostDisconnected)||void 0===i?void 0:i.call(t)}));}attributeChangedCallback(t,i,s){this._$AK(t,s);}_$EO(t,i,s=l$2){var e;const r=this.constructor._$Ep(t,s);if(void 0!==r&&!0===s.reflect){const h=(void 0!==(null===(e=s.converter)||void 0===e?void 0:e.toAttribute)?s.converter:n$2).toAttribute(i,s.type);this._$El=t,null==h?this.removeAttribute(r):this.setAttribute(r,h),this._$El=null;}}_$AK(t,i){var s;const e=this.constructor,r=e._$Ev.get(t);if(void 0!==r&&this._$El!==r){const t=e.getPropertyOptions(r),h="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==(null===(s=t.converter)||void 0===s?void 0:s.fromAttribute)?t.converter:n$2;this._$El=r,this[r]=h.fromAttribute(i,t.type),this._$El=null;}}requestUpdate(t,i,s){let e=!0;void 0!==t&&(((s=s||this.constructor.getPropertyOptions(t)).hasChanged||a$1)(this[t],i)?(this._$AL.has(t)||this._$AL.set(t,i),!0===s.reflect&&this._$El!==t&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(t,s))):e=!1),!this.isUpdatePending&&e&&(this._$E_=this._$Ej());}async _$Ej(){this.isUpdatePending=!0;try{await this._$E_;}catch(t){Promise.reject(t);}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this._$Ei&&(this._$Ei.forEach(((t,i)=>this[i]=t)),this._$Ei=void 0);let i=!1;const s=this._$AL;try{i=this.shouldUpdate(s),i?(this.willUpdate(s),null===(t=this._$ES)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostUpdate)||void 0===i?void 0:i.call(t)})),this.update(s)):this._$Ek();}catch(t){throw i=!1,this._$Ek(),t}i&&this._$AE(s);}willUpdate(t){}_$AE(t){var i;null===(i=this._$ES)||void 0===i||i.forEach((t=>{var i;return null===(i=t.hostUpdated)||void 0===i?void 0:i.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t);}_$Ek(){this._$AL=new Map,this.isUpdatePending=!1;}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$E_}shouldUpdate(t){return !0}update(t){void 0!==this._$EC&&(this._$EC.forEach(((t,i)=>this._$EO(i,this[i],t))),this._$EC=void 0),this._$Ek();}updated(t){}firstUpdated(t){}}d$1.finalized=!0,d$1.elementProperties=new Map,d$1.elementStyles=[],d$1.shadowRootOptions={mode:"open"},null==o$2||o$2({ReactiveElement:d$1}),(null!==(s$2=e$2.reactiveElementVersions)&&void 0!==s$2?s$2:e$2.reactiveElementVersions=[]).push("1.4.2"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var t$1;const i$2=window,s$1=i$2.trustedTypes,e$1=s$1?s$1.createPolicy("lit-html",{createHTML:t=>t}):void 0,o$1=`lit$${(Math.random()+"").slice(9)}$`,n$1="?"+o$1,l$1=`<${n$1}>`,h=document,r=(t="")=>h.createComment(t),d=t=>null===t||"object"!=typeof t&&"function"!=typeof t,u=Array.isArray,c=t=>u(t)||"function"==typeof(null==t?void 0:t[Symbol.iterator]),v=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,a=/-->/g,f=/>/g,_=RegExp(">|[ \t\n\f\r](?:([^\\s\"'>=/]+)([ \t\n\f\r]*=[ \t\n\f\r]*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)","g"),m=/'/g,p=/"/g,$=/^(?:script|style|textarea|title)$/i,g=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),y=g(1),x=Symbol.for("lit-noChange"),b=Symbol.for("lit-nothing"),T=new WeakMap,A=h.createTreeWalker(h,129,null,!1),E=(t,i)=>{const s=t.length-1,n=[];let h,r=2===i?"":"",d=v;for(let i=0;i"===u[0]?(d=null!=h?h:v,c=-1):void 0===u[1]?c=-2:(c=d.lastIndex-u[2].length,e=u[1],d=void 0===u[3]?_:'"'===u[3]?p:m):d===p||d===m?d=_:d===a||d===f?d=v:(d=_,h=void 0);const y=d===_&&t[i+1].startsWith("/>")?" ":"";r+=d===v?s+l$1:c>=0?(n.push(e),s.slice(0,c)+"$lit$"+s.slice(c)+o$1+y):s+o$1+(-2===c?(n.push(void 0),i):y);}const u=r+(t[s]||">")+(2===i?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return [void 0!==e$1?e$1.createHTML(u):u,n]};class C{constructor({strings:t,_$litType$:i},e){let l;this.parts=[];let h=0,d=0;const u=t.length-1,c=this.parts,[v,a]=E(t,i);if(this.el=C.createElement(v,e),A.currentNode=this.el.content,2===i){const t=this.el.content,i=t.firstChild;i.remove(),t.append(...i.childNodes);}for(;null!==(l=A.nextNode())&&c.length0){l.textContent=s$1?s$1.emptyScript:"";for(let s=0;s2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=b;}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,i=this,s,e){const o=this.strings;let n=!1;if(void 0===o)t=P$1(this,t,i,0),n=!d(t)||t!==this._$AH&&t!==x,n&&(this._$AH=t);else {const e=t;let l,h;for(t=o[0],l=0;l{var e,o;const n=null!==(e=null==s?void 0:s.renderBefore)&&void 0!==e?e:i;let l=n._$litPart$;if(void 0===l){const t=null!==(o=null==s?void 0:s.renderBefore)&&void 0!==o?o:null;n._$litPart$=l=new N(i.insertBefore(r(),t),t,void 0,null!=s?s:{});}return l._$AI(t),l}; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */var l,o;class s extends d$1{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0;}createRenderRoot(){var t,e;const i=super.createRenderRoot();return null!==(t=(e=this.renderOptions).renderBefore)&&void 0!==t||(e.renderBefore=i.firstChild),i}update(t){const i=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=Z(i,this.renderRoot,this.renderOptions);}connectedCallback(){var t;super.connectedCallback(),null===(t=this._$Do)||void 0===t||t.setConnected(!0);}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this._$Do)||void 0===t||t.setConnected(!1);}render(){return x}}s.finalized=!0,s._$litElement$=!0,null===(l=globalThis.litElementHydrateSupport)||void 0===l||l.call(globalThis,{LitElement:s});const n=globalThis.litElementPolyfillSupport;null==n||n({LitElement:s});(null!==(o=globalThis.litElementVersions)&&void 0!==o?o:globalThis.litElementVersions=[]).push("3.2.2"); /** * @typedef {import("./element/PinElement").default} PinElement * @typedef {import("lit").CSSResult} CSSResult */ class Configuration { static #pinColor = { "/Script/CoreUObject.LinearColor": i$3`3, 76, 168`, "/Script/CoreUObject.Rotator": i$3`152, 171, 241`, "/Script/CoreUObject.Transform": i$3`241, 110, 1`, "/Script/CoreUObject.Vector": i$3`215, 202, 11`, "/Script/Engine.Actor": i$3`0, 168, 242`, "/Script/Engine.GameStateBase": i$3`0, 168, 242`, "/Script/Engine.Pawn": i$3`0, 168, 242`, "/Script/Engine.PlayerState": i$3`0, 168, 242`, "bool": i$3`117, 0, 0`, "byte": i$3`0, 110, 98`, "class": i$3`88, 0, 186`, "default": i$3`167, 167, 167`, "exec": i$3`240, 240, 240`, "int": i$3`32, 224, 173`, "name": i$3`203, 129, 252`, "real": i$3`50, 187, 0`, "string": i$3`213, 0, 176`, "wildcard": i$3`128, 120, 120`, } static alphaPattern = "repeating-conic-gradient(#7c8184 0% 25%, #c2c3c4 0% 50%) 50% / 10px 10px" static colorDragEventName = "ueb-color-drag" static colorPickEventName = "ueb-color-pick" static colorWindowEventName = "ueb-color-window" static defaultCommentHeight = 96 static defaultCommentWidth = 400 static deleteNodesKeyboardKey = "Delete" static dragGeneralEventName = "ueb-drag-general" static dragEventName = "ueb-drag" static editTextEventName = { begin: "ueb-edit-text-begin", end: "ueb-edit-text-end", } static enableZoomIn = ["LeftControl", "RightControl"] // Button to enable more than 0 (1:1) zoom static expandGridSize = 400 static focusEventName = { begin: "blueprint-focus", end: "blueprint-unfocus", } static fontSize = i$3`12.5px` /** * @param {PinElement} pin * @return {CSSResult} */ static getPinColor(pin) { if (!pin) { return Configuration.#pinColor["default"] } if (Configuration.#pinColor[pin.pinType]) { return Configuration.#pinColor[pin.pinType] } if (pin.entity.PinType.PinCategory == "struct" || pin.entity.PinType.PinCategory == "object") { switch (pin.entity.PinType.PinSubCategoryObject.type) { case "ScriptStruct": return i$3`0, 88, 200` default: if (pin.entity.PinType.PinSubCategoryObject.getName().endsWith("Actor")) { return Configuration.#pinColor["/Script/Engine.Actor"] } } } return Configuration.#pinColor["default"] } static gridAxisLineColor = i$3`black` static gridExpandThreshold = 0.25 // remaining size factor threshold to cause an expansion event static gridLineColor = i$3`#353535` static gridLineWidth = 1 // pixel static gridSet = 8 static gridSetLineColor = i$3`#161616` static gridShrinkThreshold = 4 // exceding size factor threshold to cause a shrink event static gridSize = 16 // pixel static hexColorRegex = /^\s*#(?[0-9a-fA-F]{2})(?[0-9a-fA-F]{2})(?[0-9a-fA-F]{2})([0-9a-fA-F]{2})?|#(?[0-9a-fA-F])(?[0-9a-fA-F])(?[0-9a-fA-F])\s*$/ static keysSeparator = "+" static linkCurveHeight = 15 // pixel static linkCurveWidth = 80 // pixel static linkMinWidth = 100 // pixel /** * @param {Number} start * @param {Number} c1 * @param {Number} c2 */ static linkRightSVGPath = (start, c1, c2) => { let end = 100 - start; return `M ${start} 0 C ${c1} 0, ${c2} 0, 50 50 S ${end - c1 + start} 100, ${end} 100` } static maxZoom = 7 static minZoom = -12 static mouseWheelFactor = 0.2 static nodeDeleteEventName = "ueb-node-delete" static nodeDragGeneralEventName = "ueb-node-drag-general" static nodeDragEventName = "ueb-node-drag" static nodeName = (name, counter) => `${name}_${counter}` static nodeRadius = 8 // in pixel static nodeReflowEventName = "ueb-node-reflow" static nodeType = { callFunction: "/Script/BlueprintGraph.K2Node_CallFunction", comment: "/Script/UnrealEd.EdGraphNode_Comment", doN: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:Do N", dynamicCast: "/Script/BlueprintGraph.K2Node_DynamicCast", executionSequence: "/Script/BlueprintGraph.K2Node_ExecutionSequence", forEachElementInEnum: "/Script/BlueprintGraph.K2Node_ForEachElementInEnum", forEachLoop: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:ForEachLoop", forEachLoopWithBreak: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:ForEachLoopWithBreak", forLoop: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:ForLoop", forLoopWithBreak: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:ForLoopWithBreak", ifThenElse: "/Script/BlueprintGraph.K2Node_IfThenElse", knot: "/Script/BlueprintGraph.K2Node_Knot", macro: "/Script/BlueprintGraph.K2Node_MacroInstance", makeArray: "/Script/BlueprintGraph.K2Node_MakeArray", makeMap: "/Script/BlueprintGraph.K2Node_MakeMap", pawn: "/Script/Engine.Pawn", reverseForEachLoop: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:ReverseForEachLoop", select: "/Script/BlueprintGraph.K2Node_Select", variableGet: "/Script/BlueprintGraph.K2Node_VariableGet", variableSet: "/Script/BlueprintGraph.K2Node_VariableSet", whileLoop: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:WhileLoop", } static selectAllKeyboardKey = "(bCtrl=True,Key=A)" static distanceThreshold = 5 // in pixel static trackingMouseEventName = { begin: "ueb-tracking-mouse-begin", end: "ueb-tracking-mouse-end", } static windowApplyEventName = "ueb-window-apply" static windowCancelEventName = "ueb-window-cancel" static windowCloseEventName = "ueb-window-close" static ModifierKeys = [ "Ctrl", "Shift", "Alt", "Meta", ] static Keys = { /* UE name: JS name */ "Backspace": "Backspace", "Tab": "Tab", "LeftControl": "ControlLeft", "RightControl": "ControlRight", "LeftShift": "ShiftLeft", "RightShift": "ShiftRight", "LeftAlt": "AltLeft", "RightAlt": "AltRight", "Enter": "Enter", "Pause": "Pause", "CapsLock": "CapsLock", "Escape": "Escape", "Space": "Space", "PageUp": "PageUp", "PageDown": "PageDown", "End": "End", "Home": "Home", "ArrowLeft": "Left", "ArrowUp": "Up", "ArrowRight": "Right", "ArrowDown": "Down", "PrintScreen": "PrintScreen", "Insert": "Insert", "Delete": "Delete", "Zero": "Digit0", "One": "Digit1", "Two": "Digit2", "Three": "Digit3", "Four": "Digit4", "Five": "Digit5", "Six": "Digit6", "Seven": "Digit7", "Eight": "Digit8", "Nine": "Digit9", "A": "KeyA", "B": "KeyB", "C": "KeyC", "D": "KeyD", "E": "KeyE", "F": "KeyF", "G": "KeyG", "H": "KeyH", "I": "KeyI", "K": "KeyK", "L": "KeyL", "M": "KeyM", "N": "KeyN", "O": "KeyO", "P": "KeyP", "Q": "KeyQ", "R": "KeyR", "S": "KeyS", "T": "KeyT", "U": "KeyU", "V": "KeyV", "W": "KeyW", "X": "KeyX", "Y": "KeyY", "Z": "KeyZ", "NumPadZero": "Numpad0", "NumPadOne": "Numpad1", "NumPadTwo": "Numpad2", "NumPadThree": "Numpad3", "NumPadFour": "Numpad4", "NumPadFive": "Numpad5", "NumPadSix": "Numpad6", "NumPadSeven": "Numpad7", "NumPadEight": "Numpad8", "NumPadNine": "Numpad9", "Multiply": "NumpadMultiply", "Add": "NumpadAdd", "Subtract": "NumpadSubtract", "Decimal": "NumpadDecimal", "Divide": "NumpadDivide", "F1": "F1", "F2": "F2", "F3": "F3", "F4": "F4", "F5": "F5", "F6": "F6", "F7": "F7", "F8": "F8", "F9": "F9", "F10": "F10", "F11": "F11", "F12": "F12", "NumLock": "NumLock", "ScrollLock": "ScrollLock", } } /** @typedef {import("../Blueprint").default} Blueprint */ /** @template {HTMLElement} T */ class IInput { /** @type {T} */ #target get target() { return this.#target } /** @type {Blueprint} */ #blueprint get blueprint() { return this.#blueprint } /** @type {Object} */ options /** * @param {T} target * @param {Blueprint} blueprint * @param {Object} options */ constructor(target, blueprint, options = {}) { options.consumeEvent ??= false; options.listenOnFocus ??= false; options.unlistenOnTextEdit ??= false; this.#target = target; this.#blueprint = blueprint; this.options = options; let self = this; this.listenHandler = _ => self.listenEvents(); this.unlistenHandler = _ => self.unlistenEvents(); if (this.options.listenOnFocus) { this.blueprint.addEventListener(Configuration.focusEventName.begin, this.listenHandler); this.blueprint.addEventListener(Configuration.focusEventName.end, this.unlistenHandler); } if (this.options.unlistenOnTextEdit) { this.blueprint.addEventListener(Configuration.editTextEventName.begin, this.unlistenHandler); this.blueprint.addEventListener(Configuration.editTextEventName.end, this.listenHandler); } } unlistenDOMElement() { this.unlistenEvents(); this.blueprint.removeEventListener(Configuration.focusEventName.begin, this.listenHandler); this.blueprint.removeEventListener(Configuration.focusEventName.end, this.unlistenHandler); this.blueprint.removeEventListener(Configuration.editTextEventName.begin, this.unlistenHandler); this.blueprint.removeEventListener(Configuration.editTextEventName.end, this.listenHandler); } /* Subclasses will probabily override the following methods */ listenEvents() { } unlistenEvents() { } } /** @typedef {import("./IEntity").default} IEntity */ class CalculatedType { #f /** @param {Function} f */ constructor(f) { this.#f = f; } /** @param {IEntity} entity */ calculate(entity) { return this.#f(entity) } } class Observable { /** @type {Map} */ #observers = new Map() /** * @param {String} property * @param {(value: any) => {}} observer */ subscribe(property, observer) { let observers = this.#observers; if (observers.has(property)) { let propertyObservers = observers.get(property); if (propertyObservers.includes(observer)) { return false } else { propertyObservers.push(observer); } } else { let fromPrototype = false; let propertyDescriptor = Object.getOwnPropertyDescriptor(this, property); if (!propertyDescriptor) { fromPrototype = true; propertyDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this), property) ?? {}; if (!propertyDescriptor) { return false } } observers.set(property, [observer]); const isValue = "value" in propertyDescriptor; const hasSetter = "set" in propertyDescriptor; if (!(isValue || hasSetter)) { throw new Error(`Property ${property} is not a value or a setter`) } // A Symbol so it does not show up in Object.getOwnPropertyNames() const storageKey = Symbol.for(property + "Storage"); const valInfoKey = Symbol.for(property + "ValInfo"); Object.defineProperties( fromPrototype ? Object.getPrototypeOf(this) : this, { [storageKey]: { configurable: true, enumerable: false, // Non enumerable so it does not show up in for...in or Object.keys() ...(isValue ? { value: this[property], writable: true, } : { get: propertyDescriptor.get, set: propertyDescriptor.set, } ) }, [valInfoKey]: { configurable: true, enumerable: false, value: [fromPrototype, isValue] }, [property]: { configurable: true, ...(isValue && { get() { return this[storageKey] } }), set(v) { this[storageKey] = v; observers.get(property).forEach(observer => { observer(this[property]); }); }, } } ); } return true } /** * @param {String} property * @param {Object} observer */ unsubscribe(property, observer) { let observers = this.#observers.get(property); if (!observers?.includes(observer)) { return false } observers.splice(observers.indexOf(observer), 1); if (observers.length == 0) { const storageKey = Symbol.for(property + "Storage"); const valInfoKey = Symbol.for(property + "ValInfo"); const fromPrototype = this[valInfoKey][0]; this[valInfoKey][1]; Object.defineProperty( fromPrototype ? Object.getPrototypeOf(this) : this, property, Object.getOwnPropertyDescriptor(fromPrototype ? Object.getPrototypeOf(this) : this, storageKey), ); delete this[valInfoKey]; delete this[storageKey]; } return true } } /** * @typedef {import("../entity/IEntity").default} IEntity * @typedef {import("../entity/TypeInitialization").AnyValue} AnyValue */ /** * @template T * @typedef {import("../entity/TypeInitialization").AnyValueConstructor} AnyValueConstructor */ /** * @template {AnyValue} T * @typedef {import("./ISerializer").default} ISerializer */ class SerializerFactory { /** @type {Map, ISerializer>} */ static #serializers = new Map() static registerSerializer(entity, object) { SerializerFactory.#serializers.set(entity, object); } /** * @template {AnyValue} T * @param {AnyValueConstructor} entity */ static getSerializer(entity) { return SerializerFactory.#serializers.get(entity) } } /** * @template T * @typedef {import("./TypeInitialization").AnyValueConstructor} AnyValueConstructor */ class UnionType { #types get types() { return this.#types } /** @param {...AnyValueConstructor} types */ constructor(...types) { this.#types = types; } getFirstType() { return this.#types[0] } } /** * @typedef {IEntity | String | Number | Boolean | Array} AnyValue * @typedef {import("./IEntity").default} IEntity */ /** * @template {AnyValue} T * @typedef {import("./IEntity").IEntityConstructor} IEntityConstructor */ /** * @template {AnyValue} T * @typedef {IEntityConstructor | StringConstructor | NumberConstructor | BooleanConstructor | ArrayConstructor | UnionType} AnyValueConstructor */ /** @template {AnyValue} T */ class TypeInitialization { /** @type {AnyValueConstructor|AnyValueConstructor[]} */ #type get type() { return this.#type } set type(v) { this.#type = v; } #showDefault = true get showDefault() { return this.#showDefault } set showDefault(v) { this.#showDefault = v; } /** @type {T | T[] | String | (() => T) | (() => T[])} */ #value get value() { return this.#value } set value(v) { this.#value = v; } /** @type {Boolean} */ #serialized get serialized() { return this.#serialized } set serialized(v) { this.#serialized = v; } #ignored get ignored() { return this.#ignored } set ignored(v) { this.#ignored = v; } static isValueOfType(value, type) { return value != null && (value instanceof type || value.constructor === type) } static sanitize(value, targetType) { if (targetType === undefined) { targetType = value?.constructor; } if (targetType instanceof Array) { let type = targetType.find(t => TypeInitialization.isValueOfType(value, t)); if (!type) { type = targetType[0]; } targetType = type; } if (targetType && !TypeInitialization.isValueOfType(value, targetType)) { value = new targetType(value); } if (value instanceof Boolean || value instanceof Number || value instanceof String) { value = value.valueOf(); // Get the relative primitive value } return value } /** * @param {AnyValueConstructor|AnyValueConstructor[]} type * @param {Boolean} showDefault * @param {T | T[] | String | (() => T) | (() => T[])} value * @param {Boolean} serialized */ constructor(type, showDefault = true, value = undefined, serialized = false, ignored = false) { if (value === undefined) { if (type instanceof Array) { value = []; } else if (serialized) { value = ""; } else { value = () => TypeInitialization.sanitize(new type()); } } this.#type = type; this.#showDefault = showDefault; this.#value = value; this.#serialized = serialized; this.#ignored = ignored; } } /** * @typedef {import("./element/IElement").default} IElement * @typedef {import("./entity/IEntity").default} IEntity * @typedef {import("./entity/LinearColorEntity").default} LinearColorEntity * @typedef {import("./entity/TypeInitialization").AnyValue} AnyValue */ /** * @template T * @typedef {import("./entity/TypeInitialization").AnyValueConstructor} AnyValueConstructor */ class Utility { static emptyObj = {} static booleanConverter = { fromAttribute: (value, type) => { }, toAttribute: (value, type) => { if (value === true) { return "true" } if (value === false) { return "false" } return "" } } static sigmoid(x, curvature = 1.7) { return 1 / (1 + (x / (1 - x) ** -curvature)) } static clamp(val, min, max) { return Math.min(Math.max(val, min), max) } /** @param {HTMLElement} element */ static getScale(element) { const scale = getComputedStyle(element).getPropertyValue("--ueb-scale"); return scale != "" ? parseFloat(scale) : 1 } /** * @param {Number} num * @param {Number} decimals */ static minDecimals(num, decimals = 1) { const powered = num * 10 ** decimals; if (Math.abs(powered % 1) > Number.EPSILON) { // More decimal digits than required return num.toString() } return num.toFixed(decimals) } /** * @param {Number} num * @param {Number} decimals */ static roundDecimals(num, decimals = 1) { const power = 10 ** decimals; return Math.round(num * power) / power } /** * @param {Number[]} viewportLocation * @param {HTMLElement} movementElement */ static convertLocation(viewportLocation, movementElement) { const scaleCorrection = 1 / Utility.getScale(movementElement); const targetOffset = movementElement.getBoundingClientRect(); let location = [ Math.round((viewportLocation[0] - targetOffset.x) * scaleCorrection), Math.round((viewportLocation[1] - targetOffset.y) * scaleCorrection) ]; return location } /** * @param {IEntity} entity * @param {String[]} keys * @param {any} propertyDefinition * @returns {Boolean} */ static isSerialized( entity, keys, // @ts-expect-error propertyDefinition = Utility.objectGet(entity.constructor.attributes, keys) ) { if (propertyDefinition instanceof CalculatedType) { return Utility.isSerialized(entity, keys, propertyDefinition.calculate(entity)) } if (propertyDefinition instanceof TypeInitialization) { if (propertyDefinition.serialized) { return true } return Utility.isSerialized(entity, keys, propertyDefinition.type) } return false } /** @param {String[]} keys */ static objectGet(target, keys, defaultValue = undefined) { if (target === undefined) { return undefined } if (!(keys instanceof Array)) { throw new TypeError("Expected keys to be an array.") } if (keys.length == 0 || !(keys[0] in target) || target[keys[0]] === undefined) { return defaultValue } if (keys.length == 1) { return target[keys[0]] } return Utility.objectGet(target[keys[0]], keys.slice(1), defaultValue) } /** * @param {String[]} keys * @param {Boolean} create * @returns {Boolean} */ static objectSet(target, keys, value, create = false, defaultDictType = Object) { if (!(keys instanceof Array)) { throw new TypeError("Expected keys to be an array.") } if (keys.length == 1) { if (create || keys[0] in target || target[keys[0]] === undefined) { target[keys[0]] = value; return true } } else if (keys.length > 0) { if (create && !(target[keys[0]] instanceof Object)) { target[keys[0]] = new defaultDictType(); } return Utility.objectSet(target[keys[0]], keys.slice(1), value, create, defaultDictType) } return false } static equals(a, b) { a = TypeInitialization.sanitize(a); b = TypeInitialization.sanitize(b); if (a === b) { return true } if (a instanceof Array && b instanceof Array) { return a.length == b.length && !a.find((value, i) => !Utility.equals(value, b[i])) } } /** * @param {AnyValue | AnyValueConstructor} value * @returns {AnyValueConstructor | AnyValueConstructor[]} */ static getType(value) { if (value === null) { return null } if (value instanceof TypeInitialization) { return Utility.getType(value.type) } if (value instanceof UnionType) { return value.types } if (value instanceof Function) { // value is already a constructor return value } /** @ts-expect-error */ return value?.constructor } /** * @param {Number[]} location * @param {Number} gridSize */ static snapToGrid(location, gridSize) { if (gridSize === 1) { return location } return [ gridSize * Math.round(location[0] / gridSize), gridSize * Math.round(location[1] / gridSize) ] } /** * @template T * @param {Array} a * @param {Array} b */ static mergeArrays(a = [], b = []) { let result = []; for (let j = 0; j < b.length; ++j) { for (let i = 0; i < a.length; ++i) { if (a[i] == b[j]) { // Found a corresponding element in the two arrays result.push( // Take and append all the elements skipped from a ...a.splice(0, i), // Take and append all the elements skippend from b ...b.splice(0, j), // Take and append the element in common ...a.splice(0, 1) ); j = 0; i = 0; b.shift(); break } } } // Append remaining the elements in the arrays and make it unique return [...(new Set(result.concat(...a, ...b)))] } /** @param {String} value */ static escapeString(value, input = false) { return value .replaceAll('"', '\\"') // Escape " .replaceAll("\n", "\\n") // Replace newline with \n } /** @param {String} value */ static unescapeString(value, input = false) { return value .replaceAll('\\"', '"') .replaceAll("\\n", "\n") } /** @param {String} value */ static clearHTMLWhitespace(value) { return value .replaceAll(" ", "\u00A0") // whitespace .replaceAll("", "\n") // newlines .replaceAll(/(\)/g, "") // html comments } /** @param {String} value */ static capitalFirstLetter(value) { if (value.length === 0) { return value } return value.charAt(0).toLocaleUpperCase() + value.slice(1).toLocaleLowerCase() } /** @param {String} value */ static formatStringName(value) { return value .trim() .replace(/^b/, "") // Remove leading b (for boolean values) or newlines .replaceAll(/^K2(?:Node|node)?_|(?<=[a-z])(?=[A-Z])|_|\s+/g, " ") // Insert a space between a lowercase and uppercase letter, instead of an underscore or multiple spaces .split(" ") .map(v => Utility.capitalFirstLetter(v)) .join(" ") } /** @param {String} value */ static getIdFromReference(value) { return value .replace(/(?:.+\.)?([^\.]+)$/, "$1") .replaceAll(/(?<=[a-z\d])(?=[A-Z])|(?<=[a-zA-Z])(?=\d)|(?<=[A-Z]{2})(?=[A-Z][a-z])/g, "-") .toLowerCase() } /** @param {LinearColorEntity} value */ static printLinearColor(value) { return `${Math.round(value.R.valueOf() * 255)}, ${Math.round(value.G.valueOf() * 255)}, ${Math.round(value.B.valueOf() * 255)}` } /** @param {[Number, Number]} param0 */ static getPolarCoordinates([x, y], positiveTheta = false) { let theta = Math.atan2(y, x); if (positiveTheta && theta < 0) { theta = 2 * Math.PI + theta; } return [ Math.sqrt(x * x + y * y), theta, ] } /** @param {[Number, Number]} param0 */ static getCartesianCoordinates([r, theta]) { return [ r * Math.cos(theta), r * Math.sin(theta) ] } static range(begin, end, step = 1) { return Array.from({ length: Math.ceil((end - begin) / step) }, (_, i) => begin + (i * step)) } } /** * @template {IEntity} T * @typedef {new (Object) => T} IEntityConstructor */ class IEntity extends Observable { static attributes = {} constructor(values = {}) { super(); /** * @param {Object} target * @param {Object} attributes * @param {Object} values * @param {String} prefix */ const defineAllAttributes = (target, attributes, values = {}, prefix = "") => { const valuesPropertyNames = Object.getOwnPropertyNames(values); for (let attribute of Utility.mergeArrays(Object.getOwnPropertyNames(attributes), valuesPropertyNames)) { let value = Utility.objectGet(values, [attribute]); let defaultValue = attributes[attribute]; let defaultType = Utility.getType(defaultValue); if (defaultValue instanceof CalculatedType) { defaultValue = defaultValue.calculate(this); defaultType = Utility.getType(defaultValue); } if (defaultValue != null && defaultValue === defaultType) { defaultValue = new defaultType(); } if (!(attribute in attributes)) { console.warn( `Attribute ${prefix}${attribute} in the serialized data is not defined in ${this.constructor.name}.attributes` ); } else if ( valuesPropertyNames.length > 0 && !(attribute in values) && defaultValue !== undefined && !(defaultValue instanceof TypeInitialization && (!defaultValue.showDefault || defaultValue.ignored)) ) { console.warn( `${this.constructor.name} will add attribute ${prefix}${attribute} not defined in the serialized data` ); } // Not instanceof because all objects are instenceof Object, exact match needed // @ts-expect-error if (defaultType === Object) { target[attribute] = {}; defineAllAttributes(target[attribute], attributes[attribute], values[attribute], attribute + "."); continue } if (value !== undefined) { // Remember value can still be null if ( value?.constructor === String && defaultValue instanceof TypeInitialization && defaultValue.serialized && defaultValue.type !== String ) { // @ts-expect-error value = SerializerFactory.getSerializer(defaultValue.type).deserialize(value); } target[attribute] = TypeInitialization.sanitize(value, Utility.getType(defaultValue)); continue // We have a value, need nothing more } if (defaultValue instanceof TypeInitialization) { if (!defaultValue.showDefault) { target[attribute] = undefined; // Declare undefined to preserve the order of attributes continue } if (defaultValue.serialized) { defaultValue = ""; } else { defaultType = defaultValue.type; defaultValue = defaultValue.value; if (defaultValue instanceof Function) { defaultValue = defaultValue(); } } } if (defaultValue instanceof UnionType) { defaultType = defaultValue.getFirstType(); defaultValue = TypeInitialization.sanitize(null, defaultType); } if (defaultValue instanceof Array) { defaultValue = []; } target[attribute] = TypeInitialization.sanitize(defaultValue, defaultType); } }; // @ts-expect-error const attributes = this.constructor.attributes; if (values.constructor !== Object && Object.getOwnPropertyNames(attributes).length === 1) { // Where there is just one attribute, option can be the value of that attribute values = { [Object.getOwnPropertyNames(attributes)[0]]: values }; } defineAllAttributes(this, attributes, values); } unexpectedKeys() { // @ts-expect-error return Object.getOwnPropertyNames(this).length - Object.getOwnPropertyNames(this.constructor.attributes).length } } class ObjectReferenceEntity extends IEntity { static attributes = { type: String, path: String, } constructor(values = {}) { if (values.constructor !== Object) { values = { path: values }; } super(values); /** @type {String} */ this.type; /** @type {String} */ this.path; } getName() { return this.path.match(/[^\.\/]+$/)[0] } } class FunctionReferenceEntity extends IEntity { static attributes = { MemberParent: new TypeInitialization(ObjectReferenceEntity, false), MemberName: "", } constructor(values) { super(values); /** @type {ObjectReferenceEntity} */ this.MemberParent; /** @type {String} */ this.MemberName; } } class GuidEntity extends IEntity { 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({ value: guid }) } constructor(values) { if (!values) { values = GuidEntity.generateGuid().value; } super(values); /** @type {String} */ this.value; } valueOf() { return this.value } toString() { return this.value } } class IdentifierEntity extends IEntity { static attributes = { value: String, } static attributeConverter = { fromAttribute: (value, type) => new IdentifierEntity(value), toAttribute: (value, type) => value.toString() } constructor(values) { super(values); /** @type {String} */ this.value; } valueOf() { return this.value } toString() { return this.value } } class IntegerEntity extends IEntity { static attributes = { value: 0, } /** @param {Object | Number | String} values */ constructor(values = 0) { super(values); /** @type {Number} */ this.value = Math.round(this.value); } valueOf() { return this.value } toString() { return this.value.toString() } } class InvariantTextEntity extends IEntity { static lookbehind = "INVTEXT" static attributes = { value: String, } constructor(values) { super(values); /** @type {String} */ this.value; } } class KeyBindingEntity extends IEntity { static attributes = { ActionName: "", bShift: false, bCtrl: false, bAlt: false, bCmd: false, Key: IdentifierEntity, } constructor(values = {}) { values.ActionName = values.ActionName ?? ""; values.bShift = values.bShift ?? false; values.bCtrl = values.bCtrl ?? false; values.bAlt = values.bAlt ?? false; values.bCmd = values.bCmd ?? false; super(values); /** @type {String} */ this.ActionName; /** @type {Boolean} */ this.bShift; /** @type {Boolean} */ this.bCtrl; /** @type {Boolean} */ this.bAlt; /** @type {Boolean} */ this.bCmd; /** @type {IdentifierEntity} */ this.Key; } } class RealUnitEntity extends IEntity { static attributes = { value: 0, } /** @param {Object | Number | String} values */ constructor(values = 0) { super(values); this.value = Utility.clamp(this.value, 0, 1); } valueOf() { return this.value } toString() { return this.value.toFixed(6) } } class LinearColorEntity extends IEntity { static attributes = { R: RealUnitEntity, G: RealUnitEntity, B: RealUnitEntity, A: new TypeInitialization(RealUnitEntity, true, () => new RealUnitEntity(1), false, true), H: new TypeInitialization(RealUnitEntity, true, undefined, false, true), S: new TypeInitialization(RealUnitEntity, true, undefined, false, true), V: new TypeInitialization(RealUnitEntity, true, undefined, false, true), } /** @param {Number} x */ static linearToSRGB(x) { if (x <= 0) { return 0 } else if (x >= 1) { return 1 } else if (x < 0.0031308) { return x * 12.92 } else { return Math.pow(x, 1 / 2.4) * 1.055 - 0.055 } } /** @param {Number} x */ static sRGBtoLinear(x) { if (x <= 0) { return 0 } else if (x >= 1) { return 1 } else if (x < 0.04045) { return x / 12.92 } else { return Math.pow((x + 0.055) / 1.055, 2.4) } } static getWhite() { return new LinearColorEntity({ R: 1, G: 1, B: 1, }) } constructor(values) { if (values instanceof Array) { values = { R: values[0] ?? 0, G: values[1] ?? 0, B: values[2] ?? 0, A: values[3] ?? 1, }; } super(values); /** @type {RealUnitEntity} */ this.R; /** @type {RealUnitEntity} */ this.G; /** @type {RealUnitEntity} */ this.B; /** @type {RealUnitEntity} */ this.A; /** @type {RealUnitEntity} */ this.H; /** @type {RealUnitEntity} */ this.S; /** @type {RealUnitEntity} */ this.V; this.#updateHSV(); } #updateHSV() { const r = this.R.value; const g = this.G.value; const b = this.B.value; if ( !(Math.abs(r - g) > Number.EPSILON) && !(Math.abs(r - b) > Number.EPSILON) && !(Math.abs(g - b) > Number.EPSILON) ) { this.V.value = 0; return } const max = Math.max(r, g, b); const min = Math.min(r, g, b); const d = max - min; let h; switch (max) { case min: h = 0; break case r: h = (g - b) / d + (g < b ? 6 : 0); break case g: h = (b - r) / d + 2; break case b: h = (r - g) / d + 4; break } h /= 6; this.H.value = h; this.S.value = max == 0 ? 0 : d / max; this.V.value = max; } /** * @param {Number} r * @param {Number} g * @param {Number} b * @param {Number} a */ setFromRGBA(r, g, b, a = 1) { this.R.value = r; this.G.value = g; this.B.value = b; this.A.value = a; this.#updateHSV(); } /** * @param {Number} h * @param {Number} s * @param {Number} v * @param {Number} a */ setFromHSVA(h, s, v, a = 1) { const i = Math.floor(h * 6); const f = h * 6 - i; const p = v * (1 - s); const q = v * (1 - f * s); const t = v * (1 - (1 - f) * s); const values = [v, q, p, p, t, v]; const [r, g, b] = [values[i % 6], values[(i + 4) % 6], values[(i + 2) % 6]]; this.R.value = r; this.G.value = g; this.B.value = b; this.A.value = a; this.H.value = h; this.S.value = s; this.V.value = v; } setFromWheelLocation([x, y], v, a) { const [r, theta] = Utility.getPolarCoordinates([x, y], true); this.setFromHSVA(1 - theta / (2 * Math.PI), r, v, a); } toRGBA() { return [ Math.round(this.R.value * 255), Math.round(this.G.value * 255), Math.round(this.B.value * 255), Math.round(this.A.value * 255), ] } toSRGBA() { return [ Math.round(LinearColorEntity.linearToSRGB(this.R.value) * 255), Math.round(LinearColorEntity.linearToSRGB(this.G.value) * 255), Math.round(LinearColorEntity.linearToSRGB(this.B.value) * 255), Math.round(this.A.value * 255), ] } toRGBAString() { return this .toRGBA() .map(v => v.toString(16).toUpperCase().padStart(2, "0")) .join("") } toSRGBAString() { return this .toSRGBA() .map(v => v.toString(16).toUpperCase().padStart(2, "0")) .join("") } toHSVA() { return [this.H.value, this.S.value, this.V.value, this.A.value] } toNumber() { return (this.R.value << 24) + (this.G.value << 16) + (this.B.value << 8) + this.A.value } /** @param {Number} number */ setFromRGBANumber(number) { this.A.value = (number & 0xFF) / 0xff; this.B.value = ((number >> 8) & 0xFF) / 0xff; this.G.value = ((number >> 16) & 0xFF) / 0xff; this.R.value = ((number >> 24) & 0xFF) / 0xff; this.#updateHSV(); } /** @param {Number} number */ setFromSRGBANumber(number) { this.A.value = (number & 0xFF) / 0xff; this.B.value = LinearColorEntity.sRGBtoLinear(((number >> 8) & 0xFF) / 0xff); this.G.value = LinearColorEntity.sRGBtoLinear(((number >> 16) & 0xFF) / 0xff); this.R.value = LinearColorEntity.sRGBtoLinear(((number >> 24) & 0xFF) / 0xff); this.#updateHSV(); } toString() { return Utility.printLinearColor(this) } } class LocalizedTextEntity extends IEntity { static lookbehind = "NSLOCTEXT" static attributes = { namespace: String, key: String, value: String, } constructor(values) { super(values); /** @type {String} */ this.namespace; /** @type {String} */ this.key; /** @type {String} */ this.value; } toString() { return Utility.capitalFirstLetter(this.value) } } class MacroGraphReferenceEntity extends IEntity { static attributes = { MacroGraph: ObjectReferenceEntity, GraphBlueprint: ObjectReferenceEntity, GraphGuid: GuidEntity, } constructor(values) { super(values); /** @type {ObjectReferenceEntity} */ this.MacroGraph; /** @type {ObjectReferenceEntity} */ this.GraphBlueprint; /** @type {GuidEntity} */ this.GuidEntity; } getMacroName() { const colonIndex = this.MacroGraph.path.search(":"); return this.MacroGraph.path.substring(colonIndex + 1) } } class PathSymbolEntity extends IEntity { static attributes = { value: String, } constructor(values) { super(values); /** @type {String} */ this.value; } valueOf() { return this.value } toString() { return this.value } } class PinReferenceEntity extends IEntity { static attributes = { objectName: PathSymbolEntity, pinGuid: GuidEntity, } constructor(values) { super(values); /** @type {PathSymbolEntity} */ this.objectName; /** @type {GuidEntity} */ this.pinGuid; } } class RotatorEntity extends IEntity { static attributes = { R: Number, P: Number, Y: Number, } constructor(values) { super(values); /** @type {Number} */ this.R; /** @type {Number} */ this.P; /** @type {Number} */ this.Y; } } class SimpleSerializationRotatorEntity extends RotatorEntity { } class VectorEntity extends IEntity { static attributes = { X: Number, Y: Number, Z: Number, } constructor(values) { super(values); /** @type {Number} */ this.X; /** @type {Number} */ this.Y; /** @type {Number} */ this.Z; } } class SimpleSerializationVectorEntity extends VectorEntity { } /** @typedef {import("./TypeInitialization").AnyValue} AnyValue */ /** @template {AnyValue} T */ class PinEntity extends IEntity { static #typeEntityMap = { "/Script/CoreUObject.LinearColor": LinearColorEntity, "/Script/CoreUObject.Rotator": RotatorEntity, "/Script/CoreUObject.Vector": VectorEntity, "bool": Boolean, "exec": String, "int": IntegerEntity, "name": String, "real": Number, "string": String, } static #alternativeTypeEntityMap = { "/Script/CoreUObject.Vector": SimpleSerializationVectorEntity, "/Script/CoreUObject.Rotator": SimpleSerializationRotatorEntity, } static lookbehind = "Pin" static attributes = { PinId: GuidEntity, PinName: "", PinFriendlyName: new TypeInitialization(new UnionType(LocalizedTextEntity, String), false, null), PinToolTip: new TypeInitialization(String, false, ""), Direction: new TypeInitialization(String, false, ""), PinType: { PinCategory: "", PinSubCategory: "", PinSubCategoryObject: ObjectReferenceEntity, PinSubCategoryMemberReference: null, PinValueType: null, ContainerType: ObjectReferenceEntity, bIsReference: false, bIsConst: false, bIsWeakPointer: false, bIsUObjectWrapper: false, bSerializeAsSinglePrecisionFloat: false, }, LinkedTo: new TypeInitialization([PinReferenceEntity], false), DefaultValue: new CalculatedType( /** @param {PinEntity} pinEntity */ pinEntity => new TypeInitialization( PinEntity.getEntityType(pinEntity.getType(), true) ?? String, false, undefined, true ) ), AutogeneratedDefaultValue: new TypeInitialization(String, false), DefaultObject: new TypeInitialization(ObjectReferenceEntity, false, null), PersistentGuid: GuidEntity, bHidden: false, bNotConnectable: false, bDefaultValueIsReadOnly: false, bDefaultValueIsIgnored: false, bAdvancedView: false, bOrphanedPin: false, } static getEntityType(typeString, alternative = false) { const [entity, alternativeEntity] = [this.#typeEntityMap[typeString], this.#alternativeTypeEntityMap[typeString]]; return alternative && alternativeEntity !== undefined ? alternativeEntity : entity } constructor(values = {}) { super(values); /** @type {GuidEntity} */ this.PinId; /** @type {String} */ this.PinName; /** @type {LocalizedTextEntity | String} */ this.PinFriendlyName; /** @type {String} */ this.PinToolTip; /** @type {String} */ this.Direction; /** * @type {{ * PinCategory: String, * PinSubCategory: String, * PinSubCategoryObject: ObjectReferenceEntity, * PinSubCategoryMemberReference: any, * PinValueType: String, * ContainerType: ObjectReferenceEntity, * bIsReference: Boolean, * bIsConst: Boolean, * bIsWeakPointer: Boolean, * bIsUObjectWrapper: Boolean, * bSerializeAsSinglePrecisionFloat: Boolean, * }} */ this.PinType; /** @type {PinReferenceEntity[]} */ this.LinkedTo; /** @type {T} */ this.DefaultValue; /** @type {String} */ this.AutogeneratedDefaultValue; /** @type {ObjectReferenceEntity} */ this.DefaultObject; /** @type {GuidEntity} */ this.PersistentGuid; /** @type {Boolean} */ this.bHidden; /** @type {Boolean} */ this.bNotConnectable; /** @type {Boolean} */ this.bDefaultValueIsReadOnly; /** @type {Boolean} */ this.bDefaultValueIsIgnored; /** @type {Boolean} */ this.bAdvancedView; /** @type {Boolean} */ this.bOrphanedPin; } getType() { if (this.PinType.PinCategory == "struct" || this.PinType.PinCategory == "object") { return this.PinType.PinSubCategoryObject.path } return this.PinType.PinCategory } getDisplayName() { let matchResult = null; if ( this.PinToolTip // Match up until the first \n excluded or last character && (matchResult = this.PinToolTip.match(/\s*(.+?(?=\n)|.+\S)\s*/)) ) { return Utility.formatStringName(matchResult[1]) } return Utility.formatStringName(this.PinName) } /** @param {PinEntity} other */ copyTypeFrom(other) { this.PinType.PinCategory = other.PinType.PinCategory; this.PinType.PinSubCategory = other.PinType.PinSubCategory; this.PinType.PinSubCategoryObject = other.PinType.PinSubCategoryObject; this.PinType.PinSubCategoryMemberReference = other.PinType.PinSubCategoryMemberReference; this.PinType.PinValueType = other.PinType.PinValueType; this.PinType.ContainerType = other.PinType.ContainerType; this.PinType.bIsReference = other.PinType.bIsReference; this.PinType.bIsConst = other.PinType.bIsConst; this.PinType.bIsWeakPointer = other.PinType.bIsWeakPointer; this.PinType.bIsUObjectWrapper = other.PinType.bIsUObjectWrapper; this.PinType.bSerializeAsSinglePrecisionFloat = other.PinType.bSerializeAsSinglePrecisionFloat; } getDefaultValue() { return this.DefaultValue } isExecution() { return this.PinType.PinCategory === "exec" } isHidden() { return this.bHidden } isInput() { return !this.bHidden && this.Direction != "EGPD_Output" } isOutput() { return !this.bHidden && this.Direction == "EGPD_Output" } isLinked() { return this.LinkedTo?.length > 0 ?? false } /** * @param {String} targetObjectName * @param {PinEntity} targetPinEntity */ linkTo(targetObjectName, targetPinEntity) { /** @type {PinReferenceEntity[]} */ this.LinkedTo; const linkFound = this.LinkedTo?.find(pinReferenceEntity => { return pinReferenceEntity.objectName.toString() == targetObjectName && pinReferenceEntity.pinGuid.valueOf() == targetPinEntity.PinId.valueOf() }); if (!linkFound) { (this.LinkedTo ?? (this.LinkedTo = [])).push(new PinReferenceEntity({ objectName: targetObjectName, pinGuid: targetPinEntity.PinId, })); return true } return false } /** * @param {String} targetObjectName * @param {PinEntity} targetPinEntity */ unlinkFrom(targetObjectName, targetPinEntity) { const indexElement = this.LinkedTo?.findIndex(pinReferenceEntity => { return pinReferenceEntity.objectName.toString() == targetObjectName && pinReferenceEntity.pinGuid.valueOf() == targetPinEntity.PinId.valueOf() }); if (indexElement >= 0) { if (this.LinkedTo.length == 1) { this.LinkedTo = undefined; } else { this.LinkedTo.splice(indexElement, 1); } return true } return false } getSubCategory() { return this.PinType.PinSubCategoryObject.path } } class SymbolEntity extends IEntity { static attributes = { value: String } constructor(values) { super(values); /** @type {String} */ this.value; } } class VariableReferenceEntity extends IEntity { static attributes = { MemberScope: new TypeInitialization(String, false), MemberName: String, MemberGuid: GuidEntity, bSelfContext: new TypeInitialization(Boolean, false, false) } constructor(values) { super(values); /** @type {String} */ this.MemberName; /** @type {GuidEntity} */ this.GuidEntity; /** @type {Boolean} */ this.bSelfContext; } } class ObjectEntity extends IEntity { static attributes = { Class: ObjectReferenceEntity, Name: "", bIsPureFunc: new TypeInitialization(Boolean, false, false), VariableReference: new TypeInitialization(VariableReferenceEntity, false, null), SelfContextInfo: new TypeInitialization(SymbolEntity, false, null), FunctionReference: new TypeInitialization(FunctionReferenceEntity, false, null,), EventReference: new TypeInitialization(FunctionReferenceEntity, false, null,), TargetType: new TypeInitialization(ObjectReferenceEntity, false, null), MacroGraphReference: new TypeInitialization(MacroGraphReferenceEntity, false, null), Enum: new TypeInitialization(ObjectReferenceEntity, false), CommentColor: new TypeInitialization(LinearColorEntity, false), bCommentBubbleVisible_InDetailsPanel: new TypeInitialization(Boolean, false), bColorCommentBubble: new TypeInitialization(Boolean, false, false), MoveMode: new TypeInitialization(SymbolEntity, false), NodePosX: IntegerEntity, NodePosY: IntegerEntity, NodeWidth: new TypeInitialization(IntegerEntity, false), NodeHeight: new TypeInitialization(IntegerEntity, false), bCommentBubblePinned: new TypeInitialization(Boolean, false), bCommentBubbleVisible: new TypeInitialization(Boolean, false), NodeComment: new TypeInitialization(String, false), AdvancedPinDisplay: new TypeInitialization(IdentifierEntity, false, null), EnabledState: new TypeInitialization(IdentifierEntity, false, null), NodeGuid: GuidEntity, ErrorType: new TypeInitialization(IntegerEntity, false), ErrorMsg: new TypeInitialization(String, false, ""), CustomProperties: [PinEntity], } static nameRegex = /^(\w+?)(?:_(\d+))?$/ static sequencerScriptingNameRegex = /\/Script\/SequencerScripting\.MovieSceneScripting(.+)Channel/ constructor(values) { super(values); /** @type {ObjectReferenceEntity} */ this.Class; /** @type {String} */ this.Name; /** @type {Boolean?} */ this.bIsPureFunc; /** @type {VariableReferenceEntity?} */ this.VariableReference; /** @type {FunctionReferenceEntity?} */ this.FunctionReference; /** @type {FunctionReferenceEntity?} */ this.EventReference; /** @type {ObjectReferenceEntity?} */ this.TargetType; /** @type {MacroGraphReferenceEntity?} */ this.MacroGraphReference; /** @type {ObjectReferenceEntity?} */ this.Enum; /** @type {LinearColorEntity?} */ this.CommentColor; /** @type {Boolean?} */ this.bCommentBubbleVisible_InDetailsPanel; /** @type {IntegerEntity} */ this.NodePosX; /** @type {IntegerEntity} */ this.NodePosY; /** @type {IntegerEntity?} */ this.NodeWidth; /** @type {IntegerEntity?} */ this.NodeHeight; /** @type {Boolean?} */ this.bCommentBubblePinned; /** @type {Boolean?} */ this.bCommentBubbleVisible; /** @type {String?} */ this.NodeComment; /** @type {IdentifierEntity?} */ this.AdvancedPinDisplay; /** @type {IdentifierEntity?} */ this.EnabledState; /** @type {GuidEntity} */ this.NodeGuid; /** @type {IntegerEntity?} */ this.ErrorType; /** @type {String?} */ this.ErrorMsg; /** @type {PinEntity[]} */ this.CustomProperties; } getClass() { return this.Class.path } getType() { let classValue = this.getClass(); if (classValue === Configuration.nodeType.macro) { return this.MacroGraphReference.MacroGraph.path } return classValue } getObjectName(dropCounter = false) { if (dropCounter) { return this.getNameAndCounter()[0] } return this.Name } /** @returns {[String, Number]} */ getNameAndCounter() { const result = this.getObjectName(false).match(ObjectEntity.nameRegex); let name = ""; let counter = null; if (result) { if (result.length > 1) { name = result[1]; } if (result.length > 2) { counter = parseInt(result[2]); } return [name, counter] } return ["", 0] } getDisplayName() { switch (this.getType()) { case Configuration.nodeType.callFunction: if (this.FunctionReference.MemberName === "AddKey") { let result = this.FunctionReference.MemberParent.path.match(ObjectEntity.sequencerScriptingNameRegex); if (result) { return `Add Key (${Utility.formatStringName(result[1])})` } } return Utility.formatStringName(this.FunctionReference.MemberName) case Configuration.nodeType.dynamicCast: return `Cast To ${this.TargetType.getName()}` case Configuration.nodeType.executionSequence: return "Sequence" case Configuration.nodeType.ifThenElse: return "Branch" case Configuration.nodeType.forEachElementInEnum: return `For Each ${this.Enum.getName()}` case Configuration.nodeType.forEachLoopWithBreak: return "For Each Loop with Break" case Configuration.nodeType.variableGet: return "" case Configuration.nodeType.variableSet: return "SET" default: if (this.getClass() === Configuration.nodeType.macro) { return Utility.formatStringName(this.MacroGraphReference.getMacroName()) } else { return Utility.formatStringName(this.getNameAndCounter()[0]) } } } getCounter() { return this.getNameAndCounter()[1] } getNodeWidth() { return this.NodeWidth ?? this.getType() == Configuration.nodeType.comment ? Configuration.defaultCommentWidth : undefined } getNodeHeight() { return this.NodeHeight ?? this.getType() == Configuration.nodeType.comment ? Configuration.defaultCommentHeight : undefined } } 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(a(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=f(t,n.buf);return {coll:n.coll.concat(r.v),buf:r.buf}},{coll:[],buf:t.slice(r,e)},n).coll)})}function h(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 p(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 p("uintBE",n),h("uintBE("+n+")",n).map(function(t){return t.readUIntBE(0,n)})}function v(n){return p("uintLE",n),h("uintLE("+n+")",n).map(function(t){return t.readUIntLE(0,n)})}function g(n){return p("intBE",n),h("intBE("+n+")",n).map(function(t){return t.readIntBE(0,n)})}function m(n){return p("intLE",n),h("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)||"\r"===n.charAt(i)&&"\n"!==n.charAt(i+1))&&(u++,0===o&&(o=i+1)),i--;}var a=e+u,f=t-o;return r[t]={line:a,lineStart:o},{offset:t,line:a+1,column:f+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,f,c=t.index,s=c.offset,l=1;if(s===n.length)return "Got the end of the input";if(w(n)){var h=s-s%I,p=s-h,d=W(h,F,M+I,n.length),v=a(function(n){return a(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=a(function(n){return n.length<=4?n.join(" "):n.slice(0,4).join(" ")+" "+n.slice(4).join(" ")},v),(f=(8*(o.to>0?o.to-1:o.to)).toString(16).length)<2&&(f=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),f=o.to.toString().length;}var m=e-o.from;return w(n)&&(f=(8*(o.to>0?o.to-1:o.to)).toString(16).length)<2&&(f=2),i(function(t,e,u){var i,a=u===m,c=a?"> ":z;return i=w(n)?U((8*(o.from+u)).toString(16),f,"0"):U((o.from+u+1).toString(),f," "),[].concat(t,[c+i+" | "+e],a?[z+R(" ",f)+" | "+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],a=o[t];return b(e+i.length,a)}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))}),an=e(function(n,t){return t=0}).desc(t)},e.optWhitespace=hn,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 h("buffer",n).map(function(n){return Buffer.from(n)})},encodedString:function(n,t){return h("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:h("floatBE",4).map(function(n){return n.readFloatBE(0)}),floatLE:h("floatLE",4).map(function(n){return n.readFloatLE(0)}),doubleBE:h("doubleBE",8).map(function(n){return n.readDoubleBE(0)}),doubleLE:h("doubleLE",8).map(function(n){return n.readDoubleLE(0)})},n.exports=e;}])}); }(parsimmon_umd_min)); var Parsimmon = /*@__PURE__*/getDefaultExportFromCjs(parsimmon_umd_min.exports); class UnknownKeysEntity extends IEntity { static attributes = { lookbehind: new TypeInitialization(String, false, "", false, true) } constructor(values) { super(values); /** @type {String} */ this.lookbehind; } } // @ts-nocheck let P = Parsimmon; class Grammar { /* --- Factory --- */ /** @param {Grammar} r */ static getGrammarForType(r, attributeType, defaultGrammar = r.AttributeAnyValue) { if (attributeType instanceof TypeInitialization) { let result = Grammar.getGrammarForType(r, attributeType.type, defaultGrammar); if (attributeType.serialized && !(attributeType.type instanceof String)) { result = result.wrap(P.string('"'), P.string('"')); } return result } switch (Utility.getType(attributeType)) { 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(")"), (_0, grammar, _2) => grammar ) case Boolean: return r.Boolean case FunctionReferenceEntity: return r.FunctionReference case GuidEntity: return r.Guid case IdentifierEntity: return r.Identifier case IntegerEntity: return r.Integer case InvariantTextEntity: return r.InvariantText case LinearColorEntity: return r.LinearColor case LocalizedTextEntity: return r.LocalizedText case MacroGraphReferenceEntity: return r.MacroGraphReference case Number: return r.Number case ObjectReferenceEntity: return r.ObjectReference case PinEntity: return r.Pin case PinReferenceEntity: return r.PinReference case RealUnitEntity: return r.RealUnit case RotatorEntity: return r.Rotator case SimpleSerializationRotatorEntity: return r.SimpleSerializationRotator case SimpleSerializationVectorEntity: return r.SimpleSerializationVector case String: return r.String case SymbolEntity: return r.Symbol case UnionType: return attributeType.types .map(v => Grammar.getGrammarForType(r, Utility.getType(v))) .reduce((accum, cur) => !cur || accum === r.AttributeAnyValue ? r.AttributeAnyValue : accum.or(cur)) case VariableReferenceEntity: return r.VariableReference case VectorEntity: return r.Vector default: return defaultGrammar } } /** @param {Grammar} r */ static ReferencePath = (r, referencePathGrammar) => P.alt( referencePathGrammar, P.seq( P.string("/"), referencePathGrammar .map(v => v.toString()) .sepBy1(P.string(".")) .tieWith(".") .sepBy1(P.string(":")) .tieWith(":") ) .tie() .atLeast(2) .tie() ) /** @param {Grammar} r */ static createAttributeGrammar = (r, entityType, valueSeparator = P.string("=").trim(P.optWhitespace)) => r.AttributeName .skip(valueSeparator) .chain(attributeName => { // Once the attribute name is known, look into entityType.attributes to get its type const attributeKey = attributeName.split("."); const attribute = Utility.objectGet(entityType.attributes, attributeKey); let attributeValueGrammar = Grammar.getGrammarForType(r, attribute, r.AttributeAnyValue); // Returns a setter function for the attribute return attributeValueGrammar.map(attributeValue => entity => Utility.objectSet(entity, attributeKey, attributeValue, true) ) }) /** @param {Grammar} r */ static createEntityGrammar = (r, entityType, limitUnknownKeys = false) => P.seqMap( entityType.lookbehind ? P.seq(P.string(entityType.lookbehind), P.optWhitespace, P.string("(")) : P.string("("), Grammar.createAttributeGrammar(r, entityType) .trim(P.optWhitespace) // Drop spaces around a attribute assignment .sepBy(P.string(",")) // Assignments are separated by comma .skip(P.regex(/,?/).then(P.optWhitespace)), // Optional trailing comma and maybe additional space P.string(")"), (_0, attributes, _2) => { let values = {}; attributes.forEach(attributeSetter => attributeSetter(values)); return values } ) // Decide if we accept the entity or not. It is accepted if it doesn't have too many unexpected keys .chain(values => { if (limitUnknownKeys) { let unexpectedKeysCount = 0; let totalKeys = 0; for (const key in values) { unexpectedKeysCount += key in entityType.attributes ? 0 : 1; ++totalKeys; } if (unexpectedKeysCount + 0.5 > Math.sqrt(totalKeys)) { return P.fail() } } return P.succeed().map(() => new entityType(values)) }) /* --- General --- */ /** @param {Grammar} r */ InlineWhitespace = r => P.regex(/[^\S\n]+/).desc("single line whitespace") /** @param {Grammar} r */ InlineOptWhitespace = r => P.regex(/[^\S\n]*/).desc("single line optional whitespace") /** @param {Grammar} r */ MultilineWhitespace = r => P.regex(/[^\S\n]*\n\s*/).desc("whitespace with at least a newline") /** @param {Grammar} r */ Null = r => P.seq(P.string("("), r.InlineOptWhitespace, P.string(")")).map(() => null).desc("null: ()") /** @param {Grammar} r */ Boolean = r => P.alt( P.string("True"), P.string("true"), P.string("False"), P.string("false"), ).map(v => v.toLocaleLowerCase() === "true" ? true : false) .desc("either True or False") /** @param {Grammar} r */ HexDigit = r => P.regex(/[0-9a-fA-f]/).desc("hexadecimal digit") /** @param {Grammar} r */ Number = r => P.regex(/[-\+]?[0-9]+(?:\.[0-9]+)?/).map(Number).desc("a number") /** @param {Grammar} r */ RealNumber = r => P.regex(/[-\+]?[0-9]+\.[0-9]+/).map(Number).desc("a number written as real") /** @param {Grammar} r */ RealUnit = r => P.regex(/\+?[0-9]+(?:\.[0-9]+)?/).map(Number).assert(v => v >= 0 && v <= 1).desc("a number between 0 and 1") /** @param {Grammar} r */ NaturalNumber = r => P.regex(/0|[1-9]\d*/).map(Number).desc("a natural number") /** @param {Grammar} r */ ColorNumber = r => r.NaturalNumber.assert(n => 0 <= n && n < 256, "the color must be between 0 and 256 excluded") /** @param {Grammar} r */ Word = r => P.regex(/[a-zA-Z_]+/).desc("a word") /** @param {Grammar} r */ String = r => P.regex(/(?:[^"\\]|\\.)*/).wrap(P.string('"'), P.string('"')).map(Utility.unescapeString) .desc('string (with possibility to escape the quote using \")') /** @param {Grammar} r */ AttributeName = r => r.Word.sepBy1(P.string(".")).tieWith(".").desc("dot-separated words") /* --- Entity --- */ /** @param {Grammar} r */ None = r => P.string("None").map(() => new ObjectReferenceEntity({ type: "None", path: "" })).desc("none") /** @param {Grammar} r */ Integer = r => P.regex(/[\-\+]?[0-9]+/).map(v => new IntegerEntity(v)).desc("an integer") /** @param {Grammar} r */ Guid = r => r.HexDigit.times(32).tie().map(v => new GuidEntity({ value: v })).desc("32 digit hexadecimal value") /** @param {Grammar} r */ Identifier = r => P.regex(/\w+/).map(v => new IdentifierEntity(v)) /** @param {Grammar} r */ PathSymbol = r => P.regex(/[0-9\w]+/).map(v => new PathSymbolEntity({ value: v })) /** @param {Grammar} r */ PathSymbolOptSpaces = r => P.regex(/[0-9\w]+(?: [0-9\w]+)+|[0-9\w]+/).map(v => new PathSymbolEntity({ value: v })) /** @param {Grammar} r */ Symbol = r => P.regex(/\w+/).map(v => new SymbolEntity({ value: v })) /** @param {Grammar} r */ ObjectReference = r => P.alt( r.None, ...[ Grammar.ReferencePath(r, r.PathSymbolOptSpaces) .map(path => new ObjectReferenceEntity({ type: "", path: path })) ].flatMap(referencePath => [ referencePath.wrap(P.string(`"`), P.string(`"`)), referencePath.wrap(P.string(`'"`), P.string(`"'`)), ]), P.seqMap( r.Word, // Goes into referenceType P.optWhitespace, // Goes into _1 (ignored) P.alt(...[Grammar.ReferencePath(r, r.PathSymbolOptSpaces)].flatMap(referencePath => [ referencePath.wrap(P.string(`"`), P.string(`"`)), referencePath.wrap(P.string(`'"`), P.string(`"'`)) ])), // Goes into referencePath (referenceType, _1, referencePath) => new ObjectReferenceEntity({ type: referenceType, path: referencePath }) ), Grammar.ReferencePath(r, r.PathSymbol).map(path => new ObjectReferenceEntity({ type: "", path: path })), r.Word.map(type => new ObjectReferenceEntity({ type: type, path: "" })), ) /** @param {Grammar} r */ LocalizedText = r => P.seqMap( P.string(LocalizedTextEntity.lookbehind).skip(P.optWhitespace).skip(P.string("(")), // Goes into _0 (ignored) r.String.trim(P.optWhitespace), // Goes into namespace P.string(","), // Goes into _2 (ignored) r.String.trim(P.optWhitespace), // Goes into key P.string(","), // Goes into _4 (ignored) r.String.trim(P.optWhitespace), // Goes into value P.string(")"), // Goes into _6 (ignored) (_0, namespace, _2, key, _4, value, _6) => new LocalizedTextEntity({ namespace: namespace, key: key, value: value }) ) /** @param {Grammar} r */ InvariantText = r => r.String.trim(P.optWhitespace).wrap( P.string(InvariantTextEntity.lookbehind).skip(P.optWhitespace).skip(P.string("(")), P.string(")") ) .map(value => new InvariantTextEntity({ value: value })) /** @param {Grammar} r */ AttributeAnyValue = r => P.alt( // Remember to keep the order, otherwise parsing might fail r.Boolean, r.Guid, r.None, r.Null, r.Number, r.String, r.LocalizedText, r.InvariantText, Grammar.createEntityGrammar(r, VectorEntity, true), Grammar.createEntityGrammar(r, LinearColorEntity, true), r.UnknownKeys, r.ObjectReference, r.Symbol, ) /** @param {Grammar} r */ PinReference = r => P.seqMap( r.PathSymbol, // Goes into objectNAme P.whitespace, // Goes into _1 (ignored) r.Guid, // Goes into pinGuid (objectName, _1, pinGuid) => new PinReferenceEntity({ objectName: objectName, pinGuid: pinGuid, }) ) /** @param {Grammar} r */ Vector = r => Grammar.createEntityGrammar(r, VectorEntity) /** @param {Grammar} r */ Rotator = r => Grammar.createEntityGrammar(r, RotatorEntity) /** @param {Grammar} r */ SimpleSerializationRotator = r => P.seqMap( r.Number, P.string(",").trim(P.optWhitespace), r.Number, P.string(",").trim(P.optWhitespace), r.Number, (p, _1, y, _3, r) => new SimpleSerializationRotatorEntity({ R: r, P: p, Y: y, }) ) /** @param {Grammar} r */ SimpleSerializationVector = r => P.seqMap( r.Number, P.string(",").trim(P.optWhitespace), r.Number, P.string(",").trim(P.optWhitespace), r.Number, (x, _1, y, _3, z) => new SimpleSerializationVectorEntity({ X: x, Y: y, Z: z, }) ) /** @param {Grammar} r */ LinearColor = r => Grammar.createEntityGrammar(r, LinearColorEntity) /** @param {Grammar} r */ FunctionReference = r => Grammar.createEntityGrammar(r, FunctionReferenceEntity) /** @param {Grammar} r */ VariableReference = r => Grammar.createEntityGrammar(r, VariableReferenceEntity) /** @param {Grammar} r */ MacroGraphReference = r => Grammar.createEntityGrammar(r, MacroGraphReferenceEntity) /** @param {Grammar} r */ KeyBinding = r => P.alt( r.Identifier.map(identifier => new KeyBindingEntity({ Key: identifier })), Grammar.createEntityGrammar(r, KeyBindingEntity) ) /** @param {Grammar} r */ Pin = r => Grammar.createEntityGrammar(r, PinEntity) /** @param {Grammar} r */ 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); }) /** @param {Grammar} r */ Object = r => P.seqMap( P.seq(P.string("Begin"), P.whitespace, P.string("Object"), P.whitespace), P .alt( r.CustomProperties, Grammar.createAttributeGrammar(r, ObjectEntity) ) .sepBy1(P.whitespace), P.seq(r.MultilineWhitespace, P.string("End"), P.whitespace, P.string("Object")), (_0, attributes, _2) => { let values = {}; attributes.forEach(attributeSetter => attributeSetter(values)); return new ObjectEntity(values) } ) /** @returns {Parsimmon.Parser} */ MultipleObject = r => r.Object.sepBy1(P.whitespace).trim(P.optWhitespace) /* --- Others --- */ /** @param {Grammar} r */ LinearColorFromHex = r => P .string("#") .then(r.HexDigit.times(2).tie().times(3, 4)) .trim(P.optWhitespace) .map(([R, G, B, A]) => new LinearColorEntity({ R: parseInt(R, 16) / 255, G: parseInt(G, 16) / 255, B: parseInt(B, 16) / 255, A: A ? parseInt(A, 16) / 255 : 1, })) /** @param {Grammar} r */ LinearColorFromRGBList = r => P.seqMap( r.ColorNumber, P.string(",").skip(P.optWhitespace), r.ColorNumber, P.string(",").skip(P.optWhitespace), r.ColorNumber.map(Number), (R, _1, G, _3, B) => new LinearColorEntity({ R: R / 255, G: G / 255, B: B / 255, A: 1, }) ) /** @param {Grammar} r */ LinearColorFromRGB = r => P.string("rgb").then( r.LinearColorFromRGBList.wrap( P.regex(/\(\s*/), P.regex(/\s*\)/) ) ) /** @param {Grammar} r */ LinearColorFromRGBA = r => P.string("rgba").then( P.seqMap( r.ColorNumber, P.string(",").skip(P.optWhitespace), r.ColorNumber, P.string(",").skip(P.optWhitespace), r.ColorNumber.map(Number), P.string(",").skip(P.optWhitespace), P.regex(/0?\.\d+|[01]/).map(Number), (R, _1, G, _3, B, _4, A) => new LinearColorEntity({ R: R / 255, G: G / 255, B: B / 255, A: A, }) ).wrap( P.regex(/\(\s*/), P.regex(/\s*\)/) ) ) /** @param {Grammar} r */ LinearColorFromAnyColor = r => P.alt( r.LinearColorFromRGBList, r.LinearColorFromHex, r.LinearColorFromRGB, r.LinearColorFromRGBA, ) /** @param {Grammar} r */ UnknownKeys = r => P.seqMap( P.regex(/\w*\s*/).skip(P.string("(")), P.seqMap( r.AttributeName, P.string("=").trim(P.optWhitespace), r.AttributeAnyValue, (attributeName, separator, attributeValue) => entity => Utility.objectSet(entity, attributeName.split("."), attributeValue, true) ) .trim(P.optWhitespace) .sepBy(P.string(",")) // Assignments are separated by comma .skip(P.regex(/,?/).then(P.optWhitespace)), // Optional trailing comma and maybe additional space P.string(")"), (lookbehind, attributes, _2) => { let values = {}; attributes.forEach(attributeSetter => attributeSetter(values)); let result = new UnknownKeysEntity(values); if (lookbehind) { result.lookbehind = lookbehind; } return result } ) } /** * @typedef {import("../entity/TypeInitialization").AnyValue} AnyValue */ /** * @template {AnyValue} T * @typedef {import("../entity/TypeInitialization").AnyValueConstructor} AnyValueConstructor */ /** @template {AnyValue} T */ class ISerializer { // @ts-expect-error static grammar = Parsimmon.createLanguage(new Grammar()) /** @param {AnyValueConstructor} entityType */ constructor( entityType, attributePrefix = "", attributeSeparator = ",", trailingSeparator = false, attributeValueConjunctionSign = "=", attributeKeyPrinter = k => k.join(".") ) { this.entityType = entityType; this.attributePrefix = attributePrefix; this.attributeSeparator = attributeSeparator; this.trailingSeparator = trailingSeparator; this.attributeValueConjunctionSign = attributeValueConjunctionSign; this.attributeKeyPrinter = attributeKeyPrinter; } /** * @param {String} value * @returns {T} */ deserialize(value) { return this.read(value) } /** @param {T} object */ serialize(object, insideString = false, entity = object) { return this.write(entity, object, insideString) } /** * @param {String} value * @returns {T} */ read(value) { throw new Error("Not implemented") } /** * @param {T} object * @param {Boolean} insideString * @returns {String} */ write(entity, object, insideString) { throw new Error("Not implemented") } /** * @param {AnyValue} value * @param {String[]} fullKey * @param {Boolean} insideString */ writeValue(entity, value, fullKey, insideString) { const type = Utility.getType(value); const serializer = SerializerFactory.getSerializer(type); if (!serializer) { throw new Error(`Unknown value type "${type.name}", a serializer must be registered in the SerializerFactory class, check initializeSerializerFactory.js`) } return serializer.write(entity, value, insideString) } /** * @param {String[]} key * @param {Object} object * @param {Boolean} insideString * @returns {String} */ subWrite(entity, key, object, insideString) { let result = ""; let fullKey = key.concat(""); const last = fullKey.length - 1; for (const property of Object.getOwnPropertyNames(object)) { fullKey[last] = property; const value = object[property]; if (value?.constructor === Object) { // Recursive call when finding an object result += (result.length ? this.attributeSeparator : "") + this.subWrite(entity, fullKey, value, insideString); } else if (value !== undefined && this.showProperty(entity, object, fullKey, value)) { const isSerialized = Utility.isSerialized(entity, fullKey); result += (result.length ? this.attributeSeparator : "") + this.attributePrefix + this.attributeKeyPrinter(fullKey) + this.attributeValueConjunctionSign + ( isSerialized ? `"${this.writeValue(entity, value, fullKey, true)}"` : this.writeValue(entity, value, fullKey, insideString) ); } } if (this.trailingSeparator && result.length && fullKey.length === 1) { // append separator at the end if asked and there was printed content result += this.attributeSeparator; } return result } showProperty(entity, object, attributeKey, attributeValue) { // @ts-expect-error const attributes = this.entityType.attributes; const attribute = Utility.objectGet(attributes, attributeKey); if (attribute instanceof TypeInitialization) { if (attribute.ignored) { return false } return !Utility.equals(attribute.value, attributeValue) || attribute.showDefault } return true } } class ObjectSerializer extends ISerializer { constructor() { super(ObjectEntity, " ", "\n", false); } showProperty(entity, object, attributeKey, attributeValue) { switch (attributeKey.toString()) { case "Class": case "Name": case "CustomProperties": // Serielized separately return false } return super.showProperty(entity, object, attributeKey, attributeValue) } /** @param {String} value */ read(value) { const parseResult = ISerializer.grammar.Object.parse(value); if (!parseResult.status) { throw new Error("Error when trying to parse the object.") } return parseResult.value } /** @param {String} value */ readMultiple(value) { const parseResult = ISerializer.grammar.MultipleObject.parse(value); if (!parseResult.status) { throw new Error("Error when trying to parse the object.") } return parseResult.value } /** * @param {ObjectEntity} object * @param {Boolean} insideString */ write(entity, object, insideString) { let result = `Begin Object Class=${object.Class.path} Name=${this.writeValue(entity, object.Name, ["Name"], insideString)} ${this.subWrite(entity, [], object, insideString) + object .CustomProperties.map(pin => this.attributeSeparator + this.attributePrefix + "CustomProperties " + SerializerFactory.getSerializer(PinEntity).serialize(pin) ) .join("")} End Object\n`; return result } } class Copy extends IInput { static #serializer = new ObjectSerializer() /** @type {(e: ClipboardEvent) => void} */ #copyHandler constructor(target, blueprint, options = {}) { options.listenOnFocus ??= true; options.unlistenOnTextEdit ??= true; // No nodes copy if inside a text field, just text (default behavior) super(target, blueprint, options); let self = this; this.#copyHandler = _ => self.copied(); } listenEvents() { document.body.addEventListener("copy", this.#copyHandler); } unlistenEvents() { document.body.removeEventListener("copy", this.#copyHandler); } copied() { const value = this.blueprint .getNodes(true) .map(node => Copy.#serializer.serialize(node.entity, false)) .join(""); navigator.clipboard.writeText(value); } } /** * @typedef {import("../element/IElement").default} IElement * @typedef {import("../input/IInput").default} IInput */ /** @template {IElement} T */ class ITemplate { static styles = i$3`` /** @type {T} */ element /** @type {IInput[]} */ #inputObjects = [] get inputObjects() { return this.#inputObjects } /** @param {T} element */ constructed(element) { this.element = element; } /** @returns {IInput[]} */ createInputObjects() { return [] } connectedCallback() { } /** @param {Map} changedProperties */ willUpdate(changedProperties) { } /** @param {Map} changedProperties */ update(changedProperties) { } render() { return y`` } /** @param {Map} changedProperties */ firstUpdated(changedProperties) { } /** @param {Map} changedProperties */ updated(changedProperties) { } inputSetup() { this.#inputObjects = this.createInputObjects(); } cleanup() { this.#inputObjects.forEach(v => v.unlistenDOMElement()); } } /** @typedef {import("../../Blueprint").default} Blueprint */ /** * @template {HTMLElement} T * @extends IInput */ class IKeyboardShortcut extends IInput { /** @type {KeyBindingEntity[]} */ #activationKeys /** * @param {T} target * @param {Blueprint} blueprint * @param {Object} options */ constructor(target, blueprint, options = {}) { options.activateAnyKey ??= false; options.activationKeys ??= []; options.consumeEvent ??= true; options.listenOnFocus ??= true; options.unlistenOnTextEdit ??= true; // No shortcuts when inside of a text field if (!(options.activationKeys instanceof Array)) { options.activationKeys = [options.activationKeys]; } options.activationKeys = options.activationKeys.map(v => { if (v instanceof KeyBindingEntity) { return v } if (v.constructor === String) { // @ts-expect-error const parsed = ISerializer.grammar.KeyBinding.parse(v); if (parsed.status) { return parsed.value } } throw new Error("Unexpected key value") }); super(target, blueprint, options); this.#activationKeys = this.options.activationKeys ?? []; const wantsShift = keyEntry => keyEntry.bShift || keyEntry.Key == "LeftShift" || keyEntry.Key == "RightShift"; const wantsCtrl = keyEntry => keyEntry.bCtrl || keyEntry.Key == "LeftControl" || keyEntry.Key == "RightControl"; const wantsAlt = keyEntry => keyEntry.bAlt || keyEntry.Key == "LeftAlt" || keyEntry.Key == "RightAlt"; let self = this; /** @param {KeyboardEvent} e */ this.keyDownHandler = e => { if ( this.options.activateAnyKey || self.#activationKeys.some(keyEntry => wantsShift(keyEntry) == e.shiftKey && wantsCtrl(keyEntry) == e.ctrlKey && wantsAlt(keyEntry) == e.altKey && Configuration.Keys[keyEntry.Key] == e.code ) ) { if (options.consumeEvent) { e.preventDefault(); e.stopImmediatePropagation(); } self.fire(); document.removeEventListener("keydown", self.keyDownHandler); document.addEventListener("keyup", self.keyUpHandler); } }; /** @param {KeyboardEvent} e */ this.keyUpHandler = e => { if ( this.options.activateAnyKey || self.#activationKeys.some(keyEntry => keyEntry.bShift && e.key == "Shift" || keyEntry.bCtrl && e.key == "Control" || keyEntry.bAlt && e.key == "Alt" || keyEntry.bCmd && e.key == "Meta" || Configuration.Keys[keyEntry.Key] == e.code ) ) { if (options.consumeEvent) { e.stopImmediatePropagation(); } self.unfire(); document.removeEventListener("keyup", this.keyUpHandler); document.addEventListener("keydown", this.keyDownHandler); } }; } listenEvents() { document.addEventListener("keydown", this.keyDownHandler); } unlistenEvents() { document.removeEventListener("keydown", this.keyDownHandler); } // Subclasses will want to override fire() { } unfire() { } } class KeyboardCanc extends IKeyboardShortcut { /** * @param {HTMLElement} target * @param {import("../../Blueprint").default} blueprint * @param {Object} options */ constructor(target, blueprint, options = {}) { options.activationKeys = Configuration.deleteNodesKeyboardKey; super(target, blueprint, options); } fire() { this.blueprint.removeGraphElement(...this.blueprint.getNodes(true)); } } /** @typedef {import("../../Blueprint").default} Blueprint */ /** * @template {HTMLElement} T * @extends {IInput} */ class IPointing extends IInput { constructor(target, blueprint, options = {}) { options.ignoreTranslateCompensate ??= false; options.movementSpace ??= blueprint.getGridDOMElement() ?? document.documentElement; super(target, blueprint, options); this.movementSpace = options.movementSpace; } /** @param {MouseEvent} mouseEvent */ locationFromEvent(mouseEvent) { const location = Utility.convertLocation( [mouseEvent.clientX, mouseEvent.clientY], this.movementSpace ); return this.options.ignoreTranslateCompensate ? location : this.blueprint.compensateTranslation(location) } } class IMouseWheel extends IPointing { #mouseWheelHandler = /** @param {WheelEvent} e */ e => { e.preventDefault(); const location = this.locationFromEvent(e); this.wheel(Math.sign(e.deltaY * Configuration.mouseWheelFactor), location); } #mouseParentWheelHandler = /** @param {WheelEvent} e */ e => e.preventDefault() /** * @param {HTMLElement} target * @param {import("../../Blueprint").default} blueprint * @param {Object} options */ constructor(target, blueprint, options = {}) { options.listenOnFocus = true; options.strictTarget ??= false; super(target, blueprint, options); this.strictTarget = options.strictTarget; } listenEvents() { this.movementSpace.addEventListener("wheel", this.#mouseWheelHandler, false); this.movementSpace.parentElement?.addEventListener("wheel", this.#mouseParentWheelHandler); } unlistenEvents() { 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 IMouseWheel { #enableZoonIn = false get enableZoonIn() { return this.#enableZoonIn } set enableZoonIn(value) { value = Boolean(value); if (value == this.#enableZoonIn) { return } this.#enableZoonIn = value; } wheel(variation, location) { let zoomLevel = this.blueprint.getZoom(); variation = -variation; if (!this.enableZoonIn && zoomLevel == 0 && variation > 0) { return } zoomLevel += variation; this.blueprint.setZoom(zoomLevel, location); } } class KeyboardEnableZoom extends IKeyboardShortcut { /** @type {Zoom} */ #zoomInputObject /** * @param {HTMLElement} target * @param {import("../../Blueprint").default} blueprint * @param {Object} options */ constructor(target, blueprint, options = {}) { options.activationKeys = Configuration.enableZoomIn; super(target, blueprint, options); } fire() { this.#zoomInputObject = this.blueprint.getInputObject(Zoom); this.#zoomInputObject.enableZoonIn = true; } unfire() { this.#zoomInputObject.enableZoonIn = false; } } /** @typedef {import("../../Blueprint").default} Blueprint */ class KeyboardSelectAll extends IKeyboardShortcut { /** * @param {HTMLElement} target * @param {Blueprint} blueprint * @param {Object} options */ constructor(target, blueprint, options = {}) { options.activationKeys = Configuration.selectAllKeyboardKey; super(target, blueprint, options); } fire() { this.blueprint.selectAll(); } } /** * @typedef {import("../Blueprint").default} Blueprint * @typedef {import("../entity/IEntity").default} IEntity * @typedef {import("../input/IInput").default} IInput * @typedef {import("../template/ITemplate").default} ITemplate * @typedef {import("lit").PropertyDeclarations} PropertyDeclarations */ /** * @template {IEntity} T * @template {ITemplate} U */ class IElement extends s { /** @type {PropertyDeclarations} */ static properties = { } #nextUpdatedCallbacks = [] /** @type {Blueprint} */ #blueprint get blueprint() { return this.#blueprint } set blueprint(v) { this.#blueprint = v; } /** @type {T} */ #entity get entity() { return this.#entity } set entity(entity) { this.#entity = entity; } /** @type {U} */ #template get template() { return this.#template } /** @type {IInput[]} */ inputObjects = [] /** * @param {T} entity * @param {U} template */ constructor(entity, template) { super(); this.#entity = entity; this.#template = template; this.inputObjects = []; this.#template.constructed(this); } createRenderRoot() { return this } connectedCallback() { super.connectedCallback(); this.blueprint = /** @type {Blueprint} */ this.closest("ueb-blueprint"); this.template.connectedCallback(); } /** @param {Map} changedProperties */ willUpdate(changedProperties) { super.willUpdate(changedProperties); this.template.willUpdate(changedProperties); } /** @param {Map} changedProperties */ update(changedProperties) { super.update(changedProperties); this.template.update(changedProperties); } render() { return this.template.render() } /** @param {Map} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.template.firstUpdated(changedProperties); this.template.inputSetup(); } /** @param {Map} changedProperties */ updated(changedProperties) { super.updated(changedProperties); this.template.updated(changedProperties); this.#nextUpdatedCallbacks.forEach(f => f(changedProperties)); this.#nextUpdatedCallbacks = []; } disconnectedCallback() { super.disconnectedCallback(); this.template.cleanup(); } addNextUpdatedCallbacks(callback, requestUpdate = false) { this.#nextUpdatedCallbacks.push(callback); if (requestUpdate) { this.requestUpdate(); } } /** @param {IElement} element */ isSameGraph(element) { return this.blueprint && this.blueprint == element?.blueprint } /** * @template {IInput} V * @param {new (...args: any[]) => V} type */ getInputObject(type) { return /** @type {V} */ (this.template.inputObjects.find(object => object.constructor == type)) } } /** * @typedef {import("../template/IDraggableTemplate").default} IDraggableTemplate * @typedef {import("../entity/IEntity").default} IEntity */ /** * @template {IEntity} T * @template {IDraggableTemplate} U * @extends {IElement} */ class IDraggableElement extends IElement { static properties = { ...super.properties, locationX: { type: Number, attribute: false, }, locationY: { type: Number, attribute: false, }, } static dragEventName = Configuration.dragEventName static dragGeneralEventName = Configuration.dragGeneralEventName /** * @param {T} entity * @param {U} template */ constructor(entity, template) { super(entity, template); this.locationX = 0; this.locationY = 0; } /** @param {Number[]} param0 */ setLocation([x, y]) { const d = [x - this.locationX, y - this.locationY]; this.locationX = x; this.locationY = y; if (this.blueprint) { // @ts-expect-error const dragLocalEvent = new CustomEvent(this.constructor.dragEventName, { detail: { value: d, }, bubbles: false, cancelable: true }); this.dispatchEvent(dragLocalEvent); } } /** @param {Number[]} param0 */ addLocation([x, y]) { this.setLocation([this.locationX + x, this.locationY + y]); } /** @param {Number[]} value */ dispatchDragEvent(value) { // @ts-expect-error const dragEvent = new CustomEvent(this.constructor.dragGeneralEventName, { detail: { value: value }, bubbles: true, cancelable: true }); this.dispatchEvent(dragEvent); } snapToGrid() { const snappedLocation = Utility.snapToGrid([this.locationX, this.locationY], Configuration.gridSize); if (this.locationX != snappedLocation[0] || this.locationY != snappedLocation[1]) { this.setLocation(snappedLocation); } } } /** * @typedef {import("../../Blueprint").default} Blueprint * @typedef {import("../../element/IElement").default} IElement */ /** * @template {IElement} T * @extends {IPointing} */ class IMouseClickDrag extends IPointing { #mouseDownHandler = /** @param {MouseEvent} e */ e => { this.blueprint.setFocused(true); switch (e.button) { case this.options.clickButton: // Either doesn't matter or consider the click only when clicking on the parent, not descandants if (!this.options.strictTarget || e.target == e.currentTarget) { if (this.options.consumeEvent) { e.stopImmediatePropagation(); // Captured, don't call anyone else } // Attach the listeners this.#movementListenedElement.addEventListener("mousemove", this.#mouseStartedMovingHandler); document.addEventListener("mouseup", this.#mouseUpHandler); this.clickedPosition = this.locationFromEvent(e); this.blueprint.mousePosition[0] = this.clickedPosition[0]; this.blueprint.mousePosition[1] = this.clickedPosition[1]; if (this.target instanceof IDraggableElement) { this.clickedOffset = [ this.clickedPosition[0] - this.target.locationX, this.clickedPosition[1] - this.target.locationY, ]; } this.clicked(this.clickedPosition); } break default: if (!this.options.exitAnyButton) { this.#mouseUpHandler(e); } break } } #mouseStartedMovingHandler = /** @param {MouseEvent} e */ e => { if (this.options.consumeEvent) { e.stopImmediatePropagation(); // Captured, don't call anyone else } // Delegate from now on to this.#mouseMoveHandler this.#movementListenedElement.removeEventListener("mousemove", this.#mouseStartedMovingHandler); this.#movementListenedElement.addEventListener("mousemove", this.#mouseMoveHandler); // Handler calls e.preventDefault() when it receives the event, this means dispatchEvent returns false const dragEvent = this.getEvent(Configuration.trackingMouseEventName.begin); this.#trackingMouse = this.target.dispatchEvent(dragEvent) == false; const location = this.locationFromEvent(e); // Do actual actions this.mouseLocation = Utility.snapToGrid(this.clickedPosition, this.stepSize); this.startDrag(location); this.started = true; } #mouseMoveHandler = /** @param {MouseEvent} e */ e => { if (this.options.consumeEvent) { e.stopImmediatePropagation(); // Captured, don't call anyone else } const location = this.locationFromEvent(e); const movement = [e.movementX, e.movementY]; this.dragTo(location, movement); if (this.#trackingMouse) { this.blueprint.mousePosition = this.locationFromEvent(e); } } #mouseUpHandler = /** @param {MouseEvent} e */ e => { if (!this.options.exitAnyButton || e.button == this.options.clickButton) { if (this.options.consumeEvent) { e.stopImmediatePropagation(); // Captured, don't call anyone else } // Remove the handlers of "mousemove" and "mouseup" this.#movementListenedElement.removeEventListener("mousemove", this.#mouseStartedMovingHandler); this.#movementListenedElement.removeEventListener("mousemove", this.#mouseMoveHandler); document.removeEventListener("mouseup", this.#mouseUpHandler); if (this.started) { this.endDrag(); } this.unclicked(); if (this.#trackingMouse) { const dragEvent = this.getEvent(Configuration.trackingMouseEventName.end); this.target.dispatchEvent(dragEvent); this.#trackingMouse = false; } this.started = false; } } #trackingMouse = false #movementListenedElement #draggableElement clickedOffset = [0, 0] clickedPosition = [0, 0] mouseLocation = [0, 0] started = false stepSize = 1 /** * * @param {T} target * @param {Blueprint} blueprint * @param {Object} options */ constructor(target, blueprint, options = {}) { options.clickButton ??= 0; options.consumeEvent ??= true; options.draggableElement ??= target; options.exitAnyButton ??= true; options.moveEverywhere ??= false; options.movementSpace ??= blueprint?.getGridDOMElement(); options.repositionOnClick ??= false; options.strictTarget ??= false; super(target, blueprint, options); this.stepSize = parseInt(options?.stepSize ?? Configuration.gridSize); this.#movementListenedElement = this.options.moveEverywhere ? document.documentElement : this.movementSpace; this.#draggableElement = this.options.draggableElement; this.listenEvents(); } listenEvents() { this.#draggableElement.addEventListener("mousedown", this.#mouseDownHandler); if (this.options.clickButton == 2) { this.#draggableElement.addEventListener("contextmenu", e => e.preventDefault()); } } unlistenEvents() { this.#draggableElement.removeEventListener("mousedown", this.#mouseDownHandler); } getEvent(eventName) { return new CustomEvent(eventName, { detail: { tracker: this }, bubbles: true, cancelable: true }) } /* Subclasses will override the following methods */ clicked(location) { } startDrag(location) { } dragTo(location, offset) { } endDrag() { } unclicked(location) { } } class MouseScrollGraph extends IMouseClickDrag { startDrag() { this.blueprint.scrolling = true; } dragTo(location, movement) { this.blueprint.scrollDelta([-movement[0], -movement[1]]); } endDrag() { this.blueprint.scrolling = false; } } class MouseTracking extends IPointing { /** @type {IPointing} */ #mouseTracker = null /** @type {(e: MouseEvent) => void} */ #mousemoveHandler /** @type {(e: CustomEvent) => void} */ #trackingMouseStolenHandler /** @type {(e: CustomEvent) => void} */ #trackingMouseGaveBackHandler constructor(target, blueprint, options = {}) { options.listenOnFocus = true; super(target, blueprint, options); let self = this; this.#mousemoveHandler = e => { e.preventDefault(); self.blueprint.mousePosition = self.locationFromEvent(e); }; this.#trackingMouseStolenHandler = e => { if (!self.#mouseTracker) { e.preventDefault(); this.#mouseTracker = e.detail.tracker; self.unlistenMouseMove(); } }; this.#trackingMouseGaveBackHandler = e => { if (self.#mouseTracker == e.detail.tracker) { e.preventDefault(); self.#mouseTracker = null; self.listenMouseMove(); } }; } listenMouseMove() { this.target.addEventListener("mousemove", this.#mousemoveHandler); } unlistenMouseMove() { this.target.removeEventListener("mousemove", this.#mousemoveHandler); } listenEvents() { this.listenMouseMove(); this.blueprint.addEventListener( Configuration.trackingMouseEventName.begin, /** @type {(e: Event) => any} */(this.#trackingMouseStolenHandler)); this.blueprint.addEventListener( Configuration.trackingMouseEventName.end, /** @type {(e: Event) => any} */(this.#trackingMouseGaveBackHandler)); } unlistenEvents() { this.unlistenMouseMove(); this.blueprint.removeEventListener( Configuration.trackingMouseEventName.begin, /** @type {(e: Event) => any} */(this.#trackingMouseStolenHandler)); this.blueprint.removeEventListener( Configuration.trackingMouseEventName.end, /** @type {(e: Event) => any} */(this.#trackingMouseGaveBackHandler) ); } } /** * @typedef {new (...args) => IElement} ElementConstructor * @typedef {import("./IElement").default} IElement */ class ElementFactory { /** @type {Map} */ static #elementConstructors = new Map() /** * @param {String} tagName * @param {ElementConstructor} entityConstructor */ static registerElement(tagName, entityConstructor) { ElementFactory.#elementConstructors.set(tagName, entityConstructor); } /** * @param {String} tagName */ static getConstructor(tagName) { return ElementFactory.#elementConstructors.get(tagName) } } /** @typedef {import("../../element/NodeElement").default} NodeElement */ class Paste extends IInput { static #serializer = new ObjectSerializer() /** @type {(e: ClipboardEvent) => void} */ #pasteHandle constructor(target, blueprint, options = {}) { options.listenOnFocus ??= true; options.unlistenOnTextEdit ??= true; // No nodes paste if inside a text field, just text (default behavior) super(target, blueprint, options); let self = this; this.#pasteHandle = e => self.pasted(e.clipboardData.getData("Text")); } listenEvents() { document.body.addEventListener("paste", this.#pasteHandle); } unlistenEvents() { document.body.removeEventListener("paste", this.#pasteHandle); } pasted(value) { let top = 0; let left = 0; let count = 0; let nodes = Paste.#serializer.readMultiple(value).map(entity => { /** @type {NodeElement} */ // @ts-expect-error let node = new (ElementFactory.getConstructor("ueb-node"))(entity); top += node.locationY; left += node.locationX; ++count; return node }); top /= count; left /= count; if (nodes.length > 0) { this.blueprint.unselectAll(); } let mousePosition = this.blueprint.mousePosition; nodes.forEach(node => { const locationOffset = [ mousePosition[0] - left, mousePosition[1] - top, ]; node.addLocation(locationOffset); node.snapToGrid(); node.setSelected(true); }); this.blueprint.addGraphElement(...nodes); return true } } class Select extends IMouseClickDrag { constructor(target, blueprint, options) { super(target, blueprint, options); this.selectorElement = this.blueprint.selectorElement; } startDrag() { this.selectorElement.beginSelect(this.clickedPosition); } dragTo(location, movement) { this.selectorElement.selectTo(location); } endDrag() { if (this.started) { this.selectorElement.endSelect(); } } unclicked() { if (!this.started) { this.blueprint.unselectAll(); } } } class Unfocus extends IInput { /** @type {(e: MouseEvent) => void} */ #clickHandler constructor(target, blueprint, options = {}) { options.listenOnFocus = true; super(target, blueprint, options); let self = this; this.#clickHandler = e => self.clickedSomewhere(/** @type {HTMLElement} */(e.target)); if (this.blueprint.focus) { document.addEventListener("click", this.#clickHandler); } } /** @param {HTMLElement} target */ clickedSomewhere(target) { // If target is outside the blueprint grid if (!target.closest("ueb-blueprint")) { this.blueprint.setFocused(false); } } listenEvents() { document.addEventListener("click", this.#clickHandler); } unlistenEvents() { document.removeEventListener("click", this.#clickHandler); } } /** * @typedef {import("../Blueprint").default} Blueprint * @typedef {import("../element/PinElement").default} PinElement * @typedef {import("../element/SelectorElement").default} SelectorElement * @typedef {import("../entity/PinReferenceEntity").default} PinReferenceEntity */ /** @extends ITemplate */ class BlueprintTemplate extends ITemplate { static styleVariables = { "--ueb-font-size": `${Configuration.fontSize}`, "--ueb-grid-axis-line-color": `${Configuration.gridAxisLineColor}`, "--ueb-grid-expand": `${Configuration.expandGridSize}px`, "--ueb-grid-line-color": `${Configuration.gridLineColor}`, "--ueb-grid-line-width": `${Configuration.gridLineWidth}px`, "--ueb-grid-set-line-color": `${Configuration.gridSetLineColor}`, "--ueb-grid-set": `${Configuration.gridSet}`, "--ueb-grid-size": `${Configuration.gridSize}px`, "--ueb-link-min-width": `${Configuration.linkMinWidth}`, "--ueb-node-radius": `${Configuration.nodeRadius}px`, } /** @param {Blueprint} element */ constructed(element) { super.constructed(element); this.element.style.cssText = Object.entries(BlueprintTemplate.styleVariables).map(([k, v]) => `${k}:${v};`).join(""); } createInputObjects() { return [ ...super.createInputObjects(), new Copy(this.element.getGridDOMElement(), this.element), new Paste(this.element.getGridDOMElement(), this.element), new KeyboardCanc(this.element.getGridDOMElement(), this.element), new KeyboardSelectAll(this.element.getGridDOMElement(), this.element), new Zoom(this.element.getGridDOMElement(), this.element), new Select(this.element.getGridDOMElement(), this.element, { clickButton: 0, exitAnyButton: true, moveEverywhere: true, }), new MouseScrollGraph(this.element.getGridDOMElement(), this.element, { clickButton: 2, exitAnyButton: false, moveEverywhere: true, }), new Unfocus(this.element.getGridDOMElement(), this.element), new MouseTracking(this.element.getGridDOMElement(), this.element), new KeyboardEnableZoom(this.element.getGridDOMElement(), this.element), ] } render() { return y` ${this.element.zoom == 0 ? "1:1" : this.element.zoom} ` } /** @param {Map} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.element.headerElement = /** @type {HTMLElement} */(this.element.querySelector('.ueb-viewport-header')); this.element.overlayElement = /** @type {HTMLElement} */(this.element.querySelector('.ueb-viewport-overlay')); this.element.viewportElement = /** @type {HTMLElement} */(this.element.querySelector('.ueb-viewport-body')); this.element.selectorElement = /** @type {SelectorElement} */(this.element.querySelector('ueb-selector')); this.element.gridElement = /** @type {HTMLElement} */(this.element.viewportElement.querySelector(".ueb-grid")); this.element.linksContainerElement = /** @type {HTMLElement} */(this.element.querySelector("[data-links]")); this.element.linksContainerElement.append(...this.element.getLinks()); this.element.nodesContainerElement = /** @type {HTMLElement} */(this.element.querySelector("[data-nodes]")); this.element.nodesContainerElement.append(...this.element.getNodes()); this.element.viewportElement.scroll(Configuration.expandGridSize, Configuration.expandGridSize); } /** @param {Map} changedProperties */ updated(changedProperties) { super.updated(changedProperties); if (changedProperties.has("scrollX") || changedProperties.has("scrollY")) { this.element.viewportElement.scroll(this.element.scrollX, this.element.scrollY); } if (changedProperties.has("zoom")) { const previousZoom = changedProperties.get("zoom"); const minZoom = Math.min(previousZoom, this.element.zoom); const maxZoom = Math.max(previousZoom, this.element.zoom); const classes = Utility.range(minZoom, maxZoom); const getClassName = v => `ueb-zoom-${v}`; if (previousZoom < this.element.zoom) { this.element.classList.remove(...classes.filter(v => v < 0).map(getClassName)); this.element.classList.add(...classes.filter(v => v > 0).map(getClassName)); } else { this.element.classList.remove(...classes.filter(v => v > 0).map(getClassName)); this.element.classList.add(...classes.filter(v => v < 0).map(getClassName)); } } } /** @param {PinReferenceEntity} pinReference */ getPin(pinReference) { return /** @type {PinElement} */(this.element.querySelector( `ueb-node[data-name="${pinReference.objectName}"] ueb-pin[data-id="${pinReference.pinGuid}"]` )) } } /** * @typedef {import("../entity/IEntity").default} IEntity * @typedef {import("../template/ITemplate").default} ITemplate */ /** * @template {IEntity} T * @template {ITemplate} U * @extends {IElement} */ class IFromToPositionedElement extends IElement { static properties = { ...super.properties, fromX: { type: Number, attribute: false, }, fromY: { type: Number, attribute: false, }, toX: { type: Number, attribute: false, }, toY: { type: Number, attribute: false, }, } constructor(...args) { // @ts-expect-error super(...args); this.fromX = 0; this.fromY = 0; this.toX = 0; this.toY = 0; } /** @param {Number[]} param0 */ setBothLocations([x, y]) { this.fromX = x; this.fromY = y; this.toX = x; this.toY = y; } /** @param {Number[]} offset */ addSourceLocation([offsetX, offsetY]) { this.fromX += offsetX; this.fromY += offsetY; } /** @param {Number[]} offset */ addDestinationLocation([offsetX, offsetY]) { this.toX += offsetX; this.toY += offsetY; } } /** @typedef {import("../element/IFromToPositionedElement").default} IFromToPositionedElement */ /** * @template {IFromToPositionedElement} T * @extends {ITemplate} */ class IFromToPositionedTemplate extends ITemplate { /** @param {Map} changedProperties */ update(changedProperties) { super.update(changedProperties); const [fromX, fromY, toX, toY] = [ Math.round(this.element.fromX), Math.round(this.element.fromY), Math.round(this.element.toX), Math.round(this.element.toY), ]; const [left, top, width, height] = [ Math.min(fromX, toX), Math.min(fromY, toY), Math.abs(fromX - toX), Math.abs(fromY - toY), ]; if (changedProperties.has("fromX") || changedProperties.has("toX")) { this.element.style.left = `${left}px`; this.element.style.width = `${width}px`; } if (changedProperties.has("fromY") || changedProperties.has("toY")) { this.element.style.top = `${top}px`; this.element.style.height = `${height}px`; } } } class KnotEntity extends ObjectEntity { /** * @param {Object} options * @param {PinEntity} pinReferenceForType */ constructor(options = {}, pinReferenceForType = undefined) { super(options); this.Class = new ObjectReferenceEntity("/Script/BlueprintGraph.K2Node_Knot"); this.Name = "K2Node_Knot"; const inputPinEntity = new PinEntity({ PinName: "InputPin", }); const outputPinEntity = new PinEntity({ PinName: "OutputPin", Direction: "EGPD_Output", }); if (pinReferenceForType) { inputPinEntity.copyTypeFrom(pinReferenceForType); outputPinEntity.copyTypeFrom(pinReferenceForType); } this.CustomProperties = [inputPinEntity, outputPinEntity]; } } /** @typedef {import("../../Blueprint").default} Blueprint */ /** * @template {HTMLElement} T * @extends {IPointing} */ class MouseDbClick extends IPointing { static ignoreDbClick = /** @param {Number[]} location */ location => { } #mouseDbClickHandler = /** @param {MouseEvent} e */ e => { if (!this.options.strictTarget || e.target === e.currentTarget) { if (this.options.consumeEvent) { e.stopImmediatePropagation(); // Captured, don't call anyone else } this.clickedPosition = this.locationFromEvent(e); this.blueprint.mousePosition[0] = this.clickedPosition[0]; this.blueprint.mousePosition[1] = this.clickedPosition[1]; this.dbclicked(this.clickedPosition); } } #onDbClick get onDbClick() { return this.#onDbClick } set onDbClick(value) { this.#onDbClick = value; } clickedPosition = [0, 0] constructor(target, blueprint, options = {}, onDbClick = MouseDbClick.ignoreDbClick) { options.consumeEvent ??= true; options.strictTarget ??= false; super(target, blueprint, options); this.#onDbClick = onDbClick; this.listenEvents(); } listenEvents() { this.target.addEventListener("dblclick", this.#mouseDbClickHandler); } unlistenEvents() { this.target.removeEventListener("dblclick", this.#mouseDbClickHandler); } /* Subclasses will override the following method */ dbclicked(location) { this.onDbClick(location); } } /** * @typedef {import("../element/LinkElement").default} LinkElement * @typedef {import("../element/NodeElement").default} NodeElement * @typedef {import("../template/KnotNodeTemplate").default} KnotNodeTemplate */ /** @extends {IFromToPositionedTemplate} */ class LinkTemplate extends IFromToPositionedTemplate { /** * Returns a function providing the inverse multiplication y = a / x + q. The value of a and q are calculated using * the derivative of that function y' = -a / x^2 at the point p (x = p[0] and y = p[1]). This means * y'(p[0]) = m => -a / p[0]^2 = m => a = -m * p[0]^2. Now, in order to determine q we can use the starting * function: p[1] = a / p[0] + q => q = p[1] - a / p[0] * @param {Number} m slope * @param {Number[]} p reference point * @returns Maximum value */ static decreasingValue(m, p) { const a = -m * p[0] ** 2; const q = p[1] - a / p[0]; return x => a / x + q } /** * Returns a function providing a clamped line passing through two points. It is clamped after and before the * points. It is easier explained with the following ascii draw. * b ______ * / * / * / * ______/ a */ static clampedLine(a, b) { if (a[0] > b[0]) { const temp = a; a = b; b = temp; } const m = (b[1] - a[1]) / (b[0] - a[0]); const q = a[1] - m * a[0]; return x => x < a[0] ? a[1] : x > b[0] ? b[1] : m * x + q } static c1DecreasingValue = LinkTemplate.decreasingValue(-0.15, [100, 15]) static c2DecreasingValue = LinkTemplate.decreasingValue(-0.06, [500, 130]) static c2Clamped = LinkTemplate.clampedLine([0, 100], [200, 30]) #createKnot = /** @param {Number[]} location */ location => { const knotEntity = new KnotEntity({}, this.element.sourcePin.entity); const knot = /** @type {NodeElement} */(new (ElementFactory.getConstructor("ueb-node"))(knotEntity)); knot.setLocation(this.element.blueprint.snapToGrid(location)); this.element.blueprint.addGraphElement(knot); // Important: keep it before changing existing links const link = new (ElementFactory.getConstructor("ueb-link"))( /** @type {KnotNodeTemplate} */(knot.template).outputPin, this.element.destinationPin ); this.element.destinationPin = /** @type {KnotNodeTemplate} */(knot.template).inputPin; this.element.blueprint.addGraphElement(link); } createInputObjects() { return [ ...super.createInputObjects(), new MouseDbClick( this.element.querySelector(".ueb-link-area"), this.element.blueprint, undefined, (location) => this.#createKnot(location) ) ] } /** * @param {Map} changedProperties */ willUpdate(changedProperties) { super.willUpdate(changedProperties); const sourcePin = this.element.sourcePin; const destinationPin = this.element.destinationPin; if (changedProperties.has("fromX") || changedProperties.has("toX")) { const isSourceAKnot = sourcePin?.nodeElement.getType() == Configuration.nodeType.knot; const isDestinationAKnot = destinationPin?.nodeElement.getType() == Configuration.nodeType.knot; if (isSourceAKnot && (!destinationPin || isDestinationAKnot)) { if (sourcePin?.isInput() && this.element.toX > this.element.fromX + Configuration.distanceThreshold) { this.element.sourcePin = /** @type {KnotNodeTemplate} */(sourcePin.nodeElement.template).outputPin; } else if (sourcePin?.isOutput() && this.element.toX < this.element.fromX - Configuration.distanceThreshold) { this.element.sourcePin = /** @type {KnotNodeTemplate} */(sourcePin.nodeElement.template).inputPin; } } if (isDestinationAKnot && (!sourcePin || isSourceAKnot)) { if (destinationPin?.isInput() && this.element.toX < this.element.fromX + Configuration.distanceThreshold) { this.element.destinationPin = /** @type {KnotNodeTemplate} */(destinationPin.nodeElement.template).outputPin; } else if (destinationPin?.isOutput() && this.element.toX > this.element.fromX - Configuration.distanceThreshold) { this.element.destinationPin = /** @type {KnotNodeTemplate} */(destinationPin.nodeElement.template).inputPin; } } } const dx = Math.max(Math.abs(this.element.fromX - this.element.toX), 1); Math.max(Math.abs(this.element.fromY - this.element.toY), 1); const width = Math.max(dx, Configuration.linkMinWidth); // const height = Math.max(Math.abs(link.fromY - link.toY), 1) const fillRatio = dx / width; // const aspectRatio = width / height const xInverted = this.element.originatesFromInput ? this.element.fromX < this.element.toX : this.element.toX < this.element.fromX; this.element.startPixels = dx < width // If under minimum width ? (width - dx) / 2 // Start from half the empty space : 0; // Otherwise start from the beginning this.element.startPercentage = xInverted ? this.element.startPixels + fillRatio * 100 : this.element.startPixels; const c1 = this.element.startPercentage + (xInverted ? LinkTemplate.c1DecreasingValue(width) : 10 ) * fillRatio; let c2 = LinkTemplate.c2Clamped(xInverted ? -dx : dx) + this.element.startPercentage; c2 = Math.min(c2, LinkTemplate.c2DecreasingValue(width)); this.element.svgPathD = Configuration.linkRightSVGPath(this.element.startPercentage, c1, c2); } /** @param {Map} changedProperties */ update(changedProperties) { super.update(changedProperties); if (changedProperties.has("originatesFromInput")) { this.element.style.setProperty("--ueb-from-input", this.element.originatesFromInput ? "1" : "0"); } const referencePin = this.element.sourcePin ?? this.element.destinationPin; if (referencePin) { this.element.style.setProperty("--ueb-link-color-rgb", Utility.printLinearColor(referencePin.color)); } this.element.style.setProperty("--ueb-y-reflected", `${this.element.fromY > this.element.toY ? 1 : 0}`); this.element.style.setProperty("--ueb-start-percentage", `${Math.round(this.element.startPercentage)}%`); this.element.style.setProperty("--ueb-link-start", `${Math.round(this.element.startPixels)}`); } render() { const uniqueId = `ueb-id-${Math.floor(Math.random() * 1E12)}`; return y` ${this.element.linkMessageIcon || this.element.linkMessageText ? y` ${this.element.linkMessageIcon !== b ? y` ${this.element.linkMessageIcon} ` : b} ${this.element.linkMessageText !== b ? y` ${this.element.linkMessageText} ` : b} ` : b} ` } } class SVGIcon { static branchNode = y` ` static breakStruct = y` ` static cast = y` ` static close = y` ` static correct = y` ` static doN = y` ` static execPin = y` ` static expandIcon = y` ` static forEachLoop = y` ` static functionSymbol = y` ` static genericPin = y` ` static loop = y` ` static macro = y` ` static makeArray = y` ` static makeMap = y` ` static makeStruct = y` ` static referencePin = y` ` static reject = y` ` static select = y` ` static sequence = y` ` } /** * @typedef {import("./PinElement").default} PinElement * @typedef {import("lit").TemplateResult<1>} TemplateResult */ /** @extends {IFromToPositionedElement} */ class LinkElement extends IFromToPositionedElement { static properties = { ...super.properties, source: { type: String, reflect: true, }, destination: { type: String, reflect: true, }, dragging: { type: Boolean, attribute: "data-dragging", converter: Utility.booleanConverter, reflect: true, }, originatesFromInput: { type: Boolean, attribute: false, }, svgPathD: { type: String, attribute: false, }, linkMessageIcon: { type: String, attribute: false, }, linkMessageText: { type: String, attribute: false, }, } /** @type {PinElement} */ #sourcePin get sourcePin() { return this.#sourcePin } set sourcePin(pin) { this.#setPin(pin, false); } /** @type {PinElement} */ #destinationPin get destinationPin() { return this.#destinationPin } set destinationPin(pin) { this.#setPin(pin, true); } #nodeDeleteHandler #nodeDragSourceHandler #nodeDragDestinatonHandler #nodeReflowSourceHandler #nodeReflowDestinatonHandler /** @type {TemplateResult | nothing} */ linkMessageIcon = b /** @type {TemplateResult | nothing} */ linkMessageText = b /** @type {SVGPathElement} */ pathElement /** * @param {PinElement} source * @param {PinElement?} destination */ constructor(source, destination) { super({}, new LinkTemplate()); const self = this; this.#nodeDeleteHandler = () => self.remove(); this.#nodeDragSourceHandler = e => self.addSourceLocation(e.detail.value); this.#nodeDragDestinatonHandler = e => self.addDestinationLocation(e.detail.value); this.#nodeReflowSourceHandler = e => self.setSourceLocation(); this.#nodeReflowDestinatonHandler = e => self.setDestinationLocation(); this.source = null; this.destination = null; this.dragging = false; this.originatesFromInput = false; this.startPercentage = 0; this.svgPathD = ""; this.startPixels = 0; if (source) { this.sourcePin = source; if (!destination) { this.toX = this.fromX; this.toY = this.fromY; } } if (destination) { this.destinationPin = destination; if (!source) { this.fromX = this.toX; this.fromY = this.toY; } } this.#linkPins(); } /** * @param {PinElement} pin * @param {Boolean} isDestinationPin */ #setPin(pin, isDestinationPin) { const getCurrentPin = () => isDestinationPin ? this.destinationPin : this.sourcePin; if (getCurrentPin() == pin) { return } if (getCurrentPin()) { const nodeElement = getCurrentPin().getNodeElement(); nodeElement.removeEventListener(Configuration.nodeDeleteEventName, this.#nodeDeleteHandler); nodeElement.removeEventListener( Configuration.nodeDragEventName, isDestinationPin ? this.#nodeDragDestinatonHandler : this.#nodeDragSourceHandler ); nodeElement.removeEventListener( Configuration.nodeReflowEventName, isDestinationPin ? this.#nodeReflowDestinatonHandler : this.#nodeReflowSourceHandler ); this.#unlinkPins(); } isDestinationPin ? this.#destinationPin = pin : this.#sourcePin = pin; if (getCurrentPin()) { const nodeElement = getCurrentPin().getNodeElement(); nodeElement.addEventListener(Configuration.nodeDeleteEventName, this.#nodeDeleteHandler); nodeElement.addEventListener( Configuration.nodeDragEventName, isDestinationPin ? this.#nodeDragDestinatonHandler : this.#nodeDragSourceHandler ); nodeElement.addEventListener( Configuration.nodeReflowEventName, isDestinationPin ? this.#nodeReflowDestinatonHandler : this.#nodeReflowSourceHandler ); isDestinationPin ? this.setDestinationLocation() : (this.setSourceLocation(), this.originatesFromInput = this.sourcePin.isInput()); this.#linkPins(); } } #linkPins() { if (this.sourcePin && this.destinationPin) { this.sourcePin.linkTo(this.destinationPin); this.destinationPin.linkTo(this.sourcePin); } } #unlinkPins() { if (this.sourcePin && this.destinationPin) { this.sourcePin.unlinkFrom(this.destinationPin); this.destinationPin.unlinkFrom(this.sourcePin); } } disconnectedCallback() { super.disconnectedCallback(); this.#unlinkPins(); this.sourcePin = null; this.destinationPin = null; } /** @param {Number[]?} location */ setSourceLocation(location = null) { if (location == null) { const self = this; if (!this.hasUpdated || !this.sourcePin.hasUpdated) { Promise.all([this.updateComplete, this.sourcePin.updateComplete]).then(() => self.setSourceLocation()); return } location = this.sourcePin.template.getLinkLocation(); } const [x, y] = location; this.fromX = x; this.fromY = y; } /** @param {Number[]?} location */ setDestinationLocation(location = null) { if (location == null) { const self = this; if (!this.hasUpdated || !this.destinationPin.hasUpdated) { Promise.all([this.updateComplete, this.destinationPin.updateComplete]).then(() => self.setDestinationLocation()); return } location = this.destinationPin.template.getLinkLocation(); } this.toX = location[0]; this.toY = location[1]; } startDragging() { this.dragging = true; } finishDragging() { this.dragging = false; } removeMessage() { this.linkMessageIcon = ""; this.linkMessageText = ""; } setMessageConvertType() { this.linkMessageIcon = "ueb-icon-conver-type"; this.linkMessageText = `Convert ${this.sourcePin.pinType} to ${this.destinationPin.pinType}.`; } setMessageCorrect() { this.linkMessageIcon = SVGIcon.correct; this.linkMessageText = b; } setMessageReplace() { this.linkMessageIcon = SVGIcon.correct; this.linkMessageText = b; } setMessageDirectionsIncompatible() { this.linkMessageIcon = SVGIcon.reject; this.linkMessageText = y`Directions are not compatbile.`; } setMessagePlaceNode() { this.linkMessageIcon = "ueb-icon-place-node"; this.linkMessageText = y`Place a new node.`; } setMessageReplaceLink() { this.linkMessageIcon = SVGIcon.correct; this.linkMessageText = y`Replace existing input connections.`; } setMessageSameNode() { this.linkMessageIcon = SVGIcon.reject; this.linkMessageText = y`Both are on the same node.`; } setMEssagetypesIncompatible() { this.linkMessageIcon = SVGIcon.reject; this.linkMessageText = y`${this.sourcePin.pinType} is not compatible with ${this.destinationPin.pinType}.`; } } /** * @typedef {import("../template/ISelectableDraggableTemplate").default} ISelectableDraggableTemplate * @typedef {import("../entity/IEntity").default} IEntity */ /** * @template {IEntity} T * @template {ISelectableDraggableTemplate} U * @extends {IDraggableElement} */ class ISelectableDraggableElement extends IDraggableElement { static properties = { ...super.properties, selected: { type: Boolean, attribute: "data-selected", reflect: true, converter: Utility.booleanConverter, }, } constructor(...args) { // @ts-expect-error super(...args); this.selected = false; this.listeningDrag = false; let self = this; this.dragHandler = e => self.addLocation(e.detail.value); } connectedCallback() { super.connectedCallback(); this.setSelected(this.selected); } disconnectedCallback() { super.disconnectedCallback(); this.blueprint.removeEventListener(Configuration.nodeDragGeneralEventName, this.dragHandler); } setSelected(value = true) { this.selected = value; if (this.blueprint) { if (this.selected) { this.listeningDrag = true; this.blueprint.addEventListener(Configuration.nodeDragGeneralEventName, this.dragHandler); } else { this.blueprint.removeEventListener(Configuration.nodeDragGeneralEventName, this.dragHandler); this.listeningDrag = false; } } } } /** * @typedef {import("../../element/PinElement").default} PinElement * @typedef {import("../../element/LinkElement").default} LinkElement * @typedef {import("../../template/KnotNodeTemplate").default} KnotNodeTemplate */ /** @extends IMouseClickDrag */ class MouseCreateLink extends IMouseClickDrag { /** @type {NodeListOf} */ #listenedPins /** @type {PinElement} */ #knotPin = null #mouseenterHandler = /** @param {MouseEvent} e */ e => { if (!this.enteredPin) { this.linkValid = false; this.enteredPin = /** @type {PinElement} */ (e.target); const a = this.link.sourcePin ?? this.target; // Remember target might have change const b = this.enteredPin; if ( a.nodeElement.getType() == Configuration.nodeType.knot || b.nodeElement.getType() == Configuration.nodeType.knot ) { // A knot can be linked to any pin, it doesn't matter the type or input/output direction this.link.setMessageCorrect(); this.linkValid = true; } else if (a.getNodeElement() == b.getNodeElement()) { this.link.setMessageSameNode(); } else if (a.isOutput() == b.isOutput()) { this.link.setMessageDirectionsIncompatible(); } else if (a.isOutput() == b.isOutput()) { this.link.setMessageDirectionsIncompatible(); } else if (this.blueprint.getLinks([a, b]).length) { this.link.setMessageReplaceLink(); this.linkValid = true; } else { this.link.setMessageCorrect(); this.linkValid = true; } } } #mouseleaveHandler = /** @param {MouseEvent} e */ e => { if (this.enteredPin == e.target) { this.enteredPin = null; this.linkValid = false; this.link?.setMessagePlaceNode(); } } /** @type {LinkElement?} */ link /** @type {PinElement?} */ enteredPin linkValid = false startDrag(location) { if (this.target.nodeElement.getType() == Configuration.nodeType.knot) { this.#knotPin = this.target; } /** @type {LinkElement} */ // @ts-expect-error this.link = new (ElementFactory.getConstructor("ueb-link"))(this.target, null); this.blueprint.linksContainerElement.prepend(this.link); this.link.setMessagePlaceNode(); this.#listenedPins = this.blueprint.querySelectorAll("ueb-pin"); this.#listenedPins.forEach(pin => { if (pin != this.target) { const clickableElement = pin.template.getClickableElement(); clickableElement.addEventListener("mouseenter", this.#mouseenterHandler); clickableElement.addEventListener("mouseleave", this.#mouseleaveHandler); } }); this.link.startDragging(); this.link.setDestinationLocation(location); } dragTo(location, movement) { this.link.setDestinationLocation(location); } endDrag() { this.#listenedPins.forEach(pin => { pin.removeEventListener("mouseenter", this.#mouseenterHandler); pin.removeEventListener("mouseleave", this.#mouseleaveHandler); }); if (this.enteredPin && this.linkValid) { if (this.#knotPin) { const otherPin = this.#knotPin !== this.link.sourcePin ? this.link.sourcePin : this.enteredPin; // Knot pin direction correction if (this.#knotPin.isInput() && otherPin.isInput() || this.#knotPin.isOutput() && otherPin.isOutput()) { const oppositePin = this.#knotPin.isInput() ? /** @type {KnotNodeTemplate} */(this.#knotPin.nodeElement.template).outputPin : /** @type {KnotNodeTemplate} */(this.#knotPin.nodeElement.template).inputPin; if (this.#knotPin === this.link.sourcePin) { this.link.sourcePin = oppositePin; } else { this.enteredPin = oppositePin; } } } this.blueprint.addGraphElement(this.link); this.link.destinationPin = this.enteredPin; this.link.removeMessage(); this.link.finishDragging(); } else { this.link.finishDragging(); this.link.remove(); } this.enteredPin = null; this.link = null; this.#listenedPins = null; } } /** @typedef {import("../input/IInput").default} IInput */ /** * @template T * @typedef {import("../element/PinElement").default} PinElement */ /** * @template T * @extends ITemplate> */ class PinTemplate extends ITemplate { /** @type {HTMLElement} */ #iconElement get iconElement() { return this.#iconElement } connectedCallback() { super.connectedCallback(); this.element.nodeElement = this.element.closest("ueb-node"); } /** @returns {IInput[]} */ createInputObjects() { return [ new MouseCreateLink(this.getClickableElement(), this.element.blueprint, { moveEverywhere: true, }) ] } render() { const icon = y`${this.renderIcon()}`; const content = y` ${this.renderName()} ${this.element.isInput() && !this.element.entity.bDefaultValueIsIgnored ? this.renderInput() : y``} `; return y` ${this.element.isInput() ? y`${icon}${content}` : y`${content}${icon}`} ` } renderIcon() { return SVGIcon.genericPin } renderName() { return y` ${this.element.getPinDisplayName()} ` } renderInput() { return y`` } /** @param {Map} changedProperties */ updated(changedProperties) { if (this.element.isInput() && changedProperties.has("isLinked")) { // When connected, an input may drop its input fields which means the node has to reflow const node = this.element.nodeElement; node.addNextUpdatedCallbacks(() => node.dispatchReflowEvent()); node.requestUpdate(); } } /** @param {Map} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.element.style.setProperty("--ueb-pin-color-rgb", Configuration.getPinColor(this.element).cssText); this.#iconElement = this.element.querySelector(".ueb-pin-icon") ?? this.element; } getLinkLocation() { const rect = this.iconElement.getBoundingClientRect(); const location = Utility.convertLocation( [(rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2], this.element.blueprint.gridElement ); return this.element.blueprint.compensateTranslation(location) } getClickableElement() { return this.element } } /** @typedef {import("./KnotNodeTemplate").default} KnotNodeTemplate */ class KnotPinTemplate extends PinTemplate { render() { return this.element.isOutput() ? y`${this.renderIcon()}` : y`` } getLinkLocation() { const rect = ( this.element.isInput() ? /** @type {KnotNodeTemplate} */ (this.element.nodeElement.template).outputPin.template : this ) .iconElement.getBoundingClientRect(); const location = Utility.convertLocation( [ this.element.isInput() ? rect.left + 1 : rect.right + 2, (rect.top + rect.bottom) / 2, ], this.element.blueprint.gridElement ); return this.element.blueprint.compensateTranslation(location) } } /** * @typedef {import("../../Blueprint").default} Blueprint * @typedef {import("../../element/IDraggableElement").default} IDraggableElement */ /** * @template {IDraggableElement} T * @extends {IMouseClickDrag} */ class MouseMoveDraggable extends IMouseClickDrag { clicked(location) { if (this.options.repositionOnClick) { this.target.setLocation(this.stepSize > 1 ? Utility.snapToGrid(location, this.stepSize) : location ); this.clickedOffset = [0, 0]; } } dragTo(location, offset) { const targetLocation = [this.target.locationX, this.target.locationY]; const [adjustedLocation, adjustedTargetLocation] = this.stepSize > 1 ? [Utility.snapToGrid(location, this.stepSize), Utility.snapToGrid(targetLocation, this.stepSize)] : [location, targetLocation]; offset = [ adjustedLocation[0] - this.mouseLocation[0], adjustedLocation[1] - this.mouseLocation[1] ]; if (offset[0] == 0 && offset[1] == 0) { return } // Make sure it snaps on the grid offset[0] += adjustedTargetLocation[0] - this.target.locationX; offset[1] += adjustedTargetLocation[1] - this.target.locationY; this.dragAction(adjustedLocation, offset); // Reassign the position of mouse this.mouseLocation = adjustedLocation; } dragAction(location, offset) { this.target.setLocation([ location[0] - this.clickedOffset[0], location[1] - this.clickedOffset[1] ]); } } /** * @typedef {import("../entity/IEntity").default} IEntity * @typedef {import("../element/IDraggableElement").default} IDraggableElement */ /** * @template {IDraggableElement} T * @extends {ITemplate} */ class IDraggableTemplate extends ITemplate { getDraggableElement() { return /** @type {Element} */(this.element) } createDraggableObject() { return new MouseMoveDraggable(this.element, this.element.blueprint, { draggableElement: this.getDraggableElement(), }) } createInputObjects() { return [ ...super.createInputObjects(), this.createDraggableObject(), ] } } /** @typedef {import("../element/IDraggableElement").default} IDraggableElement */ /** * @template {IDraggableElement} T * @extends {IDraggableTemplate} */ class IDraggablePositionedTemplate extends IDraggableTemplate { /** @param {Map} changedProperties */ update(changedProperties) { super.update(changedProperties); if (changedProperties.has("locationX")) { this.element.style.left = `${this.element.locationX}px`; } if (changedProperties.has("locationY")) { this.element.style.top = `${this.element.locationY}px`; } } } /** * @typedef {import("../../Blueprint").default} Blueprint * @typedef {import("../../element/ISelectableDraggableElement").default} ISelectableDraggableElement */ /** @extends {MouseMoveDraggable} */ class MouseMoveNodes extends MouseMoveDraggable { startDrag() { if (!this.target.selected) { this.blueprint.unselectAll(); this.target.setSelected(true); } } dragAction(location, offset) { this.target.dispatchDragEvent(offset); } unclicked() { if (!this.started) { this.blueprint.unselectAll(); this.target.setSelected(true); } } } /** * @typedef {import("../element/ISelectableDraggableElement").default} ISelectableDraggableElement * @typedef {import("../input/mouse/MouseMoveDraggable").default} MouseMoveDraggable */ /** * @template {ISelectableDraggableElement} T * @extends {IDraggablePositionedTemplate} */ class ISelectableDraggableTemplate extends IDraggablePositionedTemplate { getDraggableElement() { return /** @type {Element} */ (this.element) } createDraggableObject() { return /** @type {MouseMoveDraggable} */ (new MouseMoveNodes(this.element, this.element.blueprint, { draggableElement: this.getDraggableElement(), })) } /** @param {Map} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); if (this.element.selected && !this.element.listeningDrag) { this.element.setSelected(true); } } } /** * @typedef {import("../element/NodeElement").default} NodeElement * @typedef {import("../element/PinElement").default} PinElement */ /** @extends {ISelectableDraggableTemplate} */ class NodeTemplate extends ISelectableDraggableTemplate { static #nodeIcon = { [Configuration.nodeType.doN]: SVGIcon.doN, [Configuration.nodeType.dynamicCast]: SVGIcon.cast, [Configuration.nodeType.executionSequence]: SVGIcon.sequence, [Configuration.nodeType.forEachElementInEnum]: SVGIcon.loop, [Configuration.nodeType.forEachLoop]: SVGIcon.forEachLoop, [Configuration.nodeType.forEachLoopWithBreak]: SVGIcon.forEachLoop, [Configuration.nodeType.forLoop]: SVGIcon.loop, [Configuration.nodeType.forLoopWithBreak]: SVGIcon.loop, [Configuration.nodeType.ifThenElse]: SVGIcon.branchNode, [Configuration.nodeType.makeArray]: SVGIcon.makeArray, [Configuration.nodeType.makeMap]: SVGIcon.makeMap, [Configuration.nodeType.select]: SVGIcon.select, [Configuration.nodeType.whileLoop]: SVGIcon.loop, default: SVGIcon.functionSymbol } #hasTargetInputNode = false toggleAdvancedDisplayHandler = () => { this.element.toggleShowAdvancedPinDisplay(); this.element.addNextUpdatedCallbacks(() => this.element.dispatchReflowEvent(), true); } /** @param {NodeElement} element */ constructed(element) { super.constructed(element); this.element.style.setProperty("--ueb-node-color", this.getColor().cssText); } getColor() { const functionColor = i$3`84, 122, 156`; const pureFunctionColor = i$3`95, 129, 90`; switch (this.element.entity.getClass()) { case Configuration.nodeType.callFunction: if (this.element.entity.bIsPureFunc) { return pureFunctionColor } return functionColor case Configuration.nodeType.makeArray: case Configuration.nodeType.makeMap: case Configuration.nodeType.select: return pureFunctionColor case Configuration.nodeType.macro: case Configuration.nodeType.executionSequence: return i$3`150,150,150` case Configuration.nodeType.dynamicCast: return i$3`46, 104, 106` } return functionColor } render() { const icon = this.renderNodeIcon(); const name = this.renderNodeName(); return y` ${icon ? y` ${icon} ` : b} ${name ? y` ${name} ${this.#hasTargetInputNode ? y` Target is ${Utility.formatStringName(this.element.entity.FunctionReference.MemberParent.getName())} `: b} ` : b} ${this.element.enabledState?.toString() == "DevelopmentOnly" ? y` Development Only ` : b} ${this.element.advancedPinDisplay ? y` ${SVGIcon.expandIcon} ` : b} ` } renderNodeIcon() { let icon = NodeTemplate.#nodeIcon[this.element.getType()]; if (icon) { return icon } if (this.element.getNodeDisplayName().startsWith("Break")) { return SVGIcon.breakStruct } if (this.element.entity.getClass() === Configuration.nodeType.macro) { return SVGIcon.macro } return NodeTemplate.#nodeIcon.default } renderNodeName() { return this.element.getNodeDisplayName() } /** @param {Map} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.setupPins(); Promise.all(this.element.getPinElements().map(n => n.updateComplete)).then(() => this.element.dispatchReflowEvent()); } setupPins() { const inputContainer = /** @type {HTMLElement} */(this.element.querySelector(".ueb-node-inputs")); const outputContainer = /** @type {HTMLElement} */(this.element.querySelector(".ueb-node-outputs")); this.element.nodeNameElement = /** @type {HTMLElement} */(this.element.querySelector(".ueb-node-name-text")); this.element.getPinElements().forEach(p => { if (p.isInput()) { inputContainer.appendChild(p); } else if (p.isOutput()) { outputContainer.appendChild(p); } }); } createPinElements() { return this.element.getPinEntities() .filter(v => !v.isHidden()) .map(v => { if (!this.#hasTargetInputNode && v.getDisplayName() === "Target") { this.#hasTargetInputNode = true; } return/** @type {PinElement} */( new (ElementFactory.getConstructor("ueb-pin"))(v, undefined, this.element) ) }) } /** * @param {NodeElement} node * @returns {NodeListOf} */ getPinElements(node) { return node.querySelectorAll("ueb-pin") } linksChanged() { } } /** * @typedef {import("../element/NodeElement").default} NodeElement * @typedef {import("../element/PinElement").default} PinElement */ class KnotNodeTemplate extends NodeTemplate { static #traversedPin = new Set() /** @type {Boolean?} */ #chainDirection = null // The node is part of a chain connected to an input or output pin /** @type {PinElement} */ #inputPin get inputPin() { return this.#inputPin } /** @type {PinElement} */ #outputPin get outputPin() { return this.#outputPin } /** @param {PinElement} startingPin */ findDirectionaPin(startingPin) { if ( startingPin.nodeElement.getType() !== Configuration.nodeType.knot || KnotNodeTemplate.#traversedPin.has(startingPin) ) { KnotNodeTemplate.#traversedPin.clear(); return true } KnotNodeTemplate.#traversedPin.add(startingPin); for (let pin of startingPin.getLinks().map(l => this.element.blueprint.getPin(l))) { if (this.findDirectionaPin(pin)) { return true } } return false } render() { return y` ` } setupPins() { this.element.getPinElements().forEach( p => /** @type {HTMLElement} */(this.element.querySelector(".ueb-node-border")).appendChild(p) ); } /** * @param {NodeElement} node * @returns {NodeListOf} */ getPinElements(node) { return node.querySelectorAll("ueb-pin") } createPinElements() { const entities = this.element.getPinEntities().filter(v => !v.isHidden()); const inputEntity = entities[entities[0].isInput() ? 0 : 1]; const outputEntity = entities[entities[0].isOutput() ? 0 : 1]; const pinElementConstructor = ElementFactory.getConstructor("ueb-pin"); return [ this.#inputPin = /** @type {PinElement} */(new pinElementConstructor( inputEntity, new KnotPinTemplate(), this.element )), this.#outputPin = /** @type {PinElement} */(new pinElementConstructor( outputEntity, new KnotPinTemplate(), this.element )), ] } linksChanged() { } } /** * @typedef {import("../element/NodeElement").default} NodeElement * @typedef {import("../element/PinElement").default} PinElement */ class VariableAccessNodeTemplate extends NodeTemplate { #hasInput = false #hasOutput = false #displayName = "" /** @param {NodeElement} element */ constructed(element) { super.constructed(element); this.element.classList.add("ueb-node-style-glass"); this.#displayName = this.element.getNodeDisplayName(); } render() { return y` ${this.#displayName ? y` ${this.#displayName} ` : b} ${this.#hasInput ? y` ` : b} ${this.#hasOutput ? y` ` : b} ` } createPinElements() { return this.element.getPinEntities() .filter(v => !v.isHidden()) .map(v => { this.#hasInput ||= v.isInput(); this.#hasOutput ||= v.isOutput(); return /** @type {PinElement} */( new (ElementFactory.getConstructor("ueb-pin"))(v, undefined, this.element) ) }) } setupPins() { super.setupPins(); let outputPin = this.element.getPinElements().find(p => !p.entity.isHidden() && !p.entity.isExecution()); this.element.style.setProperty("--ueb-node-color", outputPin.getColor().cssText); } } /** * @typedef {import("../element/NodeElement").default} NodeElement * @typedef {import("../element/PinElement").default} PinElement */ class CommentNodeTemplate extends NodeTemplate { #color = LinearColorEntity.getWhite() /** @param {NodeElement} element */ constructed(element) { if (element.entity.CommentColor) { this.#color.setFromRGBANumber(element.entity.CommentColor.toNumber()); } // Dimming the colors to 2/3 const factor = 2 / 3; this.#color.setFromRGBA( this.#color.R.value * factor, this.#color.G.value * factor, this.#color.B.value * factor, ); element.classList.add("ueb-node-style-comment", "ueb-node-resizeable"); super.constructed(element); // Keep it at the end } getColor() { return i$3`${Math.round(this.#color.R.value * 255)}, ${Math.round(this.#color.G.value * 255)}, ${Math.round(this.#color.B.value * 255)}` } getDraggableElement() { return this.element.querySelector(".ueb-node-top") } render() { const width = this.element.entity.getNodeWidth(); const height = this.element.entity.getNodeHeight(); return y` ${this.element.entity.NodeComment} ` } } /** @typedef {import("./IElement").default} IElement */ /** @extends {ISelectableDraggableElement} */ class NodeElement extends ISelectableDraggableElement { static #typeTemplateMap = { [Configuration.nodeType.comment]: CommentNodeTemplate, [Configuration.nodeType.knot]: KnotNodeTemplate, [Configuration.nodeType.variableGet]: VariableAccessNodeTemplate, [Configuration.nodeType.variableSet]: VariableAccessNodeTemplate, } static properties = { ...ISelectableDraggableElement.properties, typePath: { type: String, attribute: "data-type", reflect: true, }, nodeName: { type: String, attribute: "data-name", reflect: true, }, advancedPinDisplay: { type: String, attribute: "data-advanced-display", converter: IdentifierEntity.attributeConverter, reflect: true, }, enabledState: { type: String, attribute: "data-enabled-state", reflect: true, }, nodeDisplayName: { type: String, attribute: false, }, pureFunction: { type: Boolean, converter: Utility.booleanConverter, attribute: "data-pure-function", reflect: true, }, } static dragEventName = Configuration.nodeDragEventName static dragGeneralEventName = Configuration.nodeDragGeneralEventName get blueprint() { return super.blueprint } set blueprint(v) { super.blueprint = v; this.#pins.forEach(p => p.blueprint = v); } /** @type {HTMLElement} */ #nodeNameElement get nodeNameElement() { return this.#nodeNameElement } set nodeNameElement(value) { this.#nodeNameElement = value; } #pins /** * @param {ObjectEntity} entity * @param {NodeTemplate} template */ constructor(entity, template = undefined) { super(entity, template ?? new (NodeElement.getTypeTemplate(entity))()); this.#pins = this.template.createPinElements(); this.typePath = this.entity.getType(); this.nodeName = this.entity.getObjectName(); this.advancedPinDisplay = this.entity.AdvancedPinDisplay?.toString(); this.enabledState = this.entity.EnabledState; this.nodeDisplayName = this.entity.getDisplayName(); this.pureFunction = this.entity.bIsPureFunc; this.dragLinkObjects = []; super.setLocation([this.entity.NodePosX.value, this.entity.NodePosY.value]); this.entity.subscribe("AdvancedPinDisplay", value => this.advancedPinDisplay = value); this.entity.subscribe("Name", value => this.nodeName = value); } /** * @param {ObjectEntity} nodeEntity * @return {new () => NodeTemplate} */ static getTypeTemplate(nodeEntity) { let result = NodeElement.#typeTemplateMap[nodeEntity.getClass()]; return result ?? NodeTemplate } /** @param {String} str */ static fromSerializedObject(str) { str = str.trim(); let entity = SerializerFactory.getSerializer(ObjectEntity).deserialize(str); // @ts-expect-error return new NodeElement(entity) } disconnectedCallback() { super.disconnectedCallback(); this.dispatchDeleteEvent(); } getType() { return this.entity.getType() } getNodeName() { return this.entity.getObjectName() } getNodeDisplayName() { return this.entity.getDisplayName() } /** @param {IElement[]} nodesWhitelist */ sanitizeLinks(nodesWhitelist = []) { this.getPinElements().forEach(pin => pin.sanitizeLinks(nodesWhitelist)); } /** @param {String} name */ rename(name) { if (this.entity.Name == name) { return false } for (let sourcePinElement of this.getPinElements()) { for (let targetPinReference of sourcePinElement.getLinks()) { this.blueprint.getPin(targetPinReference).redirectLink(sourcePinElement, new PinReferenceEntity({ objectName: name, pinGuid: sourcePinElement.entity.PinId, })); } } this.entity.Name = name; } getPinElements() { return this.#pins } /** @returns {PinEntity[]} */ getPinEntities() { return this.entity.CustomProperties.filter(v => v instanceof PinEntity) } setLocation(value = [0, 0]) { let nodeConstructor = this.entity.NodePosX.constructor; // @ts-expect-error this.entity.NodePosX = new nodeConstructor(value[0]); // @ts-expect-error this.entity.NodePosY = new nodeConstructor(value[1]); super.setLocation(value); } dispatchDeleteEvent() { let deleteEvent = new CustomEvent(Configuration.nodeDeleteEventName); this.dispatchEvent(deleteEvent); } dispatchReflowEvent() { let reflowEvent = new CustomEvent(Configuration.nodeReflowEventName); this.dispatchEvent(reflowEvent); } setShowAdvancedPinDisplay(value) { this.entity.AdvancedPinDisplay = new IdentifierEntity(value ? "Shown" : "Hidden"); } toggleShowAdvancedPinDisplay() { this.setShowAdvancedPinDisplay(this.entity.AdvancedPinDisplay?.toString() != "Shown"); } } class OrderedIndexArray { /** * @param {(arrayElement: number) => number} comparisonValueSupplier * @param {number} value */ constructor(comparisonValueSupplier = v => v, value = null) { this.array = new Uint32Array(value); this.comparisonValueSupplier = comparisonValueSupplier; this.length = 0; this.currentPosition = 0; } /** @param {number} index */ get(index) { if (index >= 0 && index < this.length) { return this.array[index] } return null } getArray() { return this.array } /** @param {number} value */ 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; } } /** @param {number} element */ 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; } this.shiftRight(position); this.array[position] = element; ++this.length; return position } /** @param {number} element */ remove(element) { let position = this.getPosition(this.comparisonValueSupplier(element)); if (this.array[position] == element) { this.removeAt(position); } } /** @param {number} position */ removeAt(position) { if (position < this.currentPosition) { --this.currentPosition; } 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); } } /** * @typedef {import("../Blueprint").BoundariesInfo} BoundariesInfo * @typedef {{ * primaryBoundary: Number, * secondaryBoundary: Number, * insertionPosition?: Number, * rectangle: Number * onSecondaryAxis: Boolean * }} Metadata * @typedef {any} Rectangle */ class FastSelectionModel { /** * @param {Number[]} initialPosition * @param {Rectangle[]} rectangles * @param {(rect: Rectangle) => BoundariesInfo} boundariesFunc * @param {(rect: Rectangle, selected: Boolean) => void} selectFunc */ 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(); } 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(); 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(); 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; } } /** @typedef {import("../element/SelectorElement").default} SelectorElement */ /** @extends IFromToPositionedTemplate */ class SelectorTemplate extends IFromToPositionedTemplate { } /** @extends {IFromToPositionedElement} */ class SelectorElement extends IFromToPositionedElement { constructor() { super({}, new SelectorTemplate()); this.selectionModel = null; } /** @param {Number[]} initialPosition */ beginSelect(initialPosition) { this.blueprint.selecting = true; this.setBothLocations(initialPosition); this.selectionModel = new FastSelectionModel( initialPosition, this.blueprint.getNodes(), this.blueprint.nodeBoundariesSupplier, this.blueprint.nodeSelectToggleFunction ); } /** @param {Number[]} finalPosition */ selectTo(finalPosition) { /** @type {FastSelectionModel} */ (this.selectionModel) .selectTo(finalPosition); this.toX = finalPosition[0]; this.toY = finalPosition[1]; } endSelect() { this.blueprint.selecting = false; this.selectionModel = null; this.fromX = 0; this.fromY = 0; this.toX = 0; this.toY = 0; } } /** * @typedef {import("./element/PinElement").default} PinElement * @typedef {import("./entity/GuidEntity").default} GuidEntity * @typedef {import("./entity/PinReferenceEntity").default} PinReferenceEntity * @typedef {{ * primaryInf: Number, * primarySup: Number, * secondaryInf: Number, * secondarySup: Number, * }} BoundariesInfo */ /** @extends {IElement} */ class Blueprint extends IElement { static properties = { selecting: { type: Boolean, attribute: "data-selecting", reflect: true, converter: Utility.booleanConverter, }, scrolling: { type: Boolean, attribute: "data-scrolling", reflect: true, converter: Utility.booleanConverter, }, focused: { type: Boolean, attribute: "data-focused", reflect: true, converter: Utility.booleanConverter, }, zoom: { type: Number, attribute: "data-zoom", reflect: true, }, scrollX: { type: Number, attribute: false, }, scrollY: { type: Number, attribute: false, }, additionalX: { type: Number, attribute: false, }, additionalY: { type: Number, attribute: false, }, translateX: { type: Number, attribute: false, }, translateY: { type: Number, attribute: false, }, } static styles = BlueprintTemplate.styles /** @type {Map} */ #nodeNameCounter = new Map() /** @type {NodeElement[]}" */ nodes = [] /** @type {LinkElement[]}" */ links = [] /** @type {Number[]} */ mousePosition = [0, 0] /** @type {HTMLElement} */ gridElement /** @type {HTMLElement} */ viewportElement /** @type {HTMLElement} */ overlayElement /** @type {SelectorElement} */ selectorElement /** @type {HTMLElement} */ linksContainerElement /** @type {HTMLElement} */ nodesContainerElement /** @type {HTMLElement} */ headerElement focused = false waitingExpandUpdate = false nodeBoundariesSupplier = node => { let rect = node.getBoundingClientRect(); let gridRect = this.nodesContainerElement.getBoundingClientRect(); const scaleCorrection = 1 / this.getScale(); return /** @type {BoundariesInfo} */ { 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: NodeElement, selected: Boolean) => void}} */ nodeSelectToggleFunction = (node, selected) => { node.setSelected(selected); } /** @param {Configuration} settings */ constructor(settings = new Configuration()) { super({}, new BlueprintTemplate()); this.selecting = false; this.scrolling = false; this.focused = false; this.zoom = 0; this.scrollX = Configuration.expandGridSize; this.scrollY = Configuration.expandGridSize; this.translateX = Configuration.expandGridSize; this.translateY = Configuration.expandGridSize; } getGridDOMElement() { return this.gridElement } disconnectedCallback() { super.disconnectedCallback(); } getScroll() { return [this.scrollX, this.scrollY] } /** @param {Number[]} param0 */ setScroll([x, y], smooth = false) { this.scrollX = x; this.scrollY = y; } /** @param {Number[]} delta */ scrollDelta(delta, smooth = false) { const maxScroll = [2 * Configuration.expandGridSize, 2 * Configuration.expandGridSize]; 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] < Configuration.gridExpandThreshold * Configuration.expandGridSize) { // Expand left/top expand[i] = -1; } else if (delta[i] > 0 && finalScroll[i] > maxScroll[i] - Configuration.gridExpandThreshold * Configuration.expandGridSize) { // Expand right/bottom expand[i] = 1; } } if (expand[0] != 0 || expand[1] != 0) { this.seamlessExpand(expand); } currentScroll = this.getScroll(); finalScroll = [ currentScroll[0] + delta[0], currentScroll[1] + delta[1] ]; this.setScroll(finalScroll, smooth); } scrollCenter() { const scroll = this.getScroll(); const offset = [ this.translateX - scroll[0], this.translateY - scroll[1] ]; const targetOffset = this.getViewportSize().map(size => size / 2); const deltaOffset = [ offset[0] - targetOffset[0], offset[1] - targetOffset[1] ]; this.scrollDelta(deltaOffset, true); } 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 ] } snapToGrid(location) { return Utility.snapToGrid(location, Configuration.gridSize) } /** @param {Number[]} param0 */ seamlessExpand([x, y]) { x = Math.round(x); y = Math.round(y); let scale = this.getScale(); { // 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 [x, y] = [-x * Configuration.expandGridSize, -y * Configuration.expandGridSize]; if (x != 0) { this.scrollX += x; x /= scale; } if (y != 0) { this.scrollY += y; y /= scale; } } this.translateX += x; this.translateY += y; } progressiveSnapToGrid(x) { return Configuration.expandGridSize * Math.round(x / Configuration.expandGridSize + 0.5 * Math.sign(x)) } getZoom() { return this.zoom } setZoom(zoom, center) { zoom = Utility.clamp(zoom, Configuration.minZoom, Configuration.maxZoom); if (zoom == this.zoom) { return } let initialScale = this.getScale(); this.zoom = zoom; if (center) { requestAnimationFrame(_ => { center[0] += this.translateX; center[1] += this.translateY; 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")) } /** @param {Number[]} param0 */ compensateTranslation([x, y]) { x -= this.translateX; y -= this.translateY; return [x, y] } getNodes(selected = false) { if (selected) { return this.nodes.filter( node => node.selected ) } else { return this.nodes } } /** @param {PinReferenceEntity} pinReference */ getPin(pinReference) { let result = this.template.getPin(pinReference); if (result // Make sure it wasn't renamed in the meantime && result.nodeElement.getNodeName() == pinReference.objectName.toString()) { return result } // Slower fallback return [... this.nodes .find(n => pinReference.objectName.toString() == n.getNodeName()) ?.getPinElements() ?? []] .find(p => pinReference.pinGuid.toString() == p.getPinId().toString()) } /** * Returns the list of links in this blueprint. * @returns {LinkElement[]} Nodes */ getLinks([a, b] = []) { if (a == null != b == null) { const pin = a ?? b; return this.links.filter(link => link.sourcePin == pin || link.destinationPin == pin) } if (a != null && b != null) { return this.links.filter(link => link.sourcePin == a && link.destinationPin == b || link.sourcePin == b && link.destinationPin == a ) } return this.links } /** * @param {PinElement} sourcePin * @param {PinElement} destinationPin */ getLink(sourcePin, destinationPin, ignoreDirection = false) { return this.links.find(link => link.sourcePin == sourcePin && link.destinationPin == destinationPin || ignoreDirection && link.sourcePin == destinationPin && link.destinationPin == sourcePin ) } selectAll() { this.getNodes().forEach(node => this.nodeSelectToggleFunction(node, true)); } unselectAll() { this.getNodes().forEach(node => this.nodeSelectToggleFunction(node, false)); } /** @param {...IElement} graphElements */ addGraphElement(...graphElements) { for (let element of graphElements) { element.blueprint = this; if (element instanceof NodeElement && !this.nodes.includes(element)) { const nodeName = element.entity.getObjectName(); const homonymNode = this.nodes.find(node => node.entity.getObjectName() == nodeName); if (homonymNode) { // Inserted node keeps tha name and the homonym nodes is renamed let name = homonymNode.entity.getObjectName(true); this.#nodeNameCounter[name] = this.#nodeNameCounter[name] ?? -1; do { ++this.#nodeNameCounter[name]; } while (this.nodes.find(node => node.entity.getObjectName() == Configuration.nodeName(name, this.#nodeNameCounter[name]) )) homonymNode.rename(Configuration.nodeName(name, this.#nodeNameCounter[name])); } this.nodes.push(element); this.nodesContainerElement?.appendChild(element); } else if (element instanceof LinkElement && !this.links.includes(element)) { this.links.push(element); if (this.linksContainerElement && !this.linksContainerElement.contains(element)) { this.linksContainerElement.appendChild(element); } } } graphElements.filter(element => element instanceof NodeElement).forEach( node => /** @type {NodeElement} */(node).sanitizeLinks(graphElements) ); } /** @param {...IElement} graphElements */ removeGraphElement(...graphElements) { for (let element of graphElements) { if (element.closest("ueb-blueprint") == this) { element.remove(); let elementsArray = element instanceof NodeElement ? this.nodes : element instanceof LinkElement ? this.links : null; elementsArray?.splice( elementsArray.findIndex(v => v === element), 1 ); } } } setFocused(value = true) { if (this.focused == value) { return } let event = new CustomEvent(value ? "blueprint-focus" : "blueprint-unfocus"); this.focused = value; if (!this.focused) { this.unselectAll(); } this.dispatchEvent(event); } /** @param {Boolean} begin */ dispatchEditTextEvent(begin) { const event = new CustomEvent( begin ? Configuration.editTextEventName.begin : Configuration.editTextEventName.end ); this.dispatchEvent(event); } } customElements.define("ueb-blueprint", Blueprint); /** * @typedef {import("../element/IDraggableElement").default} IDraggableElement */ /** * @template {IDraggableElement} T * @extends {IDraggableTemplate} */ class IDraggableControlTemplate extends IDraggableTemplate { /** @type {(x: Number, y: Number) => void} */ #locationChangeCallback get locationChangeCallback() { return this.#locationChangeCallback } set locationChangeCallback(callback) { this.#locationChangeCallback = callback; } movementSpace movementSpaceSize = [0, 0] connectedCallback() { super.connectedCallback(); this.movementSpace = this.element.parentElement; const bounding = this.movementSpace.getBoundingClientRect(); this.movementSpaceSize = [bounding.width, bounding.height]; } createDraggableObject() { return new MouseMoveDraggable(this.element, this.element.blueprint, { draggableElement: this.movementSpace, ignoreTranslateCompensate: true, moveEverywhere: true, movementSpace: this.movementSpace, repositionOnClick: true, stepSize: 1, }) } /** @param {[Number, Number]} param0 */ adjustLocation([x, y]) { this.locationChangeCallback?.(x, y); return [x, y] } } /** @typedef {import("../element/ColorHandlerElement").default} ColorHandlerElement */ /** @extends {IDraggableControlTemplate} */ class ColorHandlerTemplate extends IDraggableControlTemplate { /** @param {[Number, Number]} param0 */ adjustLocation([x, y]) { const radius = Math.round(this.movementSpaceSize[0] / 2); x = x - radius; y = -(y - radius); let [r, theta] = Utility.getPolarCoordinates([x, y]); r = Math.min(r, radius), [x, y] = Utility.getCartesianCoordinates([r, theta]); this.locationChangeCallback?.(x / radius, y / radius); x = Math.round(x + radius); y = Math.round(-y + radius); return [x, y] } } /** * @typedef {import("../element/WindowElement").default} WindowElement * @typedef {import("../entity/IEntity").default} IEntity * @typedef {import("../template/IDraggableControlTemplate").default} IDraggableControlTemplate */ /** * @template {IEntity} T * @template {IDraggableControlTemplate} U * @extends {IDraggableElement} */ class IDraggableControlElement extends IDraggableElement { /** @type {WindowElement} */ windowElement /** * @param {T} entity * @param {U} template */ constructor(entity, template) { super(entity, template); } connectedCallback() { super.connectedCallback(); this.windowElement = this.closest("ueb-window"); } /** @param {Number[]} param0 */ setLocation([x, y]) { super.setLocation(this.template.adjustLocation([x, y])); } } /** @typedef {import("../template/ColorPickerWindowTemplate").default} ColorPickerWindowTemplate */ /** * @template T * @typedef {import("./WindowElement").default} WindowElement */ /** @extends {IDraggableControlElement} */ class ColorHandlerElement extends IDraggableControlElement { constructor() { super({}, new ColorHandlerTemplate()); } } /** @typedef {import("../element/ColorHandlerElement").default} ColorHandlerElement */ /** @extends {IDraggableControlTemplate} */ class ColorSliderTemplate extends IDraggableControlTemplate { /** @param {[Number, Number]} param0 */ adjustLocation([x, y]) { x = Utility.clamp(x, 0, this.movementSpaceSize[0]); y = Utility.clamp(y, 0, this.movementSpaceSize[1]); this.locationChangeCallback?.(x / this.movementSpaceSize[0], 1 - y / this.movementSpaceSize[1]); return [x, y] } } /** @typedef {import("../template/IDraggableControlTemplate").default} IDraggableControlTemplate */ /** @extends {IDraggableControlElement} */ class ColorSliderElement extends IDraggableControlElement { constructor() { super({}, new ColorSliderTemplate()); } } /** @typedef {import ("../element/InputElement").default} InputElement */ /** @extends {ITemplate} */ class InputTemplate extends ITemplate { #focusHandler = () => { this.element.blueprint.dispatchEditTextEvent(true); if (this.element.selectOnFocus) { getSelection().selectAllChildren(this.element); } } #focusoutHandler = () => { this.element.blueprint.dispatchEditTextEvent(false); document.getSelection()?.removeAllRanges(); // Deselect eventually selected text inside the input } #inputSingleLineHandler = /** @param {InputEvent} e */ e => /** @type {HTMLElement} */(e.target).querySelectorAll("br").forEach(br => br.remove()) #onKeydownBlurOnEnterHandler = /** @param {KeyboardEvent} e */ e => { if (e.code == "Enter" && !e.shiftKey) { /** @type {HTMLElement} */(e.target).blur(); } } /** @param {InputElement} element */ constructed(element) { super.constructed(element); this.element.classList.add("ueb-pin-input-content"); this.element.setAttribute("role", "textbox"); this.element.contentEditable = "true"; } connectedCallback() { this.element.addEventListener("focus", this.#focusHandler); this.element.addEventListener("focusout", this.#focusoutHandler); if (this.element.singleLine) { this.element.addEventListener("input", this.#inputSingleLineHandler); } if (this.element.blurOnEnter) { this.element.addEventListener("keydown", this.#onKeydownBlurOnEnterHandler); } } cleanup() { this.element.removeEventListener("focus", this.#focusHandler); this.element.removeEventListener("focusout", this.#focusoutHandler); if (this.element.singleLine) { this.element.removeEventListener("input", this.#inputSingleLineHandler); } if (this.element.blurOnEnter) { this.element.removeEventListener("keydown", this.#onKeydownBlurOnEnterHandler); } } } class InputElement extends IElement { static properties = { ...super.properties, singleLine: { type: Boolean, attribute: "data-single-line", converter: Utility.booleanConverter, reflect: true, }, selectOnFocus: { type: Boolean, attribute: "data-select-focus", converter: Utility.booleanConverter, reflect: true, }, blurOnEnter: { type: Boolean, attribute: "data-blur-enter", converter: Utility.booleanConverter, reflect: true, }, } constructor() { super({}, new InputTemplate()); this.singleLine = false; this.selectOnFocus = true; this.blurOnEnter = true; } } /** * @typedef {import("../../element/IDraggableElement").default} IDraggableElement */ /** * @template {IDraggableElement} T * @extends {IMouseClickDrag} */ class MouseIgnore extends IMouseClickDrag { constructor(target, blueprint, options = {}) { options.consumeEvent = true; super(target, blueprint, options); } } /** * @extends PinTemplate */ class BoolInputPinTemplate extends PinTemplate { /** @type {HTMLInputElement?} */ #input #onChangeHandler = _ => this.element.setDefaultValue(this.#input.checked) /** @param {Map} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.#input = this.element.querySelector(".ueb-pin-input"); this.#input?.addEventListener("change", this.#onChangeHandler); } cleanup() { super.cleanup(); this.#input?.removeEventListener("change", this.#onChangeHandler); } createInputObjects() { return [ ...super.createInputObjects(), new MouseIgnore(this.#input, this.element.blueprint), ] } renderInput() { return y` ` } } /** @typedef {import("../element/PinElement").default} PinElement */ class ExecPinTemplate extends PinTemplate { renderIcon() { return SVGIcon.execPin } renderName() { let pinName = this.element.entity.PinName; if (this.element.entity.PinFriendlyName) { pinName = this.element.entity.PinFriendlyName.toString(); } else if (pinName === "execute" || pinName === "then") { return y`` } return y`${Utility.formatStringName(pinName)}` } } /** * @template T * @typedef {import("../element/PinElement").default} PinElement */ /** * @template T * @extends PinTemplate */ class IInputPinTemplate extends PinTemplate { static singleLineInput = false static selectOnFocus = true /** @type {HTMLElement[]} */ #inputContentElements get inputContentElements() { return this.#inputContentElements } /** @param {String} value */ static stringFromInputToUE(value) { return value .replace(/(?=\n\s*)\n$/, "") // Remove trailing double newline .replaceAll("\n", "\\r\n") // Replace newline with \r\n (default newline in UE) } /** @param {String} value */ static stringFromUEToInput(value) { return value .replaceAll(/(?:\r|(?<=(?:^|[^\\])(?:\\\\)*)\\r)(?=\n)/g, "") // Remove \r leftover from \r\n .replace(/(?<=\n\s*)$/, "\n") // Put back trailing double newline } #onFocusOutHandler = () => this.setInputs(this.getInputs(), true) /** @param {Map} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.#inputContentElements = /** @type {HTMLElement[]} */([...this.element.querySelectorAll("ueb-input")]); if (this.#inputContentElements.length) { this.#inputContentElements.forEach(element => { element.addEventListener("focusout", this.#onFocusOutHandler); }); } } cleanup() { super.cleanup(); this.#inputContentElements.forEach(element => { element.removeEventListener("focusout", this.#onFocusOutHandler); }); } createInputObjects() { return [ ...super.createInputObjects(), ...this.#inputContentElements.map(elem => new MouseIgnore(elem, this.element.blueprint)), ] } getInput() { return this.getInputs().reduce((acc, cur) => acc + cur, "") } getInputs() { return this.#inputContentElements.map(element => // Faster than innerText which causes reflow Utility.clearHTMLWhitespace(element.innerHTML) ) } /** @param {String[]?} values */ setInputs(values = [], updateDefaultValue = true) { // @ts-expect-error this.#inputContentElements.forEach(this.constructor.singleLineInput ? (elem, i) => elem.innerText = values[i] : (elem, i) => elem.innerText = values[i].replaceAll("\n", "") ); if (updateDefaultValue) { this.setDefaultValue(values.map(v => IInputPinTemplate.stringFromInputToUE(v)), values); } this.element.addNextUpdatedCallbacks(() => this.element.nodeElement.dispatchReflowEvent()); } setDefaultValue(values = [], rawValues = values) { this.element.setDefaultValue( // @ts-expect-error values.join("") ); } renderInput() { // @ts-expect-error const singleLine = this.constructor.singleLineInput; // @ts-expect-error const selectOnFocus = this.constructor.selectOnFocus; return y` ` } } /** * @template T * @extends IInputPinTemplate */ class INumericPinTemplate extends IInputPinTemplate { static singleLineInput = true /** @param {String[]} values */ setInputs(values = [], updateDefaultValue = false) { if (!values || values.length == 0) { values = [this.getInput()]; } let parsedValues = []; for (const value of values) { let num = parseFloat(value); if (isNaN(num)) { num = 0; updateDefaultValue = false; } parsedValues.push(num); } super.setInputs(values, false); this.setDefaultValue(parsedValues, values); } /** * @param {Number[]} values * @param {String[]} rawValues */ setDefaultValue(values = [], rawValues) { this.element.setDefaultValue(/** @type {T} */(values[0])); } } /** @typedef {import("../entity/IntegerEntity").default} IntEntity */ /** @extends INumericInputPinTemplate */ class IntInputPinTemplate extends INumericPinTemplate { setDefaultValue(values = [], rawValues = values) { this.element.setDefaultValue(new IntegerEntity(values[0])); } renderInput() { return y` ` } } /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ const t={ATTRIBUTE:1,CHILD:2,PROPERTY:3,BOOLEAN_ATTRIBUTE:4,EVENT:5,ELEMENT:6},e=t=>(...e)=>({_$litDirective$:t,values:e});class i$1{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i;}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}} /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: BSD-3-Clause */const i=e(class extends i$1{constructor(t$1){var e;if(super(t$1),t$1.type!==t.ATTRIBUTE||"style"!==t$1.name||(null===(e=t$1.strings)||void 0===e?void 0:e.length)>2)throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.")}render(t){return Object.keys(t).reduce(((e,r)=>{const s=t[r];return null==s?e:e+`${r=r.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g,"-$&").toLowerCase()}:${s};`}),"")}update(e,[r]){const{style:s}=e.element;if(void 0===this.vt){this.vt=new Set;for(const t in r)this.vt.add(t);return this.render(r)}this.vt.forEach((t=>{null==r[t]&&(this.vt.delete(t),t.includes("-")?s.removeProperty(t):s[t]="");}));for(const t in r){const e=r[t];null!=e&&(this.vt.add(t),t.includes("-")?s.setProperty(t,e):s[t]=e);}return x}}); /** @typedef {import("../element/WindowElement").default} WindowElement */ /** @extends {IDraggablePositionedTemplate} */ class WindowTemplate extends IDraggablePositionedTemplate { toggleAdvancedDisplayHandler getDraggableElement() { return /** @type {WindowElement} */(this.element.querySelector(".ueb-window-top")) } createDraggableObject() { return new MouseMoveDraggable(this.element, this.element.blueprint, { draggableElement: this.getDraggableElement(), ignoreTranslateCompensate: true, movementSpace: this.element.blueprint, stepSize: 1, }) } render() { return y` ${this.renderWindowName()} this.element.remove()}"> ${SVGIcon.close} ${this.renderContent()} ` } renderWindowName() { return y`Window` } renderContent() { return y`` } apply() { this.element.dispatchEvent(new CustomEvent(Configuration.windowApplyEventName)); this.element.remove(); } cancel() { this.element.dispatchEvent(new CustomEvent(Configuration.windowCancelEventName)); this.element.remove(); } } /** @typedef {import("../element/WindowElement").default} WindowElement */ class ColorPickerWindowTemplate extends WindowTemplate { /** @type {ColorHandlerElement} */ #wheelHandler /** @type {ColorSliderElement} */ #saturationSlider /** @type {ColorSliderElement} */ #valueSlider /** @type {ColorSliderElement} */ #rSlider /** @type {ColorSliderElement} */ #gSlider /** @type {ColorSliderElement} */ #bSlider /** @type {ColorSliderElement} */ #aSlider /** @type {ColorSliderElement} */ #hSlider /** @type {ColorSliderElement} */ #sSlider /** @type {ColorSliderElement} */ #vSlider #hexRGBHandler = /** @param {UIEvent} v */ v => { // Faster than innerText which causes reflow const input = Utility.clearHTMLWhitespace(/** @type {HTMLElement} */(v.target).innerHTML); const RGBAValue = parseInt(input, 16); if (isNaN(RGBAValue)) { return } this.color.setFromRGBANumber(RGBAValue); this.element.requestUpdate(); } #hexSRGBHandler = /** @param {UIEvent} v */ v => { // Faster than innerText which causes reflow const input = Utility.clearHTMLWhitespace(/** @type {HTMLElement} */(v.target).innerHTML); const sRGBAValue = parseInt(input, 16); if (isNaN(sRGBAValue)) { return } this.color.setFromSRGBANumber(sRGBAValue); this.element.requestUpdate(); } #doOnEnter = /** @param {(e: UIEvent) => void} action */ action => /** @param {KeyboardEvent} e */ e => { if (e.code == "Enter") { e.preventDefault(); action(e); } } #color = new LinearColorEntity() get color() { return this.#color } /** @param {LinearColorEntity} value */ set color(value) { if (value.toNumber() == this.color?.toNumber()) { return } this.element.requestUpdate("color", this.#color); this.#color = value; } #fullColor = new LinearColorEntity() get fullColor() { return this.#fullColor } /** @type {LinearColorEntity} */ #initialColor get initialColor() { return this.#initialColor } #tempColor = new LinearColorEntity() #colorHexReplace(channel, value, opaque = false) { const colorHex = this.color.toRGBAString(); const result = `${colorHex.substring(0, 2 * channel)}${value}${colorHex.substring(2 + 2 * channel)}`; return opaque ? `${result.substring(0, 6)}FF` : result } connectedCallback() { super.connectedCallback(); this.#initialColor = this.element.windowOptions.getPinColor(); this.color.setFromHSVA( this.initialColor.H.value, this.initialColor.S.value, this.initialColor.V.value, this.initialColor.A.value, ); this.fullColor.setFromHSVA(this.color.H.value, 1, 1, 1); } /** @param {Map} changedProperties */ firstUpdated(changedProperties) { this.#wheelHandler = this.element.querySelector(".ueb-color-picker-wheel ueb-color-handler"); this.#saturationSlider = this.element.querySelector(".ueb-color-picker-saturation ueb-ui-slider"); this.#valueSlider = this.element.querySelector(".ueb-color-picker-value ueb-ui-slider"); this.#rSlider = this.element.querySelector(".ueb-color-picker-r ueb-ui-slider"); this.#gSlider = this.element.querySelector(".ueb-color-picker-g ueb-ui-slider"); this.#bSlider = this.element.querySelector(".ueb-color-picker-b ueb-ui-slider"); this.#aSlider = this.element.querySelector(".ueb-color-picker-a ueb-ui-slider"); this.#hSlider = this.element.querySelector(".ueb-color-picker-h ueb-ui-slider"); this.#sSlider = this.element.querySelector(".ueb-color-picker-s ueb-ui-slider"); this.#vSlider = this.element.querySelector(".ueb-color-picker-v ueb-ui-slider"); this.#wheelHandler.template.locationChangeCallback = /** * @param {Number} x in the range [0, 1] * @param {Number} y in the range [0, 1] */ (x, y) => { this.color.setFromWheelLocation([x, y], this.color.V.value, this.color.A.value); this.fullColor.setFromHSVA(this.color.H.value, 1, 1, 1); this.element.requestUpdate(); }; this.#saturationSlider.template.locationChangeCallback = /** @param {Number} x in the range [0, 1] */ (x, y) => { this.color.setFromHSVA(this.color.H.value, y, this.color.V.value, this.color.A.value); this.element.requestUpdate(); }; this.#valueSlider.template.locationChangeCallback = /** @param {Number} x in the range [0, 1] */ (x, y) => { this.color.setFromHSVA(this.color.H.value, this.color.S.value, y, this.color.A.value); this.element.requestUpdate(); }; this.#rSlider.template.locationChangeCallback = /** @param {Number} x in the range [0, 1] */ (x, y) => { this.color.setFromRGBA(x, this.color.G.value, this.color.B.value, this.color.A.value); this.element.requestUpdate(); }; this.#gSlider.template.locationChangeCallback = /** @param {Number} x in the range [0, 1] */ (x, y) => { this.color.setFromRGBA(this.color.R.value, x, this.color.B.value, this.color.A.value); this.element.requestUpdate(); }; this.#bSlider.template.locationChangeCallback = /** @param {Number} x in the range [0, 1] */ (x, y) => { this.color.setFromRGBA(this.color.R.value, this.color.G.value, x, this.color.A.value); this.element.requestUpdate(); }; this.#aSlider.template.locationChangeCallback = /** @param {Number} x in the range [0, 1] */ (x, y) => { this.color.setFromRGBA(this.color.R.value, this.color.G.value, this.color.B.value, x); this.element.requestUpdate(); }; this.#hSlider.template.locationChangeCallback = /** @param {Number} x in the range [0, 1] */ (x, y) => { this.color.setFromHSVA(x, this.color.S.value, this.color.V.value, this.color.A.value); this.element.requestUpdate(); }; this.#sSlider.template.locationChangeCallback = /** @param {Number} x in the range [0, 1] */ (x, y) => { this.color.setFromHSVA(this.color.H.value, x, this.color.V.value, this.color.A.value); this.element.requestUpdate(); }; this.#vSlider.template.locationChangeCallback = /** @param {Number} x in the range [0, 1] */ (x, y) => { this.color.setFromHSVA(this.color.H.value, this.color.S.value, x, this.color.A.value); this.element.requestUpdate(); }; } /** @param {Number} channel */ renderSlider(channel) { let channelLetter = ""; let channelValue = 0; let background = ""; const getCommonBackground = channel => `linear-gradient(to right, #${this.#colorHexReplace(channel, '00', true)}, #${this.#colorHexReplace(channel, 'ff', true)})`; switch (channel) { case 0: channelLetter = "r"; channelValue = this.color.R.value; background = getCommonBackground(channel); break case 1: channelLetter = "g"; channelValue = this.color.G.value; background = getCommonBackground(channel); break case 2: channelLetter = "b"; channelValue = this.color.B.value; background = getCommonBackground(channel); break case 3: channelLetter = "a"; channelValue = this.color.A.value; background = `${Configuration.alphaPattern}, ${getCommonBackground(channel)}`; break case 4: channelLetter = "h"; channelValue = this.color.H.value * 360; background = "linear-gradient(to right, #f00 0%, #ff0 16.666%, #0f0 33.333%, #0ff 50%, #00f 66.666%, #f0f 83.333%, #f00 100%)"; break case 5: channelLetter = "s"; channelValue = this.color.S.value; background = "linear-gradient(" + "to right," + `#${this.#tempColor.setFromHSVA( this.color.H.value, 0, this.color.V.value, 1 ), this.#tempColor.toRGBAString()},` + `#${this.#tempColor.setFromHSVA( this.color.H.value, 1, this.color.V.value, 1, ), this.#tempColor.toRGBAString()})`; break case 6: channelLetter = "v"; channelValue = this.color.V.value; background = `linear-gradient(to right, #000, #${this.fullColor.toRGBAString()})`; break } background = `background: ${background};`; return y` ${channelLetter.toUpperCase()} ` } renderContent() { const theta = this.color.H.value * 2 * Math.PI; const style = { "--ueb-color-r": this.color.R.toString(), "--ueb-color-g": this.color.G.toString(), "--ueb-color-b": this.color.B.toString(), "--ueb-color-a": this.color.A.toString(), "--ueb-color-h": this.color.H.toString(), "--ueb-color-s": this.color.S.toString(), "--ueb-color-v": this.color.V.toString(), "--ueb-color-wheel-x": `${(this.color.S.value * Math.cos(theta) * 0.5 + 0.5) * 100}%`, "--ueb-color-wheel-y": `${(this.color.S.value * Math.sin(theta) * 0.5 + 0.5) * 100}%`, }; const colorRGB = this.color.toRGBAString(); const colorSRGB = this.color.toSRGBAString(); const fullColorHex = this.fullColor.toRGBAString(); return y` Old New Advanced ${this.renderSlider(0)} ${this.renderSlider(1)} ${this.renderSlider(2)} ${this.renderSlider(3)} ${this.renderSlider(4)} ${this.renderSlider(5)} ${this.renderSlider(6)} Hex Linear Hex sRGB this.apply()}">OK this.cancel()}">Cancel ` } renderWindowName() { return y`Color Picker` } } /** * @typedef {import("../element/WindowElement").default} WindowElement * @typedef {import("../entity/LinearColorEntity").default} LinearColorEntity */ /** @extends PinTemplate */ class LinearColorInputPinTemplate extends PinTemplate { /** @type {WindowElement} */ #window /** @param {MouseEvent} e */ #launchColorPickerWindow = e => { e.preventDefault(); this.element.blueprint.setFocused(true); this.#window = /** @type {WindowElement} */ ( new (ElementFactory.getConstructor("ueb-window"))({ type: ColorPickerWindowTemplate, windowOptions: { // The created window will use the following functions to get and set the color getPinColor: () => this.element.defaultValue, /** @param {LinearColorEntity} color */ setPinColor: color => this.element.setDefaultValue(color), }, }) ); this.element.blueprint.append(this.#window); const windowApplyHandler = () => { this.element.setDefaultValue( /** @type {ColorPickerWindowTemplate} */(this.#window.template).color ); }; const windowCloseHandler = () => { this.#window.removeEventListener(Configuration.windowApplyEventName, windowApplyHandler); this.#window.removeEventListener(Configuration.windowCloseEventName, windowCloseHandler); this.#window = null; }; this.#window.addEventListener(Configuration.windowApplyEventName, windowApplyHandler); this.#window.addEventListener(Configuration.windowCloseEventName, windowCloseHandler); } renderInput() { return y` ` } } /** @typedef {import("../element/PinElement").default} PinElement */ class NameInputPinTemplate extends IInputPinTemplate { static singleLineInput = true } /** * @template {Number} T * @extends INumericPinTemplate */ class RealInputPinTemplate extends INumericPinTemplate { setDefaultValue(values = [], rawValues = values) { this.element.setDefaultValue(values[0]); } renderInput() { return y` ` } } class ReferencePinTemplate extends PinTemplate { renderIcon() { return SVGIcon.referencePin } } /** @typedef {import("../entity/RotatorEntity").default} Rotator */ /** @extends INumericPinTemplate */ class RotatorInputPinTemplate extends INumericPinTemplate { setDefaultValue(values = [], rawValues = values) { if (!(this.element.entity.DefaultValue instanceof RotatorEntity)) { throw new TypeError("Expected DefaultValue to be a VectorEntity") } let rotator = this.element.entity.DefaultValue; rotator.R = values[0]; // Roll rotator.P = values[1]; // Pitch rotator.Y = values[2]; // Yaw } renderInput() { return y` X Y Z ` } } /** @extends IInputPinTemplate */ class StringInputPinTemplate extends IInputPinTemplate { } /** @typedef {import("../entity/LinearColorEntity").default} LinearColorEntity */ /** * @template {VectorEntity} T * @extends INumericPinTemplate */ class VectorInputPinTemplate extends INumericPinTemplate { /** * @param {Number[]} values * @param {String[]} rawValues */ setDefaultValue(values, rawValues) { if (!(this.element.entity.DefaultValue instanceof VectorEntity)) { throw new TypeError("Expected DefaultValue to be a VectorEntity") } let vector = this.element.entity.DefaultValue; vector.X = values[0]; vector.Y = values[1]; vector.Z = values[2]; } renderInput() { return y` X Y Z ` } } /** * @typedef {import("../entity/PinReferenceEntity").default} PinReferenceEntity * @typedef {import("./NodeElement").default} NodeElement * @typedef {import("lit").CSSResult} CSSResult */ /** * @template T * @typedef {import("../entity/PinEntity").default} PinEntity */ /** * @template T * @extends {IElement, PinTemplate>} */ class PinElement extends IElement { static #inputPinTemplates = { "/Script/CoreUObject.LinearColor": LinearColorInputPinTemplate, "/Script/CoreUObject.Rotator": RotatorInputPinTemplate, "/Script/CoreUObject.Vector": VectorInputPinTemplate, "bool": BoolInputPinTemplate, "int": IntInputPinTemplate, "MUTABLE_REFERENCE": ReferencePinTemplate, "name": NameInputPinTemplate, "real": RealInputPinTemplate, "string": StringInputPinTemplate, } static properties = { pinId: { type: GuidEntity, converter: { fromAttribute: (value, type) => value // @ts-expect-error ? ISerializer.grammar.Guid.parse(value).value : null, toAttribute: (value, type) => value?.toString(), }, attribute: "data-id", reflect: true, }, pinType: { type: String, attribute: "data-type", reflect: true, }, advancedView: { type: String, attribute: "data-advanced-view", reflect: true, }, color: { type: LinearColorEntity, converter: { fromAttribute: (value, type) => value // @ts-expect-error ? ISerializer.grammar.LinearColorFromAnyColor.parse(value).value : null, toAttribute: (value, type) => value ? Utility.printLinearColor(value) : null, }, attribute: "data-color", reflect: true, }, defaultValue: { type: String, attribute: false, }, isLinked: { type: Boolean, converter: Utility.booleanConverter, attribute: "data-linked", reflect: true, }, pinDirection: { type: String, attribute: "data-direction", reflect: true, }, } /** * @param {PinEntity} pinEntity * @return {new () => PinTemplate} */ static getTypeTemplate(pinEntity) { if (pinEntity.PinType.bIsReference && !pinEntity.PinType.bIsConst) { return PinElement.#inputPinTemplates["MUTABLE_REFERENCE"] } if (pinEntity.getType() === "exec") { return ExecPinTemplate } let result; if (pinEntity.isInput()) { result = PinElement.#inputPinTemplates[pinEntity.getType()]; } return result ?? PinTemplate } /** @type {NodeElement} */ nodeElement connections = 0 /** * @param {PinEntity} entity * @param {PinTemplate} template * @param {NodeElement} nodeElement */ constructor(entity, template = undefined, nodeElement = undefined) { super(entity, template ?? new (PinElement.getTypeTemplate(entity))()); this.pinId = this.entity.PinId; this.pinType = this.entity.getType(); this.advancedView = this.entity.bAdvancedView; this.defaultValue = this.entity.getDefaultValue(); this.color = PinElement.properties.color.converter.fromAttribute(this.getColor().toString()); this.isLinked = false; this.pinDirection = entity.isInput() ? "input" : entity.isOutput() ? "output" : "hidden"; this.nodeElement = nodeElement; // this.entity.subscribe("DefaultValue", value => this.defaultValue = value.toString()) this.entity.subscribe("PinToolTip", value => { let matchResult = value.match(/\s*(.+?(?=\n)|.+\S)\s*/); if (matchResult) { return Utility.formatStringName(matchResult[1]) } return Utility.formatStringName(this.entity.PinName) }); } /** @return {GuidEntity} */ getPinId() { return this.entity.PinId } /** @returns {String} */ getPinName() { return this.entity.PinName } getPinDisplayName() { return this.entity.getDisplayName() } /** @return {CSSResult} */ getColor() { return Configuration.getPinColor(this) } isInput() { return this.entity.isInput() } isOutput() { return this.entity.isOutput() } getLinkLocation() { return this.template.getLinkLocation() } /** @returns {NodeElement} */ getNodeElement() { return this.nodeElement } getLinks() { return this.entity.LinkedTo ?? [] } /** @param {T} value */ setDefaultValue(value) { this.entity.DefaultValue = value; this.defaultValue = value; } /** @param {IElement[]} nodesWhitelist */ sanitizeLinks(nodesWhitelist = []) { this.entity.LinkedTo = this.getLinks().filter(pinReference => { let pin = this.blueprint.getPin(pinReference); if (pin) { if (nodesWhitelist.length && !nodesWhitelist.includes(pin.nodeElement)) { return false } let link = this.blueprint.getLink(this, pin, true); if (!link) { this.blueprint.addGraphElement(new (ElementFactory.getConstructor("ueb-link"))(this, pin)); } } return pin }); } /** @param {PinElement} targetPinElement */ linkTo(targetPinElement) { if (this.entity.linkTo(targetPinElement.getNodeElement().getNodeName(), targetPinElement.entity)) { this.isLinked = this.entity.isLinked(); this.nodeElement?.template.linksChanged(); } } /** @param {PinElement} targetPinElement */ unlinkFrom(targetPinElement) { if (this.entity.unlinkFrom(targetPinElement.getNodeElement().getNodeName(), targetPinElement.entity)) { this.isLinked = this.entity.isLinked(); this.nodeElement?.template.linksChanged(); } } /** * @param {PinElement} originalPinElement * @param {PinReferenceEntity} newReference */ redirectLink(originalPinElement, newReference) { const index = this.entity.LinkedTo.findIndex(pinReference => pinReference.objectName.toString() == originalPinElement.getNodeElement().getNodeName() && pinReference.pinGuid.valueOf() == originalPinElement.entity.PinId.valueOf() ); if (index >= 0) { this.entity.LinkedTo[index] = newReference; return true } return false } } /** * @template {WindowTemplate} T * @extends {IDraggableElement} */ class WindowElement extends IDraggableElement { static #typeTemplateMap = { "window": WindowTemplate, "color-picker": ColorPickerWindowTemplate, } static properties = { ...IDraggableElement.properties, type: { type: WindowTemplate, attribute: "data-type", reflect: true, converter: { fromAttribute: (value, type) => WindowElement.#typeTemplateMap[value], toAttribute: (value, type) => Object.entries(WindowElement.#typeTemplateMap).find(([k, v]) => value == v)[0] }, }, } constructor(options = {}) { if (options.type.constructor == String) { options.type = WindowElement.#typeTemplateMap[options.type]; } options.type ??= WindowTemplate; options.windowOptions ??= {}; super({}, new options.type()); this.type = options.type; this.windowOptions = options.windowOptions; } disconnectedCallback() { super.disconnectedCallback(); this.dispatchCloseEvent(); } dispatchCloseEvent() { let deleteEvent = new CustomEvent(Configuration.windowCloseEventName); this.dispatchEvent(deleteEvent); } } function defineElements() { customElements.define("ueb-color-handler", ColorHandlerElement); ElementFactory.registerElement("ueb-color-handler", ColorHandlerElement); customElements.define("ueb-input", InputElement); ElementFactory.registerElement("ueb-input", InputElement); customElements.define("ueb-link", LinkElement); ElementFactory.registerElement("ueb-link", LinkElement); customElements.define("ueb-node", NodeElement); ElementFactory.registerElement("ueb-node", NodeElement); customElements.define("ueb-pin", PinElement); ElementFactory.registerElement("ueb-pin", PinElement); customElements.define("ueb-selector", SelectorElement); ElementFactory.registerElement("ueb-selector", SelectorElement); customElements.define("ueb-ui-slider", ColorSliderElement); ElementFactory.registerElement("ueb-ui-slider", ColorSliderElement); customElements.define("ueb-window", WindowElement); ElementFactory.registerElement("ueb-window", WindowElement); } /** * @typedef {import("../entity/IEntity").default} IEntity * @typedef {import("../entity/TypeInitialization").AnyValue} AnyValue */ /** * @template {AnyValue} T * @typedef {import("../entity/TypeInitialization").AnyValueConstructor} AnyValueConstructor */ /** * @template {AnyValue} T * @extends ISerializer */ class GeneralSerializer extends ISerializer { /** * @param {(value: String, entity: T) => String} wrap * @param {AnyValueConstructor} entityType */ constructor(wrap, entityType, attributePrefix, attributeSeparator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter) { wrap = wrap ?? (v => `(${v})`); super(entityType, attributePrefix, attributeSeparator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter); this.wrap = wrap; } /** * @param {String} value * @returns {T} */ read(value) { // @ts-expect-error let grammar = Grammar.getGrammarForType(ISerializer.grammar, this.entityType); const parseResult = grammar.parse(value); if (!parseResult.status) { // @ts-expect-error throw new Error(`Error when trying to parse the entity ${this.entityType.prototype.constructor.name}.`) } return parseResult.value } /** * @param {T} object * @param {Boolean} insideString * @returns {String} */ write(entity, object, insideString = false) { let result = this.wrap(this.subWrite(entity, [], object, insideString), object); return result } } /** * @typedef {import("../entity/IEntity").default} IEntity * @typedef {import("../entity/TypeInitialization").AnyValue} AnyValue */ /** * @template {AnyValue} T * @typedef {import("../entity/TypeInitialization").AnyValueConstructor} AnyValueConstructor */ /** * @template {AnyValue} T * @extends {GeneralSerializer} */ class CustomSerializer extends GeneralSerializer { #objectWriter /** * @param {(v: T, insideString: Boolean) => String} objectWriter * @param {AnyValueConstructor} entityType */ constructor(objectWriter, entityType) { super(undefined, entityType); this.#objectWriter = objectWriter; } /** * @param {T} object * @param {Boolean} insideString * @returns {String} */ write(entity, object, insideString = false) { let result = this.#objectWriter(object, insideString); return result } } /** @typedef {import("../entity/TypeInitialization").AnyValue} AnyValue */ /** * @template {AnyValue} T * @typedef {import("../entity/TypeInitialization").AnyValueConstructor} AnyValueConstructor */ /** * @template {AnyValue} T * @extends {GeneralSerializer} */ class ToStringSerializer extends GeneralSerializer { /** @param {AnyValueConstructor} entityType */ constructor(entityType) { super(undefined, entityType); } /** * @param {T} object * @param {Boolean} insideString */ write(entity, object, insideString) { return !insideString && object.constructor === String ? `"${Utility.escapeString(object.toString())}"` // String will have quotes if not inside a string already : Utility.escapeString(object.toString()) } } function initializeSerializerFactory() { const bracketsWrapped = v => `(${v})`; SerializerFactory.registerSerializer( null, new CustomSerializer( (nullValue, insideString) => "()", null ) ); SerializerFactory.registerSerializer( Array, new CustomSerializer( /** @param {Array} array */ (array, insideString) => `(${array .map(v => // @ts-expect-error SerializerFactory.getSerializer(Utility.getType(v)).serialize(v, insideString) + "," ) .join("") })`, Array ) ); SerializerFactory.registerSerializer( Boolean, new CustomSerializer( /** @param {Boolean} boolean */ (boolean, insideString) => boolean ? insideString ? "true" : "True" : insideString ? "false" : "False", Boolean ) ); SerializerFactory.registerSerializer( FunctionReferenceEntity, new GeneralSerializer(bracketsWrapped, FunctionReferenceEntity) ); SerializerFactory.registerSerializer( GuidEntity, new ToStringSerializer(GuidEntity) ); SerializerFactory.registerSerializer( IdentifierEntity, new ToStringSerializer(IdentifierEntity) ); SerializerFactory.registerSerializer( IntegerEntity, new ToStringSerializer(IntegerEntity) ); SerializerFactory.registerSerializer( InvariantTextEntity, new GeneralSerializer(v => `${InvariantTextEntity.lookbehind}(${v})`, InvariantTextEntity, "", ", ", false, "", _ => "") ); SerializerFactory.registerSerializer( KeyBindingEntity, new GeneralSerializer(bracketsWrapped, KeyBindingEntity) ); SerializerFactory.registerSerializer( LinearColorEntity, new GeneralSerializer(bracketsWrapped, LinearColorEntity) ); SerializerFactory.registerSerializer( LocalizedTextEntity, new GeneralSerializer(v => `${LocalizedTextEntity.lookbehind}(${v})`, LocalizedTextEntity, "", ", ", false, "", _ => "") ); SerializerFactory.registerSerializer( MacroGraphReferenceEntity, new GeneralSerializer(bracketsWrapped, MacroGraphReferenceEntity) ); SerializerFactory.registerSerializer( Number, new CustomSerializer( /** @param {Number} value */ value => value.toString(), Number ) ); SerializerFactory.registerSerializer( ObjectEntity, new ObjectSerializer() ); SerializerFactory.registerSerializer( ObjectReferenceEntity, new CustomSerializer( objectReference => (objectReference.type ?? "") + ( objectReference.path ? objectReference.type ? `'"${objectReference.path}"'` : `"${objectReference.path}"` : "" ), ObjectReferenceEntity ) ); SerializerFactory.registerSerializer( PathSymbolEntity, new ToStringSerializer(PathSymbolEntity) ); SerializerFactory.registerSerializer( PinEntity, new GeneralSerializer(v => `${PinEntity.lookbehind} (${v})`, PinEntity, "", ",", true) ); SerializerFactory.registerSerializer( PinReferenceEntity, new GeneralSerializer(v => v, PinReferenceEntity, "", " ", false, "", _ => "") ); SerializerFactory.registerSerializer( RealUnitEntity, new ToStringSerializer(RealUnitEntity) ); SerializerFactory.registerSerializer( RotatorEntity, new GeneralSerializer(bracketsWrapped, RotatorEntity) ); SerializerFactory.registerSerializer( String, new CustomSerializer( (value, insideString) => insideString // @ts-expect-error ? Utility.escapeString(value) // @ts-expect-error : `"${Utility.escapeString(value)}"`, String ) ); SerializerFactory.registerSerializer( SimpleSerializationRotatorEntity, new CustomSerializer( (value, insideString) => `${value.P}, ${value.Y}, ${value.R}`, SimpleSerializationRotatorEntity ) ); SerializerFactory.registerSerializer( SimpleSerializationVectorEntity, new CustomSerializer( (value, insideString) => `${value.X}, ${value.Y}, ${value.Z}`, SimpleSerializationVectorEntity ) ); SerializerFactory.registerSerializer( SymbolEntity, new ToStringSerializer(SymbolEntity) ); SerializerFactory.registerSerializer( UnknownKeysEntity, new GeneralSerializer((string, entity) => `${entity.lookbehind ?? ""}(${string})`, UnknownKeysEntity) ); SerializerFactory.registerSerializer( VariableReferenceEntity, new GeneralSerializer(bracketsWrapped, VariableReferenceEntity) ); SerializerFactory.registerSerializer( VectorEntity, new GeneralSerializer(bracketsWrapped, VectorEntity) ); } initializeSerializerFactory(); defineElements(); export { Blueprint, Configuration, LinkElement, NodeElement };