/** * @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$4=new WeakMap;class o$4{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$4.get(s)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&n$4.set(s,t));}return t}toString(){return this.cssText}}const r$2=t=>new o$4("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$4(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$3=e$2.reactiveElementPolyfillSupport,n$3={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$3,reflect:!1,hasChanged:a$1},d$1="finalized";class u$1 extends HTMLElement{constructor(){super(),this._$Ei=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$El=null,this._$Eu();}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(d$1))return !1;this[d$1]=!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}_$Eu(){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$3).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$3;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){}}u$1[d$1]=!0,u$1.elementProperties=new Map,u$1.elementStyles=[],u$1.shadowRootOptions={mode:"open"},null==o$3||o$3({ReactiveElement:u$1}),(null!==(s$2=e$2.reactiveElementVersions)&&void 0!==s$2?s$2:e$2.reactiveElementVersions=[]).push("1.6.3"); /** * @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$2="$lit$",n$2=`lit$${(Math.random()+"").slice(9)}$`,l$1="?"+n$2,h=`<${l$1}>`,r=document,u=()=>r.createComment(""),d=t=>null===t||"object"!=typeof t&&"function"!=typeof t,c=Array.isArray,v=t=>c(t)||"function"==typeof(null==t?void 0:t[Symbol.iterator]),a="[ \t\n\f\r]",f=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,_=/-->/g,m=/>/g,p=RegExp(`>|${a}(?:([^\\s"'>=/]+)(${a}*=${a}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),g=/'/g,$=/"/g,y=/^(?:script|style|textarea|title)$/i,w=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),x=w(1),T=Symbol.for("lit-noChange"),A=Symbol.for("lit-nothing"),E=new WeakMap,C=r.createTreeWalker(r,129,null,!1);function P(t,i){if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==e$1?e$1.createHTML(i):i}const V=(t,i)=>{const s=t.length-1,e=[];let l,r=2===i?"":"",u=f;for(let i=0;i"===c[0]?(u=null!=l?l:f,v=-1):void 0===c[1]?v=-2:(v=u.lastIndex-c[2].length,d=c[1],u=void 0===c[3]?p:'"'===c[3]?$:g):u===$||u===g?u=p:u===_||u===m?u=f:(u=p,l=void 0);const w=u===p&&t[i+1].startsWith("/>")?" ":"";r+=u===f?s+h:v>=0?(e.push(d),s.slice(0,v)+o$2+s.slice(v)+n$2+w):s+n$2+(-2===v?(e.push(void 0),i):w);}return [P(t,r+(t[s]||"")+(2===i?"":"")),e]};class N{constructor({strings:t,_$litType$:i},e){let h;this.parts=[];let r=0,d=0;const c=t.length-1,v=this.parts,[a,f]=V(t,i);if(this.el=N.createElement(a,e),C.currentNode=this.el.content,2===i){const t=this.el.content,i=t.firstChild;i.remove(),t.append(...i.childNodes);}for(;null!==(h=C.nextNode())&&v.length0){h.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=A;}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=S(this,t,i,0),n=!d(t)||t!==this._$AH&&t!==T,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 R(i.insertBefore(u(),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$1;class s extends u$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=D(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 T}}s.finalized=!0,s._$litElement$=!0,null===(l=globalThis.litElementHydrateSupport)||void 0===l||l.call(globalThis,{LitElement:s});const n$1=globalThis.litElementPolyfillSupport;null==n$1||n$1({LitElement:s});(null!==(o$1=globalThis.litElementVersions)&&void 0!==o$1?o$1:globalThis.litElementVersions=[]).push("3.3.3"); class Configuration { static nodeColors = { black: i$3`20, 20, 20`, blue: i$3`84, 122, 156`, darkBlue: i$3`32, 80, 128`, darkerBlue: i$3`18, 18, 130`, darkTurquoise: i$3`19, 100, 137`, gray: i$3`150,150,150`, green: i$3`95, 129, 90`, intenseGreen: i$3`42, 140, 42`, lime: i$3`150, 160, 30`, red: i$3`151, 33, 32`, turquoise: i$3`46, 104, 106`, violet: i$3`126, 28, 150`, yellow: i$3`148, 116, 24`, } 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 colorWindowName = "Color Picker" static defaultCommentHeight = 96 static defaultCommentWidth = 400 static distanceThreshold = 5 // px static dragEventName = "ueb-drag" static dragGeneralEventName = "ueb-drag-general" static edgeScrollThreshold = 50 static editTextEventName = { begin: "ueb-edit-text-begin", end: "ueb-edit-text-end", } static expandGridSize = 400 static focusEventName = { begin: "blueprint-focus", end: "blueprint-unfocus", } static fontSize = i$3`13px` 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 // px static gridSet = 8 static gridSetLineColor = i$3`#161616` static gridShrinkThreshold = 4 // exceding size factor threshold to cause a shrink event static gridSize = 16 // px 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 indentation = " " static keysSeparator = /[\.\(\)]/ static knotOffset = [-Configuration.gridSize, -0.5 * Configuration.gridSize] static lineTracePattern = /LineTrace(Single|Multi)(\w*)/ static linkCurveHeight = 15 // px static linkCurveWidth = 80 // px static linkMinWidth = 100 // px static nameRegexSpaceReplacement = new RegExp( // Leading K2_ or K2Node_ is removed "^K2(?:[Nn]ode)?_" // End of a word (lower case followed by either upper case or number): "AlphaBravo" => "Alpha Bravo" + "|(?<=[a-z])(?=[A-Z0-9])" // End of upper case word (upper case followed by either word or number) + "|(?<=[A-Z])" + /* Except "UVs" */ "(? "Alpha Bravo" + "|\\s*_+\\s*" + "|\\s{2,}", "g" ) /** * @param {Number} start * @param {Number} c1 * @param {Number} c2 */ static linkRightSVGPath = (start, c1, c2) => { let end = 100 - start; return `M ${start} 0 C ${c1.toFixed(3)} 0, ${c2.toFixed(3)} 0, 50 50 S ${(end - c1 + start).toFixed(3)} 100, ` + `${end.toFixed(3)} 100` } static maxZoom = 7 static minZoom = -12 static mouseClickButton = 0 static mouseRightClickButton = 2 static mouseWheelZoomThreshold = 80 static nodeDragEventName = "ueb-node-drag" static nodeDragGeneralEventName = "ueb-node-drag-general" static nodeTitle = (name, counter) => `${name}_${counter}` static nodeRadius = 8 // px static nodeReflowEventName = "ueb-node-reflow" static paths = { actorBoundEvent: "/Script/BlueprintGraph.K2Node_ActorBoundEvent", addDelegate: "/Script/BlueprintGraph.K2Node_AddDelegate", ambientSound: "/Script/Engine.AmbientSound", asyncAction: "/Script/BlueprintGraph.K2Node_AsyncAction", blueprint: "/Script/Engine.Blueprint", blueprintGameplayTagLibrary: "/Script/GameplayTags.BlueprintGameplayTagLibrary", blueprintMapLibrary: "/Script/Engine.BlueprintMapLibrary", blueprintSetLibrary: "/Script/Engine.BlueprintSetLibrary", callArrayFunction: "/Script/BlueprintGraph.K2Node_CallArrayFunction", callDelegate: "/Script/BlueprintGraph.K2Node_CallDelegate", callFunction: "/Script/BlueprintGraph.K2Node_CallFunction", comment: "/Script/UnrealEd.EdGraphNode_Comment", commutativeAssociativeBinaryOperator: "/Script/BlueprintGraph.K2Node_CommutativeAssociativeBinaryOperator", componentBoundEvent: "/Script/BlueprintGraph.K2Node_ComponentBoundEvent", createDelegate: "/Script/BlueprintGraph.K2Node_CreateDelegate", customEvent: "/Script/BlueprintGraph.K2Node_CustomEvent", doN: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:Do N", doOnce: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:DoOnce", dynamicCast: "/Script/BlueprintGraph.K2Node_DynamicCast", eAttachmentRule: "/Script/Engine.EAttachmentRule", edGraph: "/Script/Engine.EdGraph", eDrawDebugTrace: "/Script/Engine.EDrawDebugTrace", eMaterialSamplerType: "/Script/Engine.EMaterialSamplerType", eNiagara_Float4Channel: "/Niagara/Enums/ENiagara_Float4Channel.ENiagara_Float4Channel", enum: "/Script/CoreUObject.Enum", enumLiteral: "/Script/BlueprintGraph.K2Node_EnumLiteral", eSamplerSourceMode: "/Script/Engine.ESamplerSourceMode", eSearchCase: "/Script/CoreUObject.ESearchCase", eSearchDir: "/Script/CoreUObject.ESearchDir", eSpawnActorCollisionHandlingMethod: "/Script/Engine.ESpawnActorCollisionHandlingMethod", eTextureMipValueMode: "/Script/Engine.ETextureMipValueMode", eTraceTypeQuery: "/Script/Engine.ETraceTypeQuery", event: "/Script/BlueprintGraph.K2Node_Event", eWorldPositionIncludedOffsets: "/Script/Engine.EWorldPositionIncludedOffsets", executionSequence: "/Script/BlueprintGraph.K2Node_ExecutionSequence", flipflop: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:FlipFlop", 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", functionEntry: "/Script/BlueprintGraph.K2Node_FunctionEntry", functionResult: "/Script/BlueprintGraph.K2Node_FunctionResult", gameplayTag: "/Script/GameplayTags.GameplayTag", getInputAxisKeyValue: "/Script/BlueprintGraph.K2Node_GetInputAxisKeyValue", ifThenElse: "/Script/BlueprintGraph.K2Node_IfThenElse", inputAxisKeyEvent: "/Script/BlueprintGraph.K2Node_InputAxisKeyEvent", inputDebugKey: "/Script/InputBlueprintNodes.K2Node_InputDebugKey", inputKey: "/Script/BlueprintGraph.K2Node_InputKey", inputVectorAxisEvent: "/Script/BlueprintGraph.K2Node_InputVectorAxisEvent", isValid: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:IsValid", kismetArrayLibrary: "/Script/Engine.KismetArrayLibrary", kismetMathLibrary: "/Script/Engine.KismetMathLibrary", knot: "/Script/BlueprintGraph.K2Node_Knot", linearColor: "/Script/CoreUObject.LinearColor", literal: "/Script/BlueprintGraph.K2Node_Literal", macro: "/Script/BlueprintGraph.K2Node_MacroInstance", makeArray: "/Script/BlueprintGraph.K2Node_MakeArray", makeMap: "/Script/BlueprintGraph.K2Node_MakeMap", makeSet: "/Script/BlueprintGraph.K2Node_MakeSet", makeStruct: "/Script/BlueprintGraph.K2Node_MakeStruct", materialExpressionComponentMask: "/Script/Engine.MaterialExpressionComponentMask", materialExpressionConstant: "/Script/Engine.MaterialExpressionConstant", materialExpressionConstant2Vector: "/Script/Engine.MaterialExpressionConstant2Vector", materialExpressionConstant3Vector: "/Script/Engine.MaterialExpressionConstant3Vector", materialExpressionConstant4Vector: "/Script/Engine.MaterialExpressionConstant4Vector", materialExpressionFunctionInput: "/Script/Engine.MaterialExpressionFunctionInput", materialExpressionLogarithm: "/Script/InterchangeImport.MaterialExpressionLogarithm", materialExpressionLogarithm10: "/Script/Engine.MaterialExpressionLogarithm10", materialExpressionLogarithm2: "/Script/Engine.MaterialExpressionLogarithm2", materialExpressionMaterialFunctionCall: "/Script/Engine.MaterialExpressionMaterialFunctionCall", materialExpressionSquareRoot: "/Script/Engine.MaterialExpressionSquareRoot", materialExpressionSubtract: "/Script/Engine.MaterialExpressionSubtract", materialExpressionTextureCoordinate: "/Script/Engine.MaterialExpressionTextureCoordinate", materialExpressionTextureSample: "/Script/Engine.MaterialExpressionTextureSample", materialExpressionWorldPosition: "/Script/Engine.MaterialExpressionWorldPosition", materialGraphNode: "/Script/UnrealEd.MaterialGraphNode", materialGraphNodeComment: "/Script/UnrealEd.MaterialGraphNode_Comment", metasoundEditorGraphExternalNode: "/Script/MetasoundEditor.MetasoundEditorGraphExternalNode", multiGate: "/Script/BlueprintGraph.K2Node_MultiGate", niagaraBool: "/Script/Niagara.NiagaraBool", niagaraClipboardContent: "/Script/NiagaraEditor.NiagaraClipboardContent", niagaraDataInterfaceVolumeTexture: "/Script/Niagara.NiagaraDataInterfaceVolumeTexture", niagaraFloat: "/Script/Niagara.NiagaraFloat", niagaraMatrix: "/Script/Niagara.NiagaraMatrix", niagaraNodeFunctionCall: "/Script/NiagaraEditor.NiagaraNodeFunctionCall", niagaraNodeOp: "/Script/NiagaraEditor.NiagaraNodeOp", niagaraNumeric: "/Script/Niagara.NiagaraNumeric", niagaraPosition: "/Script/Niagara.NiagaraPosition", pawn: "/Script/Engine.Pawn", pcgEditorGraphNode: "/Script/PCGEditor.PCGEditorGraphNode", pcgEditorGraphNodeInput: "/Script/PCGEditor.PCGEditorGraphNodeInput", pcgEditorGraphNodeOutput: "/Script/PCGEditor.PCGEditorGraphNodeOutput", pcgHiGenGridSizeSettings: "/Script/PCG.PCGHiGenGridSizeSettings", pcgSubgraphSettings: "/Script/PCG.PCGSubgraphSettings", promotableOperator: "/Script/BlueprintGraph.K2Node_PromotableOperator", quat4f: "/Script/CoreUObject.Quat4f", reverseForEachLoop: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:ReverseForEachLoop", rotator: "/Script/CoreUObject.Rotator", select: "/Script/BlueprintGraph.K2Node_Select", self: "/Script/BlueprintGraph.K2Node_Self", slateBlueprintLibrary: "/Script/UMG.SlateBlueprintLibrary", spawnActorFromClass: "/Script/BlueprintGraph.K2Node_SpawnActorFromClass", switchEnum: "/Script/BlueprintGraph.K2Node_SwitchEnum", switchGameplayTag: "/Script/GameplayTagsEditor.GameplayTagsK2Node_SwitchGameplayTag", switchInteger: "/Script/BlueprintGraph.K2Node_SwitchInteger", switchName: "/Script/BlueprintGraph.K2Node_SwitchName", switchString: "/Script/BlueprintGraph.K2Node_SwitchString", timeline: "/Script/BlueprintGraph.K2Node_Timeline", timeManagementBlueprintLibrary: "/Script/TimeManagement.TimeManagementBlueprintLibrary", transform: "/Script/CoreUObject.Transform", userDefinedEnum: "/Script/Engine.UserDefinedEnum", variableGet: "/Script/BlueprintGraph.K2Node_VariableGet", variableSet: "/Script/BlueprintGraph.K2Node_VariableSet", vector: "/Script/CoreUObject.Vector", vector2D: "/Script/CoreUObject.Vector2D", vector3f: "/Script/CoreUObject.Vector3f", vector4f: "/Script/CoreUObject.Vector4f", whileLoop: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:WhileLoop", } static pinInputWrapWidth = 145 // px static removeEventName = "ueb-element-delete" static scale = { [-12]: 0.133333, [-11]: 0.166666, [-10]: 0.2, [-9]: 0.233333, [-8]: 0.266666, [-7]: 0.3, [-6]: 0.333333, [-5]: 0.375, [-4]: 0.5, [-3]: 0.675, [-2]: 0.75, [-1]: 0.875, 0: 1, 1: 1.25, 2: 1.375, 3: 1.5, 4: 1.675, 5: 1.75, 6: 1.875, 7: 2, } static smoothScrollTime = 1000 // ms static stringEscapedCharacters = /["\\]/g // Try to remove static subObjectAttributeNamePrefix = "#SubObject" /** @param {ObjectEntity} objectEntity */ static subObjectAttributeNameFromEntity = (objectEntity, nameOnly = false) => this.subObjectAttributeNamePrefix + (!nameOnly && objectEntity.Class ? `_${objectEntity.Class.type}` : "") + "_" + objectEntity.Name /** @param {ObjectReferenceEntity} objectReferenceEntity */ static subObjectAttributeNameFromReference = (objectReferenceEntity, nameOnly = false) => this.subObjectAttributeNamePrefix + (!nameOnly ? "_" + objectReferenceEntity.type : "") + "_" + objectReferenceEntity.path static subObjectAttributeNameFromName = name => this.subObjectAttributeNamePrefix + "_" + name static switchTargetPattern = /\/Script\/[\w\.\/\:]+K2Node_Switch([A-Z]\w+)+/ static trackingMouseEventName = { begin: "ueb-tracking-mouse-begin", end: "ueb-tracking-mouse-end", } static unescapedBackslash = /(?<=(?:[^\\]|^)(?:\\\\)*)\\(?!\\)/ // Try to remove static windowApplyEventName = "ueb-window-apply" static windowApplyButtonText = "OK" static windowCancelEventName = "ueb-window-cancel" static windowCancelButtonText = "Cancel" static windowCloseEventName = "ueb-window-close" static CommonEnums = { [this.paths.eAttachmentRule]: [ "KeepRelative", "KeepWorld", "SnapToTarget", ], [this.paths.eDrawDebugTrace]: ["None", "ForOneFrame", "ForDuration", "Persistent"], [this.paths.eMaterialSamplerType]: [ "Color", "Grayscale", "Alpha", "Normal", "Masks", "Distance Field Font", "Linear Color", "Linear Grayscale", "Data", "External", "Virtual Color", "Virtual Grayscale", "Virtual Alpha", "Virtual Normal", "Virtual Mask", "Virtual Linear Color", "Virtual Linear Grayscal", ], [this.paths.eNiagara_Float4Channel]: [ ["NewEnumerator0", "R"], ["NewEnumerator1", "G"], ["NewEnumerator2", "B"], ["NewEnumerator3", "A"], ], [this.paths.eSamplerSourceMode]: ["From texture asset", "Shared: Wrap", "Shared: Clamp", "Hidden"], [this.paths.eSearchCase]: ["CaseSensitive", "IgnoreCase"], [this.paths.eWorldPositionIncludedOffsets]: [ "Absolute World Position (Including Material Shader Offsets)", "Absolute World Position (Excluding Material Shader Offsets)", "Camera Relative World Position (Including Material Shader Offsets)", "Camera Relative World Position (Excluding Material Shader Offsets)", ], [this.paths.eSearchDir]: ["FromStart", "FromEnd"], [this.paths.eSpawnActorCollisionHandlingMethod]: [ ["Undefined", "Default"], ["AlwaysSpawn", "Always Spawn, Ignore Collisions"], ["AdjustIfPossibleButAlwaysSpawn", "Try To Adjust Location, But Always Spawn"], ["AdjustIfPossibleButDontSpawnIfColliding", "Try To Adjust Location, Don't Spawn If Still Colliding"], ["DontSpawnIfColliding", "Do Not Spawn"], ], [this.paths.eTextureMipValueMode]: [ "None (use computed mip level)", "MipLevel (absolute, 0 is full resolution)", "MipBias (relative to the computed mip level)", "Derivative (explicit derivative to compute mip level)", ], [this.paths.eTraceTypeQuery]: [["TraceTypeQuery1", "Visibility"], ["TraceTypeQuery2", "Camera"]] } static ModifierKeys = [ "Ctrl", "Shift", "Alt", "Meta", ] static rgba = ["R", "G", "B", "A"] 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": "ArrowLeft", "ArrowUp": "ArrowUp", "ArrowRight": "ArrowRight", "ArrowDown": "ArrowDown", "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", } } class Utility { /** @param {Number} value */ static clamp(value, min = -Infinity, max = Infinity) { return Math.min(Math.max(value, min), max) } /** @param {HTMLElement} element */ static getScale(element) { // @ts-expect-error const scale = element.blueprint?.getScale() ?? getComputedStyle(element).getPropertyValue("--ueb-scale"); return scale != "" ? parseFloat(scale) : 1 } /** * @param {Number} num * @param {Number} decimals */ static minDecimals(num, decimals = 1, epsilon = 1e-8) { const powered = num * 10 ** decimals; if (Math.abs(powered % 1) > 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} num */ static printExponential(num) { if (num == Number.POSITIVE_INFINITY) { return "inf" } else if (num == Number.NEGATIVE_INFINITY) { return "-inf" } const int = Math.round(num); if (int >= 1000) { const exp = Math.floor(Math.log10(int)); const dec = Math.round(num / 10 ** (exp - 2)) / 100; // Not using num.toExponential() because of the omitted leading 0 on the exponential return `${dec}e+${exp < 10 ? "0" : ""}${exp}` } const intPart = Math.floor(num); if (intPart == 0) { return num.toString() } return this.roundDecimals(num, Math.max(0, 3 - Math.floor(num).toString().length)).toString() } /** * @param {Number} a * @param {Number} b */ static approximatelyEqual(a, b, epsilon = 1e-8) { return !(Math.abs(a - b) > epsilon) } /** * @param {Coordinates} viewportLocation * @param {HTMLElement} movementElement */ static convertLocation(viewportLocation, movementElement, ignoreScale = false) { const scaleCorrection = ignoreScale ? 1 : 1 / Utility.getScale(movementElement); const bounding = movementElement.getBoundingClientRect(); const location = /** @type {Coordinates} */([ Math.round((viewportLocation[0] - bounding.x) * scaleCorrection), Math.round((viewportLocation[1] - bounding.y) * scaleCorrection) ]); return location } /** * @param {Attribute} entity * @param {String} key * @returns {Boolean} */ static isSerialized(entity, key) { return entity["attributes"]?.[key]?.serialized ?? entity.constructor["attributes"]?.[key]?.serialized ?? false } /** @param {String[]} keys */ static objectGet(target, keys, defaultValue = undefined) { if (target === undefined) { return undefined } if (!(keys instanceof Array)) { throw new TypeError("UEBlueprint: 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 * @returns {Boolean} */ static objectSet(target, keys, value, defaultDictType = Object) { if (!(keys instanceof Array)) { throw new TypeError("Expected keys to be an array.") } if (keys.length == 1) { if (keys[0] in target || target[keys[0]] === undefined) { target[keys[0]] = value; return true } } else if (keys.length > 0) { if (!(target[keys[0]] instanceof Object)) { target[keys[0]] = new defaultDictType(); } return Utility.objectSet(target[keys[0]], keys.slice(1), value, defaultDictType) } return false } /** * @param {Number} x * @param {Number} y * @param {Number} gridSize * @returns {Coordinates} */ static snapToGrid(x, y, gridSize) { if (gridSize === 1) { return [x, y] } return [ gridSize * Math.floor(x / gridSize), gridSize * Math.floor(y / gridSize) ] } /** * @template T * @param {Array} a * @param {Array} b * @param {(l: T, r: T) => Boolean} predicate */ static mergeArrays(a = [], b = [], predicate = (l, r) => l == r) { let result = []; a = [...a]; b = [...b]; restart: while (true) { for (let j = 0; j < b.length; ++j) { for (let i = 0; i < a.length; ++i) { if (predicate(a[i], b[j])) { // Found an element in common 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) ); b.shift(); // Remove the same element from b continue restart } } } break restart } // Append remaining the elements in the arrays and make it unique return [...(new Set(result.concat(...a, ...b)))] } /** @param {String} value */ static escapeNewlines(value) { return value .replaceAll("\n", "\\n") // Replace newline with \n .replaceAll("\t", "\\t") // Replace tab with \t } /** @param {String} value */ static escapeString(value, inline = true) { let result = value.replaceAll(new RegExp(`(${Configuration.stringEscapedCharacters.source})`, "g"), '\\$1'); if (inline) { result = result .replaceAll("\n", "\\n") // Replace newline with \n .replaceAll("\t", "\\t"); // Replace tab with \t } return result } /** @param {String} value */ static unescapeString(value) { return value .replaceAll(new RegExp(Configuration.unescapedBackslash.source + "t", "g"), "\t") // Replace tab with \t .replaceAll(new RegExp(Configuration.unescapedBackslash.source + "n", "g"), "\n") // Replace newline with \n .replaceAll(new RegExp(`\\\\(${Configuration.stringEscapedCharacters.source})`, "g"), "$1") } /** @param {String} value */ static clearHTMLWhitespace(value) { return value .replaceAll(" ", "\u00A0") // whitespace .replaceAll(/|
/g, "\n") // newlines .replaceAll(/(\)/g, "") // html comments } /** @param {String} value */ static encodeHTMLWhitespace(value) { return value.replaceAll(" ", "\u00A0") } /** @param {String} value */ static capitalFirstLetter(value) { if (value.length === 0) { return value } return value.charAt(0).toUpperCase() + value.slice(1) } /** @param {String} value */ static formatStringName(value = "") { return value // Remove leading b (for boolean values) or newlines .replace(/^\s*b(?=[A-Z])/, "") // Insert a space where needed, possibly removing unnecessary elading characters .replaceAll(Configuration.nameRegexSpaceReplacement, " ") .trim() .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 {String} pathValue */ static getNameFromPath(pathValue, dropCounter = false) { // From end to the first "/" or "." const regex = dropCounter ? /([^\.\/]+?)(?:_\d+)$/ : /([^\.\/]+)$/; return pathValue.match(regex)?.[1] ?? "" } /** * @param {Number} x * @param {Number} y * @returns {Coordinates} */ 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} r * @param {Number} theta * @returns {Coordinates} */ static getCartesianCoordinates(r, theta) { return [ r * Math.cos(theta), r * Math.sin(theta) ] } /** * @param {Number} begin * @param {Number} end */ static range(begin = 0, end = 0, step = end >= begin ? 1 : -1) { return Array.from({ length: Math.ceil((end - begin) / step) }, (_, i) => begin + (i * step)) } /** @param {String[]} words */ static getFirstWordOrder(words) { return new RegExp(/\s*/.source + words.join(/[^\n]+\n\s*/.source) + /\s*/.source) } /** * @param {HTMLElement} element * @param {String} value */ static paste(element, value) { const event = new ClipboardEvent("paste", { bubbles: true, cancelable: true, clipboardData: new DataTransfer(), }); event.clipboardData.setData("text", value); element.dispatchEvent(event); } /** @param {Blueprint} blueprint */ static async copy(blueprint) { const event = new ClipboardEvent("copy", { bubbles: true, cancelable: true, clipboardData: new DataTransfer(), }); blueprint.dispatchEvent(event); } static animate( from, to, intervalSeconds, callback, acknowledgeRequestAnimationId = id => { }, timingFunction = x => { const v = x ** 3.5; return v / (v + ((1 - x) ** 3.5)) } ) { let startTimestamp; const doAnimation = currentTimestamp => { if (startTimestamp === undefined) { startTimestamp = currentTimestamp; } let delta = (currentTimestamp - startTimestamp) / intervalSeconds; if (Utility.approximatelyEqual(delta, 1) || delta > 1) { delta = 1; } else { acknowledgeRequestAnimationId(requestAnimationFrame(doAnimation)); } const currentValue = from + (to - from) * timingFunction(delta); callback(currentValue); }; acknowledgeRequestAnimationId(requestAnimationFrame(doAnimation)); } } /** * @template {IEntity} EntityT * @template {ITemplate} TemplateT */ class IElement extends s { /** @type {Blueprint} */ #blueprint get blueprint() { return this.#blueprint } set blueprint(v) { this.#blueprint = v; } /** @type {EntityT} */ #entity get entity() { return this.#entity } set entity(entity) { this.#entity = entity; } /** @type {TemplateT} */ #template get template() { return this.#template } isInitialized = false isSetup = false /** @type {IInput[]} */ inputObjects = [] /** * @param {EntityT} entity * @param {TemplateT} template */ initialize(entity, template) { this.requestUpdate(); this.#entity = entity; this.#template = template; this.#template.initialize(this); if (this.isConnected) { this.updateComplete.then(() => this.setup()); } this.isInitialized = true; } connectedCallback() { super.connectedCallback(); this.blueprint = /** @type {Blueprint} */(this.closest("ueb-blueprint")); if (this.isInitialized) { this.requestUpdate(); this.updateComplete.then(() => this.setup()); } } disconnectedCallback() { super.disconnectedCallback(); if (this.isSetup) { this.updateComplete.then(() => this.cleanup()); } this.acknowledgeDelete(); } createRenderRoot() { return this } setup() { this.template.setup(); this.isSetup = true; } cleanup() { this.template.cleanup(); this.isSetup = false; } /** @param {PropertyValues} changedProperties */ willUpdate(changedProperties) { super.willUpdate(changedProperties); this.template.willUpdate(changedProperties); } /** @param {PropertyValues} changedProperties */ update(changedProperties) { super.update(changedProperties); this.template.update(changedProperties); } render() { return this.template.render() } /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.template.firstUpdated(changedProperties); this.template.inputSetup(); } /** @param {PropertyValues} changedProperties */ updated(changedProperties) { super.updated(changedProperties); this.template.updated(changedProperties); } acknowledgeDelete() { let deleteEvent = new CustomEvent(Configuration.removeEventName); this.dispatchEvent(deleteEvent); } /** @param {IElement} element */ isSameGraph(element) { return this.blueprint && this.blueprint == element?.blueprint } } class SVGIcon { static arrayPin = x` ` static branchNode = x` ` static breakStruct = x` ` static cast = x` ` static close = x` ` static convert = x` ` static correct = x` ` static delegate = x` ` static doN = x` ` static doOnce = x` ` static enum = x` ` static event = x` ` static execPin = x` ` static expandIcon = x` ` static flipflop = x` ` static forEachLoop = x` ` static functionSymbol = x` ` static gamepad = x` ` static genericPin = x` ` static keyboard = x` ` static loop = x` ` static macro = x` ` static mapPin = x` ` static makeArray = x` ` static makeMap = x` ` static makeSet = x` ` static makeStruct = x` ` static metasoundFunction = x` ` static mouse = x` ` static node = x` ` static operationPin = x` ` static pcgStackPin = x` ` static pcgPin = x` ` static pcgParamPin = x` ` static pcgSpatialPin = x` ` static plusCircle = x` ` static questionMark = x` ` static referencePin = x` ` static reject = x` ` static setPin = x` ` static select = x` ` static sequence = x` ` static sound = x` ` static spawnActor = x` ` static switch = x` ` static timer = x` ` static touchpad = x` ` } class Reply { /** * @template T * @param {Number} position * @param {T} value * @param {PathNode} bestPath * @returns {Result} */ static makeSuccess(position, value, bestPath = null, bestPosition = 0) { return { status: true, value: value, position: position, bestParser: bestPath, bestPosition: bestPosition, } } /** * @param {PathNode} bestPath * @returns {Result} */ static makeFailure(position = 0, bestPath = null, bestPosition = 0) { return { status: false, value: null, position, bestParser: bestPath, bestPosition: bestPosition, } } /** @param {Parsernostrum} parsernostrum */ static makeContext(parsernostrum = null, input = "") { return /** @type {Context} */({ parsernostrum, input, highlighted: null, }) } static makePathNode(parser, index = 0, previous = null) { return /** @type {PathNode} */({ parent: previous, current: parser, index, }) } } /** @template T */ class Parser { static indentation = " " static highlight = "Last valid parser" /** @type {(new (...args: any) => Parser) & typeof Parser} */ Self /** @param {String} value */ static frame(value, label = "", indentation = "") { label = value ? "[ " + label + " ]" : ""; let rows = value.split("\n"); const width = Math.max(...rows.map(r => r.length)); const rightPadding = width < label.length ? " ".repeat(label.length - width) : ""; for (let i = 0; i < rows.length; ++i) { rows[i] = indentation + "| " + rows[i] + " ".repeat(width - rows[i].length) + rightPadding + " |"; } if (label.length < width) { label = label + "─".repeat(width - label.length); } const rowA = "┌─" + label + "─┐"; const rowB = indentation + "└─" + "─".repeat(label.length) + "─┘"; rows = [rowA, ...rows, rowB]; return rows.join("\n") } /** * @param {PathNode} path * @param {Number} index * @returns {PathNode} */ makePath(path, index) { return { current: this, parent: path, index } } /** * @param {Context} context * @param {PathNode} path */ isHighlighted(context, path) { if (context.highlighted instanceof Parser) { return context.highlighted === this } if (!context.highlighted || !path?.current) { return false } let a, b; for ( a = path, b = /** @type {PathNode} */(context.highlighted); a.current && b.current; a = a.parent, b = b.parent ) { if (a.current !== b.current || a.index !== b.index) { return false } } return !a.current && !b.current } /** @param {PathNode?} path */ isVisited(path) { if (!path) { return false } for (path = path.parent; path != null; path = path.parent) { if (path.current === this) { return true } } return false } /** * @param {Context} context * @param {Number} position * @param {PathNode} path * @param {Number} index * @returns {Result} */ parse(context, position, path, index) { return null } /** @param {PathNode} path */ toString(context = Reply.makeContext(null, ""), indentation = "", path = null, index = 0) { path = this.makePath(path, index); if (this.isVisited(path)) { return "<...>" } const isVisited = this.isVisited(path); const isHighlighted = this.isHighlighted(context, path); let result = isVisited ? "<...>" : this.doToString(context, isHighlighted ? "" : indentation, path, index); if (isHighlighted) { /** @type {String[]} */ result = Parser.frame(result, Parser.highlight, indentation); } return result } /** * @protected * @param {Context} context * @param {String} indentation * @param {PathNode} path * @param {Number} index */ doToString(context, indentation, path, index) { return `${this.constructor.name} does not implement toString()` } } /** @template {String} T */ class StringParser extends Parser { #value get value() { return this.#value } /** @param {T} value */ constructor(value) { super(); this.#value = value; } /** * @param {Context} context * @param {Number} position * @param {PathNode} path * @param {Number} index * @returns {Result} */ parse(context, position, path, index) { path = this.makePath(path, index); const end = position + this.#value.length; const value = context.input.substring(position, end); const result = this.#value === value ? Reply.makeSuccess(end, this.#value, path, end) : Reply.makeFailure(); return result } /** * @protected * @param {Context} context * @param {String} indentation * @param {PathNode} path * @param {Number} index */ doToString(context, indentation, path, index) { return `"${this.value.replaceAll("\n", "\\n").replaceAll('"', '\\"')}"` } } /** @extends Parser<""> */ class SuccessParser extends Parser { static instance = new SuccessParser() /** * @param {Context} context * @param {Number} position * @param {PathNode} path * @param {Number} index * @returns {Result<"">} */ parse(context, position, path, index) { path = this.makePath(path, index); return Reply.makeSuccess(position, "", path, 0) } /** * @protected * @param {Context} context * @param {String} indentation * @param {PathNode} path * @param {Number} index */ doToString(context, indentation, path, index) { return "" } } /** * @template {any[]} T * @typedef {T extends [infer A] ? A * : T extends [infer A, ...infer B] ? A | Union * : never * } Union */ /** * @template {any[]} T * @extends Parser> */ class AlternativeParser extends Parser { #parsers get parsers() { return this.#parsers } /** @param {Parser[]} parsers */ constructor(...parsers) { super(); this.#parsers = parsers; } /** * @param {Context} context * @param {Number} position * @param {PathNode} path * @param {Number} index */ parse(context, position, path, index) { path = this.makePath(path, index); const result = /** @type {Result>} */(Reply.makeSuccess(0, "")); for (let i = 0; i < this.#parsers.length; ++i) { const outcome = this.#parsers[i].parse(context, position, path, i); if (outcome.bestPosition > result.bestPosition) { result.bestParser = outcome.bestParser; result.bestPosition = outcome.bestPosition; } if (outcome.status) { result.value = outcome.value; result.position = outcome.position; return result } } result.status = false; result.value = null; return result } /** * @protected * @param {Context} context * @param {String} indentation * @param {PathNode} path * @param {Number} index */ doToString(context, indentation, path, index) { // Short syntax for optional parser if (this.#parsers.length === 2 && this.#parsers[1] instanceof SuccessParser) { let result = this.#parsers[0].toString(context, indentation, path, 0); if (!(this.#parsers[0] instanceof StringParser)) { result = "<" + result + ">"; } result += "?"; return result } const deeperIndentation = indentation + Parser.indentation; let result = "ALT<\n" + deeperIndentation + this.#parsers .map((parser, i) => parser.toString( context, deeperIndentation + " ".repeat(i === 0 ? 0 : Parser.indentation.length - 2), path, i, )) .join("\n" + deeperIndentation + "| ") + "\n" + indentation + ">"; return result } } /** * @template S * @template T * @extends Parser */ class ChainedParser extends Parser { #parser get parser() { return this.#parser } #fn /** * @param {Parser} parser * @param {(v: S, input: String, position: Number) => Parsernostrum} chained */ constructor(parser, chained) { super(); this.#parser = parser; this.#fn = chained; } /** * @param {Context} context * @param {Number} position * @param {PathNode} path * @param {Number} index * @returns {Result} */ parse(context, position, path, index) { path = this.makePath(path, index); const outcome = this.#parser.parse(context, position, path, 0); if (!outcome.status) { // @ts-expect-error return outcome } const result = this.#fn(outcome.value, context.input, outcome.position) .getParser() .parse(context, outcome.position, path, 0); if (outcome.bestPosition > result.bestPosition) { result.bestParser = outcome.bestParser; result.bestPosition = outcome.bestPosition; } return result } /** * @protected * @param {Context} context * @param {String} indentation * @param {PathNode} path * @param {Number} index */ doToString(context, indentation, path, index) { const result = this.#parser.toString(context, indentation, path, 0) + " => chained"; return result } } /** @extends Parser */ class FailureParser extends Parser { static instance = new FailureParser() /** * @param {Context} context * @param {Number} position * @param {PathNode} path * @param {Number} index */ parse(context, position, path, index) { return Reply.makeFailure() } /** * @protected * @param {Context} context * @param {String} indentation * @param {PathNode} path * @param {Number} index */ doToString(context, indentation, path, index) { return "" } } /** * @template T * @extends Parser */ class Label extends Parser { #parser get parser() { return this.#parser } #label = "" /** * @param {Parser} parser * @param {String} label */ constructor(parser, label) { super(); this.#parser = parser; this.#label = label; } /** * @param {PathNode} path * @param {Number} index */ makePath(path, index) { return path // Label does not alter the path } /** * @param {Context} context * @param {Number} position * @param {PathNode} path * @param {Number} index */ parse(context, position, path, index) { this.parse = this.#parser.parse.bind(this.#parser); return this.parse(context, position, path, index) } /** * @protected * @param {Context} context * @param {String} indentation * @param {PathNode} path * @param {Number} index */ doToString(context, indentation, path, index) { let result = this.#parser.toString(context, "", path, index); result = Parser.frame(result, this.#label, indentation); return result } } /** * @template T * @extends Parser */ class LazyParser extends Parser { #parser /** @type {Parser} */ #resolvedPraser /** @param {() => Parsernostrum} parser */ constructor(parser) { super(); this.#parser = parser; } /** * @param {PathNode} path * @param {Number} index */ makePath(path, index) { return path } /** * @param {Context} context * @param {PathNode} path */ isHighlighted(context, path) { if (super.isHighlighted(context, path)) { // If LazyParser is highlighted, then highlight its child const childrenPath = { parent: path, parser: this.#resolvedPraser, index: 0 }; context.highlighted = context.highlighted instanceof Parser ? this.#resolvedPraser : childrenPath; } return false } resolve() { if (!this.#resolvedPraser) { this.#resolvedPraser = this.#parser().getParser(); } return this.#resolvedPraser } /** * @param {Context} context * @param {Number} position * @param {PathNode} path * @param {Number} index * @returns {Result} */ parse(context, position, path, index) { this.resolve(); this.parse = this.#resolvedPraser.parse.bind(this.#resolvedPraser); return this.parse(context, position, path, index) } /** * @protected * @param {Context} context * @param {String} indentation * @param {PathNode} path * @param {Number} index */ doToString(context, indentation, path, index) { this.resolve(); this.doToString = this.#resolvedPraser.toString.bind(this.#resolvedPraser); return this.doToString(context, indentation, path, index) } } /** @extends Parser<""> */ class Lookahead extends Parser { #parser get parser() { return this.#parser } #type get type() { return this.#type } /** * @readonly * @enum {String} */ static Type = { NEGATIVE_AHEAD: "?!", NEGATIVE_BEHIND: "?} */ parse(context, position, path, index) { path = this.makePath(path, index); let result = this.#parser.parse(context, position, path, 0); result = result.status == (this.#type === Lookahead.Type.POSITIVE_AHEAD) ? Reply.makeSuccess(position, "", path, position) : Reply.makeFailure(); return result } /** * @protected * @param {Context} context * @param {String} indentation * @param {PathNode} path * @param {Number} index */ doToString(context, indentation, path, index) { return "(" + this.#type + this.#parser.toString(context, indentation, path, 0) + ")" } } /** * @template T * @extends Parser */ class RegExpParser extends Parser { /** @type {RegExp} */ #regexp get regexp() { return this.#regexp } /** @type {RegExp} */ #anchoredRegexp #matchMapper static #createEscapeable = character => String.raw`[^${character}\\]*(?:\\.[^${character}\\]*)*` static #numberRegex = /[-\+]?(?:\d*\.)?\d+/ static common = { number: new RegExp(this.#numberRegex.source + String.raw`(?!\.)`), numberInteger: /[\-\+]?\d+(?!\.\d)/, numberNatural: /\d+/, numberExponential: new RegExp(this.#numberRegex.source + String.raw`(?:[eE][\+\-]?\d+)?(?!\.)`), numberUnit: /\+?(?:0(?:\.\d+)?|1(?:\.0+)?)(?![\.\d])/, numberByte: /0*(?:25[0-5]|2[0-4]\d|1?\d?\d)(?!\d|\.)/, whitespace: /\s+/, whitespaceOpt: /\s*/, whitespaceInline: /[^\S\n]+/, whitespaceInlineOpt: /[^\S\n]*/, whitespaceMultiline: /\s*?\n\s*/, doubleQuotedString: new RegExp(`"(${this.#createEscapeable('"')})"`), singleQuotedString: new RegExp(`'(${this.#createEscapeable("'")})'`), backtickQuotedString: new RegExp("`(" + this.#createEscapeable("`") + ")`"), } /** * @param {RegExp} regexp * @param {(match: RegExpExecArray) => T} matchMapper */ constructor(regexp, matchMapper) { super(); this.#regexp = regexp; this.#anchoredRegexp = new RegExp(`^(?:${regexp.source})`, regexp.flags); this.#matchMapper = matchMapper; } /** * @param {Context} context * @param {Number} position * @param {PathNode} path * @param {Number} index * @returns {Result} */ parse(context, position, path, index) { path = this.makePath(path, index); const match = this.#anchoredRegexp.exec(context.input.substring(position)); if (match) { position += match[0].length; } const result = match ? Reply.makeSuccess(position, this.#matchMapper(match), path, position) : Reply.makeFailure(); return result } /** * @protected * @param {Context} context * @param {String} indentation * @param {PathNode} path * @param {Number} index */ doToString(context, indentation, path, index) { let result = "/" + this.#regexp.source + "/"; const shortname = Object.entries(RegExpParser.common).find(([k, v]) => v.source === this.#regexp.source)?.[0]; if (shortname) { result = "P." + shortname; } return result } } /** * @template S * @template T * @extends Parser */ class MapParser extends Parser { #parser get parser() { return this.#parser } #mapper get mapper() { return this.#mapper } /** * @param {Parser} parser * @param {(v: S) => T} mapper */ constructor(parser, mapper) { super(); this.#parser = parser; this.#mapper = mapper; } /** * @param {Context} context * @param {PathNode} path */ isHighlighted(context, path) { if (super.isHighlighted(context, path)) { // If MapParser is highlighted, then highlight its child const childrenPath = { parent: path, parser: this.#parser, index: 0 }; context.highlighted = context.highlighted instanceof Parser ? this.#parser : childrenPath; } return false } /** * @param {Context} context * @param {Number} position * @param {PathNode} path * @param {Number} index * @returns {Result} */ parse(context, position, path, index) { path = this.makePath(path, index); // @ts-expect-error const result = /** @type {Result} */(this.#parser.parse(context, position, path, 0)); if (result.status) { // @ts-expect-error result.value = this.#mapper(result.value); } return result } /** * @protected * @param {Context} context * @param {String} indentation * @param {PathNode} path * @param {Number} index */ doToString(context, indentation, path, index) { let result = this.#parser.toString(context, indentation, path, 0); if (this.#parser instanceof RegExpParser) { if (Object.values(RegExpParser.common).includes(this.#parser.regexp)) { if ( this.#parser.regexp === RegExpParser.common.numberInteger && this.#mapper === /** @type {(v: any) => BigInt} */(BigInt) ) { return "P.numberBigInteger" } return result } } let serializedMapper = this.#mapper.toString(); if (serializedMapper.length > 60 || serializedMapper.includes("\n")) { serializedMapper = "(...) => { ... }"; } result += ` -> map<${serializedMapper}>`; return result } } /** @extends {RegExpParser} */ class RegExpArrayParser extends RegExpParser { /** @param {RegExpExecArray} match */ static #mapper = match => match /** @param {RegExp} regexp */ constructor(regexp) { super(regexp, RegExpArrayParser.#mapper); } } /** @extends {RegExpParser} */ class RegExpValueParser extends RegExpParser { /** @param {RegExp} regexp */ constructor(regexp, group = 0) { super(regexp, match => match[group]); } } /** * @template {any[]} T * @extends Parser */ class SequenceParser extends Parser { #parsers get parsers() { return this.#parsers } /** @param {Parser[]} parsers */ constructor(...parsers) { super(); this.#parsers = parsers; } /** * @param {Context} context * @param {Number} position * @param {PathNode} path * @param {Number} index * @returns {Result} */ parse(context, position, path, index) { path = this.makePath(path, index); const value = /** @type {ParserValue} */(new Array(this.#parsers.length)); const result = Reply.makeSuccess(position, value); for (let i = 0; i < this.#parsers.length; ++i) { const outcome = this.#parsers[i].parse(context, result.position, path, i); if (outcome.bestPosition > result.bestPosition) { result.bestParser = outcome.bestParser; result.bestPosition = outcome.bestPosition; } if (!outcome.status) { result.status = false; result.value = null; break } result.value[i] = outcome.value; result.position = outcome.position; } return result } /** * @protected * @param {Context} context * @param {String} indentation * @param {PathNode} path * @param {Number} index */ doToString(context, indentation, path, index) { const deeperIndentation = indentation + Parser.indentation; const result = "SEQ<\n" + deeperIndentation + this.#parsers .map((parser, index) => parser.toString(context, deeperIndentation, path, index)) .join("\n" + deeperIndentation) + "\n" + indentation + ">"; return result } } /** * @template T * @extends Parser */ class TimesParser extends Parser { #parser get parser() { return this.#parser } #min get min() { return this.#min } #max get max() { return this.#max } /** @param {Parser} parser */ constructor(parser, min = 0, max = Number.POSITIVE_INFINITY) { super(); if (min > max) { throw new Error("Min is greater than max") } this.#parser = parser; this.#min = min; this.#max = max; } /** * @param {Context} context * @param {Number} position * @param {PathNode} path * @param {Number} index * @returns {Result} */ parse(context, position, path, index) { path = this.makePath(path, index); const value = /** @type {ParserValue[]} */([]); const result = Reply.makeSuccess(position, value, path); for (let i = 0; i < this.#max; ++i) { const outcome = this.#parser.parse(context, result.position, path, 0); if (outcome.bestPosition > result.bestPosition) { result.bestParser = outcome.bestParser; result.bestPosition = outcome.bestPosition; } if (!outcome.status) { if (i < this.#min) { result.status = false; result.value = null; } break } // @ts-expect-error result.value.push(outcome.value); result.position = outcome.position; } // @ts-expect-error return result } /** * @protected * @param {Context} context * @param {String} indentation * @param {PathNode} path * @param {Number} index */ doToString(context, indentation, path, index) { let result = this.parser.toString(context, indentation, path, 0); const serialized = this.#min === 0 && this.#max === 1 ? "?" : this.#min === 0 && this.#max === Number.POSITIVE_INFINITY ? "*" : this.#min === 1 && this.#max === Number.POSITIVE_INFINITY ? "+" : "{" + this.#min + (this.#min !== this.#max ? "," + this.#max : "") + "}"; result += serialized; return result } } /** @template T */ class Parsernostrum { #parser /** @type {(new (parser: Parser) => Parsernostrum) & typeof Parsernostrum} */ Self static lineColumnFromOffset(string, offset) { const lines = string.substring(0, offset).split('\n'); const line = lines.length; const column = lines[lines.length - 1].length + 1; return { line, column } } /** @param {[any, ...any] | RegExpExecArray} param0 */ static #firstElementGetter = ([v, _]) => v /** @param {[any, any, ...any] | RegExpExecArray} param0 */ static #secondElementGetter = ([_, v]) => v static #arrayFlatter = ([first, rest]) => [first, ...rest] /** * @template T * @param {T} v * @returns {T extends Array ? String : T} */ // @ts-expect-error static #joiner = v => v instanceof Array ? v.join("") : v // Prefedined parsers /** Parser accepting any valid decimal, possibly signed number */ static number = this.reg(RegExpParser.common.number).map(Number) /** Parser accepting any digits only number */ static numberInteger = this.reg(RegExpParser.common.numberInteger).map(Number) /** Parser accepting any digits only number and returns a BigInt */ // @ts-expect-error static numberBigInteger = this.reg(this.numberInteger.getParser().parser.regexp).map(BigInt) /** Parser accepting any digits only number */ static numberNatural = this.reg(RegExpParser.common.numberNatural).map(Number) /** Parser accepting any valid decimal, possibly signed, possibly in the exponential form number */ static numberExponential = this.reg(RegExpParser.common.numberExponential).map(Number) /** Parser accepting any valid decimal number between 0 and 1 */ static numberUnit = this.reg(RegExpParser.common.numberUnit).map(Number) /** Parser accepting any integer between 0 and 255 */ static numberByte = this.reg(RegExpParser.common.numberByte).map(Number) /** Parser accepting whitespace */ static whitespace = this.reg(RegExpParser.common.whitespace) /** Parser accepting whitespace */ static whitespaceOpt = this.reg(RegExpParser.common.whitespaceOpt) /** Parser accepting whitespace that spans on a single line */ static whitespaceInline = this.reg(RegExpParser.common.whitespaceInline) /** Parser accepting whitespace that spans on a single line */ static whitespaceInlineOpt = this.reg(RegExpParser.common.whitespaceInlineOpt) /** Parser accepting whitespace that contains a list a newline */ static whitespaceMultiline = this.reg(RegExpParser.common.whitespaceMultiline) /** Parser accepting a double quoted string and returns the content */ static doubleQuotedString = this.reg(RegExpParser.common.doubleQuotedString, 1) /** Parser accepting a single quoted string and returns the content */ static singleQuotedString = this.reg(RegExpParser.common.singleQuotedString, 1) /** Parser accepting a backtick quoted string and returns the content */ static backtickQuotedString = this.reg(RegExpParser.common.backtickQuotedString, 1) /** @param {Parser} parser */ constructor(parser, optimized = false) { this.#parser = parser; } /** @param {PathNode} path */ static #simplifyPath(path) { /** @type {PathNode[]} */ const array = []; while (path) { array.push(path); path = path.parent; } array.reverse(); /** @type {Map} */ let visited = new Map(); for (let i = 1; i < array.length; ++i) { const existing = visited.get(array[i].current); if (existing !== undefined) { if (array[i + 1]) { array[i + 1].parent = array[existing]; } visited = new Map([...visited.entries()].filter(([parser, index]) => index <= existing || index > i)); visited.set(array[i].current, existing); array.splice(existing + 1, i - existing); i = existing; } else { visited.set(array[i].current, i); } } return array[array.length - 1] } getParser() { return this.#parser } /** @param {String} input */ run(input) { const result = this.#parser.parse(Reply.makeContext(this, input), 0, Reply.makePathNode(), 0); if (result.position !== input.length) { result.status = false; } return /** @type {Result} */(result) } /** * @param {String} input * @throws {Error} when the parser fails to match */ parse(input, printParser = true) { const result = this.run(input); if (result.status) { return result.value } const chunkLength = 60; const chunkRange = /** @type {[Number, Number]} */( [Math.ceil(chunkLength / 2), Math.floor(chunkLength / 2)] ); const position = Parsernostrum.lineColumnFromOffset(input, result.bestPosition); let bestPosition = result.bestPosition; const inlineInput = input.replaceAll( /^(\s)+|\s{6,}|\s*?\n\s*/g, (m, startingSpace, offset) => { let replaced = startingSpace ? "..." : " ... "; if (offset <= result.bestPosition) { if (result.bestPosition < offset + m.length) { bestPosition -= result.bestPosition - offset; } else { bestPosition -= m.length - replaced.length; } } return replaced } ); const string = inlineInput.substring(0, chunkLength).trimEnd(); const leadingWhitespaceLength = Math.min( input.substring(result.bestPosition - chunkRange[0]).match(/^\s*/)[0].length, chunkRange[0] - 1, ); let offset = Math.min(bestPosition, chunkRange[0] - leadingWhitespaceLength); chunkRange[0] = Math.max(0, bestPosition - chunkRange[0]) + leadingWhitespaceLength; chunkRange[1] = Math.min(input.length, chunkRange[0] + chunkLength); let segment = inlineInput.substring(...chunkRange); if (chunkRange[0] > 0) { segment = "..." + segment; offset += 3; } if (chunkRange[1] < inlineInput.length - 1) { segment = segment + "..."; } const bestParser = this.toString(Parser.indentation, true, Parsernostrum.#simplifyPath(result.bestParser)); throw new Error( `Could not parse: ${string}\n\n` + `Input: ${segment}\n` + " " + " ".repeat(offset) + `^ From here (line: ${position.line}, ` + `column: ${position.column}, ` + `offset: ${result.bestPosition})${result.bestPosition === input.length ? ", end of string" : ""}\n` + (printParser ? "\n" + (result.bestParser ? "Last valid parser matched:" : "No parser matched:") + bestParser + "\n" : "" ) ) } // Parsers /** * @template {String} S * @param {S} value */ static str(value) { return new this(new StringParser(value)) } /** @param {RegExp} value */ static reg(value, group = 0) { return new this(new RegExpValueParser(value, group)) } /** @param {RegExp} value */ static regArray(value) { return new this(new RegExpArrayParser(value)) } static success() { return new this(SuccessParser.instance) } static failure() { return new this(FailureParser.instance) } // Combinators /** * @template {Parsernostrum[]} P * @param {P} parsers * @returns {Parsernostrum>} */ static seq(...parsers) { return new this(new SequenceParser(...parsers.map(p => p.getParser()))) } /** * @template {Parsernostrum[]} P * @param {P} parsers * @returns {Parsernostrum>>} */ static alt(...parsers) { return new this(new AlternativeParser(...parsers.map(p => p.getParser()))) } /** * @template {Parsernostrum} P * @param {P} parser */ static lookahead(parser) { return new this(new Lookahead(parser.getParser(), Lookahead.Type.POSITIVE_AHEAD)) } /** * @template {Parsernostrum} P * @param {() => P} parser * @returns {Parsernostrum>} */ static lazy(parser) { return new this(new LazyParser(parser)) } /** @param {Number} min */ times(min, max = min) { return new Parsernostrum(new TimesParser(this.#parser, min, max)) } many() { return this.times(0, Number.POSITIVE_INFINITY) } /** @param {Number} n */ atLeast(n) { return this.times(n, Number.POSITIVE_INFINITY) } /** @param {Number} n */ atMost(n) { return this.times(0, n) } /** * @param {any} emptyResult * @returns {Parsernostrum} */ opt(emptyResult = "") { let success = Parsernostrum.success(); if (emptyResult !== "") { success = success.map(() => emptyResult); } // @ts-expect-error return Parsernostrum.alt(this, success) } /** * @template {Parsernostrum} P * @param {P} separator */ sepBy(separator, atLeast = 1, allowTrailing = false) { let result = Parsernostrum.seq( this, Parsernostrum.seq(separator, this).map(Parsernostrum.#secondElementGetter).atLeast(atLeast - 1), ...(allowTrailing ? [separator.opt([])] : []) ).map(Parsernostrum.#arrayFlatter); if (atLeast === 0) { result = result.opt([]); } return result } skipSpace() { return Parsernostrum.seq(this, Parsernostrum.whitespaceOpt).map(Parsernostrum.#firstElementGetter) } /** * @template R * @param {(v: T) => R} fn * @returns {Parsernostrum} */ map(fn) { return new Parsernostrum(new MapParser(this.#parser, fn)) } /** * @template {Parsernostrum} P * @param {(v: T, input: String, position: Number) => P} fn * @returns {P} */ chain(fn) { // @ts-expect-error return new Parsernostrum(new ChainedParser(this.#parser, fn)) } /** * @param {(v: T, input: String, position: Number) => boolean} fn * @return {Parsernostrum} */ assert(fn) { return this.chain((v, input, position) => fn(v, input, position) ? Parsernostrum.success().map(() => v) : Parsernostrum.failure() ) } join(value = "") { return this.map(Parsernostrum.#joiner) } /** @return {Parsernostrum} */ label(value = "") { return new Parsernostrum(new Label(this.#parser, value)) } /** @param {Parsernostrum | Parser | PathNode} highlight */ toString(indentation = "", newline = false, highlight = null) { if (highlight instanceof Parsernostrum) { highlight = highlight.getParser(); } const context = Reply.makeContext(this, ""); context.highlighted = highlight; const path = Reply.makePathNode(); return (newline ? "\n" + indentation : "") + this.#parser.toString(context, indentation, path) } } /** @abstract */ class IEntity { /** @type {(v: String) => String} */ static same = v => v /** @type {(entity: IEntity, serialized: String) => String} */ static notWrapped = (entity, serialized) => serialized /** @type {(entity: IEntity, serialized: String) => String} */ static defaultWrapped = (entity, serialized) => `${entity.#lookbehind}(${serialized})` static wrap = this.defaultWrapped static attributeSeparator = "," static keySeparator = "=" /** @type {(k: String) => String} */ static printKey = k => k static grammar = Parsernostrum.lazy(() => this.createGrammar()) /** @type {P} */ static unknownEntityGrammar static unknownEntity /** @type {{ [key: String]: typeof IEntity }} */ static attributes = {} /** @type {String | String[]} */ static lookbehind = "" /** @type {(type: typeof IEntity) => InstanceType} */ static default static nullable = false static ignored = false // Never serialize or deserialize static serialized = false // Value is written and read as string static expected = false // Must be there static inlined = false // The key is a subobject or array and printed as inlined (A.B=123, A(0)=123) /** @type {Boolean} */ static quoted // Key is serialized with quotes static silent = false // Do not serialize if default static trailing = false // Add attribute separator after the last attribute when serializing /** @type {String[]} */ #keys get keys() { return this.#keys ?? Object.keys(this) } set keys(value) { this.#keys = [... new Set(value)]; } #lookbehind = /** @type {String} */(this.constructor.lookbehind) get lookbehind() { return this.#lookbehind.trim() } set lookbehind(value) { this.#lookbehind = value; } #ignored = /** @type {typeof IEntity} */(this.constructor).ignored get ignored() { return this.#ignored } set ignored(value) { this.#ignored = value; } #inlined = /** @type {typeof IEntity} */(this.constructor).inlined get inlined() { return this.#inlined } set inlined(value) { this.#inlined = value; } #quoted get quoted() { return this.#quoted ?? /** @type {typeof IEntity} */(this.constructor).quoted ?? false } set quoted(value) { this.#quoted = value; } /** @type {Boolean} */ #trailing get trailing() { return this.#trailing ?? /** @type {typeof IEntity} */(this.constructor).trailing ?? false } set trailing(value) { this.#trailing = value; } constructor(values = {}) { const attributes = /** @type {typeof IEntity} */(this.constructor).attributes; const keys = Utility.mergeArrays( Object.keys(values), Object.entries(attributes).filter(([k, v]) => v.default !== undefined).map(([k, v]) => k) ); for (const key of keys) { if (values[key] !== undefined) { if (values[key].constructor === Object) { // It is part of a nested key (words separated by ".") values[key] = new ( attributes[key] !== undefined ? attributes[key] : IEntity.unknownEntity )(values[key]); } const computedEntity = /** @type {ComputedTypeEntityConstructor} */(attributes[key]); this[key] = values[key]; if (computedEntity?.compute) { /** @type {typeof IEntity} */ const actualEntity = computedEntity.compute(this); const parsed = actualEntity.grammar.run(values[key].toString()); if (parsed.status) { this[key] = parsed.value; } } continue } const attribute = attributes[key]; if (attribute.default !== undefined) { this[key] = attribute.default(attribute); continue } } } /** * @protected * @returns {P} */ static createGrammar() { return this.unknownEntityGrammar } static actualClass() { let self = this; while (!self.name) { self = Object.getPrototypeOf(self); } return self } static className() { return this.actualClass().name } /** * @protected * @template {typeof IEntity} T * @this {T} * @returns {T} */ static asUniqueClass() { let result = this; if (this.name.length) { // @ts-expect-error result = (() => class extends this { })(); // Comes from a lambda otherwise the class will have name "result" result.grammar = result.createGrammar(); // Reassign grammar to capture the correct this from subclass } return result } /** * @template {typeof IEntity} T * @this {T} * @param {String} value */ static withLookbehind(value) { const result = this.asUniqueClass(); result.lookbehind = value; return result } /** * @template {typeof IEntity} T * @this {T} * @param {(type: T) => (InstanceType | NullEntity)} value * @returns {T} */ static withDefault(value = type => new type()) { const result = this.asUniqueClass(); result.default = value; return result } /** * @template {typeof IEntity} T * @this {T} */ static flagNullable(value = true) { const result = this.asUniqueClass(); result.nullable = value; return result } /** * @template {typeof IEntity} T * @this {T} */ static flagIgnored(value = true) { const result = this.asUniqueClass(); result.ignored = value; return result } /** * @template {typeof IEntity} T * @this {T} */ static flagSerialized(value = true) { const result = this.asUniqueClass(); result.serialized = value; return result } /** * @template {typeof IEntity} T * @this {T} */ static flagInlined(value = true) { const result = this.asUniqueClass(); result.inlined = value; return result } /** * @template {typeof IEntity} T * @this {T} */ static flagQuoted(value = true) { const result = this.asUniqueClass(); result.quoted = value; return result } /** * @template {typeof IEntity} T * @this {T} */ static flagSilent(value = true) { const result = this.asUniqueClass(); result.silent = value; return result } /** * @template {typeof IEntity} T * @this {T} */ static flagTrailing(value = true) { const result = this.asUniqueClass(); result.trailing = value; return result } /** * @protected * @param {String} string */ static asSerializedString(string) { return `"${string.replaceAll(/(?<=(?:[^\\]|^)(?:\\\\)*?)"/g, '\\"')}"` } /** @param {String} key */ showProperty(key) { /** @type {IEntity} */ let value = this[key]; const valueType = /** @type {typeof IEntity} */(value.constructor); if (valueType.silent && valueType.default !== undefined) { if (valueType["#default"] === undefined) { valueType["#default"] = valueType.default(valueType); } const defaultValue = valueType["#default"]; return !value.equals(defaultValue) } return true } /** * * @param {String} attributeName * @param {(v: any) => void} callback */ listenAttribute(attributeName, callback) { const descriptor = Object.getOwnPropertyDescriptor(this, attributeName); const setter = descriptor.set; if (setter) { descriptor.set = v => { setter(v); callback(v); }; Object.defineProperties(this, { [attributeName]: descriptor }); } else if (descriptor.value) { Object.defineProperties(this, { ["#" + attributeName]: { value: descriptor.value, writable: true, enumerable: false, }, [attributeName]: { enumerable: true, get() { return this["#" + attributeName] }, set(v) { callback(v); this["#" + attributeName] = v; }, }, }); } } /** @this {IEntity | Array} */ doSerialize( insideString = false, indentation = "", Self = /** @type {typeof IEntity} */(this.constructor), printKey = Self.printKey, keySeparator = Self.keySeparator, attributeSeparator = Self.attributeSeparator, wrap = Self.wrap, ) { const isSelfOverriden = Self !== this.constructor; let result = ""; let first = true; const keys = this instanceof IEntity ? this.keys : Object.keys(this); for (const key of keys) { /** @type {IEntity} */ const value = this[key]; const valueType = /** @type {typeof IEntity} */(value?.constructor); if (value === undefined || this instanceof IEntity && !this.showProperty(key)) { continue } if (first) { first = false; } else { result += attributeSeparator; } let keyValue = this instanceof Array ? `(${key})` : key; if (keyValue.length && (Self.attributes[key]?.quoted || value.quoted)) { keyValue = `"${keyValue}"`; } if (value.inlined) { const inlinedPrintKey = valueType.className() === "ArrayEntity" ? k => printKey(`${keyValue}${k}`) : k => printKey(`${keyValue}.${k}`); result += value.serialize( insideString, indentation, undefined, inlinedPrintKey, keySeparator, attributeSeparator, Self.notWrapped ); continue } keyValue = printKey(keyValue); if (keyValue.length) { result += (attributeSeparator.includes("\n") ? indentation : "") + keyValue + keySeparator; } let serialization = value?.serialize(insideString, indentation); result += serialization; } if (this instanceof IEntity && (isSelfOverriden && Self.trailing || this.trailing) && result.length) { result += attributeSeparator; } return wrap(/** @type {IEntity} */(this), result) } /** @this {IEntity | Array} */ serialize( insideString = false, indentation = "", Self = /** @type {typeof IEntity} */(this.constructor), printKey = Self.printKey, keySeparator = Self.keySeparator, attributeSeparator = Self.attributeSeparator, wrap = Self.wrap, ) { Self !== this.constructor; let result = this instanceof Array ? IEntity.prototype.doSerialize.bind(this)(insideString, indentation, Self, printKey, keySeparator, attributeSeparator, wrap) : this.doSerialize(insideString, indentation, Self, printKey, keySeparator, attributeSeparator, wrap); if (Self.serialized) { result = IEntity.asSerializedString(result); } return result } equals(other) { if (!(other instanceof IEntity)) { return false } const thisKeys = Object.keys(this); const otherKeys = Object.keys(other); const thisType = /** @type {typeof IEntity} */(this.constructor).actualClass(); const otherType = /** @type {typeof IEntity} */(other.constructor).actualClass(); if ( thisKeys.length !== otherKeys.length || this.lookbehind != other.lookbehind || !(other instanceof thisType) && !(this instanceof otherType) ) { return false } for (let i = 0; i < thisKeys.length; ++i) { const k = thisKeys[i]; if (!otherKeys.includes(k)) { return false } const a = this[k]; const b = other[k]; if (a instanceof IEntity) { if (!a.equals(b)) { return false } } else if (a instanceof Array && b instanceof Array) { if (a.length !== b.length) { return false } for (let j = 0; j < a.length; ++j) { if (!(a[j] instanceof IEntity && a[j].equals(b[j])) && a[j] !== b[j]) { return false } } } else { if (a !== b) { return false } } } return true } } class BooleanEntity extends IEntity { static grammar = this.createGrammar() static booleanConverter = { fromAttribute: (value, type) => { }, toAttribute: (value, type) => { if (value === true) { return "true" } if (value === false) { return "false" } return "" } } #uppercase = true get uppercase() { return this.#uppercase } set uppercase(value) { this.#uppercase = value; } /** @returns {P} */ static createGrammar() { return Parsernostrum.regArray(/(true)|(True)|(false)|(False)/) .map(v => { const result = (v[1] ?? v[2]) ? new this(true) : new this(false); result.uppercase = (v[2] ?? v[4]) !== undefined; return result }) .label("BooleanEntity") } constructor(value = false) { super(); this.value = value; } serialize( insideString = false, indentation = "", Self = /** @type {typeof IEntity} */(this.constructor), ) { let result = this.value ? this.#uppercase ? "True" : "true" : this.#uppercase ? "False" : "false"; if (Self.serialized) { result = `"${result}"`; } return result } valueOf() { return this.value } } class ElementFactory { /** @type {Map} */ static #elementConstructors = new Map() /** * @param {String} tagName * @param {IElementConstructor} entityConstructor */ static registerElement(tagName, entityConstructor) { ElementFactory.#elementConstructors.set(tagName, entityConstructor); } /** @param {String} tagName */ static getConstructor(tagName) { return ElementFactory.#elementConstructors.get(tagName) } } /** @template {(typeof IEntity)[]} T */ class AlternativesEntity extends IEntity { /** @type {(typeof IEntity)[]} */ static alternatives = [] static className() { let result = super.className(); if (this.alternatives.length) { result += ".accepting(" + this.alternatives.map(v => v.className()).join(", ") + ")"; } return result } static createGrammar() { const grammars = this.alternatives.map(entity => entity.grammar); if (this.alternatives.length == 0 || grammars.includes(this.unknownEntityGrammar)) { return this.unknownEntityGrammar } return Parsernostrum.alt(...grammars) } /** * @template {(typeof IEntity)[]} Types * @param {Types} types */ static accepting(...types) { const result = /** @type {typeof AlternativesEntity & { alternatives: Types }} */( this.asUniqueClass() ); result.alternatives = types; result.grammar = result.createGrammar(); return result } } class Grammar { /** @type {String} */ // @ts-expect-error static numberRegexSource = Parsernostrum.number.getParser().parser.regexp.source static separatedBy = (source, separator, min = 1) => new RegExp( source + "(?:" + separator + source + ")" + (min === 1 ? "*" : min === 2 ? "+" : `{${min},}`) ) static Regex = class { static HexDigit = /[0-9a-fA-F]/ static InsideString = /(?:[^"\\]|\\.)*/ static InsideSingleQuotedString = /(?:[^'\\]|\\.)*/ static Integer = /[\-\+]?\d+(?!\d|\.)/ static Number = /[-\+]?(?:\d*\.)?\d+(?!\d|\.)/ static RealUnit = /\+?(?:0(?:\.\d+)?|1(?:\.0+)?)(?![\.\d])/ // A number between 0 and 1 included static Word = Grammar.separatedBy("[a-zA-Z]", "_") static Symbol = /[a-zA-Z_]\w*/ static DotSeparatedSymbols = Grammar.separatedBy(this.Symbol.source, "\\.") static MultipleWordsSymbols = Grammar.separatedBy(this.Symbol.source, "(?:\\.|\\ +)") static PathFragment = Grammar.separatedBy(this.Symbol.source, "[\\.:]") static PathSpaceFragment = Grammar.separatedBy(this.Symbol.source, "[\\.:\\ ]") static Path = new RegExp(`(?:\\/${this.PathFragment.source}){2,}`) // Multiple (2+) /PathFragment } /* --- Primitive --- */ static null = Parsernostrum.reg(/\(\s*\)/).map(() => null) static true = Parsernostrum.reg(/true/i).map(() => true) static false = Parsernostrum.reg(/false/i).map(() => false) static number = Parsernostrum.regArray( // @ts-expect-error new RegExp(`(${Parsernostrum.number.getParser().parser.regexp.source})|(\\+?inf)|(-inf)`) ).map(([_0, n, plusInf, minusInf]) => n ? Number(n) : plusInf ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY) // @ts-expect-error static bigInt = Parsernostrum.reg(new RegExp(Parsernostrum.number.getParser().parser.regexp.source)).map(BigInt) .map(result => result[2] !== undefined ? Number.POSITIVE_INFINITY : result[3] !== undefined ? Number.NEGATIVE_INFINITY : Number(result[1]) ) static naturalNumber = Parsernostrum.lazy(() => Parsernostrum.reg(/\d+/).map(Number)) static string = Parsernostrum.doubleQuotedString.map(insideString => Utility.unescapeString(insideString)) /* --- Fragment --- */ static colorValue = Parsernostrum.numberByte static word = Parsernostrum.reg(Grammar.Regex.Word) static symbol = Parsernostrum.reg(Grammar.Regex.Symbol) static symbolQuoted = Parsernostrum.reg(new RegExp('"(' + Grammar.Regex.Symbol.source + ')"'), 1) static attributeName = Parsernostrum.reg(Grammar.Regex.DotSeparatedSymbols) static attributeNameQuoted = Parsernostrum.reg(new RegExp('"(' + Grammar.Regex.InsideString.source + ')"'), 1) static guid = Parsernostrum.reg(new RegExp(`${Grammar.Regex.HexDigit.source}{32}`)) static commaSeparation = Parsernostrum.reg(/\s*,\s*(?!\))/) static commaOrSpaceSeparation = Parsernostrum.reg(/\s*,\s*(?!\))|\s+/) static equalSeparation = Parsernostrum.reg(/\s*=\s*/) static hexColorChannel = Parsernostrum.reg(new RegExp(Grammar.Regex.HexDigit.source + "{2}")) /* --- Factory --- */ /** * @param {typeof IEntity} entityType * @param {String[]} key * @returns {typeof IEntity} */ static getAttribute(entityType, [key, ...keys]) { const attribute = entityType?.attributes?.[key]; if (!attribute) { return } if (attribute.prototype instanceof AlternativesEntity) { for (const alternative of /** @type {typeof AlternativesEntity} */(attribute).alternatives) { const candidate = this.getAttribute(alternative, keys); if (candidate) { return candidate } } } if (keys.length > 0) { return this.getAttribute(attribute, keys) } return attribute } /** @param {typeof IEntity} entityType */ static createAttributeGrammar( entityType, attributeNameGrammar = this.attributeName, valueSeparator = this.equalSeparation, handleObjectSet = (values, attributeKey, attributeValue) => { }, ) { return Parsernostrum.seq( attributeNameGrammar, valueSeparator, ).chain(([attributeName, _1]) => { const attributeKey = attributeName.split(Configuration.keysSeparator); const attributeValue = this.getAttribute(entityType, attributeKey); const grammar = attributeValue ? attributeValue.grammar : IEntity.unknownEntityGrammar; const inlined = attributeKey.length > 1; return grammar.map(attributeValue => values => { Utility.objectSet(values, attributeKey, attributeValue); attributeKey.reduce( (acc, cur, i) => { acc[cur]["inlined"] = inlined && i < attributeKey.length - 1; return acc[cur] }, values ); handleObjectSet(values, attributeKey, attributeValue); } ) }) } /** * @template {typeof IEntity & (new (...values: any) => InstanceType)} T * @param {T} entityType * @param {Number} completeness * @return {Parsernostrum>} */ static createEntityGrammar(entityType, entriesSeparator = this.commaSeparation, completeness = null, minKeys = 1) { const lookbehind = entityType.lookbehind instanceof Array ? entityType.lookbehind.join("|") : entityType.lookbehind; return Parsernostrum.seq( Parsernostrum.reg(new RegExp(String.raw`(${lookbehind}\s*)\(\s*`), 1), this.createAttributeGrammar(entityType).sepBy(entriesSeparator, minKeys), Parsernostrum.reg(/\s*(,\s*)?\)/, 1), // optional trailing comma ) .map(([lookbehind, attributes, trailing]) => { let values = {}; if (lookbehind.length) { values["lookbehind"] = lookbehind; } attributes.forEach(attributeSetter => attributeSetter(values)); values["trailing"] = trailing !== undefined; 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 (entityType.lookbehind instanceof Array || entityType.lookbehind !== lookbehind) { entityType = entityType.withLookbehind(lookbehind); } const keys = Object.keys(values); const expectedKeys = Object.keys(entityType.attributes); return completeness != null ? Parsernostrum.success() .assert( v => keys.filter(k => expectedKeys.includes(k)).length / expectedKeys.length >= completeness ) .map(() => new entityType(values)) : Parsernostrum.success().map(() => new entityType(values)) }) } } class ColorChannelEntity extends IEntity { static grammar = this.createGrammar() constructor(value = 0) { super(); this.value = value; } /** @returns {P} */ static createGrammar() { return Parsernostrum.number.map(v => new this(v)) } serialize( insideString = false, indentation = "", Self = /** @type {typeof IEntity} */(this.constructor), ) { let result = this.value.toFixed(6); if (Self.serialized) { result = `"${result}"`; } return result } valueOf() { return this.value } } class LinearColorEntity extends IEntity { static attributes = { ...super.attributes, R: ColorChannelEntity.withDefault(), G: ColorChannelEntity.withDefault(), B: ColorChannelEntity.withDefault(), A: ColorChannelEntity.withDefault(type => new type(1)), } static grammar = this.createGrammar() #H = new ColorChannelEntity() get H() { return this.#H } set H(value) { this.#H = value; } #S = new ColorChannelEntity() get S() { return this.#S } set S(value) { this.#S = value; } #V = new ColorChannelEntity() get V() { return this.#V } set V(value) { this.#V = value; } constructor(values) { super(values); if (values instanceof Array) { values = { R: values[0] ?? 0, G: values[1] ?? 0, B: values[2] ?? 0, A: values[3] ?? 1, }; } /** @type {InstanceType} */ this.R; /** @type {InstanceType} */ this.G; /** @type {InstanceType} */ this.B; /** @type {InstanceType} */ this.A; this.#updateHSV(); } /** @returns {P} */ static createGrammar() { return Grammar.createEntityGrammar(this, Grammar.commaSeparation, 0.5).label("LinearColorEntity") } /** @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} 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: new ColorChannelEntity(1), G: new ColorChannelEntity(1), B: new ColorChannelEntity(1), }) } static getLinearColorFromHexGrammar() { const hexDigit = /[0-9a-fA-F]/; return Parsernostrum.regArray(new RegExp( "#(" + hexDigit.source + "{2})" + "(" + hexDigit.source + "{2})" + "(" + hexDigit.source + "{2})" + "(" + hexDigit.source + "{2})?" )).map(([m, R, G, B, A]) => new this({ R: parseInt(R, 16) / 255, G: parseInt(G, 16) / 255, B: parseInt(B, 16) / 255, A: parseInt(A ?? "FF", 16) / 255, })) } static getLinearColorRGBListGrammar() { return Parsernostrum.seq( Parsernostrum.numberByte, Grammar.commaSeparation, Parsernostrum.numberByte, Grammar.commaSeparation, Parsernostrum.numberByte, ).map(([R, _1, G, _3, B]) => new this({ R: R / 255, G: G / 255, B: B / 255, A: 1, })) } static getLinearColorRGBGrammar() { return Parsernostrum.seq( Parsernostrum.reg(/rgb\s*\(\s*/), this.getLinearColorRGBListGrammar(), Parsernostrum.reg(/\s*\)/) ).map(([_0, linearColor, _2]) => linearColor) } static getLinearColorRGBAGrammar() { return Parsernostrum.seq( Parsernostrum.reg(/rgba\s*\(\s*/), this.getLinearColorRGBListGrammar(), Parsernostrum.reg(/\s*\)/) ).map(([_0, linearColor, _2]) => linearColor) } static getLinearColorFromAnyFormat() { return Parsernostrum.alt( this.getLinearColorFromHexGrammar(), this.getLinearColorRGBAGrammar(), this.getLinearColorRGBGrammar(), this.getLinearColorRGBListGrammar(), ) } #updateHSV() { const r = this.R.value; const g = this.G.value; const b = this.B.value; if (Utility.approximatelyEqual(r, g) && Utility.approximatelyEqual(r, b) && Utility.approximatelyEqual(g, b)) { this.S.value = 0; this.V.value = r; 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; } /** * @param {Number} x * @param {Number} y * @param {Number} v * @param {Number} a */ setFromWheelLocation(x, y, v, a) { const [r, theta] = Utility.getPolarCoordinates(x, y, true); this.setFromHSVA(1 - theta / (2 * Math.PI), r, v, a); } toDimmedColor(minV = 0) { const result = new LinearColorEntity(); result.setFromRGBANumber(this.toNumber()); result.setFromHSVA( result.H.value, result.S.value * 0.6, Math.pow(result.V.value + minV, 0.55) * 0.7 ); return result } toCSSRGBValues() { const r = Math.round(this.R.value * 255); const g = Math.round(this.G.value * 255); const b = Math.round(this.B.value * 255); return i$3`${r}, ${g}, ${b}` } 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 ( Math.round(this.R.value * 0xff) << 24) + (Math.round(this.G.value * 0xff) << 16) + (Math.round(this.B.value * 0xff) << 8) + Math.round(this.A.value * 0xff) } /** @returns {[Number, Number, Number, Number]} */ toArray() { return [this.R.value, this.G.value, this.B.value, 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 LinearColorEntity.printLinearColor(this) } } /** @param {ObjectEntity} entity */ function nodeColor(entity) { switch (entity.getType()) { case Configuration.paths.materialExpressionConstant2Vector: case Configuration.paths.materialExpressionConstant3Vector: case Configuration.paths.materialExpressionConstant4Vector: return Configuration.nodeColors.yellow case Configuration.paths.materialExpressionFunctionInput: case Configuration.paths.materialExpressionTextureCoordinate: case Configuration.paths.materialExpressionWorldPosition: case Configuration.paths.pcgEditorGraphNodeInput: case Configuration.paths.pcgEditorGraphNodeOutput: return Configuration.nodeColors.red case Configuration.paths.makeStruct: return Configuration.nodeColors.darkBlue case Configuration.paths.materialExpressionMaterialFunctionCall: return Configuration.nodeColors.blue case Configuration.paths.materialExpressionTextureSample: return Configuration.nodeColors.darkTurquoise } switch (entity.getClass()) { case Configuration.paths.callFunction: return entity.bIsPureFunc?.valueOf() ? Configuration.nodeColors.green : Configuration.nodeColors.blue case Configuration.paths.niagaraNodeFunctionCall: return Configuration.nodeColors.darkerBlue case Configuration.paths.dynamicCast: return Configuration.nodeColors.turquoise case Configuration.paths.inputDebugKey: case Configuration.paths.inputKey: return Configuration.nodeColors.red case Configuration.paths.createDelegate: case Configuration.paths.enumLiteral: case Configuration.paths.makeArray: case Configuration.paths.makeMap: case Configuration.paths.materialGraphNode: case Configuration.paths.select: return Configuration.nodeColors.green case Configuration.paths.executionSequence: case Configuration.paths.ifThenElse: case Configuration.paths.macro: case Configuration.paths.multiGate: return Configuration.nodeColors.gray case Configuration.paths.functionEntry: case Configuration.paths.functionResult: return Configuration.nodeColors.violet case Configuration.paths.timeline: return Configuration.nodeColors.yellow } if (entity.switchTarget()) { return Configuration.nodeColors.lime } if (entity.isEvent()) { return Configuration.nodeColors.red } if (entity.isComment()) { return (entity.CommentColor ? entity.CommentColor : LinearColorEntity.getWhite()) .toDimmedColor() .toCSSRGBValues() } const pcgSubobject = entity.getPcgSubobject(); if (pcgSubobject) { if (pcgSubobject.NodeTitleColor) { return pcgSubobject.NodeTitleColor.toDimmedColor(0.1).toCSSRGBValues() } switch (entity.PCGNode?.getName(true)) { case "Branch": case "Select": return Configuration.nodeColors.intenseGreen } } if (entity.bIsPureFunc?.valueOf()) { return Configuration.nodeColors.green } return Configuration.nodeColors.blue } /** @template {typeof IEntity} T */ class MirroredEntity extends IEntity { /** @type {typeof IEntity} */ static type /** @param {() => InstanceType} getter */ constructor(getter = null) { super(); const self = /** @type {typeof MirroredEntity} */(this.constructor); getter ??= self.default !== undefined ? /** @type {MirroredEntity} */(self.default(self)).getter : getter; this.getter = getter; } static createGrammar(elementGrammar = this.type?.grammar ?? Parsernostrum.lazy(() => this.unknownEntityGrammar)) { return this.type?.grammar.map(v => new this(() => v)) } /** * @template {typeof IEntity} T * @this {T} * @param {(type: T) => (InstanceType | NullEntity)} value * @returns {T} */ // @ts-expect-error static withDefault(value = type => new type(() => new (type.type)())) { // @ts-expect-error return super.withDefault(value) } /** * @template {typeof IEntity} T * @param {T} type */ static of(type) { const result = /** @type {{type: T, grammar: P> } & typeof MirroredEntity} */( this.asUniqueClass() ); result.type = type; result.grammar = result.createGrammar(); return result } doSerialize( insideString = false, indentation = "", Self = /** @type {typeof MirroredEntity} */(this.constructor), printKey = Self.printKey, keySeparator = Self.keySeparator, attributeSeparator = Self.attributeSeparator, wrap = Self.wrap, ) { const value = this.getter(); return value.serialize(insideString, indentation, Self.type, printKey, keySeparator, attributeSeparator, wrap) } /** @param {IEntity} other */ equals(other) { if (other instanceof MirroredEntity) { other = other.getter?.(); } return this.getter?.().equals(other) } valueOf() { this.valueOf = this.getter().valueOf.bind(this.getter()); return this.valueOf() } toString() { this.toString = this.getter().toString.bind(this.getter()); return this.toString() } } class NumberEntity extends IEntity { static numberRegexSource = String.raw`${Grammar.numberRegexSource}(?<=(?:\.(\d*0+))?)` static grammar = this.createGrammar() /** @type {Number} */ static precision // Can override this.precision #precision get precision() { return /** @type {typeof NumberEntity} */(this.constructor).precision ?? this.#precision } set precision(value) { this.#precision = value; } /** * @protected * @type {Number} */ _value get value() { return this._value } set value(value) { if (value === -0) { value = 0; } this._value = value; } constructor(value = 0, precision = null) { super(); this.value = Number(value); if (precision !== null) { this.#precision = Number(precision); } } /** @returns {P} */ static createGrammar() { return Parsernostrum.regArray( new RegExp(`(?${this.numberRegexSource})|(?\\+?inf)|(?-inf)`) ).map(({ 2: precision, groups: { n, posInf, negInf } }) => new this( n ? Number(n) : posInf ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY, precision?.length ) ).label("NumberEntity") } /** * @template {typeof NumberEntity} T * @this {T} * @returns {T} */ static withPrecision(value = 0) { const result = this.asUniqueClass(); result.precision = value; return result } /** @param {Number} num */ static printNumber(num) { if (num == Number.POSITIVE_INFINITY) { return "inf" } else if (num == Number.NEGATIVE_INFINITY) { return "-inf" } return Utility.minDecimals(num) } serialize( insideString = false, indentation = "", Self = /** @type {typeof NumberEntity} */(this.constructor), ) { if (this.value === Number.POSITIVE_INFINITY) { return "+inf" } if (this.value === Number.NEGATIVE_INFINITY) { return "-inf" } const precision = Self.precision ?? this.precision; let result = precision !== undefined ? this.value.toFixed(precision) : this.value.toString(); if (Self.serialized) { result = `"${result}"`; } return result } valueOf() { return this.value } toString() { return this.value.toString() } } class VectorEntity extends IEntity { static attributes = { ...super.attributes, X: NumberEntity.withDefault(), Y: NumberEntity.withDefault(), Z: NumberEntity.withDefault(), } static grammar = this.createGrammar() constructor(values) { super(values); /** @type {InstanceType} */ this.X; /** @type {InstanceType} */ this.Y; /** @type {InstanceType} */ this.Z; } /** @returns {P} */ static createGrammar() { return Grammar.createEntityGrammar(this, Grammar.commaSeparation, 1).label("VectorEntity") } /** @returns {[Number, Number, Number]} */ toArray() { return [this.X.valueOf(), this.Y.valueOf(), this.Z.valueOf()] } } const sequencerScriptingNameRegex = /\/Script\/SequencerScripting\.MovieSceneScripting(.+)Channel/; const keyNameValue = { "A_AccentGrave": "à", "Add": "Num +", "C_Cedille": "ç", "Decimal": "Num .", "Divide": "Num /", "E_AccentAigu": "é", "E_AccentGrave": "è", "F1": "F1", // Otherwise F and number will be separated "F10": "F10", "F11": "F11", "F12": "F12", "F2": "F2", "F3": "F3", "F4": "F4", "F5": "F5", "F6": "F6", "F7": "F7", "F8": "F8", "F9": "F9", "Gamepad_Special_Left_X": "Touchpad Button X Axis", "Gamepad_Special_Left_Y": "Touchpad Button Y Axis", "Mouse2D": "Mouse XY 2D-Axis", "Multiply": "Num *", "Section": "§", "Subtract": "Num -", "Tilde": "`", }; /** @param {String} value */ function numberFromText(value = "") { value = value.toLowerCase(); switch (value) { case "zero": return 0 case "one": return 1 case "two": return 2 case "three": return 3 case "four": return 4 case "five": return 5 case "six": return 6 case "seven": return 7 case "eight": return 8 case "nine": return 9 } } function keyName(value) { /** @type {String} */ let result = keyNameValue[value]; if (result) { return result } result = numberFromText(value)?.toString(); if (result) { return result } const match = value.match(/NumPad([a-zA-Z]+)/); if (match) { result = numberFromText(match[1]).toString(); if (result) { return "Num " + result } } } /** * @param {ObjectEntity} entity * @returns {String} */ function nodeTitle(entity) { let input; switch (entity.getType()) { case Configuration.paths.asyncAction: if (entity.ProxyFactoryFunctionName) { return Utility.formatStringName(entity.ProxyFactoryFunctionName?.toString()) } case Configuration.paths.actorBoundEvent: case Configuration.paths.componentBoundEvent: return `${Utility.formatStringName(entity.DelegatePropertyName?.toString())} (${entity.ComponentPropertyName?.toString() ?? "Unknown"})` case Configuration.paths.callDelegate: return `Call ${entity.DelegateReference?.MemberName?.toString() ?? "None"}` case Configuration.paths.createDelegate: return "Create Event" case Configuration.paths.customEvent: if (entity.CustomFunctionName) { return entity.CustomFunctionName?.toString() } case Configuration.paths.dynamicCast: if (!entity.TargetType) { return "Bad cast node" // Target type not found } return `Cast To ${entity.TargetType?.getName()}` case Configuration.paths.enumLiteral: return `Literal enum ${entity.Enum?.getName()}` case Configuration.paths.event: return `Event ${(entity.EventReference?.MemberName?.toString() ?? "").replace(/^Receive/, "")}` case Configuration.paths.executionSequence: return "Sequence" case Configuration.paths.forEachElementInEnum: return `For Each ${entity.Enum?.getName()}` case Configuration.paths.forEachLoopWithBreak: return "For Each Loop with Break" case Configuration.paths.functionEntry: return entity.FunctionReference?.MemberName?.toString() === "UserConstructionScript" ? "Construction Script" : entity.FunctionReference?.MemberName?.toString() case Configuration.paths.functionResult: return "Return Node" case Configuration.paths.ifThenElse: return "Branch" case Configuration.paths.makeStruct: if (entity.StructType) { return `Make ${entity.StructType.getName()}` } case Configuration.paths.materialExpressionComponentMask: { const materialObject = entity.getMaterialSubobject(); if (materialObject) { return `Mask ( ${Configuration.rgba .filter(k => /** @type {MirroredEntity} */(materialObject[k]).getter().value === true) .map(v => v + " ") .join("")})` } } case Configuration.paths.materialExpressionConstant: input ??= [entity.getCustomproperties().find(pinEntity => pinEntity.PinName.toString() == "Value")?.DefaultValue]; case Configuration.paths.materialExpressionConstant2Vector: input ??= [ entity.getCustomproperties().find(pinEntity => pinEntity.PinName?.toString() == "X")?.DefaultValue, entity.getCustomproperties().find(pinEntity => pinEntity.PinName?.toString() == "Y")?.DefaultValue, ]; case Configuration.paths.materialExpressionConstant3Vector: case Configuration.paths.materialExpressionConstant4Vector: if (!input) { const vector = entity.getCustomproperties() .find(pinEntity => pinEntity.PinName?.toString() == "Constant") ?.DefaultValue; input = vector instanceof VectorEntity ? [vector.X, vector.Y, vector.Z].map(v => v.valueOf()) : vector instanceof LinearColorEntity ? [vector.R, vector.G, vector.B, vector.A].map(v => v.valueOf()) : /** @type {Number[]} */([]); } if (input.length > 0) { return input.map(v => Utility.printExponential(v)).join(",") } break case Configuration.paths.materialExpressionFunctionInput: { const materialObject = entity.getMaterialSubobject(); const inputName = materialObject?.InputName ?? "In"; const inputType = materialObject?.InputType?.value.match(/^.+?_(\w+)$/)?.[1] ?? "Vector3"; return `Input ${inputName} (${inputType})` } case Configuration.paths.materialExpressionLogarithm: return "Ln" case Configuration.paths.materialExpressionLogarithm10: return "Log10" case Configuration.paths.materialExpressionLogarithm2: return "Log2" case Configuration.paths.materialExpressionMaterialFunctionCall: const materialFunction = entity.getMaterialSubobject()?.MaterialFunction; if (materialFunction) { return materialFunction.getName() } break case Configuration.paths.materialExpressionSquareRoot: return "Sqrt" case Configuration.paths.materialExpressionSubtract: const materialObject = entity.getMaterialSubobject(); if (materialObject) { return `Subtract(${materialObject.ConstA ?? "1"},${materialObject.ConstB ?? "1"})` } case Configuration.paths.metasoundEditorGraphExternalNode: { const name = entity["ClassName"]?.["Name"]; if (name) { switch (name) { case "Add": return "+" default: return name } } } case Configuration.paths.pcgEditorGraphNodeInput: return "Input" case Configuration.paths.pcgEditorGraphNodeOutput: return "Output" case Configuration.paths.spawnActorFromClass: let className = entity.getCustomproperties() .find(pinEntity => pinEntity.PinName.toString() == "ReturnValue") ?.PinType ?.PinSubCategoryObject ?.getName(); if (className === "Actor") { className = null; } return `SpawnActor ${Utility.formatStringName(className ?? "NONE")}` case Configuration.paths.switchEnum: return `Switch on ${entity.Enum?.getName() ?? "Enum"}` case Configuration.paths.switchInteger: return `Switch on Int` case Configuration.paths.variableGet: return "" case Configuration.paths.variableSet: return "SET" } let switchTarget = entity.switchTarget(); if (switchTarget) { if (switchTarget[0] !== "E") { switchTarget = Utility.formatStringName(switchTarget); } return `Switch on ${switchTarget}` } if (entity.isComment()) { return entity.NodeComment.toString() } const keyNameSymbol = entity.getHIDAttribute(); if (keyNameSymbol) { const name = keyNameSymbol.toString(); let title = keyName(name) ?? Utility.formatStringName(name); if (entity.getClass() === Configuration.paths.inputDebugKey) { title = "Debug Key " + title; } else if (entity.getClass() === Configuration.paths.getInputAxisKeyValue) { title = "Get " + title; } return title } if (entity.getClass() === Configuration.paths.macro) { return Utility.formatStringName(entity.MacroGraphReference?.getMacroName()) } if (entity.isMaterial() && entity.getMaterialSubobject()) { let result = nodeTitle(entity.getMaterialSubobject()); result = result.match(/Material Expression (.+)/)?.[1] ?? result; return result } if (entity.isPcg() && entity.getPcgSubobject()) { let pcgSubobject = entity.getPcgSubobject(); let result = pcgSubobject.NodeTitle ? pcgSubobject.NodeTitle.toString() : nodeTitle(pcgSubobject); return result } const subgraphObject = entity.getSubgraphObject(); if (subgraphObject) { return subgraphObject.Graph.getName() } const settingsObject = entity.getSettingsObject(); if (settingsObject) { if (settingsObject.ExportPath.type === Configuration.paths.pcgHiGenGridSizeSettings) { return `Grid Size: ${( settingsObject.HiGenGridSize?.toString().match(/\d+/)?.[0]?.concat("00") ?? settingsObject.HiGenGridSize?.toString().match(/^\w+$/)?.[0] ) ?? "256"}` } if (settingsObject.BlueprintElementInstance) { return Utility.formatStringName(settingsObject.BlueprintElementType.getName()) } if (settingsObject.Operation) { const match = settingsObject.Name?.toString().match(/PCGMetadata(\w+)Settings_\d+/); if (match) { return Utility.formatStringName(match[1] + ": " + settingsObject.Operation) } } const settingsSubgraphObject = settingsObject.getSubgraphObject(); if (settingsSubgraphObject && settingsSubgraphObject.Graph) { return settingsSubgraphObject.Graph.getName() } } let memberName = entity.FunctionReference?.MemberName?.toString(); if (memberName) { const memberParent = entity.FunctionReference.MemberParent?.path ?? ""; switch (memberName) { case "AddKey": let result = memberParent.match(sequencerScriptingNameRegex); if (result) { return `Add Key (${Utility.formatStringName(result[1])})` } case "Concat_StrStr": return "Append" } const memberNameTraceLineMatch = memberName.match(Configuration.lineTracePattern); if (memberNameTraceLineMatch) { return "Line Trace" + (memberNameTraceLineMatch[1] === "Multi" ? " Multi " : " ") + (memberNameTraceLineMatch[2] === "" ? "By Channel" : Utility.formatStringName(memberNameTraceLineMatch[2]) ) } switch (memberParent) { case Configuration.paths.blueprintGameplayTagLibrary: case Configuration.paths.kismetMathLibrary: case Configuration.paths.slateBlueprintLibrary: case Configuration.paths.timeManagementBlueprintLibrary: const leadingLetter = memberName.match(/[BF]([A-Z]\w+)/); if (leadingLetter) { // Some functions start with B or F (Like FCeil, FMax, BMin) memberName = leadingLetter[1]; } switch (memberName) { case "Abs": return "ABS" case "BooleanAND": return "AND" case "BooleanNAND": return "NAND" case "BooleanOR": return "OR" case "Exp": return "e" case "LineTraceSingle": return "Line Trace By Channel" case "Max": return "MAX" case "MaxInt64": return "MAX" case "Min": return "MIN" case "MinInt64": return "MIN" case "Not_PreBool": return "NOT" case "Sin": return "SIN" case "Sqrt": return "SQRT" case "Square": return "^2" // Dot products not respecting MemberName pattern case "CrossProduct2D": return "cross" case "Vector4_CrossProduct3": return "cross3" case "DotProduct2D": case "Vector4_DotProduct": return "dot" case "Vector4_DotProduct3": return "dot3" } if (memberName.startsWith("Add_")) { return "+" } if (memberName.startsWith("And_")) { return "&" } if (memberName.startsWith("Conv_")) { return "" // Conversion nodes do not have visible names } if (memberName.startsWith("Cross_")) { return "cross" } if (memberName.startsWith("Divide_")) { return String.fromCharCode(0x00f7) } if (memberName.startsWith("Dot_")) { return "dot" } if (memberName.startsWith("EqualEqual_")) { return "==" } if (memberName.startsWith("Greater_")) { return ">" } if (memberName.startsWith("GreaterEqual_")) { return ">=" } if (memberName.startsWith("Less_")) { return "<" } if (memberName.startsWith("LessEqual_")) { return "<=" } if (memberName.startsWith("Multiply_")) { return String.fromCharCode(0x2a2f) } if (memberName.startsWith("Not_")) { return "~" } if (memberName.startsWith("NotEqual_")) { return "!=" } if (memberName.startsWith("Or_")) { return "|" } if (memberName.startsWith("Percent_")) { return "%" } if (memberName.startsWith("Subtract_")) { return "-" } if (memberName.startsWith("Xor_")) { return "^" } break case Configuration.paths.blueprintSetLibrary: { const setOperationMatch = memberName.match(/Set_(\w+)/); if (setOperationMatch) { return Utility.formatStringName(setOperationMatch[1]).toUpperCase() } } break case Configuration.paths.blueprintMapLibrary: { const setOperationMatch = memberName.match(/Map_(\w+)/); if (setOperationMatch) { return Utility.formatStringName(setOperationMatch[1]).toUpperCase() } } break case Configuration.paths.kismetArrayLibrary: { const arrayOperationMath = memberName.match(/Array_(\w+)/); if (arrayOperationMath) { return arrayOperationMath[1].toUpperCase() } } break } return Utility.formatStringName(memberName) } if (entity.OpName) { switch (entity.OpName.toString()) { case "Boolean::LogicAnd": return "Logic AND" case "Boolean::LogicEq": return "==" case "Boolean::LogicNEq": return "!=" case "Boolean::LogicNot": return "Logic NOT" case "Boolean::LogicOr": return "Logic OR" case "Matrix::MatrixMultiply": return "Multiply (Matrix * Matrix)" case "Matrix::MatrixVectorMultiply": return "Multiply (Matrix * Vector4)" case "Numeric::Abs": return "Abs" case "Numeric::Add": return "+" case "Numeric::DistancePos": return "Distance" case "Numeric::Mul": return String.fromCharCode(0x2a2f) } return Utility.formatStringName(entity.OpName.toString()).replaceAll("::", " ") } if (entity.FunctionDisplayName) { return Utility.formatStringName(entity.FunctionDisplayName.toString()) } if (entity.ObjectRef) { return entity.ObjectRef.getName() } return Utility.formatStringName(entity.getNameAndCounter()[0]) } /** @param {ObjectEntity} entity */ function nodeIcon(entity) { if (entity.isMaterial() || entity.isPcg() || entity.isNiagara()) { return null } switch (entity.getType()) { case Configuration.paths.addDelegate: case Configuration.paths.asyncAction: case Configuration.paths.callDelegate: case Configuration.paths.createDelegate: case Configuration.paths.functionEntry: case Configuration.paths.functionResult: return SVGIcon.node case Configuration.paths.customEvent: return SVGIcon.event case Configuration.paths.doN: return SVGIcon.doN case Configuration.paths.doOnce: return SVGIcon.doOnce case Configuration.paths.dynamicCast: return SVGIcon.cast case Configuration.paths.enumLiteral: return SVGIcon.enum case Configuration.paths.event: return SVGIcon.event case Configuration.paths.executionSequence: case Configuration.paths.multiGate: return SVGIcon.sequence case Configuration.paths.flipflop: return SVGIcon.flipflop case Configuration.paths.forEachElementInEnum: case Configuration.paths.forLoop: case Configuration.paths.forLoopWithBreak: case Configuration.paths.whileLoop: return SVGIcon.loop case Configuration.paths.forEachLoop: case Configuration.paths.forEachLoopWithBreak: return SVGIcon.forEachLoop case Configuration.paths.ifThenElse: return SVGIcon.branchNode case Configuration.paths.isValid: return SVGIcon.questionMark case Configuration.paths.makeArray: return SVGIcon.makeArray case Configuration.paths.makeMap: return SVGIcon.makeMap case Configuration.paths.makeSet: return SVGIcon.makeSet case Configuration.paths.makeStruct: return SVGIcon.makeStruct case Configuration.paths.metasoundEditorGraphExternalNode: return SVGIcon.metasoundFunction case Configuration.paths.select: return SVGIcon.select case Configuration.paths.spawnActorFromClass: return SVGIcon.spawnActor case Configuration.paths.timeline: return SVGIcon.timer } if (entity.switchTarget()) { return SVGIcon.switch } if (nodeTitle(entity).startsWith("Break")) { return SVGIcon.breakStruct } if (entity.getClass() === Configuration.paths.macro) { return SVGIcon.macro } const hidValue = entity.getHIDAttribute()?.toString(); if (hidValue) { if (hidValue.includes("Mouse")) { return SVGIcon.mouse } else if (hidValue.includes("Gamepad_Special")) { return SVGIcon.keyboard // It is called Touchpad in UE } else if (hidValue.includes("Gamepad") || hidValue.includes("Steam")) { return SVGIcon.gamepad } else if (hidValue.includes("Touch")) { return SVGIcon.touchpad } else { return SVGIcon.keyboard } } if (entity.getDelegatePin()) { return SVGIcon.event } if (entity.ObjectRef?.type === Configuration.paths.ambientSound) { return SVGIcon.sound } return SVGIcon.functionSymbol } /** @template {typeof IEntity} T */ class ArrayEntity extends IEntity { /** @type {typeof IEntity} */ static type static grammar = this.createGrammar() get length() { return this.values.length } /** @param {(ExtractType)[]} values */ constructor(values = []) { super(); this.values = values; } /** @returns {P>} */ static createGrammar(elementGrammar = this.type?.grammar ?? Parsernostrum.lazy(() => this.unknownEntityGrammar)) { return this.inlined ? elementGrammar : Parsernostrum.seq( Parsernostrum.reg(/\(\s*/), elementGrammar.sepBy(Grammar.commaSeparation).opt(), Parsernostrum.reg(/\s*(,\s*)?\)/, 1), ).map(([_0, values, trailing]) => { values = values instanceof Array ? values : []; let Self = this; if ((trailing !== undefined) !== Self.trailing) { Self = Self.flagTrailing(trailing !== undefined); } const result = new Self(values); return result }).label(`ArrayEntity of ${this.type?.className() ?? "unknown values"}`) } /** * @template {typeof IEntity} T * @this {T} */ static flagInlined(value = true) { const result = this.asUniqueClass(); result.inlined = value; result.grammar = /** @type {P} */(result.createGrammar()); return result } /** * @template {typeof IEntity} T * @param {T} type */ static of(type) { const result = /** @type {{type: T, grammar: P> } & typeof ArrayEntity} */( this.asUniqueClass() ); result.type = type; result.grammar = /** @type {P} */(result.createGrammar()); return result } doSerialize( insideString = false, indentation = "", Self = /** @type {typeof ArrayEntity} */(this.constructor), printKey = Self.printKey, keySeparator = Self.keySeparator, attributeSeparator = Self.attributeSeparator, wrap = Self.wrap, ) { if (Self.inlined) { return super.serialize.bind( this.values, insideString, indentation, Self, printKey, keySeparator, attributeSeparator, wrap )() } let result = this.values.map(v => v?.serialize(insideString)).join(Self.attributeSeparator); if (this.trailing) { result += Self.attributeSeparator; } return `(${result})` } valueOf() { return this.values } /** @param {IEntity} other */ equals(other) { if (!(other instanceof ArrayEntity) || this.values.length !== other.values.length) { return false } for (let i = 0; i < this.values.length; ++i) { if (!this.values[i].equals(other.values[i])) { return false } } return true } } var crypto; if (typeof window === "undefined") { // When used in nodejs, mainly for test purpose import('crypto').then(mod => crypto = mod.default).catch(); } else { crypto = window.crypto; } class GuidEntity extends IEntity { static grammar = this.createGrammar() static generateGuid() { let values = new Uint32Array(4); crypto.getRandomValues(values); let guid = ""; values.forEach(n => { guid += ("0".repeat(8) + n.toString(16).toUpperCase()).slice(-8); }); return guid } constructor(value = GuidEntity.generateGuid()) { super(); this.value = value; } /** @returns {P} */ static createGrammar() { return Parsernostrum.reg(/[0-9A-F]{32}/i).map(v => new this(v)).label("GuidEntity") } serialize( insideString = false, indentation = "", Self = /** @type {typeof IEntity} */(this.constructor), ) { let result = this.value; if (Self.serialized) { result = `"${result}"`; } return result } toString() { return this.value } } class IntegerEntity extends NumberEntity { static grammar = this.createGrammar() get value() { return super.value } set value(value) { value = Math.trunc(value); if (value >= 1 << 31 && value < -(1 << 31)) { value = Math.floor(value); super.value = value; } } /** @returns {P} */ static createGrammar() { return Parsernostrum.numberInteger.map(v => new this(v)) } } class NaturalNumberEntity extends IntegerEntity { static grammar = this.createGrammar() get value() { return super.value } set value(value) { value = Math.round(Utility.clamp(value, 0)); super.value = value; } /** @returns {P} */ static createGrammar() { return Parsernostrum.numberNatural.map(v => new this(v)) } } const colors = { [Configuration.paths.niagaraBool]: i$3`146, 0, 0`, [Configuration.paths.niagaraDataInterfaceVolumeTexture]: i$3`0, 168, 242`, [Configuration.paths.niagaraFloat]: i$3`160, 250, 68`, [Configuration.paths.niagaraMatrix]: i$3`0, 88, 200`, [Configuration.paths.niagaraNumeric]: i$3`0, 88, 200`, [Configuration.paths.niagaraPosition]: i$3`251, 146, 251`, [Configuration.paths.quat4f]: i$3`0, 88, 200`, [Configuration.paths.rotator]: i$3`157, 177, 251`, [Configuration.paths.transform]: i$3`227, 103, 0`, [Configuration.paths.vector]: i$3`251, 198, 34`, [Configuration.paths.vector3f]: i$3`250, 200, 36`, [Configuration.paths.vector4f]: i$3`0, 88, 200`, "Any": i$3`132, 132, 132`, "Any[]": i$3`132, 132, 132`, "audio": i$3`252, 148, 252`, "blue": i$3`0, 0, 255`, "bool": i$3`146, 0, 0`, "byte": i$3`0, 109, 99`, "class": i$3`88, 0, 186`, "default": i$3`255, 255, 255`, "delegate": i$3`255, 56, 56`, "enum": i$3`0, 109, 99`, "exec": i$3`240, 240, 240`, "float": i$3`160, 252, 70`, "green": i$3`0, 255, 0`, "int": i$3`31, 224, 172`, "int32": i$3`30, 224, 172`, "int64": i$3`169, 223, 172`, "interface": i$3`238, 252, 168`, "name": i$3`201, 128, 251`, "object": i$3`0, 168, 242`, "Param": i$3`255, 166, 39`, "Param[]": i$3`255, 166, 39`, "Point": i$3`63, 137, 255`, "Point[]": i$3`63, 137, 255`, "real": i$3`54, 208, 0`, "red": i$3`255, 0, 0`, "string": i$3`251, 0, 208`, "struct": i$3`0, 88, 200`, "Surface": i$3`69, 196, 126`, "Surface[]": i$3`69, 196, 126`, "text": i$3`226, 121, 167`, "time": i$3`148, 252, 252`, "Volume": i$3`230, 69, 188`, "Volume[]": i$3`230, 69, 188`, "wildcard": i$3`128, 120, 120`, }; const pinColorMaterial = i$3`120, 120, 120`; /** @param {PinEntity} entity */ function pinColor(entity) { if (entity.PinType.PinCategory?.toString() === "mask") { const result = colors[entity.PinType.PinSubCategory?.toString()]; if (result) { return result } } else if (entity.PinType.PinCategory?.toString() === "optional") { return pinColorMaterial } return colors[entity.getType()] ?? colors[entity.PinType.PinCategory?.toString().toLowerCase()] ?? colors["default"] } /** @param {PinEntity} entity */ function pinTitle(entity) { let result = entity.PinFriendlyName ? entity.PinFriendlyName.toString() : Utility.formatStringName(entity.PinName?.toString() ?? ""); let match; if (match = entity.PinToolTip?.toString().match(/\s*(.+?(?=\n)|.+\S)\s*/)) { if (match[1].toLowerCase() === result.toLowerCase()) { return match[1] // In case they match, then keep the case of the PinToolTip } } return result } class ByteEntity extends IntegerEntity { static grammar = this.createGrammar() get value() { return super.value } set value(value) { value = Math.trunc(value); if (value >= 0 && value < 1 << 8) { super.value = value; } } /** @returns {P} */ createGrammar() { // @ts-expect-error return Parsernostrum.numberByte.map(v => new this(v)) } } class StringEntity extends IEntity { static grammar = this.createGrammar() static escapedCharacters = /['"\\]/g static unescapedBackslash = /(?<=(?:[^\\]|^)(?:\\\\)*)\\(?!\\)/ constructor(value = "") { super(); this.value = value; } /** @returns {P} */ static createGrammar() { return Parsernostrum.doubleQuotedString .map(insideString => new this(StringEntity.unescape(insideString))) .label("StringEntity") } /** @param {String} value */ static escape(value, inline = true) { let result = value.replaceAll(new RegExp(`(${StringEntity.escapedCharacters.source})`, "g"), '\\$1'); if (inline) { result = result .replaceAll("\n", "\\n") // Replace newline with \n .replaceAll("\t", "\\t"); // Replace tab with \t } return result } /** @param {String} value */ static unescape(value) { return value .replaceAll(new RegExp(StringEntity.unescapedBackslash.source + "t", "g"), "\t") // Replace tab with \t .replaceAll(new RegExp(StringEntity.unescapedBackslash.source + "n", "g"), "\n") // Replace newline with \n .replaceAll(new RegExp(`\\\\(${StringEntity.escapedCharacters.source})`, "g"), "$1") } doSerialize(insideString = false) { let result = `"${StringEntity.escape(this.value)}"`; if (insideString) { result = StringEntity.escape(result, false); } return result } valueOf() { return this.value } toString() { return this.value } } class ComputedTypeEntity extends IEntity { static grammar = this.createGrammar() /** @type {(entity: IEntity) => typeof IEntity} */ static f static createGrammar() { return StringEntity.grammar } /** * @template {typeof ComputedTypeEntity.f} T * @param {T} producer */ static from(producer) { const result = /** @type {(typeof ComputedTypeEntity) & { f: T }} */(this.asUniqueClass()); result.f = producer; return result } /** @param {IEntity} entity */ static compute(entity) { return this.f(entity) } } class SymbolEntity extends IEntity { static attributeConverter = { fromAttribute: (value, type) => new this(value), toAttribute: (value, type) => value.toString() } static grammar = this.createGrammar() /** @returns {P} */ static createGrammar() { return Grammar.symbol.map(v => new this(v)).label("SymbolEntity") } constructor(value = "") { super(); this.value = value; } serialize( insideString = false, indentation = "", Self = /** @type {typeof IEntity} */(this.constructor), ) { let result = this.value; if (Self.serialized) { result = `"${result}"`; } return result } toString() { return this.value } } class EnumEntity extends SymbolEntity { static grammar = this.createGrammar() /** @returns {P} */ static createGrammar() { return Grammar.symbol.map(v => new this(v)) } } class EnumDisplayValueEntity extends EnumEntity { static grammar = this.createGrammar() /** @returns {P} */ static createGrammar() { return Parsernostrum.reg(Grammar.Regex.InsideString).map(v => new this(v)) } } class InvariantTextEntity extends IEntity { static lookbehind = "INVTEXT" static grammar = this.createGrammar() constructor(value = "") { super(); this.value = value; } /** @returns {P} */ static createGrammar() { return Parsernostrum.alt( Parsernostrum.seq( Parsernostrum.reg(new RegExp(`${this.lookbehind}\\s*\\(`)), Parsernostrum.doubleQuotedString, Parsernostrum.reg(/\s*\)/) ).map(([_0, value, _2]) => Number(value)), Parsernostrum.reg(new RegExp(this.lookbehind)).map(() => 0) // InvariantTextEntity can not have arguments ) .map(value => new this(value)) .label("InvariantTextEntity") } doSerialize() { return this.lookbehind + "(" + this.value + ")" } valueOf() { return this.value } } class LocalizedTextEntity extends IEntity { static attributeSeparator = ", " static printKey = k => "" static lookbehind = "NSLOCTEXT" static attributes = { ...super.attributes, namespace: StringEntity.withDefault(), key: StringEntity.withDefault(), value: StringEntity.withDefault(), } static grammar = this.createGrammar() constructor(values = {}) { super(values); /** @type {InstanceType} */ this.namespace; /** @type {InstanceType} */ this.key; /** @type {InstanceType} */ this.value; } /** @returns {P} */ static createGrammar() { return Parsernostrum.regArray(new RegExp( String.raw`${LocalizedTextEntity.lookbehind}\s*\(` + String.raw`\s*"(?${Grammar.Regex.InsideString.source})"\s*,` + String.raw`\s*"(?${Grammar.Regex.InsideString.source})"\s*,` + String.raw`\s*"(?${Grammar.Regex.InsideString.source})"\s*` + String.raw`(?,\s+)?` + String.raw`\)`, "m" )).map(({ groups: { namespace, key, value, trailing } }) => { return new this({ namespace: new (this.attributes.namespace)(Utility.unescapeString(namespace)), key: new (this.attributes.namespace)(Utility.unescapeString(key)), value: new (this.attributes.namespace)(Utility.unescapeString(value)), trailing: trailing !== undefined, }) }).label("LocalizedTextEntity") } toString() { return Utility.capitalFirstLetter(this.value.valueOf()) } } class FormatTextEntity extends IEntity { static attributeSeparator = ", " static lookbehind = ["LOCGEN_FORMAT_NAMED", "LOCGEN_FORMAT_ORDERED"] static grammar = this.createGrammar() /** @param {(StringEntity | LocalizedTextEntity | InvariantTextEntity | FormatTextEntity)[]} values */ constructor(values) { super(); this.values = values; } /** @returns {P} */ static createGrammar() { return Parsernostrum.lazy(() => Parsernostrum.seq( // Resulting regex: /(LOCGEN_FORMAT_NAMED|LOCGEN_FORMAT_ORDERED)\s*/ Parsernostrum.reg(new RegExp(String.raw`(${this.lookbehind.join("|")})\s*\(\s*`), 1), Parsernostrum.alt( ...[StringEntity, LocalizedTextEntity, InvariantTextEntity, FormatTextEntity].map(type => type.grammar) ).sepBy(Parsernostrum.reg(/\s*\,\s*/)), Parsernostrum.reg(/\s*\)/) ) .map(([lookbehind, values]) => { const result = new this(values); result.lookbehind = lookbehind; return result })) .label("FormatTextEntity") } doSerialize( insideString = false, indentation = "", Self = /** @type {typeof FormatTextEntity} */(this.constructor), printKey = Self.printKey, keySeparator = Self.keySeparator, attributeSeparator = Self.attributeSeparator, wrap = Self.wrap, ) { const separator = Self.attributeSeparator; return this.lookbehind + "(" + this.values.map(v => v.serialize(insideString)).join(separator) + (Self.trailing ? separator : "") + ")" } toString() { const pattern = this.values?.[0]?.toString(); // The pattern is always the first element of the array if (!pattern) { return "" } const values = this.values.slice(1).map(v => v?.valueOf()); let result = this.lookbehind == "LOCGEN_FORMAT_NAMED" ? pattern.replaceAll(/\{([a-zA-Z]\w*)\}/g, (substring, arg) => { const argLocation = values.indexOf(arg) + 1; return argLocation > 0 && argLocation < values.length ? values[argLocation] : substring }) : this.lookbehind == "LOCGEN_FORMAT_ORDERED" ? pattern.replaceAll(/\{(\d+)\}/g, (substring, arg) => { const argValue = Number(arg); return argValue < values.length ? values[argValue] : substring }) : ""; return result } } class Integer64Entity extends IEntity { static grammar = this.createGrammar() /** * @protected * @type {bigint} */ _value get value() { return this._value } set value(value) { if (value >= -(1n << 63n) && value < 1n << 63n) { this._value = value; } } /** @param {bigint | Number} value */ constructor(value = 0n) { super(); this.value = BigInt(value); } /** @returns {P} */ static createGrammar() { return Parsernostrum.numberBigInteger.map(v => new this(v)) } serialize( insideString = false, indentation = "", Self = /** @type {typeof IEntity} */(this.constructor), ) { let result = this.value.toString(); if (Self.serialized) { result = `"${result}"`; } return result } valueOf() { return this.value } toString() { return this.value.toString() } } class ObjectReferenceEntity extends IEntity { /** @protected */ static _quotedParser = Parsernostrum.regArray(new RegExp( `'"(${Grammar.Regex.InsideString.source})"'` + "|" + `'(${Grammar.Regex.InsideSingleQuotedString.source})'` )).map(([_0, a, b]) => a ?? b) static typeReference = Parsernostrum.reg( // @ts-expect-error new RegExp(Grammar.Regex.Path.source + "|" + Grammar.symbol.getParser().regexp.source) ) static fullReferenceGrammar = this.createFullReferenceGrammar() static grammar = this.createGrammar() #type get type() { return this.#type } set type(value) { this.#type = value; } #path get path() { return this.#path } set path(value) { this.#path = value; } #fullEscaped /** @type {String} */ #full get full() { return this.#full } set full(value) { this.#full = value; } constructor(type = "None", path = "", full = null) { super(); this.#type = type; this.#path = path; this.#full = full ?? ( this.type.includes("/") || this.path ? `"${this.type + (this.path ? (`'${this.path}'`) : "")}"` : this.type ); } /** @returns {P} */ static createGrammar() { return Parsernostrum.alt( this.createFullReferenceSerializedGrammar(), this.createFullReferenceGrammar(), this.createTypeReferenceGrammar(), ).label("ObjectReferenceEntity") } /** @returns {P} */ static createFullReferenceGrammar() { return Parsernostrum.regArray( new RegExp( // @ts-expect-error "(" + this.typeReference.getParser().regexp.source + ")" // @ts-expect-error + "(?:" + this._quotedParser.getParser().parser.regexp.source + ")" ) ).map(([full, type, ...path]) => new this(type, path.find(v => v), full)) } /** @returns {P} */ static createFullReferenceSerializedGrammar() { return Parsernostrum.regArray( new RegExp( '"(' + Grammar.Regex.InsideString.source + "?)" + "(?:'(" + Grammar.Regex.InsideSingleQuotedString.source + `?)')?"` ) ).map(([full, type, path]) => new this(type, path, full)) } /** @returns {P} */ static createTypeReferenceGrammar() { return this.typeReference.map(v => new this(v, "", v)) } static createNoneInstance() { return new ObjectReferenceEntity("None", "", "None") } getName(dropCounter = false) { return Utility.getNameFromPath(this.path.replace(/_C$/, ""), dropCounter) } doSerialize(insideString = false) { if (insideString) { if (this.#fullEscaped === undefined) { this.#fullEscaped = Utility.escapeString(this.#full, false); } return this.#fullEscaped } return this.full } /** @param {IEntity} other */ equals(other) { if (!(other instanceof ObjectReferenceEntity)) { return false } return this.type == other.type && this.path == other.path } } class PinReferenceEntity extends IEntity { static grammar = this.createGrammar() /** * @param {SymbolEntity} objectName * @param {GuidEntity} pinGuid */ constructor(objectName = null, pinGuid = null) { super(); this.objectName = objectName; this.pinGuid = pinGuid; } /** @returns {P} */ static createGrammar() { return Parsernostrum.seq( SymbolEntity.grammar, Parsernostrum.whitespace, GuidEntity.grammar ) .map(([objectName, _1, pinGuid]) => new this(objectName, pinGuid)) .label("PinReferenceEntity") } doSerialize() { return this.objectName.serialize() + " " + this.pinGuid.serialize() } } class FunctionReferenceEntity extends IEntity { static attributes = { ...super.attributes, MemberParent: ObjectReferenceEntity, MemberName: StringEntity, MemberGuid: GuidEntity, } static grammar = this.createGrammar() constructor(values) { super(values); /** @type {InstanceType} */ this.MemberParent; /** @type {InstanceType} */ this.MemberName; /** @type {InstanceType} */ this.MemberGuid; } /** @returns {P} */ static createGrammar() { return Grammar.createEntityGrammar(this, Grammar.commaSeparation, 0, 0) } } class PinTypeEntity extends IEntity { static attributes = { ...super.attributes, PinCategory: StringEntity.withDefault(), PinSubCategory: StringEntity, PinSubCategoryObject: ObjectReferenceEntity, PinSubCategoryMemberReference: FunctionReferenceEntity, ContainerType: SymbolEntity, bIsReference: BooleanEntity, bIsConst: BooleanEntity, bIsWeakPointer: BooleanEntity, bIsUObjectWrapper: BooleanEntity, bSerializeAsSinglePrecisionFloat: BooleanEntity, } static grammar = this.createGrammar() constructor(values = {}) { super(values); /** @type {InstanceType} */ this.PinCategory; /** @type {InstanceType} */ this.PinSubCategory; /** @type {InstanceType} */ this.PinSubCategoryObject; /** @type {InstanceType} */ this.PinSubCategoryMemberReference; /** @type {InstanceType} */ this.ContainerType; /** @type {InstanceType} */ this.bIsReference; /** @type {InstanceType} */ this.bIsConst; /** @type {InstanceType} */ this.bIsWeakPointer; /** @type {InstanceType} */ this.bIsUObjectWrapper; /** @type {InstanceType} */ this.bIsUObjectWrapper; /** @type {InstanceType} */ this.bSerializeAsSinglePrecisionFloat; } /** @returns {P} */ static createGrammar() { return Grammar.createEntityGrammar(this).label("PinTypeEntity") } /** @param {PinTypeEntity} other */ copyTypeFrom(other) { for (const key of this.keys) { if (other[key] !== undefined) { this[key] = other[key]; } } } } class Vector2DEntity extends IEntity { static attributes = { ...super.attributes, X: NumberEntity.withDefault(), Y: NumberEntity.withDefault(), } static grammar = this.createGrammar() constructor(values) { super(values); /** @type {InstanceType} */ this.X; /** @type {InstanceType} */ this.Y; } /** @returns {P} */ static createGrammar() { return Grammar.createEntityGrammar(this, Grammar.commaSeparation, 1).label("Vector2DEntity") } /** @returns {[Number, Number]} */ toArray() { return [this.X.valueOf(), this.Y.valueOf()] } } class RBSerializationVector2DEntity extends Vector2DEntity { static grammar = this.createGrammar() /** @returns {P} */ static createGrammar() { return Parsernostrum.alt( Parsernostrum.regArray(new RegExp( /X\s*=\s*/.source + "(?" + Grammar.numberRegexSource + ")" + "\\s+" + /Y\s*=\s*/.source + "(?" + Grammar.numberRegexSource + ")" )).map(({ groups: { x, y } }) => new this({ X: new (Vector2DEntity.attributes.X)(x), Y: new (Vector2DEntity.attributes.Y)(y), })), Vector2DEntity.grammar.map(v => new this({ X: v.X, Y: v.Y, })) ).label("RBSerializationVector2DEntity") } } class RotatorEntity extends IEntity { static attributes = { ...super.attributes, R: NumberEntity.withDefault(), P: NumberEntity.withDefault(), Y: NumberEntity.withDefault(), } static grammar = this.createGrammar() constructor(values) { super(values); /** @type {InstanceType} */ this.R; /** @type {InstanceType} */ this.P; /** @type {InstanceType} */ this.Y; } /** @returns {P} */ static createGrammar() { return Grammar.createEntityGrammar(this, Grammar.commaSeparation, 1).label("RotatorEntity") } getRoll() { return this.R } getPitch() { return this.P } getYaw() { return this.Y } } class SimpleSerializationRotatorEntity extends RotatorEntity { static attributeSeparator = ", " static grammar = this.createGrammar() /** @returns {P} */ static createGrammar() { return Parsernostrum.alt( Parsernostrum.regArray(new RegExp( `(${NumberEntity.numberRegexSource})` + String.raw`\s*,\s*` + `(${NumberEntity.numberRegexSource})` + String.raw`\s*,\s*` + `(${NumberEntity.numberRegexSource})` )).map(([_, p, pPrecision, y, yPrecision, r, rPrecision]) => new this({ R: new (RotatorEntity.attributes.R)(r, rPrecision?.length), P: new (RotatorEntity.attributes.P)(p, pPrecision?.length), Y: new (RotatorEntity.attributes.Y)(y, yPrecision?.length), })), RotatorEntity.grammar.map(v => new this({ R: v.R, P: v.P, Y: v.Y, })) ).label("SimpleSerializationRotatorEntity") } doSerialize() { const attributeSeparator = /** @type {typeof SimpleSerializationRotatorEntity} */( this.constructor ).attributeSeparator; return this.P.serialize() + attributeSeparator + this.Y.serialize() + attributeSeparator + this.R.serialize() + (this.trailing ? attributeSeparator : "") } } class SimpleSerializationVector2DEntity extends Vector2DEntity { static attributeSeparator = ", " static grammar = this.createGrammar() /** @returns {P} */ static createGrammar() { return Parsernostrum.alt( Parsernostrum.regArray(new RegExp( `(${NumberEntity.numberRegexSource})` + String.raw`\s*,\s*` + `(${NumberEntity.numberRegexSource})` )).map(([_, x, xPrecision, y, yPrecision]) => new this({ X: new (Vector2DEntity.attributes.X)(x, xPrecision?.length), Y: new (Vector2DEntity.attributes.Y)(y, yPrecision?.length), })), Vector2DEntity.grammar.map(v => new this({ X: v.X, Y: v.Y, })) ).label("SimpleSerializationVector2DEntity") } doSerialize() { const attributeSeparator = /** @type {typeof SimpleSerializationVector2DEntity} */( this.constructor ).attributeSeparator; return this.X.serialize() + attributeSeparator + this.Y.serialize() + (this.trailing ? attributeSeparator : "") } } class Vector4DEntity extends IEntity { static attributes = { ...super.attributes, X: NumberEntity.withDefault(), Y: NumberEntity.withDefault(), Z: NumberEntity.withDefault(), W: NumberEntity.withDefault(), } static grammar = this.createGrammar() constructor(values) { super(values); /** @type {InstanceType} */ this.X; /** @type {InstanceType} */ this.Y; /** @type {InstanceType} */ this.Z; /** @type {InstanceType} */ this.W; } /** @returns {P} */ static createGrammar() { return Grammar.createEntityGrammar(this, Grammar.commaSeparation, 1).label("Vector4DEntity") } /** @returns {[Number, Number, Number, Number]} */ toArray() { return [this.X.valueOf(), this.Y.valueOf(), this.Z.valueOf(), this.W.valueOf()] } } class SimpleSerializationVector4DEntity extends Vector4DEntity { static grammar = this.createGrammar() /** @returns {P } */ static createGrammar() { return Parsernostrum.alt( Parsernostrum.regArray(new RegExp( `(${Grammar.numberRegexSource})` + String.raw`\s*,\s*` + `(${Grammar.numberRegexSource})` + String.raw`\s*,\s*` + `(${Grammar.numberRegexSource})` + String.raw`\s*,\s*` + `(${Grammar.numberRegexSource})` )) .map(([_0, x, y, z, w]) => new this({ X: new (Vector4DEntity.attributes.X)(x), Y: new (Vector4DEntity.attributes.Y)(y), Z: new (Vector4DEntity.attributes.Z)(z), W: new (Vector4DEntity.attributes.W)(w), })), Vector4DEntity.grammar ) } } class SimpleSerializationVectorEntity extends VectorEntity { static allowShortSerialization = false static attributeSeparator = ", " static grammar = this.createGrammar() /** @returns {P} */ static createGrammar() { return Parsernostrum.alt( Parsernostrum.regArray(new RegExp( `(${NumberEntity.numberRegexSource})` // If allow simple serialization then it can parse only a single number ... + (this.allowShortSerialization ? `(?:` : "") + String.raw`\s*,\s*` + `(${NumberEntity.numberRegexSource})` + String.raw`\s*,\s*` + `(${NumberEntity.numberRegexSource})` // ... that will be assigned to X and the rest is optional and set to 0 + (this.allowShortSerialization ? `)?` : "") )) .map(([_, x, xPrecision, y, yPrecision, z, zPrecision]) => new this({ X: new (VectorEntity.attributes.X)(x, xPrecision?.length), Y: new (VectorEntity.attributes.Y)(y, yPrecision?.length), Z: new (VectorEntity.attributes.Z)(z, zPrecision?.length), })), VectorEntity.grammar.map(v => new this({ X: v.X, Y: v.Y, Z: v.Z, })) ) } /** * @template {typeof SimpleSerializationVectorEntity} T * @this {T} */ static flagAllowShortSerialization(value = true) { const result = this.asUniqueClass(); if (value !== result.allowShortSerialization) { result.allowShortSerialization = value; result.grammar = result.createGrammar(); } return result } doSerialize() { const attributeSeparator = /** @type {typeof SimpleSerializationVectorEntity} */( this.constructor ).attributeSeparator; return this.X.serialize() + attributeSeparator + this.Y.serialize() + attributeSeparator + this.Z.serialize() + (this.trailing ? attributeSeparator : "") } } /** @template {IEntity} T */ class PinEntity extends IEntity { static lookbehind = "Pin" static #typeEntityMap = { "bool": BooleanEntity, "byte": ByteEntity, "enum": EnumEntity, "exec": StringEntity, "int": IntegerEntity, "int64": Integer64Entity, "name": StringEntity, "real": NumberEntity, "string": StringEntity, [Configuration.paths.linearColor]: LinearColorEntity, [Configuration.paths.niagaraPosition]: VectorEntity, [Configuration.paths.rotator]: RotatorEntity, [Configuration.paths.vector]: VectorEntity, [Configuration.paths.vector2D]: Vector2DEntity, [Configuration.paths.vector4f]: Vector4DEntity, } static #alternativeTypeEntityMap = { "enum": EnumDisplayValueEntity, "rg": RBSerializationVector2DEntity, [Configuration.paths.niagaraPosition]: SimpleSerializationVectorEntity.flagAllowShortSerialization(), [Configuration.paths.rotator]: SimpleSerializationRotatorEntity, [Configuration.paths.vector]: SimpleSerializationVectorEntity, [Configuration.paths.vector2D]: SimpleSerializationVector2DEntity, [Configuration.paths.vector3f]: SimpleSerializationVectorEntity, [Configuration.paths.vector4f]: SimpleSerializationVector4DEntity, } static attributes = { PinId: GuidEntity.withDefault(), PinName: StringEntity.withDefault(), PinFriendlyName: AlternativesEntity.accepting( LocalizedTextEntity, FormatTextEntity, InvariantTextEntity, StringEntity ), PinToolTip: StringEntity, Direction: StringEntity, PinType: PinTypeEntity.withDefault().flagInlined(), LinkedTo: ArrayEntity.of(PinReferenceEntity).withDefault().flagSilent(), SubPins: ArrayEntity.of(PinReferenceEntity), ParentPin: PinReferenceEntity, DefaultValue: ComputedTypeEntity.from( /** @param {PinEntity} pinEntity */ pinEntity => pinEntity.getEntityType(true)?.flagSerialized() ?? StringEntity ), AutogeneratedDefaultValue: StringEntity, DefaultObject: ObjectReferenceEntity, PersistentGuid: GuidEntity, bHidden: BooleanEntity, bNotConnectable: BooleanEntity, bDefaultValueIsReadOnly: BooleanEntity, bDefaultValueIsIgnored: BooleanEntity, bAdvancedView: BooleanEntity, bOrphanedPin: BooleanEntity, } static grammar = this.createGrammar() #recomputesNodeTitleOnChange = false set recomputesNodeTitleOnChange(value) { this.#recomputesNodeTitleOnChange = value; } get recomputesNodeTitleOnChange() { return this.#recomputesNodeTitleOnChange } /** @type {ObjectEntity} */ #objectEntity = null get objectEntity() { try { /* * Why inside a try block ? * It is because of this issue: https://stackoverflow.com/questions/61237153/access-private-method-in-an-overriden-method-called-from-the-base-class-construc * super(values) will call IEntity constructor while this instance is not yet fully constructed * IEntity will call computedEntity.compute(this) to initialize DefaultValue from this class * Which in turn calls pinEntity.getEntityType(true) * Which calls this.getType() * Which calls this.objectEntity?.isPcg() * Which would access #objectEntity through get objectEntity() * And this would violate the private access rule (because this class is not yet constructed) * If this issue in the future will be fixed in all the major browsers, please remove this try catch */ return this.#objectEntity } catch (e) { return null } } set objectEntity(value) { this.#objectEntity = value; } #pinIndex get pinIndex() { return this.#pinIndex } set pinIndex(value) { this.#pinIndex = value; } constructor(values = {}) { super(values); /** @type {InstanceType} */ this.PinId; /** @type {InstanceType} */ this.PinName; /** @type {InstanceType} */ this.PinFriendlyName; /** @type {InstanceType} */ this.PinToolTip; /** @type {InstanceType} */ this.Direction; /** @type {InstanceType} */ this.PinType; /** @type {InstanceType} */ this.LinkedTo; /** @type {T} */ this.DefaultValue; /** @type {InstanceType} */ this.AutogeneratedDefaultValue; /** @type {InstanceType} */ this.DefaultObject; /** @type {InstanceType} */ this.PersistentGuid; /** @type {InstanceType} */ this.bHidden; /** @type {InstanceType} */ this.bNotConnectable; /** @type {InstanceType} */ this.bDefaultValueIsReadOnly; /** @type {InstanceType} */ this.bDefaultValueIsIgnored; /** @type {InstanceType} */ this.bAdvancedView; /** @type {InstanceType} */ this.bOrphanedPin; /** @type {ObjectEntity} */ this.objectEntity; } /** @returns {P} */ static createGrammar() { return Grammar.createEntityGrammar(this) } /** @param {ObjectEntity} objectEntity */ static fromLegacyObject(objectEntity) { return new PinEntity(objectEntity) } getType() { const category = this.PinType.PinCategory?.toString().toLocaleLowerCase(); if (category === "struct" || category === "class" || category === "object" || category === "type") { return this.PinType.PinSubCategoryObject?.path } if (this.isEnum()) { return "enum" } if (this.objectEntity?.isPcg()) { const pcgSuboject = this.objectEntity.getPcgSubobject(); const pinObjectReference = this.isInput() ? pcgSuboject.InputPins?.valueOf()[this.pinIndex] : pcgSuboject.OutputPins?.valueOf()[this.pinIndex]; if (pinObjectReference) { /** @type {ObjectEntity} */ const pinObject = pcgSuboject[Configuration.subObjectAttributeNameFromReference(pinObjectReference, true)]; let allowedTypes = pinObject.Properties?.AllowedTypes?.toString() ?? ""; if (allowedTypes == "") { allowedTypes = this.PinType.PinCategory ?? ""; if (allowedTypes == "") { allowedTypes = "Any"; } } if (allowedTypes) { if ( pinObject.Properties.bAllowMultipleData?.valueOf() !== false && pinObject.Properties.bAllowMultipleConnections?.valueOf() !== false ) { allowedTypes += "[]"; } return allowedTypes } } } if (category === "optional") { const subCategory = this.PinType.PinSubCategory?.toString(); switch (subCategory) { case "red": return "real" case "rg": return "rg" case "rgb": return Configuration.paths.vector case "rgba": return Configuration.paths.linearColor default: return subCategory } } return category } /** @returns {typeof IEntity} */ getEntityType(alternative = false) { const type = this.getType(); const entity = PinEntity.#typeEntityMap[type]; const alternativeEntity = PinEntity.#alternativeTypeEntityMap[type]; return alternative && alternativeEntity !== undefined ? alternativeEntity : entity } pinTitle() { return pinTitle(this) } /** @param {PinEntity} other */ copyTypeFrom(other) { this.PinType = other.PinType; } getDefaultValue(maybeCreate = false) { if (this.DefaultValue === undefined && maybeCreate) { this.DefaultValue = /** @type {T} */(new (this.getEntityType(true))()); } return this.DefaultValue } isEnum() { const type = this.PinType.PinSubCategoryObject?.type; return type === Configuration.paths.enum || type === Configuration.paths.userDefinedEnum || type?.toLowerCase() === "enum" } isExecution() { return this.PinType.PinCategory.toString() === "exec" } isHidden() { return this.bHidden?.valueOf() } isInput() { return !this.isHidden() && this.Direction?.toString() != "EGPD_Output" } isOutput() { return !this.isHidden() && this.Direction?.toString() == "EGPD_Output" } isLinked() { return this.LinkedTo?.length > 0 ?? false } /** * @param {String} targetObjectName * @param {PinEntity} targetPinEntity * @returns true if it was not already linked to the tarket */ linkTo(targetObjectName, targetPinEntity) { const linkFound = this.LinkedTo.values?.some(pinReferenceEntity => pinReferenceEntity.objectName.toString() == targetObjectName && pinReferenceEntity.pinGuid.toString() == targetPinEntity.PinId.toString() ); if (!linkFound) { this.LinkedTo.values.push(new PinReferenceEntity(new SymbolEntity(targetObjectName), targetPinEntity.PinId)); return true } return false // Already linked } /** * @param {String} targetObjectName * @param {PinEntity} targetPinEntity * @returns true if it was linked to the target */ unlinkFrom(targetObjectName, targetPinEntity) { const indexElement = this.LinkedTo.values?.findIndex(pinReferenceEntity => { return pinReferenceEntity.objectName.toString() == targetObjectName && pinReferenceEntity.pinGuid.toString() == targetPinEntity.PinId.toString() }); if (indexElement >= 0) { this.LinkedTo.values.splice(indexElement, 1); if (this.LinkedTo.length === 0 && PinEntity.attributes.LinkedTo.default === undefined) { this.LinkedTo.values = []; } return true } return false } getSubCategory() { return this.PinType.PinSubCategoryObject?.path } pinColor() { return pinColor(this) } } /** @param {PinEntity} pinEntity */ const indexFromUpperCaseLetterName = pinEntity => pinEntity.PinName?.toString().match(/^\s*([A-Z])\s*$/)?.[1]?.charCodeAt(0) - "A".charCodeAt(0); /** @param {ObjectEntity} entity */ function nodeVariadic(entity) { /** @type {() => PinEntity[]} */ let pinEntities; /** @type {(pinEntity: PinEntity) => Number} */ let pinIndexFromEntity; /** @type {(newPinIndex: Number, minIndex: Number, maxIndex: Number, newPin: PinEntity) => String} */ let pinNameFromIndex; const type = entity.getType(); let prefix; let name; switch (type) { case Configuration.paths.commutativeAssociativeBinaryOperator: case Configuration.paths.promotableOperator: name = entity.FunctionReference?.MemberName?.toString(); switch (name) { default: if ( !name?.startsWith("Add_") && !name?.startsWith("Subtract_") && !name?.startsWith("Multiply_") && !name?.startsWith("Divide_") ) { break } case "And_Int64Int64": case "And_IntInt": case "BMax": case "BMin": case "BooleanAND": case "BooleanNAND": case "BooleanOR": case "Concat_StrStr": case "FMax": case "FMin": case "Max": case "MaxInt64": case "Min": case "MinInt64": case "Or_Int64Int64": case "Or_IntInt": pinEntities ??= () => entity.getPinEntities().filter(pinEntity => pinEntity.isInput()); pinIndexFromEntity ??= indexFromUpperCaseLetterName; pinNameFromIndex ??= (index, min = -1, max = -1) => { const result = String.fromCharCode(index >= 0 ? index : max + "A".charCodeAt(0) + 1); entity.NumAdditionalInputs = new NaturalNumberEntity(pinEntities().length - 1); return result }; break } break case Configuration.paths.executionSequence: prefix ??= "Then"; case Configuration.paths.multiGate: prefix ??= "Out"; pinEntities ??= () => entity.getPinEntities().filter(pinEntity => pinEntity.isOutput()); pinIndexFromEntity ??= pinEntity => Number( pinEntity.PinName?.toString().match(new RegExp(String.raw`^\s*${prefix}[_\s]+(\d+)\s*$`, "i"))?.[1] ); pinNameFromIndex ??= (index, min = -1, max = -1, newPin) => `${prefix} ${index >= 0 ? index : min > 0 ? `${prefix} 0` : max + 1}`; break // case Configuration.paths.niagaraNodeOp: // pinEntities ??= () => entity.getPinEntities().filter(pinEntity => pinEntity.isInput()) // pinIndexFromEntity ??= indexFromUpperCaseLetterName // pinNameFromIndex ??= (index, min = -1, max = -1, newPin) => { // const result = String.fromCharCode(index >= 0 ? index : max + "A".charCodeAt(0) + 1) // entity.AddedPins ??= [] // entity.AddedPins.push(newPin) // return result // } // break case Configuration.paths.switchInteger: pinEntities ??= () => entity.getPinEntities().filter(pinEntity => pinEntity.isOutput()); pinIndexFromEntity ??= pinEntity => Number(pinEntity.PinName?.toString().match(/^\s*(\d+)\s*$/)?.[1]); pinNameFromIndex ??= (index, min = -1, max = -1, newPin) => (index < 0 ? max + 1 : index).toString(); break case Configuration.paths.switchGameplayTag: pinNameFromIndex ??= (index, min = -1, max = -1, newPin) => { const result = `Case_${index >= 0 ? index : min > 0 ? "0" : max + 1}`; entity.PinNames ??= new ArrayEntity(); entity.PinNames.valueOf().push(new StringEntity(result)); delete entity.PinTags.valueOf()[entity.PinTags.length - 1]; entity.PinTags.valueOf()[entity.PinTags.length] = null; return result }; case Configuration.paths.switchName: case Configuration.paths.switchString: pinEntities ??= () => entity.getPinEntities().filter(pinEntity => pinEntity.isOutput()); pinIndexFromEntity ??= pinEntity => Number(pinEntity.PinName.toString().match(/^\s*Case[_\s]+(\d+)\s*$/i)?.[1]); pinNameFromIndex ??= (index, min = -1, max = -1, newPin) => { const result = `Case_${index >= 0 ? index : min > 0 ? "0" : max + 1}`; entity.PinNames ??= new ArrayEntity(); entity.PinNames.valueOf().push(new StringEntity(result)); return result }; break } if (pinEntities) { return () => { let min = Number.MAX_SAFE_INTEGER; let max = Number.MIN_SAFE_INTEGER; let values = []; const modelPin = pinEntities().reduce( (acc, cur) => { const value = pinIndexFromEntity(cur); if (!isNaN(value)) { values.push(value); min = Math.min(value, min); if (value > max) { max = value; return cur } } else if (acc === undefined) { return cur } return acc }, undefined ); if (min === Number.MAX_SAFE_INTEGER || max === Number.MIN_SAFE_INTEGER) { min = undefined; max = undefined; } if (!modelPin) { return null } values.sort((a, b) => a < b ? -1 : a === b ? 0 : 1); let prev = values[0]; let index = values.findIndex( // Search for a gap value => { const result = value - prev > 1; prev = value; return result } ); const newPin = new PinEntity(modelPin); newPin.PinId = new GuidEntity(); newPin.PinName = new StringEntity(pinNameFromIndex(index, min, max, newPin)); newPin.PinToolTip = undefined; if (newPin.DefaultValue) { // @ts-expect-error newPin.DefaultValue = new (newPin.DefaultValue.constructor)(); } entity.getCustomproperties(true).push(newPin); return newPin } } } class MacroGraphReferenceEntity extends IEntity { static attributes = { ...super.attributes, MacroGraph: ObjectReferenceEntity, GraphBlueprint: ObjectReferenceEntity, GraphGuid: GuidEntity, } static grammar = this.createGrammar() constructor(values) { super(values); /** @type {InstanceType} */ this.MacroGraph; /** @type {InstanceType} */ this.GraphBlueprint; /** @type {InstanceType} */ this.GraphGuid; } /** @returns {P} */ static createGrammar() { return Grammar.createEntityGrammar(this) } getMacroName() { const colonIndex = this.MacroGraph.path.search(":"); return this.MacroGraph.path.substring(colonIndex + 1) } } class NullEntity extends IEntity { static grammar = this.createGrammar() /** @returns {P} */ static createGrammar() { // @ts-expect-error return Parsernostrum.reg(new RegExp(String.raw`\(${Parsernostrum.whitespaceInlineOpt.getParser().regexp.source}\)`)) .map(v => new this()) .label("NullEntity") } serialize( insideString = false, indentation = "", Self = /** @type {typeof IEntity} */(this.constructor) ) { let result = "()"; if (Self.serialized) { result = `"${result}"`; } return result } } class ScriptVariableEntity extends IEntity { static attributes = { ...super.attributes, ScriptVariable: ObjectReferenceEntity, OriginalChangeId: GuidEntity, } static grammar = this.createGrammar() constructor(values = {}) { super(values); /** @type {InstanceType} */ this.ScriptVariable; /** @type {InstanceType} */ this.OriginalChangeId; } /** @returns {P} */ static createGrammar() { return Grammar.createEntityGrammar(this).label("ScriptVariableEntity") } } class UnknownPinEntity extends PinEntity { static attributes = { ...super.attributes, PinId: GuidEntity } static grammar = this.createGrammar() /** @returns {P} */ static createGrammar() { return Parsernostrum.seq( // Lookbehind Parsernostrum.reg(new RegExp(`(${Grammar.Regex.Symbol.source}\\s*)\\(\\s*`), 1), Grammar.createAttributeGrammar(this).sepBy(Grammar.commaSeparation), Parsernostrum.reg(/\s*(?:,\s*)?\)/) ).map(([lookbehind, attributes, _2]) => { lookbehind ??= ""; let values = {}; if (lookbehind.length) { values.lookbehind = lookbehind; } attributes.forEach(attributeSetter => attributeSetter(values)); return new this(values) }).label("UnknownPinEntity") } } class VariableReferenceEntity extends IEntity { static attributes = { ...super.attributes, MemberScope: StringEntity, MemberName: StringEntity.withDefault(), MemberGuid: GuidEntity, bSelfContext: BooleanEntity, } static grammar = this.createGrammar() constructor(values) { super(values); /** @type {InstanceType} */ this.MemberScope; /** @type {InstanceType} */ this.MemberName; /** @type {InstanceType} */ this.MemberGuid; /** @type {InstanceType} */ this.bSelfContext; } /** @returns {P} */ static createGrammar() { return Grammar.createEntityGrammar(this).label("VariableReferenceEntity") } } class ObjectEntity extends IEntity { #exported = false get exported() { return this.#exported } set exported(value) { this.#exported = value; } static #nameRegex = /^(\w+?)(?:_(\d+))?$/ /** @type {(k: String) => String} */ static printKey = k => !k.startsWith(Configuration.subObjectAttributeNamePrefix) ? k : "" static attributeSeparator = "\n" static wrap = this.notWrapped static trailing = true static attributes = { ...super.attributes, Class: ObjectReferenceEntity, Name: StringEntity, Archetype: ObjectReferenceEntity, ExportPath: ObjectReferenceEntity, ObjectRef: ObjectReferenceEntity, BlueprintElementType: ObjectReferenceEntity, BlueprintElementInstance: ObjectReferenceEntity, ConstA: MirroredEntity.of(NumberEntity), ConstB: MirroredEntity.of(NumberEntity), PinTags: ArrayEntity.of(NullEntity).flagInlined(), PinNames: ArrayEntity.of(StringEntity).flagInlined(), AxisKey: SymbolEntity, InputAxisKey: SymbolEntity, InputName: StringEntity, InputType: SymbolEntity, NumAdditionalInputs: NaturalNumberEntity, bIsPureFunc: BooleanEntity, bIsConstFunc: BooleanEntity, bIsCaseSensitive: BooleanEntity, VariableReference: VariableReferenceEntity, SelfContextInfo: SymbolEntity, DelegatePropertyName: StringEntity, DelegateOwnerClass: ObjectReferenceEntity, ComponentPropertyName: StringEntity, EventReference: FunctionReferenceEntity, FunctionReference: FunctionReferenceEntity, FunctionScript: ObjectReferenceEntity, CustomFunctionName: StringEntity, TargetType: ObjectReferenceEntity, MacroGraphReference: MacroGraphReferenceEntity, Enum: ObjectReferenceEntity, EnumEntries: ArrayEntity.of(StringEntity).flagInlined(), InputKey: SymbolEntity, OpName: StringEntity, CachedChangeId: GuidEntity, FunctionDisplayName: StringEntity, AddedPins: ArrayEntity.of(UnknownPinEntity).withDefault().flagInlined().flagSilent(), ChangeId: GuidEntity, MaterialFunction: ObjectReferenceEntity, bOverrideFunction: BooleanEntity, bInternalEvent: BooleanEntity, bConsumeInput: BooleanEntity, bExecuteWhenPaused: BooleanEntity, bOverrideParentBinding: BooleanEntity, bControl: BooleanEntity, bAlt: BooleanEntity, bShift: BooleanEntity, bCommand: BooleanEntity, CommentColor: LinearColorEntity, bCommentBubbleVisible_InDetailsPanel: BooleanEntity, bColorCommentBubble: BooleanEntity, ProxyFactoryFunctionName: StringEntity, ProxyFactoryClass: ObjectReferenceEntity, ProxyClass: ObjectReferenceEntity, StructType: ObjectReferenceEntity, MaterialExpression: ObjectReferenceEntity, MaterialExpressionComment: ObjectReferenceEntity, MoveMode: SymbolEntity, TimelineName: StringEntity, TimelineGuid: GuidEntity, SizeX: MirroredEntity.of(IntegerEntity), SizeY: MirroredEntity.of(IntegerEntity), Text: MirroredEntity.of(StringEntity), MaterialExpressionEditorX: MirroredEntity.of(IntegerEntity), MaterialExpressionEditorY: MirroredEntity.of(IntegerEntity), NodeTitle: StringEntity, NodeTitleColor: LinearColorEntity, PositionX: MirroredEntity.of(IntegerEntity), PositionY: MirroredEntity.of(IntegerEntity), SettingsInterface: ObjectReferenceEntity, PCGNode: ObjectReferenceEntity, HiGenGridSize: SymbolEntity, Operation: SymbolEntity, NodePosX: IntegerEntity, NodePosY: IntegerEntity, NodeHeight: IntegerEntity, NodeWidth: IntegerEntity, Graph: ObjectReferenceEntity, SubgraphInstance: StringEntity, InputPins: ArrayEntity.of(ObjectReferenceEntity).flagInlined(), OutputPins: ArrayEntity.of(ObjectReferenceEntity).flagInlined(), bExposeToLibrary: BooleanEntity, bCanRenameNode: BooleanEntity, bCommentBubblePinned: BooleanEntity, bCommentBubbleVisible: BooleanEntity, NodeComment: StringEntity, AdvancedPinDisplay: SymbolEntity, DelegateReference: VariableReferenceEntity, EnabledState: SymbolEntity, NodeGuid: GuidEntity, ErrorType: IntegerEntity, ErrorMsg: StringEntity, ScriptVariables: ArrayEntity.of(ScriptVariableEntity), Node: MirroredEntity.of(ObjectReferenceEntity), ExportedNodes: StringEntity, CustomProperties: ArrayEntity.of(AlternativesEntity.accepting(PinEntity, UnknownPinEntity)).withDefault().flagSilent(), } static customPropertyGrammar = Parsernostrum.seq( Parsernostrum.reg(/CustomProperties\s+/), this.attributes.CustomProperties.type.grammar, ).map(([_0, pin]) => values => { /** @type {InstanceType} */( values.CustomProperties ??= new (this.attributes.CustomProperties)() ).values.push(pin); }) static inlinedArrayEntryGrammar = Parsernostrum.seq( Parsernostrum.alt( Grammar.symbolQuoted.map(v => [v, true]), Grammar.symbol.map(v => [v, false]), ), Parsernostrum.reg(new RegExp(String.raw`\s*\(\s*(\d+)\s*\)\s*\=\s*`), 1).map(Number) ) .chain( /** @param {[[keyof ObjectEntity.attributes, Boolean], Number]} param */ ([[symbol, quoted], index]) => (this.attributes[symbol]?.grammar ?? IEntity.unknownEntityGrammar).map(currentValue => values => { if (values[symbol] === undefined) { let arrayEntity = ArrayEntity; if (quoted != arrayEntity.quoted) { arrayEntity = arrayEntity.flagQuoted(quoted); } if (!arrayEntity.inlined) { arrayEntity = arrayEntity.flagInlined(); } values[symbol] = new arrayEntity(); } /** @type {ArrayEntity} */ const target = values[symbol]; target.values[index] = currentValue; } ) ) static grammar = this.createGrammar() static grammarMultipleObjects = Parsernostrum.seq( Parsernostrum.whitespaceOpt, this.grammar, Parsernostrum.seq( Parsernostrum.whitespace, this.grammar, ) .map(([_0, object]) => object) .many(), Parsernostrum.whitespaceOpt ).map(([_0, first, remaining, _4]) => [first, ...remaining]) constructor(values = {}) { if (("NodePosX" in values) !== ("NodePosY" in values)) { const entries = Object.entries(values); const [key, position] = "NodePosX" in values ? ["NodePosY", Object.keys(values).indexOf("NodePosX") + 1] : ["NodePosX", Object.keys(values).indexOf("NodePosY")]; entries.splice(position, 0, [key, new IntegerEntity(0)]); values = Object.fromEntries(entries); } super(values); // Attributes /** @type {InstanceType} */ this.AddedPins; /** @type {InstanceType} */ this.AdvancedPinDisplay; /** @type {InstanceType} */ this.Archetype; /** @type {InstanceType} */ this.AxisKey; /** @type {InstanceType} */ this.bIsPureFunc; /** @type {InstanceType} */ this.BlueprintElementInstance; /** @type {InstanceType} */ this.ConstA; /** @type {InstanceType} */ this.ConstB; /** @type {InstanceType} */ this.BlueprintElementType; /** @type {InstanceType} */ this.Class; /** @type {InstanceType} */ this.CommentColor; /** @type {InstanceType} */ this.ComponentPropertyName; /** @type {InstanceType} */ this.CustomFunctionName; /** @type {ArrayEntity} */ this.CustomProperties; /** @type {InstanceType} */ this.DelegatePropertyName; /** @type {InstanceType} */ this.DelegateReference; /** @type {InstanceType} */ this.EnabledState; /** @type {InstanceType} */ this.Enum; /** @type {InstanceType} */ this.EnumEntries; /** @type {InstanceType} */ this.EventReference; /** @type {InstanceType} */ this.ExportedNodes; /** @type {InstanceType} */ this.ExportPath; /** @type {InstanceType} */ this.FunctionDisplayName; /** @type {InstanceType} */ this.FunctionReference; /** @type {InstanceType} */ this.FunctionScript; /** @type {InstanceType} */ this.Graph; /** @type {InstanceType} */ this.HiGenGridSize; /** @type {InstanceType} */ this.InputAxisKey; /** @type {InstanceType} */ this.InputKey; /** @type {InstanceType} */ this.InputName; /** @type {InstanceType} */ this.InputPins; /** @type {InstanceType} */ this.InputType; /** @type {InstanceType} */ this.MacroGraphReference; /** @type {InstanceType} */ this.MaterialExpression; /** @type {InstanceType} */ this.MaterialExpressionComment; /** @type {InstanceType} */ this.MaterialExpressionEditorX; /** @type {InstanceType} */ this.MaterialExpressionEditorY; /** @type {InstanceType} */ this.MaterialFunction; /** @type {InstanceType} */ this.Name; /** @type {InstanceType} */ this.Node; /** @type {InstanceType} */ this.NodeComment; /** @type {InstanceType} */ this.NodeHeight; /** @type {InstanceType} */ this.NodePosX; /** @type {InstanceType} */ this.NodePosY; /** @type {InstanceType} */ this.NodeTitle; /** @type {InstanceType} */ this.NodeTitleColor; /** @type {InstanceType} */ this.NodeWidth; /** @type {InstanceType} */ this.NumAdditionalInputs; /** @type {InstanceType} */ this.ObjectRef; /** @type {InstanceType} */ this.Operation; /** @type {InstanceType} */ this.OpName; /** @type {InstanceType} */ this.OutputPins; /** @type {InstanceType} */ this.PCGNode; /** @type {InstanceType} */ this.PinTags; /** @type {InstanceType} */ this.PinNames; /** @type {InstanceType} */ this.PositionX; /** @type {InstanceType} */ this.PositionY; /** @type {InstanceType} */ this.ProxyFactoryFunctionName; /** @type {InstanceType} */ this.ScriptVariables; /** @type {InstanceType} */ this.SettingsInterface; /** @type {InstanceType} */ this.SizeX; /** @type {InstanceType} */ this.SizeY; /** @type {InstanceType} */ this.StructType; /** @type {InstanceType} */ this.SubgraphInstance; /** @type {InstanceType} */ this.TargetType; /** @type {InstanceType} */ this.Text; /** @type {InstanceType} */ this.Text; /** @type {InstanceType} */ this.VariableReference; // Legacy nodes pins if (this["Pins"] instanceof ArrayEntity) { this["Pins"].valueOf().forEach( /** @param {ObjectReferenceEntity} objectReference */ objectReference => { const pinObject = this[Configuration.subObjectAttributeNameFromReference(objectReference, true)]; if (pinObject) { const pinEntity = PinEntity.fromLegacyObject(pinObject); pinEntity.LinkedTo = new (PinEntity.attributes.LinkedTo)(); this.getCustomproperties(true).push(pinEntity); this.CustomProperties.ignored = true; } } ); } /** @type {ObjectEntity} */ const materialSubobject = this.getMaterialSubobject(); if (materialSubobject) { const obj = materialSubobject; obj.SizeX !== undefined && (obj.SizeX.getter = () => this.NodeWidth); obj.SizeY && (obj.SizeY.getter = () => this.NodeHeight); obj.Text && (obj.Text.getter = () => this.NodeComment); obj.MaterialExpressionEditorX && (obj.MaterialExpressionEditorX.getter = () => this.NodePosX); obj.MaterialExpressionEditorY && (obj.MaterialExpressionEditorY.getter = () => this.NodePosY); if (this.getType() === Configuration.paths.materialExpressionComponentMask) { const rgbaPins = Configuration.rgba.map(pinName => { const result = this.getPinEntities().find(pin => pin.PinName.toString() === pinName); result.recomputesNodeTitleOnChange = true; return result }); // Reorder keys so that the added ones stay first obj.keys = [...Configuration.rgba, ...obj.keys]; const silentBool = MirroredEntity.of(BooleanEntity).withDefault().flagSilent(); obj["R"] = new silentBool(() => rgbaPins[0].DefaultValue); obj["G"] = new silentBool(() => rgbaPins[1].DefaultValue); obj["B"] = new silentBool(() => rgbaPins[2].DefaultValue); obj["A"] = new silentBool(() => rgbaPins[3].DefaultValue); } else if (this.getType() === Configuration.paths.materialExpressionSubtract) { const silentNumber = MirroredEntity .of(NumberEntity.withPrecision(6)) .withDefault(() => new MirroredEntity(() => new NumberEntity(1))) .flagSilent(); const pinA = this.getCustomproperties().find(pin => pin.PinName?.toString() === "A"); const pinB = this.getCustomproperties().find(pin => pin.PinName?.toString() === "B"); if (pinA || pinB) { // Reorder keys so that the added ones stay first obj.keys = ["ConstA", "ConstB", ...obj.keys]; if (pinA) { pinA.recomputesNodeTitleOnChange = true; obj.ConstA = new silentNumber(() => pinA.DefaultValue); } if (pinB) { pinB.recomputesNodeTitleOnChange = true; obj.ConstB = new silentNumber(() => pinB.DefaultValue); } } } } /** @type {ObjectEntity} */ const pcgObject = this.getPcgSubobject(); if (pcgObject) { pcgObject.PositionX && (pcgObject.PositionX.getter = () => this.NodePosX); pcgObject.PositionY && (pcgObject.PositionY.getter = () => this.NodePosY); pcgObject.getSubobjects().forEach( /** @param {ObjectEntity} obj */ obj => { if (obj.Node !== undefined) { const nodeRef = obj.Node.getter(); if ( nodeRef.type === this.PCGNode.type && nodeRef.path === `${this.Name}.${this.PCGNode.path}` ) { obj.Node.getter = () => new ObjectReferenceEntity( this.PCGNode.type, `${this.Name}.${this.PCGNode.path}`, nodeRef.full, ); } } } ); } let inputIndex = 0; let outputIndex = 0; this.getCustomproperties().forEach((pinEntity, i) => { pinEntity.objectEntity = this; pinEntity.pinIndex = pinEntity.isInput() ? inputIndex++ : pinEntity.isOutput() ? outputIndex++ : i; }); } /** @returns {P} */ static createGrammar() { return Parsernostrum.seq( Parsernostrum.reg(/Begin +Object/), Parsernostrum.seq( Parsernostrum.whitespace, Parsernostrum.alt( this.createSubObjectGrammar(), this.customPropertyGrammar, Grammar.createAttributeGrammar(this, Parsernostrum.reg(Grammar.Regex.MultipleWordsSymbols)), Grammar.createAttributeGrammar( this, Grammar.attributeNameQuoted, undefined, (values, attributeKey, attributeValue) => { Utility.objectSet(values, [...attributeKey, "quoted"], true); }, ), this.inlinedArrayEntryGrammar, ) ) .map(([_0, entry]) => entry) .many(), Parsernostrum.reg(/\s+End +Object/), ) .map(([_0, attributes, _2]) => { const values = {}; attributes.forEach(attributeSetter => attributeSetter(values)); return new this(values) }) .label("ObjectEntity") } static createSubObjectGrammar() { return Parsernostrum.lazy(() => this.grammar) .map(object => values => { object.trailing = false; values[Configuration.subObjectAttributeNameFromEntity(object)] = object; } ) } /** @type {String} */ #class getClass() { if (!this.#class) { this.#class = (this.Class?.path ? this.Class.path : this.Class?.type) ?? this.ExportPath?.type ?? ""; if (this.#class && !this.#class.startsWith("/")) { // Old path names did not start with /Script or /Engine, check tests/resources/LegacyNodes.js let path = Object.values(Configuration.paths).find(path => path.endsWith("." + this.#class)); if (path) { this.#class = path; } } } return this.#class } getType() { let classValue = this.getClass(); if (this.MacroGraphReference?.MacroGraph?.path) { return this.MacroGraphReference.MacroGraph.path } if (this.MaterialExpression) { return this.MaterialExpression.type } return classValue } getObjectName(dropCounter = false) { if (dropCounter) { return this.getNameAndCounter()[0] } return this.Name.toString() } /** @returns {[String, Number]} */ getNameAndCounter() { const result = this.getObjectName().match(ObjectEntity.#nameRegex); return result ? [result[1] ?? "", parseInt(result[2] ?? "0")] : ["", 0] } getCounter() { return this.getNameAndCounter()[1] } getNodeWidth() { return this.NodeWidth ?? this.isComment() ? Configuration.defaultCommentWidth : undefined } /** @param {Number} value */ setNodeWidth(value) { if (!this.NodeWidth) { this.NodeWidth = new IntegerEntity(); } this.NodeWidth.value = value; } getNodeHeight() { return this.NodeHeight ?? this.isComment() ? Configuration.defaultCommentHeight : undefined } /** @param {Number} value */ setNodeHeight(value) { if (!this.NodeHeight) { this.NodeHeight = new IntegerEntity(); } this.NodeHeight.value = value; } getNodePosX() { return this.NodePosX?.value ?? 0 } /** @param {Number} value */ setNodePosX(value) { if (!this.NodePosX) { this.NodePosX = new IntegerEntity(); } this.NodePosX.value = Math.round(value); } getNodePosY() { return this.NodePosY?.value ?? 0 } /** @param {Number} value */ setNodePosY(value) { if (!this.NodePosY) { this.NodePosY = new IntegerEntity(); } this.NodePosY.value = Math.round(value); } getCustomproperties(canCreate = false) { return this.CustomProperties.values } /** @returns {PinEntity[]} */ getPinEntities() { return this.getCustomproperties().filter(v => v.constructor === PinEntity) } /** @returns {ObjectEntity[]} */ getSubobjects() { return Object.keys(this) .filter(k => k.startsWith(Configuration.subObjectAttributeNamePrefix)) .flatMap(k => [this[k], .../** @type {ObjectEntity} */(this[k]).getSubobjects()]) } switchTarget() { const switchMatch = this.getClass().match(Configuration.switchTargetPattern); if (switchMatch) { return switchMatch[1] } } isEvent() { switch (this.getClass()) { case Configuration.paths.actorBoundEvent: case Configuration.paths.componentBoundEvent: case Configuration.paths.customEvent: case Configuration.paths.event: case Configuration.paths.inputAxisKeyEvent: case Configuration.paths.inputVectorAxisEvent: return true } return false } isComment() { switch (this.getClass()) { case Configuration.paths.comment: case Configuration.paths.materialGraphNodeComment: return true } return false } isMaterial() { return this.getClass() === Configuration.paths.materialGraphNode // return [ // Configuration.paths.materialExpressionConstant, // Configuration.paths.materialExpressionConstant2Vector, // Configuration.paths.materialExpressionConstant3Vector, // Configuration.paths.materialExpressionConstant4Vector, // Configuration.paths.materialExpressionLogarithm, // Configuration.paths.materialExpressionLogarithm10, // Configuration.paths.materialExpressionLogarithm2, // Configuration.paths.materialExpressionMaterialFunctionCall, // Configuration.paths.materialExpressionSquareRoot, // Configuration.paths.materialExpressionTextureCoordinate, // Configuration.paths.materialExpressionTextureSample, // Configuration.paths.materialGraphNode, // Configuration.paths.materialGraphNodeComment, // ] // .includes(this.getClass()) } /** @return {ObjectEntity} */ getMaterialSubobject() { const expression = this.MaterialExpression ?? this.MaterialExpressionComment; return expression ? this[Configuration.subObjectAttributeNameFromReference(expression, true)] : null } isPcg() { return this.getClass() === Configuration.paths.pcgEditorGraphNode || this.getPcgSubobject() != null } isNiagara() { return this.Class && (this.Class.type ? this.Class.type : this.Class.path)?.startsWith("/Script/NiagaraEditor.") } /** @return {ObjectEntity} */ getPcgSubobject() { const node = this.PCGNode; return node ? this[Configuration.subObjectAttributeNameFromReference(node, true)] : null } /** @return {ObjectEntity} */ getSettingsObject() { const settings = this.SettingsInterface; return settings ? this[Configuration.subObjectAttributeNameFromReference(settings, true)] : null } /** @return {ObjectEntity} */ getSubgraphObject() { const name = this.SubgraphInstance; return name ? this[Configuration.subObjectAttributeNameFromName(name)] : null } isDevelopmentOnly() { const nodeClass = this.getClass(); return this.EnabledState?.toString() === "DevelopmentOnly" || nodeClass.includes("Debug", Math.max(0, nodeClass.lastIndexOf("."))) } getHIDAttribute() { return this.InputKey ?? this.AxisKey ?? this.InputAxisKey } getDelegatePin() { return this.getCustomproperties().find(pin => pin.PinType.PinCategory.toString() === "delegate") } nodeColor() { return nodeColor(this) } nodeIcon() { return nodeIcon(this) } additionalPinInserter() { return nodeVariadic(this) } /** @param {String} key */ showProperty(key) { switch (key) { case "Class": case "Name": case "Archetype": case "ExportPath": case "CustomProperties": // Serielized separately, check doWrite() return false } return super.showProperty(key) } /** @param {typeof ObjectEntity} Self */ doSerialize( insideString = false, indentation = "", Self = /** @type {typeof ObjectEntity} */(this.constructor), printKey = Self.printKey, keySeparator = Self.keySeparator, attributeSeparator = Self.attributeSeparator, wrap = Self.wrap, ) { const deeperIndentation = indentation + Configuration.indentation; const initial_trailing = this.trailing; this.trailing = false; const content = super.doSerialize(insideString, deeperIndentation, Self, printKey, keySeparator, attributeSeparator, wrap); this.trailing = initial_trailing; let result = indentation + "Begin Object" + ((this.Class?.type || this.Class?.path) // && Self.attributes.Class.ignored !== true // && this.Class.ignored !== true ? ` Class${keySeparator}${this.Class.serialize(insideString)}` : "" ) + (this.Name // && Self.attributes.Name.ignored !== true // && this.Name.ignored !== true ? ` Name${keySeparator}${this.Name.serialize(insideString)}` : "" ) + (this.Archetype // && Self.attributes.Archetype.ignored !== true // && this.Archetype.ignored !== true ? ` Archetype${keySeparator}${this.Archetype.serialize(insideString)}` : "" ) + ((this.ExportPath?.type || this.ExportPath?.path) // && Self.attributes.ExportPath.ignored !== true // && this.ExportPath.ignored !== true ? ` ExportPath${keySeparator}${this.ExportPath.serialize(insideString)}` : "" ) + (content ? attributeSeparator + content : "") + (Self.attributes.CustomProperties.ignored !== true && this.CustomProperties.ignored !== true ? this.getCustomproperties() .map(pin => attributeSeparator + deeperIndentation + printKey("CustomProperties ") + pin.serialize(insideString) ) .join("") : "" ) + attributeSeparator + indentation + "End Object" + (this.trailing ? attributeSeparator : ""); return result } } class KnotEntity extends ObjectEntity { /** * @param {Object} values * @param {PinEntity} pinReferenceForType */ constructor(values = {}, pinReferenceForType = undefined) { values.Class = new ObjectReferenceEntity(Configuration.paths.knot); values.Name = new (ObjectEntity.attributes.Name)("K2Node_Knot"); const inputPinEntity = new PinEntity( { PinName: new (PinEntity.attributes.PinName)("InputPin") }, ); const outputPinEntity = new PinEntity( { PinName: new (PinEntity.attributes.PinName)("OutputPin"), Direction: new (PinEntity.attributes.Direction)("EGPD_Output"), }, ); if (pinReferenceForType) { inputPinEntity.copyTypeFrom(pinReferenceForType); outputPinEntity.copyTypeFrom(pinReferenceForType); } values["CustomProperties"] = new (ObjectEntity.attributes.CustomProperties)([inputPinEntity, outputPinEntity]); super(values); } } /** * @typedef {{ * consumeEvent?: Boolean, * listenOnFocus?: Boolean, * unlistenOnTextEdit?: Boolean, * }} Options */ /** @template {Element} T */ class IInput { /** @type {T} */ #target get target() { return this.#target } /** @type {Blueprint} */ #blueprint get blueprint() { return this.#blueprint } consumeEvent /** @type {Object} */ options listenHandler = () => this.listenEvents() unlistenHandler = () => this.unlistenEvents() /** * @param {T} target * @param {Blueprint} blueprint * @param {Options} options */ constructor(target, blueprint, options = {}) { options.consumeEvent ??= false; options.listenOnFocus ??= false; options.unlistenOnTextEdit ??= false; this.#target = target; this.#blueprint = blueprint; this.consumeEvent = options.consumeEvent; this.options = options; } setup() { 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); } if (this.blueprint.focused) { this.listenEvents(); } } cleanup() { 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() { } } class KeyBindingEntity extends IEntity { static attributes = { ...super.attributes, ActionName: StringEntity, bShift: BooleanEntity, bCtrl: BooleanEntity, bAlt: BooleanEntity, bCmd: BooleanEntity, Key: SymbolEntity, } static grammar = this.createGrammar() constructor(values) { super(values); /** @type {InstanceType} */ this.ActionName; /** @type {InstanceType} */ this.bShift; /** @type {InstanceType} */ this.bCtrl; /** @type {InstanceType} */ this.bAlt; /** @type {InstanceType} */ this.bCmd; /** @type {InstanceType} */ this.Key; } /** @returs {P} */ static createGrammar() { return Parsernostrum.alt( SymbolEntity.grammar.map(identifier => new this({ Key: identifier })), Grammar.createEntityGrammar(this), ) } } /** * @typedef {import("../IInput.js").Options & { * activationKeys?: String | KeyBindingEntity | (String | KeyBindingEntity)[], * consumeEvent?: Boolean, * listenOnFocus?: Boolean, * unlistenOnTextEdit?: Boolean, * }} Options */ /** * @template {Element} T * @extends IInput */ class KeyboardShortcut extends IInput { static #ignoreEvent = /** @param {KeyboardShortcut} self */ self => { } /** @type {KeyBindingEntity[]} */ #activationKeys pressedKey = "" /** * @param {T} target * @param {Blueprint} blueprint * @param {Options} options */ constructor( target, blueprint, options = {}, onKeyDown = KeyboardShortcut.#ignoreEvent, onKeyUp = KeyboardShortcut.#ignoreEvent ) { 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) { const parsed = KeyBindingEntity.grammar.run(v); if (parsed.status) { return parsed.value } } throw new Error("Unexpected key value") }); super(target, blueprint, options); this.onKeyDown = onKeyDown; this.onKeyUp = onKeyUp; this.#activationKeys = this.options.activationKeys ?? []; /** @param {KeyBindingEntity} keyEntry */ const wantsShift = keyEntry => keyEntry.bShift?.valueOf() || keyEntry.Key.valueOf() == "LeftShift" || keyEntry.Key.valueOf() == "RightShift"; /** @param {KeyBindingEntity} keyEntry */ const wantsCtrl = keyEntry => keyEntry.bCtrl?.valueOf() || keyEntry.Key.valueOf() == "LeftControl" || keyEntry.Key.valueOf() == "RightControl"; /** @param {KeyBindingEntity} keyEntry */ const wantsAlt = keyEntry => keyEntry.bAlt?.valueOf() || keyEntry.Key.valueOf() == "LeftAlt" || keyEntry.Key.valueOf() == "RightAlt"; let self = this; /** @param {KeyboardEvent} e */ this.keyDownHandler = e => { if ( self.#activationKeys.some(keyEntry => wantsShift(keyEntry) == e.shiftKey && wantsCtrl(keyEntry) == e.ctrlKey && wantsAlt(keyEntry) == e.altKey && Configuration.Keys[keyEntry.Key.value] == e.code ) ) { if (this.consumeEvent) { e.preventDefault(); e.stopImmediatePropagation(); } this.pressedKey = e.code; self.fire(); document.removeEventListener("keydown", self.keyDownHandler); document.addEventListener("keyup", self.keyUpHandler); } }; /** @param {KeyboardEvent} e */ this.keyUpHandler = e => { if ( self.#activationKeys.some(keyEntry => keyEntry.bShift?.valueOf() && e.key == "Shift" || keyEntry.bCtrl?.valueOf() && e.key == "Control" || keyEntry.bAlt?.valueOf() && e.key == "Alt" || keyEntry.bCmd?.valueOf() && e.key == "Meta" || Configuration.Keys[keyEntry.Key.value] == e.code ) ) { if (this.consumeEvent) { e.stopImmediatePropagation(); } self.unfire(); this.pressedKey = ""; document.removeEventListener("keyup", this.keyUpHandler); document.addEventListener("keydown", this.keyDownHandler); } }; } listenEvents() { document.addEventListener("keydown", this.keyDownHandler); } unlistenEvents() { document.removeEventListener("keydown", this.keyDownHandler); } /* Subclasses can override */ fire() { this.onKeyDown(this); } unfire() { this.onKeyUp(this); } } /** * @typedef {import("../IInput.js").Options & { * ignoreTranslateCompensate?: Boolean, * ignoreScale?: Boolean, * movementSpace?: HTMLElement, * enablerKey?: KeyboardShortcut, * }} Options */ /** * @template {Element} T * @extends {IInput} */ class IPointing extends IInput { #location = /** @type {Coordinates} */([0, 0]) get location() { return this.#location } /** @type {KeyboardShortcut?} */ #enablerKey get enablerKey() { return this.#enablerKey } #enablerActivated = true get enablerActivated() { return this.#enablerActivated } /** * @param {T} target * @param {Blueprint} blueprint * @param {Options} options */ constructor(target, blueprint, options = {}) { options.ignoreTranslateCompensate ??= false; options.ignoreScale ??= false; options.movementSpace ??= blueprint.getGridDOMElement() ?? document.documentElement; super(target, blueprint, options); /** @type {HTMLElement} */ this.movementSpace = options.movementSpace; if (options.enablerKey) { this.#enablerKey = options.enablerKey; this.#enablerKey.onKeyDown = () => this.#enablerActivated = true; this.#enablerKey.onKeyUp = () => this.#enablerActivated = false; this.#enablerKey.consumeEvent = false; this.#enablerKey.listenEvents(); this.#enablerActivated = false; } } /** @param {MouseEvent} mouseEvent */ setLocationFromEvent(mouseEvent) { let location = Utility.convertLocation( [mouseEvent.clientX, mouseEvent.clientY], this.movementSpace, this.options.ignoreScale ); location = this.options.ignoreTranslateCompensate ? location : this.blueprint.compensateTranslation(location[0], location[1]); this.#location = [...location]; return this.#location } } /** * @typedef {import("./IMouseClickDrag.js").Options & { * }} Options */ /** * @template {Element} T * @extends {IPointing} */ class MouseClick extends IPointing { static #ignoreEvent = /** @param {MouseClick} self */ self => { } /** @param {MouseEvent} e */ #mouseDownHandler = e => { this.blueprint.setFocused(true); if (this.enablerKey && !this.enablerActivated) { return } switch (e.button) { case this.options.clickButton: // Either doesn't matter or consider the click only when clicking on the target, not descandants if (!this.options.strictTarget || e.target === e.currentTarget) { if (this.consumeEvent) { e.stopImmediatePropagation(); // Captured, don't call anyone else } // Attach the listeners document.addEventListener("mouseup", this.#mouseUpHandler); this.setLocationFromEvent(e); this.clickedPosition[0] = this.location[0]; this.clickedPosition[1] = this.location[1]; this.blueprint.mousePosition[0] = this.location[0]; this.blueprint.mousePosition[1] = this.location[1]; this.clicked(this.clickedPosition); } break default: if (!this.options.exitAnyButton) { this.#mouseUpHandler(e); } break } } /** @param {MouseEvent} e */ #mouseUpHandler = e => { if (!this.options.exitAnyButton || e.button == this.options.clickButton) { if (this.consumeEvent) { e.stopImmediatePropagation(); // Captured, don't call anyone else } // Remove the handlers of "mousemove" and "mouseup" document.removeEventListener("mouseup", this.#mouseUpHandler); this.unclicked(); } } clickedPosition = [0, 0] /** * @param {T} target * @param {Blueprint} blueprint * @param {Options} options */ constructor( target, blueprint, options = {}, onClick = MouseClick.#ignoreEvent, onUnclick = MouseClick.#ignoreEvent, ) { options.clickButton ??= Configuration.mouseClickButton; options.consumeEvent ??= true; options.exitAnyButton ??= true; options.strictTarget ??= false; super(target, blueprint, options); this.onClick = onClick; this.onUnclick = onUnclick; this.listenEvents(); } listenEvents() { this.target.addEventListener("mousedown", this.#mouseDownHandler); if (this.options.clickButton === Configuration.mouseRightClickButton) { this.target.addEventListener("contextmenu", e => e.preventDefault()); } } unlistenEvents() { this.target.removeEventListener("mousedown", this.#mouseDownHandler); } /* Subclasses will override the following methods */ clicked(location) { this.onClick(this); } unclicked(location) { this.onUnclick(this); } } /** * @typedef {import("./IPointing.js").Options & { * consumeEvent?: Boolean, * strictTarget?: Boolean, * }} Options */ /** * @template {HTMLElement} T * @extends {IPointing} */ class MouseDbClick extends IPointing { /** @param {Coordinates} location */ static ignoreDbClick = location => { } /** @param {MouseEvent} e */ #mouseDbClickHandler = e => { if (!this.options.strictTarget || e.target === e.currentTarget) { if (this.consumeEvent) { e.stopImmediatePropagation(); // Captured, don't call anyone else } this.clickedPosition = this.setLocationFromEvent(e); this.blueprint.mousePosition = [...this.clickedPosition]; this.dbclicked(this.clickedPosition); } } #onDbClick get onDbClick() { return this.#onDbClick } set onDbClick(value) { this.#onDbClick = value; } clickedPosition = /** @type {Coordinates} */([0, 0]) /** * @param {T} target * @param {Blueprint} blueprint * @param {Options} options */ 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 */ /** @param {Coordinates} location */ dbclicked(location) { this.onDbClick(location); } } class Shortcuts { static deleteNodes = "Delete" static duplicateNodes = "(bCtrl=True,Key=D)" static enableLinkDelete = "LeftAlt" static enableZoomIn = ["LeftControl", "RightControl"] // Button to enable more than 1:1 zoom static selectAllNodes = "(bCtrl=True,Key=A)" } /** @template {IElement} ElementT */ class ITemplate { /** @type {ElementT} */ element get blueprint() { return this.element.blueprint } /** @type {IInput[]} */ #inputObjects = [] get inputObjects() { return this.#inputObjects } /** @param {ElementT} element */ initialize(element) { this.element = element; } createInputObjects() { return /** @type {IInput[]} */([]) } setup() { this.#inputObjects.forEach(v => v.setup()); } cleanup() { this.#inputObjects.forEach(v => v.cleanup()); } /** @param {PropertyValues} changedProperties */ willUpdate(changedProperties) { } /** @param {PropertyValues} changedProperties */ update(changedProperties) { } render() { return x`` } /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { } /** @param {PropertyValues} changedProperties */ updated(changedProperties) { } inputSetup() { this.#inputObjects = this.createInputObjects(); } } /** * @template {IFromToPositionedElement} T * @extends {ITemplate} */ class IFromToPositionedTemplate extends ITemplate { /** @param {PropertyValues} 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`; } } } /** @extends {IFromToPositionedTemplate} */ class LinkTemplate extends IFromToPositionedTemplate { /** @param {Number} x */ static sigmoidPositive(x, curvature = 3.7, length = 1.1) { return 1 - Math.exp(-((x / length) ** curvature)) } /** * 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 {Coordinates} p reference point */ static decreasingValue(m, p) { const a = -m * p[0] ** 2; const q = p[1] - a / p[0]; /** @param {Number} x */ 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.05, [500, 130]) static c2Clamped = LinkTemplate.clampedLine([0, 80], [200, 40]) #uniqueId = `ueb-id-${Math.floor(Math.random() * 1E12)}` /** @param {Coordinates} location */ #createKnot = location => { const knotEntity = new KnotEntity({}, this.element.source.entity); const knot = /** @type {NodeElementConstructor} */(ElementFactory.getConstructor("ueb-node")) .newObject(knotEntity); knot.setLocation(...this.blueprint.snapToGrid(...location)); const knotTemplate = /** @type {KnotNodeTemplate} */(knot.template); this.blueprint.addGraphElement(knot); // Important: keep it before changing existing links const inputPin = this.element.getInputPin(); const outputPin = this.element.getOutputPin(); this.element.source = null; this.element.destination = null; const link = /** @type {LinkElementConstructor} */(ElementFactory.getConstructor("ueb-link")) .newObject(outputPin, knotTemplate.inputPin); this.blueprint.addGraphElement(link); this.element.source = knotTemplate.outputPin; this.element.destination = inputPin; } createInputObjects() { /** @type {HTMLElement} */ const linkArea = this.element.querySelector(".ueb-link-area"); return [ ...super.createInputObjects(), new MouseDbClick( linkArea, this.blueprint, undefined, /** @param {Coordinates} location */ location => { location[0] += Configuration.knotOffset[0]; location[1] += Configuration.knotOffset[1]; location = Utility.snapToGrid(location[0], location[1], Configuration.gridSize); this.#createKnot(location); }, ), new MouseClick( linkArea, this.blueprint, { enablerKey: new KeyboardShortcut(this.blueprint, this.blueprint, { activationKeys: Shortcuts.enableLinkDelete, }) }, () => this.blueprint.removeGraphElement(this.element), ), ] } /** @param {PropertyValues} changedProperties */ willUpdate(changedProperties) { super.willUpdate(changedProperties); const sourcePin = this.element.source; const destinationPin = this.element.destination; if (changedProperties.has("fromX") || changedProperties.has("toX")) { const from = this.element.fromX; const to = this.element.toX; const isSourceAKnot = sourcePin?.nodeElement.getType() == Configuration.paths.knot; const isDestinationAKnot = destinationPin?.nodeElement.getType() == Configuration.paths.knot; if (isSourceAKnot && (!destinationPin || isDestinationAKnot)) { if (sourcePin?.isInput() && to > from + Configuration.distanceThreshold) { this.element.source = /** @type {KnotNodeTemplate} */(sourcePin.nodeElement.template).outputPin; } else if (sourcePin?.isOutput() && to < from - Configuration.distanceThreshold) { this.element.source = /** @type {KnotNodeTemplate} */(sourcePin.nodeElement.template).inputPin; } } if (isDestinationAKnot && (!sourcePin || isSourceAKnot)) { if (destinationPin?.isInput() && to < from - Configuration.distanceThreshold) { this.element.destination = /** @type {KnotNodeTemplate} */(destinationPin.nodeElement.template).outputPin; } else if (destinationPin?.isOutput() && to > from + Configuration.distanceThreshold) { this.element.destination = /** @type {KnotNodeTemplate} */(destinationPin.nodeElement.template).inputPin; } } } const dx = Math.max(Math.abs(this.element.fromX - this.element.toX), 1); const dy = 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 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; const aspectRatio = dy / Math.max(30, dx); const c2 = LinkTemplate.c2Clamped(dx) * LinkTemplate.sigmoidPositive(fillRatio * 1.2 + aspectRatio * 0.5, 1.5, 1.8) + this.element.startPercentage; this.element.svgPathD = Configuration.linkRightSVGPath(this.element.startPercentage, c1, c2); } /** @param {PropertyValues} 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.getOutputPin(true); if (referencePin) { this.element.style.setProperty("--ueb-link-color-rgb", LinearColorEntity.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() { return x` ${this.element.linkMessageIcon || this.element.linkMessageText ? x` ` : A} ` } } /** * @template {IEntity} EntityT * @template {ITemplate} TemplateT * @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() { super(); this.fromX = 0; this.fromY = 0; this.toX = 0; this.toY = 0; } /** @param {Coordinates} param0 */ setBothLocations([x, y]) { this.fromX = x; this.fromY = y; this.toX = x; this.toY = y; } /** * @param {Number} x * @param {Number} y */ addSourceLocation(x, y) { this.fromX += x; this.fromY += y; } /** * @param {Number} x * @param {Number} y */ addDestinationLocation(x, y) { this.toX += x; this.toY += y; } } /** @extends {IFromToPositionedElement} */ class LinkElement extends IFromToPositionedElement { static properties = { ...super.properties, dragging: { type: Boolean, attribute: "data-dragging", converter: BooleanEntity.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} */ #source get source() { return this.#source } set source(pin) { this.#setPin(pin, false); } /** @type {PinElement} */ #destination get destination() { return this.#destination } set destination(pin) { this.#setPin(pin, true); } #nodeDeleteHandler = () => this.remove() /** @param {UEBDragEvent} e */ #nodeDragSourceHandler = e => this.addSourceLocation(...e.detail.value) /** @param {UEBDragEvent} e */ #nodeDragDestinatonHandler = e => this.addDestinationLocation(...e.detail.value) #nodeReflowSourceHandler = e => this.setSourceLocation() #nodeReflowDestinatonHandler = e => this.setDestinationLocation() /** @type {TemplateResult | nothing} */ linkMessageIcon = A /** @type {TemplateResult | nothing} */ linkMessageText = A /** @type {SVGPathElement} */ pathElement constructor() { super(); this.dragging = false; this.originatesFromInput = false; this.startPercentage = 0; this.svgPathD = ""; this.startPixels = 0; } /** * @param {PinElement} source * @param {PinElement?} destination */ static newObject(source, destination) { const result = new LinkElement(); result.initialize(source, destination); return result } /** * @param {PinElement} source * @param {PinElement?} destination */ // @ts-expect-error initialize(source, destination) { super.initialize({}, new LinkTemplate()); if (source) { this.source = source; if (!destination) { this.toX = this.fromX; this.toY = this.fromY; } } if (destination) { this.destination = destination; if (!source) { this.fromX = this.toX; this.fromY = this.toY; } } } /** * @param {PinElement} pin * @param {Boolean} isDestinationPin */ #setPin(pin, isDestinationPin) { const getCurrentPin = () => isDestinationPin ? this.destination : this.source; if (getCurrentPin() == pin) { return } if (getCurrentPin()) { const nodeElement = getCurrentPin().getNodeElement(); nodeElement.removeEventListener(Configuration.removeEventName, this.#nodeDeleteHandler); nodeElement.removeEventListener( Configuration.nodeDragEventName, isDestinationPin ? this.#nodeDragDestinatonHandler : this.#nodeDragSourceHandler ); nodeElement.removeEventListener( Configuration.nodeReflowEventName, isDestinationPin ? this.#nodeReflowDestinatonHandler : this.#nodeReflowSourceHandler ); this.#unlinkPins(); } isDestinationPin ? this.#destination = pin : this.#source = pin; if (getCurrentPin()) { const nodeElement = getCurrentPin().getNodeElement(); nodeElement.addEventListener(Configuration.removeEventName, 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.source.isInput()); this.#linkPins(); } } #linkPins() { if (this.source && this.destination) { this.source.linkTo(this.destination); this.destination.linkTo(this.source); } } #unlinkPins() { if (this.source && this.destination) { this.source.unlinkFrom(this.destination, false); this.destination.unlinkFrom(this.source, false); } } cleanup() { super.cleanup(); this.#unlinkPins(); this.source = null; this.destination = null; } /** @param {Coordinates} location */ setSourceLocation(location = null, canPostpone = true) { if (location == null) { const self = this; if (canPostpone && (!this.hasUpdated || !this.source.hasUpdated)) { Promise.all([this.updateComplete, this.source.updateComplete]) .then(() => self.setSourceLocation(null, false)); return } location = this.source.template.getLinkLocation(); } const [x, y] = location; this.fromX = x; this.fromY = y; } /** @param {Coordinates} location */ setDestinationLocation(location = null, canPostpone = true) { if (location == null) { const self = this; if (canPostpone && (!this.hasUpdated || !this.destination.hasUpdated)) { Promise.all([this.updateComplete, this.destination.updateComplete]) .then(() => self.setDestinationLocation(null, false)); return } location = this.destination.template.getLinkLocation(); } this.toX = location[0]; this.toY = location[1]; } getInputPin(getSomething = false) { if (this.source?.isInput()) { return this.source } if (this.destination?.isInput()) { return this.destination } if (getSomething) { return this.source ?? this.destination } } /** @param {PinElement} pin */ setInputPin(pin) { if (this.source?.isInput()) { this.source = pin; } this.destination = pin; } getOutputPin(getSomething = false) { if (this.source?.isOutput()) { return this.source } if (this.destination?.isOutput()) { return this.destination } if (getSomething) { return this.source ?? this.destination } } /** @param {PinElement} pin */ setOutputPin(pin) { if (this.destination?.isOutput()) { this.destination = pin; } this.source = pin; } startDragging() { this.dragging = true; } finishDragging() { this.dragging = false; } removeMessage() { this.linkMessageIcon = A; this.linkMessageText = A; } setMessageConvertType() { this.linkMessageIcon = SVGIcon.convert; this.linkMessageText = x`Convert ${this.source.pinType} to ${this.destination.pinType}.`; } setMessageCorrect() { this.linkMessageIcon = SVGIcon.correct; this.linkMessageText = A; } setMessageReplace() { this.linkMessageIcon = SVGIcon.correct; this.linkMessageText = A; } setMessageDirectionsIncompatible() { this.linkMessageIcon = SVGIcon.reject; this.linkMessageText = x`Directions are not compatbile.`; } setMessagePlaceNode() { this.linkMessageIcon = A; this.linkMessageText = x`Place a new node.`; } setMessageReplaceLink() { this.linkMessageIcon = SVGIcon.correct; this.linkMessageText = x`Replace existing input connections.`; } setMessageReplaceOutputLink() { this.linkMessageIcon = SVGIcon.correct; this.linkMessageText = x`Replace existing output connections.`; } setMessageSameNode() { this.linkMessageIcon = SVGIcon.reject; this.linkMessageText = x`Both are on the same node.`; } /** * @param {PinElement} a * @param {PinElement} b */ setMessageTypesIncompatible(a, b) { this.linkMessageIcon = SVGIcon.reject; this.linkMessageText = x`${Utility.capitalFirstLetter(a.pinType)} is not compatible with ${Utility.capitalFirstLetter(b.pinType)}.`; } } /** * @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, }, sizeX: { type: Number, attribute: false, }, sizeY: { type: Number, attribute: false, }, } static dragEventName = Configuration.dragEventName static dragGeneralEventName = Configuration.dragGeneralEventName constructor() { super(); this.locationX = 0; this.locationY = 0; this.sizeX = 0; this.sizeY = 0; } computeSizes() { const bounding = this.getBoundingClientRect(); this.sizeX = this.blueprint.scaleCorrect(bounding.width); this.sizeY = this.blueprint.scaleCorrect(bounding.height); } /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.computeSizes(); } /** * @param {Number} x * @param {Number} y */ setLocation(x, y, acknowledge = true) { const dx = x - this.locationX; const dy = y - this.locationY; this.locationX = x; this.locationY = y; if (this.blueprint && acknowledge) { const dragLocalEvent = new CustomEvent( /** @type {typeof IDraggableElement} */(this.constructor).dragEventName, { detail: { value: [dx, dy], }, bubbles: false, cancelable: true, } ); this.dispatchEvent(dragLocalEvent); } } /** * @param {Number} x * @param {Number} y */ addLocation(x, y, acknowledge = true) { this.setLocation(this.locationX + x, this.locationY + y, acknowledge); } /** @param {Coordinates} value */ acknowledgeDrag(value) { const dragEvent = new CustomEvent( /** @type {typeof IDraggableElement} */(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[0], snappedLocation[1]); } } topBoundary(justSelectableArea = false) { return this.template.topBoundary(justSelectableArea) } rightBoundary(justSelectableArea = false) { return this.template.rightBoundary(justSelectableArea) } bottomBoundary(justSelectableArea = false) { return this.template.bottomBoundary(justSelectableArea) } leftBoundary(justSelectableArea = false) { return this.template.leftBoundary(justSelectableArea) } } /** * @typedef {import("./IPointing.js").Options & { * clickButton?: Number, * consumeEvent?: Boolean, * draggableElement?: HTMLElement, * exitAnyButton?: Boolean, * moveEverywhere?: Boolean, * movementSpace?: HTMLElement, * repositionOnClick?: Boolean, * scrollGraphEdge?: Boolean, * strictTarget?: Boolean, * stepSize?: Number, * }} Options */ /** * @template {IElement} T * @extends {IPointing} */ class IMouseClickDrag extends IPointing { /** @param {MouseEvent} e */ #mouseDownHandler = 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.consumeEvent) { e.stopImmediatePropagation(); // Captured, don't call anyone else } // Attach the listeners this.#movementListenedElement.addEventListener("mousemove", this.#mouseStartedMovingHandler); document.addEventListener("mouseup", this.#mouseUpHandler); this.setLocationFromEvent(e); this.clickedPosition[0] = this.location[0]; this.clickedPosition[1] = this.location[1]; this.blueprint.mousePosition[0] = this.location[0]; this.blueprint.mousePosition[1] = this.location[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 } } /** @param {MouseEvent} e */ #mouseStartedMovingHandler = e => { if (this.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; this.setLocationFromEvent(e); // Do actual actions this.lastLocation = Utility.snapToGrid(this.clickedPosition[0], this.clickedPosition[1], this.stepSize); this.startDrag(this.location); this.started = true; this.#mouseMoveHandler(e); } /** @param {MouseEvent} e */ #mouseMoveHandler = e => { if (this.consumeEvent) { e.stopImmediatePropagation(); // Captured, don't call anyone else } const location = this.setLocationFromEvent(e); const movement = [e.movementX, e.movementY]; this.dragTo(location, movement); if (this.#trackingMouse) { this.blueprint.mousePosition = location; } if (this.options.scrollGraphEdge) { const movementNorm = Math.sqrt(movement[0] * movement[0] + movement[1] * movement[1]); const threshold = this.blueprint.scaleCorrect(Configuration.edgeScrollThreshold); const leftThreshold = this.blueprint.template.gridLeftVisibilityBoundary() + threshold; const rightThreshold = this.blueprint.template.gridRightVisibilityBoundary() - threshold; let scrollX = 0; if (location[0] < leftThreshold) { scrollX = location[0] - leftThreshold; } else if (location[0] > rightThreshold) { scrollX = location[0] - rightThreshold; } const topThreshold = this.blueprint.template.gridTopVisibilityBoundary() + threshold; const bottomThreshold = this.blueprint.template.gridBottomVisibilityBoundary() - threshold; let scrollY = 0; if (location[1] < topThreshold) { scrollY = location[1] - topThreshold; } else if (location[1] > bottomThreshold) { scrollY = location[1] - bottomThreshold; } scrollX = Utility.clamp(this.blueprint.scaleCorrectReverse(scrollX) ** 3 * movementNorm * 0.6, -20, 20); scrollY = Utility.clamp(this.blueprint.scaleCorrectReverse(scrollY) ** 3 * movementNorm * 0.6, -20, 20); this.blueprint.scrollDelta(scrollX, scrollY); } } /** @param {MouseEvent} e */ #mouseUpHandler = e => { if (!this.options.exitAnyButton || e.button == this.options.clickButton) { if (this.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 get draggableElement() { return this.#draggableElement } clickedOffset = /** @type {Coordinates} */([0, 0]) clickedPosition = /** @type {Coordinates} */([0, 0]) lastLocation = /** @type {Coordinates} */([0, 0]) started = false stepSize = 1 /** * @param {T} target * @param {Blueprint} blueprint * @param {Options} options */ constructor(target, blueprint, options = {}) { options.clickButton ??= Configuration.mouseClickButton; options.consumeEvent ??= true; options.draggableElement ??= target; options.exitAnyButton ??= true; options.moveEverywhere ??= false; options.movementSpace ??= blueprint?.getGridDOMElement(); options.repositionOnClick ??= false; options.scrollGraphEdge ??= false; options.strictTarget ??= false; super(target, blueprint, options); this.stepSize = Number(options.stepSize ?? Configuration.gridSize); this.#movementListenedElement = this.options.moveEverywhere ? document.documentElement : this.movementSpace; this.#draggableElement = /** @type {HTMLElement} */(this.options.draggableElement); this.listenEvents(); } listenEvents() { super.listenEvents(); this.#draggableElement.addEventListener("mousedown", this.#mouseDownHandler); if (this.options.clickButton === Configuration.mouseRightClickButton) { this.#draggableElement.addEventListener("contextmenu", e => e.preventDefault()); } } unlistenEvents() { super.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) { } } /** @typedef {import("./IMouseClickDrag.js").Options} Options */ /** * @template {IDraggableElement} T * @extends {IMouseClickDrag} */ class MouseMoveDraggable extends IMouseClickDrag { /** @param {Coordinates} location */ clicked(location) { if (this.options.repositionOnClick) { this.target.setLocation(...(this.stepSize > 1 ? Utility.snapToGrid(location[0], location[1], this.stepSize) : location )); this.clickedOffset = [0, 0]; } } /** * @param {Coordinates} location * @param {Coordinates} offset */ dragTo(location, offset) { const targetLocation = [ this.target.locationX ?? this.lastLocation[0], this.target.locationY ?? this.lastLocation[1], ]; const [adjustedLocation, adjustedTargetLocation] = this.stepSize > 1 ? [ Utility.snapToGrid(location[0], location[1], this.stepSize), Utility.snapToGrid(targetLocation[0], targetLocation[1], this.stepSize) ] : [location, targetLocation]; offset = [ adjustedLocation[0] - this.lastLocation[0], adjustedLocation[1] - this.lastLocation[1], ]; if (offset[0] == 0 && offset[1] == 0) { return } // Make sure it snaps on the grid offset[0] += adjustedTargetLocation[0] - targetLocation[0]; offset[1] += adjustedTargetLocation[1] - targetLocation[1]; this.dragAction(adjustedLocation, offset); // Reassign the position of mouse this.lastLocation = adjustedLocation; } /** * @param {Coordinates} location * @param {Coordinates} offset */ dragAction(location, offset) { this.target.setLocation(location[0] - this.clickedOffset[0], location[1] - this.clickedOffset[1]); } } /** * @typedef {import("./MouseMoveDraggable.js").Options & { * onClicked?: () => void, * onStartDrag?: () => void, * onDrag?: (location: Coordinates, movement: Coordinates) => void, * onEndDrag?: () => void, * }} Options */ class MouseClickDrag extends MouseMoveDraggable { #onClicked #onStartDrag #onDrag #onEndDrag /** * @param {HTMLElement} target * @param {Blueprint} blueprint * @param {Options} options */ constructor(target, blueprint, options = {}) { super(target, blueprint, options); if (options.onClicked) { this.#onClicked = options.onClicked; } if (options.onStartDrag) { this.#onStartDrag = options.onStartDrag; } if (options.onDrag) { this.#onDrag = options.onDrag; } if (options.onEndDrag) { this.#onEndDrag = options.onEndDrag; } } /** @param {Coordinates} location */ clicked(location) { super.clicked(location); this.#onClicked?.(); } startDrag() { super.startDrag(); this.#onStartDrag?.(); } /** * @param {Coordinates} location * @param {Coordinates} movement */ dragAction(location, movement) { this.#onDrag?.(location, movement); } endDrag() { super.endDrag(); this.#onEndDrag?.(); } } /** @typedef {import("./IMouseClickDrag.js").Options} Options */ /** @extends {MouseMoveDraggable} */ class MouseMoveNodes extends MouseMoveDraggable { /** * @param {NodeElement} target * @param {Blueprint} blueprint * @param {Options} options */ constructor(target, blueprint, options = {}) { super(target, blueprint, options); this.draggableElement.classList.add("ueb-draggable"); } startDrag() { if (!this.target.selected) { this.blueprint.unselectAll(); this.target.setSelected(true); } } dragAction(location, offset) { this.target.acknowledgeDrag(offset); } unclicked() { if (!this.started) { this.blueprint.unselectAll(); this.target.setSelected(true); } else { this.blueprint.getNodes(true).forEach(node => node.boundComments .filter(comment => !node.isInsideComment(comment)) .forEach(comment => node.unbindFromComment(comment)) ); this.blueprint.getCommentNodes().forEach(comment => /** @type {CommentNodeTemplate} */(comment.template).manageNodesBind() ); } } } /** * @template {IDraggableElement} T * @extends {ITemplate} */ class IDraggableTemplate extends ITemplate { /** @returns {HTMLElement} */ getDraggableElement() { return this.element } createDraggableObject() { const draggableElement = this.getDraggableElement(); return new MouseMoveDraggable(this.element, this.blueprint, { draggableElement }) } createInputObjects() { return [ ...super.createInputObjects(), this.createDraggableObject(), new KeyboardShortcut( this.element, this.blueprint, { activationKeys: [ Configuration.Keys.ArrowUp, Configuration.Keys.ArrowRight, Configuration.Keys.ArrowDown, Configuration.Keys.ArrowLeft, ] }, self => self.target.acknowledgeDrag([ self.pressedKey === Configuration.Keys.ArrowLeft ? -Configuration.gridSize : self.pressedKey === Configuration.Keys.ArrowRight ? Configuration.gridSize : 0, self.pressedKey === Configuration.Keys.ArrowUp ? -Configuration.gridSize : self.pressedKey === Configuration.Keys.ArrowDown ? Configuration.gridSize : 0, ]) ) ] } topBoundary(justSelectableArea = false) { return this.element.locationY } rightBoundary(justSelectableArea = false) { return this.element.locationX + this.element.sizeX } bottomBoundary(justSelectableArea = false) { return this.element.locationY + this.element.sizeY } leftBoundary(justSelectableArea = false) { return this.element.locationX } centerInViewport() { const minMargin = Math.min( this.blueprint.template.viewportSize[0] / 10, this.blueprint.template.viewportSize[1] / 10 ); const dl = this.leftBoundary() - this.blueprint.template.gridLeftVisibilityBoundary(); const dr = this.blueprint.template.gridRightVisibilityBoundary() - this.rightBoundary(); let avgX = Math.max((dl + dr) / 2, minMargin); const dt = this.topBoundary() - this.blueprint.template.gridTopVisibilityBoundary(); const db = this.blueprint.template.gridBottomVisibilityBoundary() - this.bottomBoundary(); let avgY = Math.max((dt + db) / 2, minMargin); this.blueprint.scrollDelta(dl - avgX, dt - avgY, true); } } /** * @template {IDraggableElement} T * @extends {IDraggableTemplate} */ class IDraggablePositionedTemplate extends IDraggableTemplate { /** @param {PropertyValues} 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`; } } } /** * @template {NodeElement} T * @extends {IDraggablePositionedTemplate} */ class ISelectableDraggableTemplate extends IDraggablePositionedTemplate { /** @returns {HTMLElement} */ getDraggableElement() { return this.element } createDraggableObject() { return /** @type {MouseMoveDraggable} */(new MouseMoveNodes(this.element, this.blueprint, { draggableElement: this.getDraggableElement(), scrollGraphEdge: true, })) } /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); if (this.element.selected && !this.element.listeningDrag) { this.element.setSelected(true); } } } /** @extends {ISelectableDraggableTemplate} */ class NodeTemplate extends ISelectableDraggableTemplate { static nodeStyleClasses = ["ueb-node-style-default"] #hasSubtitle = false /** @type {() => PinEntity} */ pinInserter /** @type {HTMLElement} */ inputContainer /** @type {HTMLElement} */ outputContainer /** @type {PinElement} */ pinElement addPinHandler = () => { const pin = this.pinInserter?.(); if (pin) { if (this.defaultPin && this.defaultPin.isInput() === pin.isInput()) { this.defaultPin.before(this.createPinElement(pin)); } else { (pin.isInput() ? this.inputContainer : this.outputContainer).appendChild(this.createPinElement(pin)); } this.element.acknowledgeReflow(); } } toggleAdvancedDisplayHandler = () => { this.element.toggleShowAdvancedPinDisplay(); this.element.requestUpdate(); this.element.updateComplete.then(() => this.element.acknowledgeReflow()); } /** @param {PinEntity} pinEntity */ createPinElement(pinEntity) { const pinElement = /** @type {PinElementConstructor} */(ElementFactory.getConstructor("ueb-pin")) .newObject(pinEntity, undefined, this.element); if (this.pinInserter && !this.defaultPin && pinElement.getPinName() === "Default") { this.defaultPin = pinElement; this.defaultPin.classList.add("ueb-node-variadic-default"); } return pinElement } /** @param {NodeElement} element */ initialize(element) { super.initialize(element); this.element.classList.add(.../** @type {typeof NodeTemplate} */(this.constructor).nodeStyleClasses); this.element.style.setProperty("--ueb-node-color", this.getColor().cssText); this.pinInserter = this.element.entity.additionalPinInserter(); if (this.pinInserter) { this.element.classList.add("ueb-node-is-variadic"); } } getColor() { return this.element.entity.nodeColor() } render() { return x`
${this.renderTop()}
${this.pinInserter ? x`
Add pin ${SVGIcon.plusCircle}
`: A} ${this.element.entity.isDevelopmentOnly() ? x`
Development Only
` : A} ${this.element.advancedPinDisplay ? x`
${SVGIcon.expandIcon}
` : A}
` } renderNodeIcon() { return this.element.entity.nodeIcon() } renderNodeName() { return this.element.nodeDisplayName } renderTop() { const icon = this.renderNodeIcon(); const name = this.renderNodeName(); return x`
${icon ? x`
${icon}
` : A} ${name ? x`
${name} ${this.#hasSubtitle && this.getTargetType().length > 0 ? x`
Target is ${Utility.formatStringName(this.getTargetType())}
`: A}
` : A}
` } /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.inputContainer = this.element.querySelector(".ueb-node-inputs"); this.outputContainer = this.element.querySelector(".ueb-node-outputs"); this.setupPins(); this.element.updateComplete.then(() => this.element.acknowledgeReflow()); } setupPins() { this.element.nodeNameElement = /** @type {HTMLElement} */(this.element.querySelector(".ueb-node-name-text")); let hasInput = false; let hasOutput = false; for (const p of this.getPinElements()) { if (p === this.defaultPin) { continue } if (p.isInput()) { this.inputContainer.appendChild(p); hasInput = true; } else if (p.isOutput()) { this.outputContainer.appendChild(p); hasOutput = true; } } if (this.defaultPin) { (this.defaultPin.isInput() ? this.inputContainer : this.outputContainer).appendChild(this.defaultPin); } if (hasInput) { this.element.classList.add("ueb-node-has-inputs"); } if (hasOutput) { this.element.classList.add("ueb-node-has-outputs"); } } getPinElements() { return this.element.getPinElements() } createPinElements() { return this.element.getPinEntities() .filter(v => !v.isHidden()) .map(pinEntity => { this.#hasSubtitle = this.#hasSubtitle || pinEntity.PinName.toString() === "self" && pinEntity.pinTitle() === "Target"; return this.createPinElement(pinEntity) }) } getTargetType() { return this.element.entity.FunctionReference?.MemberParent?.getName() ?? "Untitled" } linksChanged() { } } class IResizeableTemplate extends NodeTemplate { #THandler = document.createElement("div") #RHandler = document.createElement("div") #BHandler = document.createElement("div") #LHandler = document.createElement("div") #TRHandler = document.createElement("div") #BRHandler = document.createElement("div") #BLHandler = document.createElement("div") #TLHandler = document.createElement("div") /** @param {NodeElement} element */ initialize(element) { super.initialize(element); this.element.classList.add("ueb-resizeable"); this.#THandler.classList.add("ueb-resizeable-top"); this.#RHandler.classList.add("ueb-resizeable-right"); this.#BHandler.classList.add("ueb-resizeable-bottom"); this.#LHandler.classList.add("ueb-resizeable-left"); this.#TRHandler.classList.add("ueb-resizeable-top-right"); this.#BRHandler.classList.add("ueb-resizeable-bottom-right"); this.#BLHandler.classList.add("ueb-resizeable-bottom-left"); this.#TLHandler.classList.add("ueb-resizeable-top-left"); } /** @param {PropertyValues} changedProperties */ update(changedProperties) { super.update(changedProperties); if (this.element.sizeX >= 0 && changedProperties.has("sizeX")) { this.element.style.width = `${this.element.sizeX}px`; } if (this.element.sizeY >= 0 && changedProperties.has("sizeY")) { this.element.style.height = `${this.element.sizeY}px`; } } /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.element.append( this.#THandler, this.#RHandler, this.#BHandler, this.#LHandler, this.#TRHandler, this.#BRHandler, this.#BLHandler, this.#TLHandler ); } createInputObjects() { return [ ...super.createInputObjects(), new MouseClickDrag(this.#THandler, this.blueprint, { onDrag: (location, movement) => { movement[1] = location[1] - this.element.topBoundary(); if (this.setSizeY(this.element.sizeY - movement[1])) { this.element.addLocation(0, movement[1], false); } }, onEndDrag: () => this.endResize(), }), new MouseClickDrag(this.#RHandler, this.blueprint, { onDrag: (location, movement) => { movement[0] = location[0] - this.element.rightBoundary(); this.setSizeX(this.element.sizeX + movement[0]); }, onEndDrag: () => this.endResize(), }), new MouseClickDrag(this.#BHandler, this.blueprint, { onDrag: (location, movement) => { movement[1] = location[1] - this.element.bottomBoundary(); this.setSizeY(this.element.sizeY + movement[1]); }, onEndDrag: () => this.endResize(), }), new MouseClickDrag(this.#LHandler, this.blueprint, { onDrag: (location, movement) => { movement[0] = location[0] - this.element.leftBoundary(); if (this.setSizeX(this.element.sizeX - movement[0])) { this.element.addLocation(movement[0], 0, false); } }, onEndDrag: () => this.endResize(), }), new MouseClickDrag(this.#TRHandler, this.blueprint, { onDrag: (location, movement) => { movement[0] = location[0] - this.element.rightBoundary(); movement[1] = location[1] - this.element.topBoundary(); this.setSizeX(this.element.sizeX + movement[0]); if (this.setSizeY(this.element.sizeY - movement[1])) { this.element.addLocation(0, movement[1], false); } }, onEndDrag: () => this.endResize(), }), new MouseClickDrag(this.#BRHandler, this.blueprint, { onDrag: (location, movement) => { movement[0] = location[0] - this.element.rightBoundary(); movement[1] = location[1] - this.element.bottomBoundary(); this.setSizeX(this.element.sizeX + movement[0]); this.setSizeY(this.element.sizeY + movement[1]); }, onEndDrag: () => this.endResize(), }), new MouseClickDrag(this.#BLHandler, this.blueprint, { onDrag: (location, movement) => { movement[0] = location[0] - this.element.leftBoundary(); movement[1] = location[1] - this.element.bottomBoundary(); if (this.setSizeX(this.element.sizeX - movement[0])) { this.element.addLocation(movement[0], 0, false); } this.setSizeY(this.element.sizeY + movement[1]); }, onEndDrag: () => this.endResize(), }), new MouseClickDrag(this.#TLHandler, this.blueprint, { onDrag: (location, movement) => { movement[0] = location[0] - this.element.leftBoundary(); movement[1] = location[1] - this.element.topBoundary(); if (this.setSizeX(this.element.sizeX - movement[0])) { this.element.addLocation(movement[0], 0, false); } if (this.setSizeY(this.element.sizeY - movement[1])) { this.element.addLocation(0, movement[1], false); } }, onEndDrag: () => this.endResize(), }), ] } /** @param {Number} value */ setSizeX(value) { this.element.setNodeWidth(value); return true } /** @param {Number} value */ setSizeY(value) { this.element.setNodeHeight(value); return true } endResize() { } } class CommentNodeTemplate extends IResizeableTemplate { #selectableAreaHeight = 0 /** @param {NodeElement} element */ initialize(element) { super.initialize(element); element.classList.add("ueb-node-style-comment", "ueb-node-resizeable"); element.sizeX = 25 * Configuration.gridSize; element.sizeY = 6 * Configuration.gridSize; super.initialize(element); // Keep it at the end because it calls this.getColor() where this.#color must be initialized } /** @returns {HTMLElement} */ getDraggableElement() { return this.element.querySelector(".ueb-node-top") } render() { return x`
` } /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); const bounding = this.getDraggableElement().getBoundingClientRect(); this.#selectableAreaHeight = bounding.height; } manageNodesBind() { let nodes = this.blueprint.getNodes(); for (let node of nodes) { if ( node.topBoundary() >= this.element.topBoundary() && node.rightBoundary() <= this.element.rightBoundary() && node.bottomBoundary() <= this.element.bottomBoundary() && node.leftBoundary() >= this.element.leftBoundary() ) { node.bindToComment(this.element); } else { node.unbindFromComment(this.element); } } } /** @param {Number} value */ setSizeX(value) { value = Math.round(value); if (value >= 2 * Configuration.gridSize) { this.element.setNodeWidth(value); return true } return false } /** @param {Number} value */ setSizeY(value) { value = Math.round(value); if (value >= 2 * Configuration.gridSize) { this.element.setNodeHeight(value); return true } return false } endResize() { this.manageNodesBind(); } topBoundary(justSelectableArea = false) { return this.element.locationY } rightBoundary(justSelectableArea = false) { return this.element.locationX + this.element.sizeX } bottomBoundary(justSelectableArea = false) { return justSelectableArea ? this.element.locationY + this.#selectableAreaHeight : super.bottomBoundary() } leftBoundary(justSelectableArea = false) { return this.element.locationX } } /** * @typedef {import("./IMouseClickDrag.js").Options & { * scrollGraphEdge?: Boolean, * }} Options */ /** @extends IMouseClickDrag */ class MouseCreateLink extends IMouseClickDrag { /** @type {NodeListOf} */ #listenedPins /** @type {PinElement} */ #knotPin = null /** @param {MouseEvent} e */ #mouseenterHandler = e => { if (!this.enteredPin) { this.linkValid = false; this.enteredPin = /** @type {PinElement} */(e.target); const a = this.link.source ?? this.target; // Remember target might have change const b = this.enteredPin; const outputPin = a.isOutput() ? a : b; if ( a.nodeElement.getType() === Configuration.paths.knot || b.nodeElement.getType() === Configuration.paths.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 (this.blueprint.getLinks(a, b).length) { this.link.setMessageReplaceLink(); this.linkValid = true; } else if (outputPin.entity.getType() === "exec" && outputPin.isLinked) { this.link.setMessageReplaceOutputLink(); this.linkValid = true; } else if ( (a.entity.PinType.PinCategory.valueOf() != "object" || b.entity.PinType.PinCategory.valueOf() != "object") && a.pinType != b.pinType ) { this.link.setMessageTypesIncompatible(a, b); this.linkValid = false; } else { this.link.setMessageCorrect(); this.linkValid = true; } } } /** @param {MouseEvent} e */ #mouseleaveHandler = e => { if (this.enteredPin == e.target) { this.enteredPin = null; this.linkValid = false; this.link?.setMessagePlaceNode(); } } /** @type {LinkElement?} */ link /** @type {PinElement?} */ enteredPin linkValid = false /** * @param {PinElement} target * @param {Blueprint} blueprint * @param {Options} options */ constructor(target, blueprint, options = {}) { options.scrollGraphEdge ??= true; super(target, blueprint, options); } startDrag(location) { if (this.target.nodeElement.getType() == Configuration.paths.knot) { this.#knotPin = this.target; } /** @type {LinkElement} */ this.link = /** @type {LinkElementConstructor} */(ElementFactory.getConstructor("ueb-link")) .newObject(this.target, null); this.blueprint.template.linksContainerElement.prepend(this.link); this.link.setMessagePlaceNode(); this.#listenedPins = this.blueprint.querySelectorAll("ueb-pin"); this.#listenedPins.forEach(pin => { if (pin != this.target) { pin.addEventListener("mouseenter", this.#mouseenterHandler); pin.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); }); this.#listenedPins = null; if (this.enteredPin && this.linkValid) { // Knot can use wither the input or output (by default) part indifferently, check if a switch is needed if (this.#knotPin) { const otherPin = this.#knotPin !== this.link.source ? this.link.source : this.enteredPin; // Knot pin direction correction if (this.#knotPin.isInput() && otherPin.isInput() || this.#knotPin.isOutput() && otherPin.isOutput()) { const oppositePin = /** @type {KnotPinTemplate} */(this.#knotPin.template).getOppositePin(); if (this.#knotPin === this.link.source) { this.link.source = oppositePin; } else { this.enteredPin = oppositePin; } } } else if (this.enteredPin.nodeElement.getType() === Configuration.paths.knot) { this.#knotPin = this.enteredPin; if (this.link.source.isOutput()) { // Knot uses by default the output pin, let's switch to keep it coherent with the source node we have this.enteredPin = /** @type {KnotPinTemplate} */(this.enteredPin.template).getOppositePin(); } } if (!this.link.source.getLinks().find(ref => ref.equals(this.enteredPin.createPinReference()))) { this.blueprint.addGraphElement(this.link); this.link.destination = this.enteredPin; } else { this.link.remove(); } } else { this.link.remove(); } this.enteredPin = null; this.link.removeMessage(); this.link.finishDragging(); this.link = null; } } class VariableManagementNodeTemplate extends NodeTemplate { #hasInput = false #hasOutput = false displayName = "" static nodeStyleClasses = ["ueb-node-style-glass"] /** @param {NodeElement} element */ initialize(element) { super.initialize(element); this.displayName = this.element.nodeDisplayName; } render() { return x`
${this.displayName ? x`
${this.displayName}
` : A} ${this.#hasInput ? x`
` : A} ${this.#hasOutput ? x`
` : A} ${this.pinInserter ? x`
Add pin ${SVGIcon.plusCircle}
`: A}
` } createPinElements() { return this.element.getPinEntities() .filter(v => !v.isHidden()) .map(v => { this.#hasInput ||= v.isInput(); this.#hasOutput ||= v.isOutput(); const result = /** @type {PinElementConstructor} */(ElementFactory.getConstructor("ueb-pin")) .newObject(v, undefined, this.element); return result }) } } class MetasoundOperationTemplate extends VariableManagementNodeTemplate { static nodeStyleClasses = ["ueb-node-style-metasound", "ueb-node-style-operation"] } class VariableConversionNodeTemplate extends VariableManagementNodeTemplate { static nodeStyleClasses = [...super.nodeStyleClasses, "ueb-node-style-conversion"] } class VariableOperationNodeTemplate extends VariableManagementNodeTemplate { static nodeStyleClasses = [...super.nodeStyleClasses, "ueb-node-style-operation"] } /** * @template {IEntity} T * @typedef {import("../../element/PinElement.js").default} PinElement */ /** * @template {IEntity} T * @extends ITemplate> */ class PinTemplate extends ITemplate { static canWrapInput = true /** @type {HTMLElement} */ #iconElement get iconElement() { return this.#iconElement } /** @type {HTMLElement} */ #wrapperElement get wrapperElement() { return this.#wrapperElement } isNameRendered = true /** @param {PinElement} element */ initialize(element) { super.initialize(element); if (this.element.nodeElement) { const nodeTemplate = this.element.nodeElement.template; this.isNameRendered = !( nodeTemplate instanceof VariableConversionNodeTemplate || nodeTemplate instanceof VariableOperationNodeTemplate || nodeTemplate instanceof MetasoundOperationTemplate ); } } setup() { super.setup(); this.element.nodeElement = this.element.closest("ueb-node"); const nodeTemplate = this.element.nodeElement.template; if ( nodeTemplate instanceof VariableConversionNodeTemplate || nodeTemplate instanceof VariableOperationNodeTemplate ) { this.isNameRendered = false; this.element.requestUpdate(); } } /** @returns {IInput[]} */ createInputObjects() { return [ new MouseCreateLink(this.element, this.blueprint, { moveEverywhere: true, draggableElement: this.getClickableElement(), }) ] } render() { const icon = x`
${this.renderIcon()}
`; const content = x`
${this.isNameRendered ? this.renderName() : A} ${this.isInputRendered() ? this.renderInput() : x``}
`; return x`
${this.element.isInput() ? x`${icon}${content}` : x`${content}${icon}`}
` } renderIcon() { if (this.element.nodeElement.entity.isPcg()) { switch (this.element.entity.getType()) { case "Any": return SVGIcon.pcgPin case "Param": case "Param[]": return SVGIcon.pcgParamPin case "Spatial": case "Spatial[]": return SVGIcon.pcgSpatialPin case "Any[]": case "Point[]": case "Surface[]": case "Volume[]": if (this.element.isOutput()) { return SVGIcon.pcgPin } case "Point": case "Surface": case "Volume": return SVGIcon.pcgStackPin } } switch (this.element.entity.PinType.ContainerType?.toString()) { case "Array": return SVGIcon.arrayPin case "Set": return SVGIcon.setPin case "Map": return SVGIcon.mapPin } if (this.element.entity.PinType.PinCategory?.toString().toLocaleLowerCase() === "delegate") { return SVGIcon.delegate } if (this.element.nodeElement?.template instanceof VariableOperationNodeTemplate) { return SVGIcon.operationPin } return SVGIcon.genericPin } renderName() { let name = this.element.getPinDisplayName(); const nodeElement = this.element.nodeElement; const pinName = this.element.getPinName(); if ( nodeElement.getType() == Configuration.paths.makeStruct && pinName == nodeElement.entity.StructType.getName() ) { name = pinName; } return x` ${name} ` } isInputRendered() { return this.element.isInput() && !this.element.entity.bDefaultValueIsIgnored?.valueOf() && !this.element.entity.PinType.bIsReference?.valueOf() } renderInput() { return x`` } /** @param {PropertyValues} changedProperties */ updated(changedProperties) { super.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; this.element.requestUpdate(); this.element.updateComplete.then(() => node.acknowledgeReflow()); } } /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.element.style.setProperty("--ueb-pin-color-rgb", this.element.entity.pinColor().cssText); this.#iconElement = this.element.querySelector(".ueb-pin-icon svg") ?? this.element; this.#wrapperElement = this.element.querySelector(".ueb-pin-wrapper"); } getLinkLocation() { const rect = this.iconElement.getBoundingClientRect(); /** @type {[Number, Number]} */ const boundingLocation = [this.element.isInput() ? rect.left : rect.right + 1, (rect.top + rect.bottom) / 2]; const location = Utility.convertLocation(boundingLocation, this.blueprint.template.gridElement); return this.blueprint.compensateTranslation(location[0], location[1]) } getClickableElement() { return this.#wrapperElement ?? this.element } } /** * @template {IEntity} T * @extends PinTemplate */ class MinimalPinTemplate extends PinTemplate { render() { return x`
${this.renderIcon()}
` } } class EventNodeTemplate extends NodeTemplate { static nodeStyleClasses = [...super.nodeStyleClasses, "ueb-node-style-event"] #delegatePinElement /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.element.querySelector(".ueb-node-top").appendChild(this.createDelegatePinElement()); } renderTop() { const icon = this.renderNodeIcon(); const name = this.renderNodeName(); const customEvent = this.element.getType() === Configuration.paths.customEvent && (this.element.entity.CustomFunctionName || this.element.entity.FunctionReference.MemberParent); return x`
${icon ? x`
${icon}
` : A} ${name ? x`
${name} ${customEvent ? x`
Custom Event
`: A}
` : A}
` } getPinElements() { return this.element.getPinElements().filter(v => v.entity.PinType.PinCategory?.toString() !== "delegate") } createDelegatePinElement() { if (!this.#delegatePinElement) { this.#delegatePinElement = /** @type {PinElementConstructor} */(ElementFactory.getConstructor("ueb-pin")) .newObject( this.element.getPinEntities().find(v => !v.isHidden() && v.PinType.PinCategory?.toString() === "delegate"), new MinimalPinTemplate(), this.element ); this.#delegatePinElement.template.isNameRendered = false; } return this.#delegatePinElement } createPinElements() { return [ this.createDelegatePinElement(), ...this.element.getPinEntities() .filter(v => !v.isHidden() && v.PinType.PinCategory?.toString() !== "delegate") .map(pinEntity => /** @type {PinElementConstructor} */(ElementFactory.getConstructor("ueb-pin")) .newObject(pinEntity, undefined, this.element) ) ] } } /** @extends MinimalPinTemplate */ class KnotPinTemplate extends MinimalPinTemplate { render() { return this.element.isOutput() ? super.render() : x`` } getOppositePin() { const nodeTemplate = /** @type {KnotNodeTemplate} */(this.element.nodeElement.template); return this.element.isOutput() ? nodeTemplate.inputPin : nodeTemplate.outputPin } getLinkLocation() { const rect = ( this.element.isInput() ? /** @type {KnotNodeTemplate} */(this.element.nodeElement.template).outputPin.template : this ) .iconElement.getBoundingClientRect(); /** @type {Coordinates} */ const boundingLocation = [this.element.isInput() ? rect.left : rect.right + 1, (rect.top + rect.bottom) / 2]; const location = Utility.convertLocation(boundingLocation, this.blueprint.template.gridElement); return this.blueprint.compensateTranslation(location[0], location[1]) } } 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 {NodeElement} element */ initialize(element) { super.initialize(element); this.element.classList.add("ueb-node-style-minimal"); } /** @param {PinElement} startingPin */ findDirectionaPin(startingPin) { if ( startingPin.nodeElement.getType() !== Configuration.paths.knot || KnotNodeTemplate.#traversedPin.has(startingPin) ) { KnotNodeTemplate.#traversedPin.clear(); return true } KnotNodeTemplate.#traversedPin.add(startingPin); for (let pin of startingPin.getLinks().map(l => this.blueprint.getPin(l))) { if (this.findDirectionaPin(pin)) { return true } } return false } render() { return x`
` } setupPins() { for (const p of this.getPinElements()) { /** @type {HTMLElement} */(this.element.querySelector(".ueb-node-border")).appendChild(p); } } 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 = /** @type {PinElementConstructor} */(ElementFactory.getConstructor("ueb-pin")); let result = [ this.#inputPin = pinElementConstructor.newObject(inputEntity, new KnotPinTemplate(), this.element), this.#outputPin = pinElementConstructor.newObject(outputEntity, new KnotPinTemplate(), this.element), ]; return result } linksChanged() { } } class MetasoundNodeTemplate extends NodeTemplate { static nodeStyleClasses = ["ueb-node-style-metasound"] } class VariableAccessNodeTemplate extends VariableManagementNodeTemplate { /** @param {NodeElement} element */ initialize(element) { super.initialize(element); const type = element.getType(); if ( type === Configuration.paths.variableGet || type === Configuration.paths.self ) { this.element.classList.add("ueb-node-style-getter"); this.displayName = ""; } else if (type === Configuration.paths.variableSet) { this.element.classList.add("ueb-node-style-setter"); } } 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); } } /** * @param {ObjectEntity} nodeEntity * @return {new () => NodeTemplate} */ function nodeTemplateClass(nodeEntity) { if ( nodeEntity.getClass() === Configuration.paths.callFunction || nodeEntity.getClass() === Configuration.paths.commutativeAssociativeBinaryOperator || nodeEntity.getClass() === Configuration.paths.callArrayFunction ) { const memberParent = nodeEntity.FunctionReference?.MemberParent?.path ?? ""; const memberName = nodeEntity.FunctionReference?.MemberName?.toString(); if ( memberName && ( memberParent === Configuration.paths.kismetMathLibrary || memberParent === Configuration.paths.kismetArrayLibrary )) { if (memberName.startsWith("Conv_")) { return VariableConversionNodeTemplate } if ( memberName.startsWith("Add_") || memberName.startsWith("And_") || memberName.startsWith("Boolean") // Boolean logic operations || memberName.startsWith("Cross_") || memberName.startsWith("Dot_") || memberName.startsWith("Not_") || memberName.startsWith("Or_") || memberName.startsWith("Percent_") || memberName.startsWith("Xor_") ) { return VariableOperationNodeTemplate } switch (memberName) { case "Abs": case "Array_Add": case "Array_AddUnique": case "Array_Identical": case "BMax": case "BMin": case "CrossProduct2D": case "DotProduct2D": case "Exp": case "FMax": case "FMin": case "GetPI": case "Max": case "MaxInt64": case "Min": case "MinInt64": case "Sqrt": case "Square": case "Vector4_CrossProduct3": case "Vector4_DotProduct": case "Vector4_DotProduct3": // Trigonometry case "Acos": case "Asin": case "Cos": case "DegAcos": case "DegCos": case "DegSin": case "DegTan": case "Sin": case "Tan": return VariableOperationNodeTemplate } } if (memberParent === Configuration.paths.blueprintSetLibrary) { return VariableOperationNodeTemplate } if (memberParent === Configuration.paths.blueprintMapLibrary) { return VariableOperationNodeTemplate } } switch (nodeEntity.getClass()) { case Configuration.paths.comment: case Configuration.paths.materialGraphNodeComment: return CommentNodeTemplate case Configuration.paths.createDelegate: return NodeTemplate case Configuration.paths.metasoundEditorGraphExternalNode: if (nodeEntity["ClassName"]?.["Name"] == "Add") { return MetasoundOperationTemplate } return MetasoundNodeTemplate case Configuration.paths.niagaraNodeOp: if ( [ "Boolean::LogicEq", "Boolean::LogicNEq", "Numeric::Abs", "Numeric::Add", "Numeric::Mul", ].includes(nodeEntity.OpName?.toString()) ) { return VariableOperationNodeTemplate } break case Configuration.paths.promotableOperator: return VariableOperationNodeTemplate case Configuration.paths.knot: return KnotNodeTemplate case Configuration.paths.literal: case Configuration.paths.self: case Configuration.paths.variableGet: case Configuration.paths.variableSet: return VariableAccessNodeTemplate } if (nodeEntity.isEvent()) { return EventNodeTemplate } return NodeTemplate } /** * @template {IEntity} EntityT * @template {ISelectableDraggableTemplate} TemplateT * @extends {IDraggableElement} */ class ISelectableDraggableElement extends IDraggableElement { static properties = { ...super.properties, selected: { type: Boolean, attribute: "data-selected", reflect: true, converter: BooleanEntity.booleanConverter, }, } /** @param {UEBDragEvent} e */ dragHandler = e => this.addLocation(...e.detail.value) constructor() { super(); this.selected = false; this.listeningDrag = false; } setup() { super.setup(); this.setSelected(this.selected); } cleanup() { super.cleanup(); 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; } } } } /** @extends {ISelectableDraggableElement} */ class NodeElement extends ISelectableDraggableElement { static properties = { ...ISelectableDraggableElement.properties, typePath: { type: String, attribute: "data-type", reflect: true, }, nodeTitle: { type: String, attribute: "data-title", reflect: true, }, advancedPinDisplay: { type: String, attribute: "data-advanced-display", converter: SymbolEntity.attributeConverter, reflect: true, }, enabledState: { type: String, attribute: "data-enabled-state", reflect: true, }, nodeDisplayName: { type: String, attribute: false, }, pureFunction: { type: Boolean, converter: BooleanEntity.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; } /** @type {PinElement[]} */ #pins = [] /** @type {NodeElement[]} */ boundComments = [] #commentDragged = false /** @param {UEBDragEvent} e */ #commentDragHandler = e => { // If selected, it will already drag, also must check if under nested comments, it must drag just once if (!this.selected && !this.#commentDragged) { this.#commentDragged = true; this.requestUpdate(); this.updateComplete.then(() => this.#commentDragged = false); this.addLocation(...e.detail.value); } } /** @param {String} str */ static fromSerializedObject(str) { str = str.trim(); let entity = ObjectEntity.grammar.parse(str); return NodeElement.newObject(/** @type {ObjectEntity} */(entity)) } /** * @param {ObjectEntity} entity * @param {NodeTemplate} template */ static newObject(entity = new ObjectEntity(), template = new (nodeTemplateClass(entity))()) { const result = new NodeElement(); result.initialize(entity, template); return result } /** @param {String} name */ #redirectLinksAfterRename(name) { for (let sourcePinElement of this.getPinElements()) { for (let targetPinReference of sourcePinElement.getLinks()) { this.blueprint.getPin(targetPinReference).redirectLink( sourcePinElement, new PinReferenceEntity( new SymbolEntity(name), sourcePinElement.entity.PinId, ) ); } } } initialize(entity = new ObjectEntity(), template = new (nodeTemplateClass(entity))()) { this.typePath = entity.getType(); this.nodeTitle = entity.getObjectName(); this.advancedPinDisplay = entity.AdvancedPinDisplay?.toString(); this.enabledState = entity.EnabledState; this.nodeDisplayName = nodeTitle(entity); this.pureFunction = entity.bIsPureFunc?.valueOf(); this.dragLinkObjects = []; super.initialize(entity, template); this.#pins = this.template.createPinElements(); super.setLocation(this.entity.getNodePosX(), this.entity.getNodePosY()); if (this.entity.NodeWidth && this.entity.NodeHeight) { this.sizeX = this.entity.NodeWidth.value; this.sizeY = this.entity.NodeHeight.value; } else { this.updateComplete.then(() => this.computeSizes()); } entity.listenAttribute( "Name", /** @param {InstanceType} newName */ newName => { this.nodeTitle = newName.value; this.nodeDisplayName = nodeTitle(entity); this.#redirectLinksAfterRename(newName.value); } ); } async getUpdateComplete() { let result = await super.getUpdateComplete(); for (const pin of this.getPinElements()) { result &&= await pin.updateComplete; } return result } /** @param {NodeElement} commentNode */ bindToComment(commentNode) { if (commentNode != this && !this.boundComments.includes(commentNode)) { commentNode.addEventListener(Configuration.nodeDragEventName, this.#commentDragHandler); this.boundComments.push(commentNode); } } /** @param {NodeElement} commentNode */ unbindFromComment(commentNode) { const commentIndex = this.boundComments.indexOf(commentNode); if (commentIndex >= 0) { commentNode.removeEventListener(Configuration.nodeDragEventName, this.#commentDragHandler); this.boundComments[commentIndex] = this.boundComments[this.boundComments.length - 1]; this.boundComments.pop(); } } /** @param {NodeElement} commentNode */ isInsideComment(commentNode) { return this.topBoundary() >= commentNode.topBoundary() && this.rightBoundary() <= commentNode.rightBoundary() && this.bottomBoundary() <= commentNode.bottomBoundary() && this.leftBoundary() >= commentNode.leftBoundary() } getType() { return this.entity.getType() } getNodeName() { return this.entity.getObjectName() } computeNodeDisplayName() { this.nodeDisplayName = nodeTitle(this.entity); } /** @param {Number} value */ setNodeWidth(value) { this.entity.setNodeWidth(value); this.sizeX = value; this.acknowledgeReflow(); } /** @param {Number} value */ setNodeHeight(value) { this.entity.setNodeHeight(value); this.sizeY = value; this.acknowledgeReflow(); } /** @param {IElement[]} nodesWhitelist */ sanitizeLinks(nodesWhitelist = []) { this.getPinElements().forEach(pin => pin.sanitizeLinks(nodesWhitelist)); } getPinElements() { return this.#pins } /** @returns {PinEntity[]} */ getPinEntities() { return this.entity.getPinEntities() } setLocation(x = 0, y = 0, acknowledge = true) { this.entity.setNodePosX(x); this.entity.setNodePosY(y); super.setLocation(x, y, acknowledge); } acknowledgeReflow() { this.requestUpdate(); this.updateComplete.then(() => this.computeSizes()); let reflowEvent = new CustomEvent(Configuration.nodeReflowEventName); this.dispatchEvent(reflowEvent); } setShowAdvancedPinDisplay(value) { this.entity.AdvancedPinDisplay = new SymbolEntity(value ? "Shown" : "Hidden"); this.advancedPinDisplay = this.entity.AdvancedPinDisplay; } toggleShowAdvancedPinDisplay() { this.setShowAdvancedPinDisplay(this.entity.AdvancedPinDisplay?.toString() != "Shown"); } } class BlueprintEntity extends ObjectEntity { /** @type {Map} */ #objectEntitiesNameCounter = new Map() /** @type {ObjectEntity[]}" */ #objectEntities = [] get objectEntities() { return this.#objectEntities } /** @param {ObjectEntity} entity */ getHomonymObjectEntity(entity) { const name = entity.getObjectName(false); return this.#objectEntities.find(entity => entity.getObjectName() == name) } /** @param {String} name */ takeFreeName(name) { name = name.replace(/_\d+$/, ""); const counter = (this.#objectEntitiesNameCounter.get(name) ?? -1) + 1; this.#objectEntitiesNameCounter.set(name, counter); return Configuration.nodeTitle(name, counter) } /** @param {ObjectEntity} entity */ addObjectEntity(entity) { if (!this.#objectEntities.includes(entity)) { this.#objectEntities.push(entity); const [name, counter] = entity.getNameAndCounter(); this.#objectEntitiesNameCounter.set( name, Math.max((this.#objectEntitiesNameCounter.get(name) ?? 0), counter) ); return true } return false } /** @param {ObjectEntity} entity */ removeObjectEntity(entity) { const index = this.#objectEntities.indexOf(entity); if (index >= 0) { const last = this.#objectEntities.pop(); if (index < this.#objectEntities.length) { this.#objectEntities[index] = last; } return true } return false } /** @param {ObjectEntity} entity */ mergeWith(entity) { if (!entity.ScriptVariables || entity.ScriptVariables.length === 0) { return this } if (!this.ScriptVariables || this.ScriptVariables.length === 0) { this.ScriptVariables = entity.ScriptVariables; } let scriptVariables = Utility.mergeArrays( this.ScriptVariables.valueOf(), entity.ScriptVariables.valueOf(), (l, r) => l.OriginalChangeId.value == r.OriginalChangeId.value ); if (scriptVariables.length === this.ScriptVariables.length) { return this } const entries = scriptVariables.concat(scriptVariables).map((v, i) => { const name = Configuration.subObjectAttributeNameFromReference(v.ScriptVariable, i >= scriptVariables.length); return [ name, this[name] ?? entity[name] ] }); entries.push( ...Object.entries(this).filter(([k, v]) => !k.startsWith(Configuration.subObjectAttributeNamePrefix) && k !== "ExportedNodes" ) ); return new BlueprintEntity(Object.fromEntries(entries)) } } /** * @typedef {import("../IInput.js").Options & { * listenOnFocus?: Boolean, * unlistenOnTextEdit?: Boolean, * }} Options */ class Copy extends IInput { /** @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() { window.addEventListener("copy", this.#copyHandler); } unlistenEvents() { window.removeEventListener("copy", this.#copyHandler); } getSerializedText() { const allNodes = this.blueprint.getNodes(true).map(n => n.entity); const exported = allNodes.filter(n => n.exported).map(n => n.serialize()); const result = allNodes.filter(n => !n.exported).map(n => n.serialize()); if (exported.length) { this.blueprint.entity.ExportedNodes.value = btoa(exported.join("")); result.splice(0, 0, this.blueprint.entity.serialize(false)); delete this.blueprint.entity.ExportedNodes; } return result.join("") } copied() { const value = this.getSerializedText(); navigator.clipboard.writeText(value); return value } } /** * @typedef {import("../IInput.js").Options & { * listenOnFocus?: Boolean, * unlistenOnTextEdit?: Boolean, * }} Options */ class Cut extends IInput { /** @type {(e: ClipboardEvent) => void} */ #cutHandler /** * @param {Element} target * @param {Blueprint} blueprint * @param {Options} options */ 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.#cutHandler = () => self.cut(); } listenEvents() { window.addEventListener("cut", this.#cutHandler); } unlistenEvents() { window.removeEventListener("cut", this.#cutHandler); } getSerializedText() { return this.blueprint .getNodes(true) .map(node => node.entity.serialize()) .join("") } cut() { this.blueprint.template.getCopyInputObject().copied(); this.blueprint.removeGraphElement(...this.blueprint.getNodes(true)); } } /** * @typedef {import("../IInput.js").Options & { * listenOnFocus?: Boolean, * unlistenOnTextEdit?: Boolean, * }} Options */ class Paste extends IInput { /** @type {(e: ClipboardEvent) => void} */ #pasteHandle /** * @param {Element} target * @param {Blueprint} blueprint * @param {Options} options */ 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() { window.addEventListener("paste", this.#pasteHandle); } unlistenEvents() { window.removeEventListener("paste", this.#pasteHandle); } /** @param {String} value */ pasted(value) { let top = 0; let left = 0; let count = 0; let nodes = ObjectEntity.grammarMultipleObjects.parse(value).map(entity => { let node = /** @type {NodeElementConstructor} */(ElementFactory.getConstructor("ueb-node")) .newObject(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; for (const node of nodes) { node.addLocation(mousePosition[0] - left, mousePosition[1] - top); node.snapToGrid(); node.setSelected(true); } this.blueprint.addGraphElement(...nodes); return nodes } } /** * @typedef {import("./IPointing.js").Options & { * listenOnFocus?: Boolean, * strictTarget?: Boolean, * }} Options */ class MouseWheel extends IPointing { /** @param {MouseWheel} self */ static #ignoreEvent = self => { } #variation = 0 get variation() { return this.#variation } /** @param {WheelEvent} e */ #mouseWheelHandler = e => { if (this.enablerKey && !this.enablerActivated) { return } e.preventDefault(); this.#variation = e.deltaY; this.setLocationFromEvent(e); this.wheel(); } /** @param {WheelEvent} e */ #mouseParentWheelHandler = e => e.preventDefault() /** * @param {HTMLElement} target * @param {Blueprint} blueprint * @param {Options} options */ constructor( target, blueprint, options = {}, onWheel = MouseWheel.#ignoreEvent, ) { options.listenOnFocus = true; options.strictTarget ??= false; super(target, blueprint, options); this.strictTarget = options.strictTarget; this.onWheel = onWheel; } 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 can override */ wheel() { this.onWheel(this); } } class Zoom extends MouseWheel { #accumulatedVariation = 0 #enableZoonIn = false get enableZoonIn() { return this.#enableZoonIn } set enableZoonIn(value) { if (value == this.#enableZoonIn) { return } this.#enableZoonIn = value; } wheel() { this.#accumulatedVariation += -this.variation; if (Math.abs(this.#accumulatedVariation) < Configuration.mouseWheelZoomThreshold) { return } let zoomLevel = this.blueprint.getZoom(); if (!this.enableZoonIn && zoomLevel == 0 && this.#accumulatedVariation > 0) { return } zoomLevel += Math.sign(this.#accumulatedVariation); this.blueprint.setZoom(zoomLevel, this.location); this.#accumulatedVariation = 0; } } /** * @typedef {import("./KeyboardShortcut.js").Options & { * activationKeys?: String | KeyBindingEntity | (String | KeyBindingEntity)[], * }} Options */ class KeyboardEnableZoom extends KeyboardShortcut { /** @type {Zoom} */ #zoomInputObject /** * @param {HTMLElement} target * @param {Blueprint} blueprint * @param {Options} options */ constructor(target, blueprint, options = {}) { options.activationKeys = Shortcuts.enableZoomIn; super(target, blueprint, options); } fire() { this.#zoomInputObject = this.blueprint.template.getZoomInputObject(); this.#zoomInputObject.enableZoonIn = true; } unfire() { this.#zoomInputObject.enableZoonIn = false; } } class MouseScrollGraph extends IMouseClickDrag { startDrag() { this.blueprint.scrolling = true; } /** * @param {Coordinates} location * @param {Coordinates} movement */ dragTo(location, movement) { this.blueprint.scrollDelta(-movement[0], -movement[1]); } endDrag() { this.blueprint.scrolling = false; } } /** * @typedef {import("./IPointing.js").Options & { * listenOnFocus?: Boolean, * }} Options */ class MouseTracking extends IPointing { /** @type {IPointing} */ #mouseTracker = null /** @param {MouseEvent} e */ #mousemoveHandler = e => { e.preventDefault(); this.setLocationFromEvent(e); this.blueprint.mousePosition = [...this.location]; } /** @param {CustomEvent} e */ #trackingMouseStolenHandler = e => { if (!this.#mouseTracker) { e.preventDefault(); this.#mouseTracker = e.detail.tracker; this.unlistenMouseMove(); } } /** @param {CustomEvent} e */ #trackingMouseGaveBackHandler = e => { if (this.#mouseTracker == e.detail.tracker) { e.preventDefault(); this.#mouseTracker = null; this.listenMouseMove(); } } /** * @param {Element} target * @param {Blueprint} blueprint * @param {Options} options */ constructor(target, blueprint, options = {}) { options.listenOnFocus = true; super(target, blueprint, options); } 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 {import("./IMouseClickDrag.js").Options & { * scrollGraphEdge?: Boolean, * }} Options */ class Select extends IMouseClickDrag { constructor(target, blueprint, options = {}) { options.scrollGraphEdge ??= true; super(target, blueprint, options); this.selectorElement = this.blueprint.template.selectorElement; } startDrag() { this.selectorElement.beginSelect(this.clickedPosition); } /** * @param {Coordinates} location * @param {Coordinates} movement */ dragTo(location, movement) { this.selectorElement.selectTo(location); } endDrag() { if (this.started) { this.selectorElement.endSelect(); } } unclicked() { if (!this.started) { this.blueprint.unselectAll(); } } } /** * @typedef {import("../IInput.js").Options & { * listenOnFocus?: Boolean, * }} Options */ class Unfocus extends IInput { /** @param {MouseEvent} e */ #clickHandler = e => this.clickedSomewhere(/** @type {HTMLElement} */(e.target)) /** * @param {HTMLElement} target * @param {Blueprint} blueprint * @param {Options} options */ constructor(target, blueprint, options = {}) { options.listenOnFocus = true; super(target, blueprint, options); 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); } } /** @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`, } #resizeObserver = new ResizeObserver(entries => { const size = entries.find(entry => entry.target === this.viewportElement)?.devicePixelContentBoxSize?.[0]; if (size) { this.viewportSize[0] = size.inlineSize; this.viewportSize[1] = size.blockSize; } }) /** @type {Copy} */ #copyInputObject /** @type {Paste} */ #pasteInputObject /** @type {Zoom} */ #zoomInputObject /** @type {HTMLElement} */ headerElement /** @type {HTMLElement} */ overlayElement /** @type {HTMLElement} */ viewportElement /** @type {SelectorElement} */ selectorElement /** @type {HTMLElement} */ gridElement /** @type {HTMLElement} */ linksContainerElement /** @type {HTMLElement} */ nodesContainerElement viewportSize = [0, 0] /** @param {Blueprint} element */ initialize(element) { super.initialize(element); this.element.style.cssText = Object.entries(BlueprintTemplate.styleVariables) .map(([k, v]) => `${k}:${v};`).join(""); const htmlTemplate = /** @type {HTMLTemplateElement} */( this.element.querySelector(":scope > template") )?.content.textContent; if (htmlTemplate) { this.element.requestUpdate(); this.element.updateComplete.then(() => { this.blueprint.mousePosition = [ Math.round(this.viewportSize[0] / 2), Math.round(this.viewportSize[1] / 2), ]; this.getPasteInputObject().pasted(htmlTemplate); this.blueprint.unselectAll(); }); } } setup() { super.setup(); this.#resizeObserver.observe(this.viewportElement, { box: "device-pixel-content-box", }); const bounding = this.viewportElement.getBoundingClientRect(); this.viewportSize[0] = bounding.width; this.viewportSize[1] = bounding.height; if (this.blueprint.nodes.length > 0) { this.blueprint.requestUpdate(); this.blueprint.updateComplete.then(() => this.centerContentInViewport()); } } cleanup() { super.cleanup(); this.#resizeObserver.unobserve(this.viewportElement); } createInputObjects() { const gridElement = this.element.getGridDOMElement(); this.#copyInputObject = new Copy(gridElement, this.blueprint); this.#pasteInputObject = new Paste(gridElement, this.blueprint); this.#zoomInputObject = new Zoom(gridElement, this.blueprint); return [ ...super.createInputObjects(), this.#copyInputObject, this.#pasteInputObject, this.#zoomInputObject, new Cut(gridElement, this.blueprint), new KeyboardShortcut(gridElement, this.blueprint, { activationKeys: Shortcuts.duplicateNodes }, () => this.blueprint.template.getPasteInputObject().pasted( this.blueprint.template.getCopyInputObject().copied() ) ), new KeyboardShortcut(gridElement, this.blueprint, { activationKeys: Shortcuts.deleteNodes }, () => this.blueprint.removeGraphElement(...this.blueprint.getNodes(true))), new KeyboardShortcut(gridElement, this.blueprint, { activationKeys: Shortcuts.selectAllNodes }, () => this.blueprint.selectAll()), new Select(gridElement, this.blueprint, { clickButton: Configuration.mouseClickButton, exitAnyButton: true, moveEverywhere: true, }), new MouseScrollGraph(gridElement, this.blueprint, { clickButton: Configuration.mouseRightClickButton, exitAnyButton: false, moveEverywhere: true, }), new Unfocus(gridElement, this.blueprint), new MouseTracking(gridElement, this.blueprint), new KeyboardEnableZoom(gridElement, this.blueprint), ] } render() { return x`
Zoom ${this.blueprint.zoom == 0 ? "1:1" : (this.blueprint.zoom > 0 ? "+" : "") + this.blueprint.zoom}
` } /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.headerElement = this.blueprint.querySelector('.ueb-viewport-header'); this.overlayElement = this.blueprint.querySelector('.ueb-viewport-overlay'); this.viewportElement = this.blueprint.querySelector('.ueb-viewport-body'); this.selectorElement = this.blueprint.querySelector('ueb-selector'); this.gridElement = this.viewportElement.querySelector(".ueb-grid"); this.linksContainerElement = this.blueprint.querySelector("[data-links]"); this.linksContainerElement.append(...this.blueprint.getLinks()); this.nodesContainerElement = this.blueprint.querySelector("[data-nodes]"); this.nodesContainerElement.append(...this.blueprint.getNodes()); this.viewportElement.scroll(Configuration.expandGridSize, Configuration.expandGridSize); } /** @param {PropertyValues} changedProperties */ willUpdate(changedProperties) { super.willUpdate(changedProperties); if (this.headerElement && changedProperties.has("zoom")) { this.headerElement.classList.add("ueb-zoom-changed"); this.headerElement.addEventListener( "animationend", () => this.headerElement.classList.remove("ueb-zoom-changed") ); } } /** @param {PropertyValues} changedProperties */ updated(changedProperties) { super.updated(changedProperties); if (changedProperties.has("scrollX") || changedProperties.has("scrollY")) { this.viewportElement.scroll(this.blueprint.scrollX, this.blueprint.scrollY); } if (changedProperties.has("zoom")) { this.blueprint.style.setProperty("--ueb-scale", this.blueprint.getScale()); const previousZoom = changedProperties.get("zoom"); const minZoom = Math.min(previousZoom, this.blueprint.zoom); const maxZoom = Math.max(previousZoom, this.blueprint.zoom); const classes = Utility.range(minZoom, maxZoom); const getClassName = v => `ueb-zoom-${v}`; if (previousZoom < this.blueprint.zoom) { this.blueprint.classList.remove(...classes.filter(v => v < 0).map(getClassName)); this.blueprint.classList.add(...classes.filter(v => v > 0).map(getClassName)); } else { this.blueprint.classList.remove(...classes.filter(v => v > 0).map(getClassName)); this.blueprint.classList.add(...classes.filter(v => v < 0).map(getClassName)); } } } getCommentNodes(justSelected = false) { return this.blueprint.querySelectorAll( `ueb-node[data-type="${Configuration.paths.comment}"]${justSelected ? '[data-selected="true"]' : ''}` + `, ueb-node[data-type="${Configuration.paths.materialGraphNodeComment}"]${justSelected ? '[data-selected="true"]' : ''}` ) } /** @param {PinReferenceEntity} pinReference */ getPin(pinReference) { return /** @type {PinElement} */(this.blueprint.querySelector( `ueb-node[data-title="${pinReference.objectName}"] ueb-pin[data-id="${pinReference.pinGuid}"]` )) } getCopyInputObject() { return this.#copyInputObject } getPasteInputObject() { return this.#pasteInputObject } getZoomInputObject() { return this.#zoomInputObject } /** * @param {Number} x * @param {Number} y */ isPointVisible(x, y) { return false } gridTopVisibilityBoundary() { return this.blueprint.scaleCorrect(this.blueprint.scrollY) - this.blueprint.translateY } gridRightVisibilityBoundary() { return this.gridLeftVisibilityBoundary() + this.blueprint.scaleCorrect(this.viewportSize[0]) } gridBottomVisibilityBoundary() { return this.gridTopVisibilityBoundary() + this.blueprint.scaleCorrect(this.viewportSize[1]) } gridLeftVisibilityBoundary() { return this.blueprint.scaleCorrect(this.blueprint.scrollX) - this.blueprint.translateX } centerViewport(x = 0, y = 0, smooth = true) { const centerX = this.gridLeftVisibilityBoundary() + this.blueprint.scaleCorrect(this.viewportSize[0] / 2); const centerY = this.gridTopVisibilityBoundary() + this.blueprint.scaleCorrect(this.viewportSize[1] / 2); this.blueprint.scrollDelta( this.blueprint.scaleCorrectReverse(x - centerX), this.blueprint.scaleCorrectReverse(y - centerY), smooth ); } centerContentInViewport(smooth = true) { let avgX = 0; let avgY = 0; let minX = Number.MAX_SAFE_INTEGER; let maxX = Number.MIN_SAFE_INTEGER; let minY = Number.MAX_SAFE_INTEGER; let maxY = Number.MIN_SAFE_INTEGER; const nodes = this.blueprint.getNodes(); for (const node of nodes) { avgX += node.leftBoundary() + node.rightBoundary(); avgY += node.topBoundary() + node.bottomBoundary(); minX = Math.min(minX, node.leftBoundary()); maxX = Math.max(maxX, node.rightBoundary()); minY = Math.min(minY, node.topBoundary()); maxY = Math.max(maxY, node.bottomBoundary()); } avgX = Math.round(maxX - minX <= this.viewportSize[0] ? (maxX + minX) / 2 : avgX / (2 * nodes.length) ); avgY = Math.round(maxY - minY <= this.viewportSize[1] ? (maxY + minY) / 2 : avgY / (2 * nodes.length) ); this.centerViewport(avgX, avgY, smooth); } } /** @extends {IElement} */ class Blueprint extends IElement { static properties = { selecting: { type: Boolean, attribute: "data-selecting", reflect: true, converter: BooleanEntity.booleanConverter, }, scrolling: { type: Boolean, attribute: "data-scrolling", reflect: true, converter: BooleanEntity.booleanConverter, }, focused: { type: Boolean, attribute: "data-focused", reflect: true, converter: BooleanEntity.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, }, } /** @param {NodeElement} node */ static nodeBoundariesSupplier = node => { return { primaryInf: node.leftBoundary(true), primarySup: node.rightBoundary(true), // Counter intuitive here: the y (secondary axis is positive towards the bottom, therefore upper bound "sup" is bottom) secondaryInf: node.topBoundary(true), secondarySup: node.bottomBoundary(true), } } /** @type {(node: NodeElement, selected: Boolean) => void}} */ static nodeSelectToggleFunction = (node, selected) => { node.setSelected(selected); } #xScrollingAnimationId = 0 #yScrollingAnimationId = 0 /** @type {NodeElement[]}" */ nodes = [] /** @type {LinkElement[]}" */ links = [] /** @type {Map} */ nodesNames = new Map() /** @type {Coordinates} */ mousePosition = [0, 0] waitingExpandUpdate = false constructor() { super(); 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; super.initialize(new BlueprintEntity(), new BlueprintTemplate()); } initialize() { // Initialized in the constructor, this method does nothing } getGridDOMElement() { return this.template.gridElement } getScroll() { return [this.scrollX, this.scrollY] } /** * @param {Number} x * @param {Number} y */ setScroll(x, y) { this.scrollX = x; this.scrollY = y; } scrollDelta(x = 0, y = 0, smooth = false, scrollTime = Configuration.smoothScrollTime) { if (smooth) { let previousScrollDelta = [0, 0]; if (this.#xScrollingAnimationId) { cancelAnimationFrame(this.#xScrollingAnimationId); } if (this.#yScrollingAnimationId) { cancelAnimationFrame(this.#yScrollingAnimationId); } Utility.animate( 0, x, scrollTime, x => { this.scrollDelta(x - previousScrollDelta[0], 0, false); previousScrollDelta[0] = x; }, id => this.#xScrollingAnimationId = id ); Utility.animate( 0, y, scrollTime, y => { this.scrollDelta(0, y - previousScrollDelta[1], false); previousScrollDelta[1] = y; }, id => this.#yScrollingAnimationId = id ); } else { const maxScroll = [2 * Configuration.expandGridSize, 2 * Configuration.expandGridSize]; let currentScroll = this.getScroll(); let finalScroll = [ currentScroll[0] + x, currentScroll[1] + y ]; let expand = [0, 0]; for (let i = 0; i < 2; ++i) { if (finalScroll[i] < Configuration.gridExpandThreshold * Configuration.expandGridSize) { // Expand left/top expand[i] = -1; } else if ( finalScroll[i] > maxScroll[i] - Configuration.gridExpandThreshold * Configuration.expandGridSize ) { // Expand right/bottom expand[i] = 1; } } if (expand[0] != 0 || expand[1] != 0) { this.seamlessExpand(expand[0], expand[1]); } currentScroll = this.getScroll(); finalScroll = [ currentScroll[0] + x, currentScroll[1] + y ]; this.setScroll(finalScroll[0], finalScroll[1]); } } scrollCenter(smooth = false) { 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[0], deltaOffset[1], smooth); } getViewportSize() { return [ this.template.viewportElement.clientWidth, this.template.viewportElement.clientHeight, ] } getScrollMax() { return [ this.template.viewportElement.scrollWidth - this.template.viewportElement.clientWidth, this.template.viewportElement.scrollHeight - this.template.viewportElement.clientHeight, ] } /** * @param {Number} x * @param {Number} y */ snapToGrid(x, y) { return Utility.snapToGrid(x, y, Configuration.gridSize) } /** * @param {Number} x * @param {Number} y */ 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) { 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 Configuration.scale[this.getZoom()] } /** @param {Number} value */ scaleCorrect(value) { return value / this.getScale() } /** @param {Number} value */ scaleCorrectReverse(value) { return value * this.getScale() } /** * @param {Number} x * @param {Number} y * @returns {[Number, Number]} */ compensateTranslation(x, y) { x -= this.translateX; y -= this.translateY; return [x, y] } getNodes( selected = false, [t, r, b, l] = [ Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, ] ) { let result = this.nodes; if (selected) { result = result.filter(n => n.selected); } if ( t > Number.MIN_SAFE_INTEGER || r < Number.MAX_SAFE_INTEGER || b < Number.MAX_SAFE_INTEGER || l > Number.MIN_SAFE_INTEGER ) { result = result.filter(n => { return n.topBoundary() >= t && n.rightBoundary() <= r && n.bottomBoundary() <= b && n.leftBoundary() >= l }); } return result } getCommentNodes(justSelected = false) { let result = /** @type {NodeElement[]} */([...this.template.getCommentNodes(justSelected)]); if (result.length === 0) { result = this.nodes.filter(n => n.getType() === Configuration.paths.comment && (!justSelected || n.selected) ); } return result } /** @param {PinReferenceEntity} pinReference */ getPin(pinReference) { let result = this.template.getPin(pinReference); // Remember could be renamed in the meantime and DOM not yet updated if (!result || result.nodeElement.getNodeName() != pinReference.objectName.toString()) { // Slower fallback result = [... this.nodes .find(n => pinReference.objectName.toString() == n.getNodeName()) ?.getPinElements() ?? []] .find(p => pinReference.pinGuid.toString() == p.getPinId().toString()); } return result } /** * @param {PinElement?} a * @param {PinElement?} b */ getLinks(a = null, b = null) { if ((a == null) != (b == null)) { const pin = a ?? b; return this.links.filter(link => link.source == pin || link.destination == pin) } if (a != null && b != null) { return this.links.filter(link => link.source == a && link.destination == b || link.source == b && link.destination == a ) } return this.links } /** * @param {PinElement} sourcePin * @param {PinElement} destinationPin */ getLink(sourcePin, destinationPin, strictDirection = false) { return this.links.find(link => link.source == sourcePin && link.destination == destinationPin || !strictDirection && link.source == destinationPin && link.destination == sourcePin ) } selectAll() { this.getNodes().forEach(node => Blueprint.nodeSelectToggleFunction(node, true)); } unselectAll() { this.getNodes().forEach(node => Blueprint.nodeSelectToggleFunction(node, false)); } /** @param {...IElement} graphElements */ addGraphElement(...graphElements) { /** @param {CustomEvent} event */ const removeEventHandler = event => { const target = event.currentTarget; target.removeEventListener(Configuration.removeEventName, removeEventHandler); const [graphElementsArray, entity] = target instanceof NodeElement ? [this.nodes, target.entity] : target instanceof LinkElement ? [this.links] : null; // @ts-expect-error const index = graphElementsArray?.indexOf(target); if (index >= 0) { const last = graphElementsArray.pop(); if (index < graphElementsArray.length) { graphElementsArray[index] = last; } } if (entity) { this.entity.removeObjectEntity(entity); } }; for (const element of graphElements) { element.blueprint = this; if (element instanceof NodeElement && !this.nodes.includes(element)) { if (element.getType() == Configuration.paths.niagaraClipboardContent) { this.entity = this.entity.mergeWith(element.entity); const additionalSerialization = atob(element.entity.ExportedNodes.toString()); this.template.getPasteInputObject().pasted(additionalSerialization) .forEach(node => node.entity._exported = true); continue } const name = element.entity.getObjectName(); const homonym = this.entity.getHomonymObjectEntity(element.entity); if (homonym) { homonym.Name.value = this.entity.takeFreeName(name); homonym.Name = homonym.Name; } this.nodes.push(element); this.entity.addObjectEntity(element.entity); element.addEventListener(Configuration.removeEventName, removeEventHandler); this.template.nodesContainerElement?.appendChild(element); } else if (element instanceof LinkElement && !this.links.includes(element)) { this.links.push(element); element.addEventListener(Configuration.removeEventName, removeEventHandler); if (this.template.linksContainerElement && !this.template.linksContainerElement.contains(element)) { this.template.linksContainerElement.appendChild(element); } } } graphElements.filter(element => element instanceof NodeElement).forEach( node => /** @type {NodeElement} */(node).sanitizeLinks(graphElements) ); graphElements .filter(element => element instanceof NodeElement && element.getType() == Configuration.paths.comment) .forEach(element => element.updateComplete.then(() => /** @type {CommentNodeTemplate} */(element.template).manageNodesBind() )); } /** @param {...IElement} graphElements */ removeGraphElement(...graphElements) { for (let element of graphElements) { if (element.closest("ueb-blueprint") !== this) { return } element.remove(); } } setFocused(value = true) { if (this.focused == value) { return } let event = new CustomEvent(value ? Configuration.focusEventName.begin : Configuration.focusEventName.end); this.focused = value; if (!this.focused) { this.unselectAll(); } this.dispatchEvent(event); } /** @param {Boolean} begin */ acknowledgeEditText(begin) { const event = new CustomEvent( begin ? Configuration.editTextEventName.begin : Configuration.editTextEventName.end ); this.dispatchEvent(event); } } customElements.define("ueb-blueprint", Blueprint); /** * @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] /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.movementSpace = this.element.parentElement; } setup() { super.setup(); const bounding = this.movementSpace.getBoundingClientRect(); this.movementSpaceSize = [bounding.width, bounding.height]; } createDraggableObject() { return new MouseMoveDraggable(this.element, this.blueprint, { draggableElement: this.movementSpace, ignoreTranslateCompensate: true, moveEverywhere: true, movementSpace: this.movementSpace, repositionOnClick: true, stepSize: 1, }) } /** * @param {Number} x * @param {Number} y * @returns {Coordinates} */ adjustLocation(x, y) { this.locationChangeCallback?.(x, y); return [x, y] } } /** @extends {IDraggableControlTemplate} */ class ColorHandlerTemplate extends IDraggableControlTemplate { /** * @param {Number} x * @param {Number} y * @returns {Coordinates} */ 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] } } /** * @template {IEntity} T * @template {IDraggableControlTemplate} U * @extends {IDraggableElement} */ class IDraggableControlElement extends IDraggableElement { /** @type {WindowElement} */ windowElement setup() { super.setup(); this.windowElement = this.closest("ueb-window"); } /** * @param {Number} x * @param {Number} y */ setLocation(x, y) { super.setLocation(...this.template.adjustLocation(x, y)); } } /** @extends {IDraggableControlElement} */ class ColorHandlerElement extends IDraggableControlElement { constructor() { super(); super.initialize({}, new ColorHandlerTemplate()); } static newObject() { return new ColorHandlerElement() } initialize() { // Initialized in the constructor, this method does nothing } } /** @extends {IDraggableControlTemplate} */ class ColorSliderTemplate extends IDraggableControlTemplate { /** * @param {Number} x * @param {Number} y * @return {Coordinates} */ 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] } } /** @extends {IDraggableControlElement} */ class ColorSliderElement extends IDraggableControlElement { constructor() { super(); super.initialize({}, new ColorSliderTemplate()); } static newObject() { return new ColorSliderElement() } initialize() { // Initialized in the constructor, this method does nothing } } /** * @template {IDraggableElement} T * @extends {IMouseClickDrag} */ class MouseIgnore extends IMouseClickDrag { constructor(target, blueprint, options = {}) { options.consumeEvent = true; super(target, blueprint, options); } } /** @extends {ITemplate} */ class DropdownTemplate extends ITemplate { /** @type {HTMLSelectElement} */ #selectElement /** @type {HTMLSelectElement} */ #referenceSelectElement #changeHandler = e => this.element.selectedOption = /** @type {HTMLSelectElement} */(e.target) .selectedOptions[0] .value render() { return x` ` } /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.#selectElement = this.element.querySelector("select:first-child"); this.#referenceSelectElement = this.element.querySelector("select:last-child"); const event = new Event("input", { bubbles: true }); this.#selectElement.dispatchEvent(event); } /** @param {PropertyValues} changedProperties */ updated(changedProperties) { super.updated(changedProperties); const bounding = this.#referenceSelectElement.getBoundingClientRect(); this.element.style.setProperty("--ueb-dropdown-width", bounding.width + "px"); } createInputObjects() { return [ ...super.createInputObjects(), // Prevents creating links when selecting text and other undesired mouse actions detection new MouseIgnore(this.element, this.blueprint), ] } setSelectedValue(value) { /** @type {HTMLOptionElement} */(this.element.querySelector(`option[value="${value}"]`)).defaultSelected = true; } getSelectedValue() { return this.#selectElement.value } } /** @extends {IElement} */ class DropdownElement extends IElement { static properties = { ...super.properties, options: { type: Object, }, selectedOption: { type: String, }, } constructor() { super(); super.initialize({}, new DropdownTemplate()); this.options = /** @type {[String, String][]} */([]); this.selectedOption = ""; } /** @param {[String, String][]} options */ static newObject(options) { const result = new DropdownElement(); return result } initialize() { // Initialized in the constructor, this method does nothing } getValue() { return this.template.getSelectedValue() } } /** @extends {ITemplate} */ class InputTemplate extends ITemplate { #focusHandler = () => { this.blueprint.acknowledgeEditText(true); if (this.element.selectOnFocus) { getSelection().selectAllChildren(this.element); } } #focusoutHandler = () => { this.blueprint.acknowledgeEditText(false); getSelection().removeAllRanges(); // Deselect eventually selected text inside the input } /** @param {Event} e */ #inputSingleLineHandler = e => /** @type {HTMLElement} */(e.target).querySelectorAll("br").forEach(br => br.remove()) /** @param {KeyboardEvent} e */ #onKeydownBlurOnEnterHandler = e => { if (e.code == "Enter" && !e.shiftKey) { /** @type {HTMLElement} */(e.target).blur(); } } /** @param {InputElement} element */ initialize(element) { super.initialize(element); this.element.classList.add("ueb-pin-input-content"); this.element.setAttribute("role", "textbox"); this.element.contentEditable = "true"; } /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); const event = new Event("input", { bubbles: true }); this.element.dispatchEvent(event); } createInputObjects() { return [ ...super.createInputObjects(), // Prevents creating links when selecting text and other undesired mouse actions detection new MouseIgnore(this.element, this.blueprint), ] } setup() { super.setup(); 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() { super.cleanup(); this.element.removeEventListener("focus", this.#focusHandler); this.element.removeEventListener("focusout", this.#focusoutHandler); this.element.removeEventListener("input", this.#inputSingleLineHandler); this.element.removeEventListener("keydown", this.#onKeydownBlurOnEnterHandler); } } /** @extends {IElement} */ class InputElement extends IElement { static properties = { ...super.properties, singleLine: { type: Boolean, attribute: "data-single-line", converter: BooleanEntity.booleanConverter, reflect: true, }, selectOnFocus: { type: Boolean, attribute: "data-select-focus", converter: BooleanEntity.booleanConverter, reflect: true, }, blurOnEnter: { type: Boolean, attribute: "data-blur-enter", converter: BooleanEntity.booleanConverter, reflect: true, }, } constructor() { super(); this.singleLine = false; this.selectOnFocus = true; this.blurOnEnter = true; super.initialize({}, new InputTemplate()); } static newObject() { return new InputElement() } initialize() { // Initialized in the constructor, this method does nothing } } /** @extends PinTemplate */ class BoolPinTemplate extends PinTemplate { /** @type {HTMLInputElement?} */ #input #onChangeHandler = () => { const entity = this.element.getDefaultValue(); entity.value = this.#input.checked; this.element.setDefaultValue(entity); } /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.#input = this.element.querySelector(".ueb-pin-input"); } setup() { super.setup(); this.#input?.addEventListener("change", this.#onChangeHandler); } cleanup() { super.cleanup(); this.#input?.removeEventListener("change", this.#onChangeHandler); } createInputObjects() { return [ ...super.createInputObjects(), new MouseIgnore(this.#input, this.blueprint), ] } renderInput() { return x` ` } } /** * @template {IEntity} T * @extends PinTemplate */ class IInputPinTemplate extends PinTemplate { static singleLineInput = false static selectOnFocus = true static saveEachInputChange = false // Otherwise save only on focus out /** @type {HTMLElement} */ #inputWrapper get inputWrapper() { return this.#inputWrapper } /** @type {HTMLElement[]} */ #inputContentElements /** @param {String} value */ static stringFromInputToUE(value) { return value .replace(/(?=\n\s*)\n$/, "") // Remove trailing double newline } /** @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 } #setInput = () => this.setInputs(this.getInputs(), true) /** @param {Event} event */ #checkWrapHandler = event => this.#updateWrapClass(/** @type {HTMLElement} */(event.target)) /** @param {HTMLElement} inputElement*/ #updateWrapClass(inputElement) { if (this.element.querySelector(".ueb-pin-name")?.getBoundingClientRect().width < 20) { // Do not wrap if the pin name is just a letter (like A, B, V, ...) return } const width = this.blueprint.scaleCorrect(this.#inputWrapper.getBoundingClientRect().width) + this.nameWidth; const inputWrapped = this.element.classList.contains("ueb-pin-input-wrap"); if (!inputWrapped && width > Configuration.pinInputWrapWidth) { this.element.classList.add("ueb-pin-input-wrap"); } else if (inputWrapped && width <= Configuration.pinInputWrapWidth) { this.element.classList.remove("ueb-pin-input-wrap"); } } /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); const Self = /** @type {typeof IInputPinTemplate} */(this.constructor); if (Self.canWrapInput && this.isInputRendered()) { this.element.addEventListener("input", this.#checkWrapHandler); this.nameWidth = this.blueprint.scaleCorrect( this.element.querySelector(".ueb-pin-name")?.getBoundingClientRect().width ?? 0 ); } this.#inputWrapper = this.element.querySelector(".ueb-pin-input-wrapper"); this.#inputContentElements = /** @type {HTMLElement[]} */([...this.element.querySelectorAll("ueb-input")]); } setup() { super.setup(); const Self = /** @type {typeof IInputPinTemplate} */(this.constructor); if (Self.saveEachInputChange) { this.element.addEventListener("input", this.#setInput); } else { this.element.addEventListener("focusout", this.#setInput); } if (Self.canWrapInput && this.isInputRendered()) { this.element.addEventListener("input", this.#checkWrapHandler); this.element.nodeElement.addEventListener(Configuration.nodeReflowEventName, this.#checkWrapHandler); } } cleanup() { super.cleanup(); this.element.nodeElement.removeEventListener(Configuration.nodeReflowEventName, this.#checkWrapHandler); this.element.removeEventListener("input", this.#checkWrapHandler); this.element.removeEventListener("input", this.#setInput); this.element.removeEventListener("focusout", this.#setInput); } 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) { this.#inputContentElements.forEach(/** @type {typeof IInputPinTemplate } */(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.requestUpdate(); this.element.nodeElement.acknowledgeReflow(); } setDefaultValue(values = [], rawValues = values) { this.element.setDefaultValue( // @ts-expect-error values.join("") ); } renderInput() { const Self = /** @type {typeof IInputPinTemplate} */(this.constructor); const singleLine = Self.singleLineInput; const selectOnFocus = Self.selectOnFocus; return x`
` } } /** @extends IInputPinTemplate */ class EnumPinTemplate extends IInputPinTemplate { static saveEachInputChange = true // Otherwise save only on focus out /** @type {DropdownElement} */ #dropdownElement #dropdownEntries = [] setup() { super.setup(); const enumEntries = this.element.nodeElement.entity.EnumEntries?.valueOf(); this.#dropdownEntries = enumEntries?.map(k => { if (k.valueOf() === "") { k = new StringEntity("None"); } return [ k, this.element.nodeElement.getPinEntities().find(pinEntity => k === pinEntity.PinName) ?.PinFriendlyName.toString() ?? k ] }) ?? Configuration.CommonEnums[this.element.entity.getSubCategory()]?.map(k => k instanceof Array ? k : [k, Utility.formatStringName(k)] ) ?? []; const defaultEntry = this.element.getDefaultValue().toString(); if (!this.#dropdownEntries.find(([k, v]) => k === defaultEntry)) { this.#dropdownEntries.push([defaultEntry, Utility.formatStringName(defaultEntry)]); } this.element.requestUpdate(); } renderInput() { return x` ` } /** @param {PropertyValues} changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.#dropdownElement = this.element.querySelector("ueb-dropdown"); } getInputs() { return [this.#dropdownElement.getValue()] } /** * @this {EnumPinTemplate} * @param {String[]} values * @param {String[]} rawValues */ setDefaultValue(values = [], rawValues) { const value = this.element.getDefaultValue(); value.value = values[0]; this.element.setDefaultValue(value); this.element.requestUpdate(); } } class ExecPinTemplate extends PinTemplate { renderIcon() { return SVGIcon.execPin } renderName() { let pinName = this.element.entity.PinName?.toString(); if (this.element.entity.PinFriendlyName) { pinName = this.element.entity.PinFriendlyName.toString(); } else if (pinName === "execute" || pinName === "then") { return x`` } return x`${this.element.getPinDisplayName()}` } } /** * @template {IEntity} T * @extends IInputPinTemplate */ class INumericPinTemplate extends IInputPinTemplate { static singleLineInput = true /** * @this {INumericPinTemplate} * @param {String[]} values */ setInputs(values = [], updateDefaultValue = false) { if (!values || values.length == 0) { values = [this.getInput()]; } super.setInputs(values, false); if (updateDefaultValue) { let parsedValues = []; for (const value of values) { let num = parseFloat(value); if (isNaN(num)) { num = 0; updateDefaultValue = false; } parsedValues.push(num); } this.setDefaultValue(parsedValues, values); } } /** * @this {INumericPinTemplate} * @param {Number[]} values * @param {String[]} rawValues */ setDefaultValue(values = [], rawValues) { const value = this.element.getDefaultValue(); value.value = values[0]; this.element.setDefaultValue(value); this.element.requestUpdate(); } } /** @extends INumericPinTemplate */ class Int64PinTemplate extends INumericPinTemplate { /** * @param {Number[]} values * @param {String[]} rawValues */ setDefaultValue(values = [], rawValues) { const value = this.element.getDefaultValue(); value.value = BigInt(values[0]); this.element.setDefaultValue(value); this.element.requestUpdate(); } renderInput() { return x`
` } } /** @extends INumericPinTemplate */ class IntPinTemplate extends INumericPinTemplate { renderInput() { return x`
` } } /** * @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="important",n=" !"+i,o=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.includes("-")?r:r.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g,"-$&").toLowerCase()}:${s};`}),"")}update(e,[r]){const{style:s}=e.element;if(void 0===this.ht){this.ht=new Set;for(const t in r)this.ht.add(t);return this.render(r)}this.ht.forEach((t=>{null==r[t]&&(this.ht.delete(t),t.includes("-")?s.removeProperty(t):s[t]="");}));for(const t in r){const e=r[t];if(null!=e){this.ht.add(t);const r="string"==typeof e&&e.endsWith(n);t.includes("-")||r?s.setProperty(t,r?e.slice(0,-11):e,r?i:""):s[t]=e;}}return T}}); /** @extends {IDraggablePositionedTemplate} */ class WindowTemplate extends IDraggablePositionedTemplate { toggleAdvancedDisplayHandler getDraggableElement() { return /** @type {WindowElement} */(this.element.querySelector(".ueb-window-top")) } createDraggableObject() { return new MouseMoveDraggable(this.element, this.blueprint, { draggableElement: this.getDraggableElement(), ignoreScale: true, ignoreTranslateCompensate: false, movementSpace: this.blueprint, stepSize: 1, }) } setup() { const leftBoundary = this.blueprint.template.gridLeftVisibilityBoundary(); const topBoundary = this.blueprint.template.gridTopVisibilityBoundary(); this.element.locationX = this.blueprint.scaleCorrectReverse(this.blueprint.mousePosition[0] - leftBoundary); this.element.locationY = this.blueprint.scaleCorrectReverse(this.blueprint.mousePosition[1] - topBoundary); this.element.updateComplete.then(() => { const bounding = this.blueprint.getBoundingClientRect(); if (this.element.locationX + this.element.sizeX > bounding.width) { this.element.locationX = bounding.width - this.element.sizeX; } this.element.locationX = Math.max(0, this.element.locationX); if (this.element.locationY + this.element.sizeY > bounding.height) { this.element.locationY = bounding.height - this.element.sizeY; } this.element.locationY = Math.max(0, this.element.locationY); }); } render() { return x`
${this.renderWindowName()}
${SVGIcon.close}
${this.renderContent()}
` } renderWindowName() { return x`Window` } renderContent() { return x`` } apply() { this.element.dispatchEvent(new CustomEvent(Configuration.windowApplyEventName)); this.element.remove(); } cancel() { this.element.dispatchEvent(new CustomEvent(Configuration.windowCancelEventName)); this.element.remove(); } } 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 } 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 } /** @param {WindowElement} element */ initialize(element) { super.initialize(element); 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 {PropertyValues} 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 x`
${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 x`
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
${Configuration.windowApplyButtonText}
${Configuration.windowCancelButtonText}
` } renderWindowName() { return x`${Configuration.colorWindowName}` } } /** @extends PinTemplate */ class LinearColorPinTemplate extends PinTemplate { /** @type {WindowElement} */ #window /** @param {MouseEvent} e */ #launchColorPickerWindow = e => { e.preventDefault(); this.blueprint.setFocused(true); /** @type {WindowElement} */ this.#window = /** @type {WindowElementConstructor} */(ElementFactory.getConstructor("ueb-window")) .newObject({ type: new 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.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 x` ` } } class NamePinTemplate extends IInputPinTemplate { static singleLineInput = true } /** * @template {NumberEntity} T * @extends INumericPinTemplate */ class RealPinTemplate extends INumericPinTemplate { renderInput() { return x`
` } } class ReferencePinTemplate extends PinTemplate { renderIcon() { return SVGIcon.referencePin } } /** @extends INumericPinTemplate */ class RotatorPinTemplate extends INumericPinTemplate { #getR() { return NumberEntity.printNumber(this.element.getDefaultValue()?.R.valueOf() ?? 0) } #getP() { return NumberEntity.printNumber(this.element.getDefaultValue()?.P.valueOf() ?? 0) } #getY() { return NumberEntity.printNumber(this.element.getDefaultValue()?.Y.valueOf() ?? 0) } setDefaultValue(values = [], rawValues = values) { const rotator = this.element.getDefaultValue(true); if (!(rotator instanceof RotatorEntity)) { throw new TypeError("Expected DefaultValue to be a RotatorEntity") } rotator.R.value = values[0]; // Roll rotator.P.value = values[1]; // Pitch rotator.Y.value = values[2]; // Yaw this.element.requestUpdate("DefaultValue", rotator); } renderInput() { return x`
X
Y
Z
` } } /** @extends IInputPinTemplate */ class StringPinTemplate extends IInputPinTemplate { } /** * @extends INumericPinTemplate */ class Vector2DPinTemplate extends INumericPinTemplate { #getX() { return NumberEntity.printNumber(this.element.getDefaultValue()?.X.valueOf() ?? 0) } #getY() { return NumberEntity.printNumber(this.element.getDefaultValue()?.Y.valueOf() ?? 0) } /** * @param {Number[]} values * @param {String[]} rawValues */ setDefaultValue(values, rawValues) { const vector = this.element.getDefaultValue(true); vector.X.value = values[0]; vector.Y.value = values[1]; this.element.setDefaultValue(vector); } renderInput() { return x`
X
Y
` } } /** @extends INumericPinTemplate */ class Vector4DPinTemplate extends INumericPinTemplate { #getX() { return NumberEntity.printNumber(this.element.getDefaultValue()?.X.valueOf() ?? 0) } #getY() { return NumberEntity.printNumber(this.element.getDefaultValue()?.Y.valueOf() ?? 0) } #getZ() { return NumberEntity.printNumber(this.element.getDefaultValue()?.Z.valueOf() ?? 0) } #getW() { return NumberEntity.printNumber(this.element.getDefaultValue()?.W.valueOf() ?? 0) } /** * @param {Number[]} values * @param {String[]} rawValues */ setDefaultValue(values, rawValues) { const vector = this.element.getDefaultValue(true); if (!(vector instanceof Vector4DEntity)) { throw new TypeError("Expected DefaultValue to be a Vector4DEntity") } vector.X.value = values[0]; vector.Y.value = values[1]; vector.Z.value = values[2]; vector.W.value = values[3]; this.element.requestUpdate("DefaultValue", vector); } renderInput() { return x`
X
Y
Z
W
` } } /** @extends INumericPinTemplate */ class VectorPinTemplate extends INumericPinTemplate { #getX() { return NumberEntity.printNumber(this.element.getDefaultValue()?.X.valueOf() ?? 0) } #getY() { return NumberEntity.printNumber(this.element.getDefaultValue()?.Y.valueOf() ?? 0) } #getZ() { return NumberEntity.printNumber(this.element.getDefaultValue()?.Z.valueOf() ?? 0) } /** * @param {Number[]} values * @param {String[]} rawValues */ setDefaultValue(values, rawValues) { const vector = this.element.getDefaultValue(true); if (!(vector instanceof VectorEntity)) { throw new TypeError("Expected DefaultValue to be a VectorEntity") } vector.X.value = values[0]; vector.Y.value = values[1]; vector.Z.value = values[2]; this.element.requestUpdate("DefaultValue", vector); } renderInput() { return x`
X
Y
Z
` } } const inputPinTemplates = { "bool": BoolPinTemplate, "byte": IntPinTemplate, "enum": EnumPinTemplate, "int": IntPinTemplate, "int64": Int64PinTemplate, "MUTABLE_REFERENCE": ReferencePinTemplate, "name": NamePinTemplate, "real": RealPinTemplate, "rg": Vector2DPinTemplate, "string": StringPinTemplate, [Configuration.paths.linearColor]: LinearColorPinTemplate, [Configuration.paths.niagaraBool]: BoolPinTemplate, [Configuration.paths.niagaraPosition]: VectorPinTemplate, [Configuration.paths.rotator]: RotatorPinTemplate, [Configuration.paths.vector]: VectorPinTemplate, [Configuration.paths.vector2D]: Vector2DPinTemplate, [Configuration.paths.vector3f]: VectorPinTemplate, [Configuration.paths.vector4f]: Vector4DPinTemplate, }; /** @param {PinEntity} entity */ function pinTemplate(entity) { if (entity.PinType.ContainerType?.toString() === "Array") { return PinTemplate } if (entity.PinType.bIsReference?.valueOf() && !entity.PinType.bIsConst?.valueOf()) { return inputPinTemplates["MUTABLE_REFERENCE"] } const type = entity.getType(); if (type === "exec") { return ExecPinTemplate } return (entity.isInput() ? inputPinTemplates[type] : PinTemplate) ?? PinTemplate } /** * @template {IEntity} T * @extends {IElement, PinTemplate>} */ class PinElement extends IElement { static properties = { pinId: { type: GuidEntity, converter: { fromAttribute: (value, type) => value ? GuidEntity.grammar.parse(value) : null, toAttribute: (value, type) => /** @type {String} */(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 ? LinearColorEntity.getLinearColorFromAnyFormat().parse(value) : null, toAttribute: (value, type) => value ? LinearColorEntity.printLinearColor(value) : null, }, attribute: "data-color", reflect: true, }, defaultValue: { type: String, attribute: false, }, isLinked: { type: Boolean, converter: BooleanEntity.booleanConverter, attribute: "data-linked", reflect: true, }, pinDirection: { type: String, attribute: "data-direction", reflect: true, }, connectable: { type: Boolean, converter: BooleanEntity.booleanConverter, attribute: "data-connectable", reflect: true, } } /** @type {NodeElement} */ nodeElement static newObject( entity = new PinEntity(), template = /** @type {PinTemplate} */(new (pinTemplate(entity))()), nodeElement = undefined ) { const result = new PinElement(); result.initialize(entity, template, nodeElement); return result } initialize( entity = /** @type {PinEntity} */(new PinEntity()), template = /** @type {PinTemplate} */(new (pinTemplate(entity))()), nodeElement = undefined ) { this.nodeElement = nodeElement; this.advancedView = entity.bAdvancedView?.valueOf(); this.isLinked = false; this.connectable = !entity.bNotConnectable?.valueOf(); super.initialize(entity, template); this.pinType = this.entity.getType(); this.defaultValue = this.entity.getDefaultValue(); this.color = PinElement.properties.color.converter.fromAttribute(this.getColor().toString()); this.pinDirection = entity.isInput() ? "input" : entity.isOutput() ? "output" : "hidden"; } setup() { super.setup(); this.nodeElement = this.closest("ueb-node"); } createPinReference() { return new PinReferenceEntity(new SymbolEntity(this.nodeElement.getNodeName()), this.getPinId()) } getPinId() { return this.entity.PinId } getPinName() { return this.entity.PinName?.toString() ?? "" } getPinDisplayName() { return this.entity.pinTitle() } /** @return {CSSResult} */ getColor() { return this.entity.pinColor() } isInput() { return this.entity.isInput() } isOutput() { return this.entity.isOutput() } getLinkLocation() { return this.template.getLinkLocation() } getNodeElement() { return this.nodeElement } getLinks() { return this.entity.LinkedTo?.valueOf() ?? [] } getDefaultValue(maybeCreate = false) { return this.defaultValue = this.entity.getDefaultValue(maybeCreate) } /** @param {T} value */ setDefaultValue(value) { this.entity.DefaultValue = value; this.defaultValue = value; if (this.entity.recomputesNodeTitleOnChange) { this.nodeElement?.computeNodeDisplayName(); } } /** @param {IElement[]} nodesWhitelist */ sanitizeLinks(nodesWhitelist = []) { this.entity.LinkedTo = new (PinEntity.attributes.LinkedTo)( this.entity.LinkedTo?.valueOf().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); if (!link) { link = /** @type {LinkElementConstructor} */(ElementFactory.getConstructor("ueb-link")) .newObject(this, pin); this.blueprint.addGraphElement(link); } } return pin }) ); this.isLinked = this.entity.isLinked(); } /** @param {PinElement} targetPinElement */ linkTo(targetPinElement) { const pinReference = this.createPinReference(); if ( this.isLinked && this.isOutput() && (this.pinType === "exec" || targetPinElement.pinType === "exec") && !this.getLinks().some(ref => pinReference.equals(ref))) { this.unlinkFromAll(); } if (this.entity.linkTo(targetPinElement.getNodeElement().getNodeName(), targetPinElement.entity)) { this.isLinked = this.entity.isLinked(); this.nodeElement?.template.linksChanged(); if (this.entity.recomputesNodeTitleOnChange) { this.nodeElement?.computeNodeDisplayName(); } } } /** @param {PinElement} targetPinElement */ unlinkFrom(targetPinElement, removeLink = true) { if (this.entity.unlinkFrom(targetPinElement.getNodeElement().getNodeName(), targetPinElement.entity)) { this.isLinked = this.entity.isLinked(); this.nodeElement?.template.linksChanged(); if (removeLink) { this.blueprint.getLink(this, targetPinElement)?.remove(); // Might be called after the link is removed } if (this.entity.recomputesNodeTitleOnChange) { this.nodeElement?.computeNodeDisplayName(); } } } unlinkFromAll() { const isLinked = this.getLinks().length; this.getLinks().map(ref => this.blueprint.getPin(ref)).forEach(pin => this.unlinkFrom(pin)); if (isLinked) { this.nodeElement?.template.linksChanged(); } } /** * @param {PinElement} originalPinElement * @param {PinReferenceEntity} newReference */ redirectLink(originalPinElement, newReference) { const index = this.getLinks().findIndex(pinReference => pinReference.objectName.toString() == originalPinElement.getNodeElement().getNodeName() && pinReference.pinGuid.toString() == originalPinElement.entity.PinId.toString() ); if (index >= 0) { this.entity.LinkedTo[index] = newReference; return true } return false } } 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 {typeof import("../Blueprint.js").default.nodeBoundariesSupplier} BoundariesFunction * @typedef {typeof import("../Blueprint.js").default.nodeSelectToggleFunction} SelectionFunction * @typedef {{ * primaryBoundary: Number, * secondaryBoundary: Number, * insertionPosition?: Number, * rectangle: Number * onSecondaryAxis: Boolean * }} Metadata */ class FastSelectionModel { /** * @param {Coordinates} initialPosition * @param {NodeElement[]} rectangles * @param {BoundariesFunction} boundariesFunc * @param {SelectionFunction} 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, 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; } } /** @extends IFromToPositionedTemplate */ class SelectorTemplate extends IFromToPositionedTemplate { } /** @extends {IFromToPositionedElement} */ class SelectorElement extends IFromToPositionedElement { /** @type {FastSelectionModel} */ selectionModel = null constructor() { super(); super.initialize({}, new SelectorTemplate()); } static newObject() { return new SelectorElement() } initialize() { // Initialized in the constructor, this method does nothing } /** @param {Coordinates} initialPosition */ beginSelect(initialPosition) { const blueprintConstructor = /** @type {BlueprintConstructor} */(this.blueprint.constructor); this.blueprint.selecting = true; this.setBothLocations(initialPosition); this.selectionModel = new FastSelectionModel( initialPosition, this.blueprint.getNodes(), blueprintConstructor.nodeBoundariesSupplier, blueprintConstructor.nodeSelectToggleFunction ); } /** @param {Coordinates} finalPosition */ selectTo(finalPosition) { 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; } } /** * @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.constructor === v)?.[0], }, }, } static newObject(entity = {}, template = entity.type ?? new WindowTemplate()) { const result = new WindowElement(); result.initialize(entity, template); return result } initialize(entity = {}, template = entity.type ?? new WindowTemplate()) { entity.windowOptions ??= {}; this.type = entity.type; this.windowOptions = entity.windowOptions; super.initialize(entity, template); } computeSizes() { const bounding = this.getBoundingClientRect(); this.sizeX = bounding.width; this.sizeY = bounding.height; } cleanup() { super.cleanup(); this.acknowledgeClose(); } acknowledgeClose() { let deleteEvent = new CustomEvent(Configuration.windowCloseEventName); this.dispatchEvent(deleteEvent); } } function defineElements() { const define = (tag, type) => { customElements.define(tag, type); ElementFactory.registerElement(tag, type); }; define("ueb-color-handler", ColorHandlerElement); define("ueb-dropdown", DropdownElement); define("ueb-input", InputElement); define("ueb-link", LinkElement); define("ueb-node", NodeElement); define("ueb-pin", PinElement); define("ueb-selector", SelectorElement); define("ueb-ui-slider", ColorSliderElement); define("ueb-window", WindowElement); } class UnknownKeysEntity extends IEntity { static grammar = this.createGrammar() static { IEntity.unknownEntity = this; } /** @returns {P} */ static createGrammar() { return Parsernostrum.seq( // Lookbehind Parsernostrum.reg(new RegExp(`(${Grammar.Regex.Path.source}|${Grammar.Regex.Symbol.source}\\s*)?\\(\\s*`), 1), Parsernostrum.seq(Grammar.attributeName, Grammar.equalSeparation).map(([attribute, equal]) => attribute) .chain(attributeName => this.unknownEntityGrammar.map(attributeValue => values => values[attributeName] = attributeValue ) ) .sepBy(Grammar.commaSeparation), Parsernostrum.reg(/\s*(?:,\s*)?\)/), ).map(([lookbehind, attributes, _2]) => { lookbehind ??= ""; let values = {}; if (lookbehind.length) { values.lookbehind = lookbehind; } attributes.forEach(attributeSetter => attributeSetter(values)); return new this(values) }).label("UnknownKeysEntity") } } function initializeSerializerFactory() { IEntity.unknownEntityGrammar = Parsernostrum.alt( // Remember to keep the order, otherwise parsing might fail BooleanEntity.grammar, GuidEntity.grammar, Parsernostrum.str("None").map(() => ObjectReferenceEntity.createNoneInstance()), NullEntity.grammar, NumberEntity.grammar, Parsernostrum.alt( ObjectReferenceEntity.fullReferenceGrammar, Parsernostrum.regArray(new RegExp( // @ts-expect-error `"(${Grammar.Regex.Path.source})'(${Grammar.Regex.Path.source}|${Grammar.symbol.getParser().regexp.source})'"` )).map(([full, type, path]) => new ObjectReferenceEntity(type, path, full)) ), StringEntity.grammar, LocalizedTextEntity.grammar, InvariantTextEntity.grammar, FormatTextEntity.grammar, PinReferenceEntity.grammar, Vector4DEntity.grammar, VectorEntity.grammar, Vector2DEntity.grammar, RotatorEntity.grammar, LinearColorEntity.grammar, UnknownKeysEntity.grammar, SymbolEntity.grammar, ArrayEntity.of(PinReferenceEntity).grammar, ArrayEntity.of(AlternativesEntity.accepting(NumberEntity, StringEntity, SymbolEntity)).grammar, Parsernostrum.lazy(() => ArrayEntity.createGrammar(IEntity.unknownEntityGrammar)), ); } initializeSerializerFactory(); defineElements(); export { Blueprint, Configuration, LinkElement, NodeElement, Utility };