diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9c7c58b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/node_modules
+node_modules
+node_modules/
\ No newline at end of file
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..1742c61
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,4 @@
+examples/
+test/
+.gitignore
+package-lock.json
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f7b1d13..47d724f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -27,6 +27,15 @@ Thank you for your interest in contributing! FEAScript is in early development,
where the server will be available at `http://127.0.0.1:8000/`. Static file server npm packages like [serve](https://github.com/vercel/serve#readme) and [Vite](https://vite.dev/) can also be used.
+## Branching & Workflow
+
+To contribute a new feature or fix:
+
+- Do not commit directly to `main` or `dev`.
+- Instead, start your work in a feature branch based on the `dev` branch.
+
+**If you are not a member of the repository (e.g., an external contributor), you must first [fork the repository](https://docs.github.com/en/get-started/quickstart/fork-a-repo).** Make your changes in your fork, then submit a Pull Request from your fork's feature branch into the`dev` branch.
+
## File Structure Guidelines
All files in the FEAScript-core codebase should follow this structure:
diff --git a/README.md b/README.md
index a9744d3..5487633 100644
--- a/README.md
+++ b/README.md
@@ -6,24 +6,67 @@
> 🚧 **FEAScript is currently under heavy development.** Functionality and interfaces may change rapidly as new features and enhancements are introduced. 🚧
-## Getting Started
+## Installation
-FEAScript is entirely implemented in pure JavaScript and requires only a simple HTML page to operate. All simulations are executed locally in your browser, without the need for any cloud services.
+FEAScript is entirely implemented in pure JavaScript and requires only a simple HTML page to operate. All simulations are executed locally in your browser, without the need for any cloud services. You can use FEAScript in your projects through one of the following methods:
+
+### Option 1: NPM Installation
+
+```bash
+# Install FEAScript and its peer dependencies
+npm install feascript mathjs plotly.js
+```
+
+Then import it in your JavaScript/TypeScript file:
+
+```javascript
+// ES Modules
+import { FEAScriptModel, plotSolution } from "feascript";
+
+// CommonJS
+const { FEAScriptModel, plotSolution } = require("feascript");
+```
+
+**Important:** FEAScript is built as an ES module. If you're starting a new project, make sure to configure it to use ES modules by running:
+
+```bash
+# Create package.json with type=module for ES modules support
+echo '{"type":"module"}' > package.json
+```
+
+If you already have a package.json file, manually add `"type": "module"` to enable ES modules in your project.
+
+### Option 2: Direct Import from CDN
+
+Add this line to your HTML or JavaScript module:
+
+```javascript
+import { FEAScriptModel, plotSolution } from "https://core.feascript.com/dist/feascript.esm.js";
+```
+
+### Option 3: Download and Use Locally
+
+1. Download the latest release from [GitHub Releases](https://github.com/FEAScript/FEAScript-core/releases)
+2. Include it in your project:
+
+```html
+
+```
### Example Usage
```javascript
-// Import required modules
-import { FEAScriptModel, plotSolution } from "https://core.feascript.com/src/index.js";
+// Import FEAScript library
+import { FEAScriptModel, plotSolution } from "https://core.feascript.com/dist/feascript.esm.js";
-// Create a new FEAScript model
+// Create and configure model
const model = new FEAScriptModel();
-
-// Configure the solver
model.setSolverConfig("solverType"); // e.g., "solidHeatTransfer" for a stationary solid heat transfer case
-
-// Define mesh configuration (assuming a rectangular domain for 2D)
model.setMeshConfig({
+ // Define mesh configuration (assuming a rectangular domain for 2D)
meshDimension: "1D" | "2D", // Mesh dimension
elementOrder: "linear" | "quadratic", // Element order
numElementsX: number, // Number of elements in x-direction
@@ -32,16 +75,13 @@ model.setMeshConfig({
maxY: number, // Domain length in y-direction (for 2D)
});
-// Define boundary conditions
-model.addBoundaryCondition("boundaryIndex", ["conditionType", ...parameters]);
-
-// Set solver method
-model.setSolverMethod("solverMethod"); // e.g., "lusolve" for LU decomposition
+// Apply boundary conditions
+model.addBoundaryCondition("boundaryIndex", ["conditionType", /* parameters */]);
-// Solve the problem
+// Solve
const { solutionVector, nodesCoordinates } = model.solve();
-// Visualize results
+// Plot results
plotSolution(
solutionVector,
nodesCoordinates,
diff --git a/dist/feascript.cjs.js b/dist/feascript.cjs.js
new file mode 100644
index 0000000..6084723
--- /dev/null
+++ b/dist/feascript.cjs.js
@@ -0,0 +1,8 @@
+"use strict";Object.defineProperty(exports,"__esModule",{value:!0});class e{constructor({meshDimension:e,elementOrder:t}){this.meshDimension=e,this.elementOrder=t}getGaussPointsAndWeights(){let e=[],t=[];return"linear"===this.elementOrder?(e[0]=.5,t[0]=1):"quadratic"===this.elementOrder&&(e[0]=(1-Math.sqrt(.6))/2,e[1]=.5,e[2]=(1+Math.sqrt(.6))/2,t[0]=5/18,t[1]=8/18,t[2]=5/18),{gaussPoints:e,gaussWeights:t}}}let t="basic";function n(e){"debug"===t&&console.log("%c[DEBUG] "+e,"color: #2196F3; font-weight: bold;")}function s(e){console.log("%c[INFO] "+e,"color: #4CAF50; font-weight: bold;")}function o(e){console.log("%c[ERROR] "+e,"color: #F44336; font-weight: bold;")}class i{constructor({meshDimension:e,elementOrder:t}){this.meshDimension=e,this.elementOrder=t}getBasisFunctions(e,t=null){let n=[],s=[],i=[];if("1D"===this.meshDimension)"linear"===this.elementOrder?(n[0]=1-e,n[1]=e,s[0]=-1,s[1]=1):"quadratic"===this.elementOrder&&(n[0]=1-3*e+2*e**2,n[1]=4*e-4*e**2,n[2]=2*e**2-e,s[0]=4*e-3,s[1]=4-8*e,s[2]=4*e-1);else if("2D"===this.meshDimension){if(null===t)return void o("Eta coordinate is required for 2D elements");if("linear"===this.elementOrder){function r(e){return 1-e}n[0]=r(e)*r(t),n[1]=r(e)*t,n[2]=e*r(t),n[3]=e*t,s[0]=-1*r(t),s[1]=-1*t,s[2]=1*r(t),s[3]=1*t,i[0]=-1*r(e),i[1]=1*r(e),i[2]=-1*e,i[3]=1*e}else if("quadratic"===this.elementOrder){function a(e){return 2*e**2-3*e+1}function l(e){return-4*e**2+4*e}function d(e){return 2*e**2-e}function h(e){return 4*e-3}function m(e){return-8*e+4}function u(e){return 4*e-1}n[0]=a(e)*a(t),n[1]=a(e)*l(t),n[2]=a(e)*d(t),n[3]=l(e)*a(t),n[4]=l(e)*l(t),n[5]=l(e)*d(t),n[6]=d(e)*a(t),n[7]=d(e)*l(t),n[8]=d(e)*d(t),s[0]=h(e)*a(t),s[1]=h(e)*l(t),s[2]=h(e)*d(t),s[3]=m(e)*a(t),s[4]=m(e)*l(t),s[5]=m(e)*d(t),s[6]=u(e)*a(t),s[7]=u(e)*l(t),s[8]=u(e)*d(t),i[0]=a(e)*h(t),i[1]=a(e)*m(t),i[2]=a(e)*u(t),i[3]=l(e)*h(t),i[4]=l(e)*m(t),i[5]=l(e)*u(t),i[6]=d(e)*h(t),i[7]=d(e)*m(t),i[8]=d(e)*u(t)}}return{basisFunction:n,basisFunctionDerivKsi:s,basisFunctionDerivEta:i}}}class r{constructor({numElementsX:e=null,maxX:t=null,numElementsY:n=null,maxY:s=null,meshDimension:o=null,elementOrder:i="linear",parsedMesh:r=null}){this.numElementsX=e,this.numElementsY=n,this.maxX=t,this.maxY=s,this.meshDimension=o,this.elementOrder=i,this.parsedMesh=r}generateMesh(){if(this.parsedMesh){if(this.parsedMesh.nodalNumbering&&"object"==typeof this.parsedMesh.nodalNumbering&&!Array.isArray(this.parsedMesh.nodalNumbering)){const e=this.parsedMesh.nodalNumbering.quadElements||[];if(this.parsedMesh.nodalNumbering.triangleElements,n("Initial parsed mesh nodal numbering from GMSH format: "+JSON.stringify(this.parsedMesh.nodalNumbering)),this.parsedMesh.elementTypes[3]||this.parsedMesh.elementTypes[10]){const t=[];for(let n=0;n0&&void 0===this.parsedMesh.boundaryElements[0]){const e=[];for(let t=1;t{if(1===e.dimension){const t=this.parsedMesh.boundaryNodePairs[e.tag]||[];t.length>0&&(this.parsedMesh.boundaryElements[e.tag]||(this.parsedMesh.boundaryElements[e.tag]=[]),t.forEach((t=>{const s=t[0],i=t[1];n(`Processing boundary node pair: [${s}, ${i}] for boundary ${e.tag} (${e.name||"unnamed"})`);let r=!1;for(let t=0;t0&&void 0===this.parsedMesh.boundaryElements[0])){const e=[];for(let t=1;t{if("constantTemp"===this.boundaryConditions[s][0]){const o=this.boundaryConditions[s][1];n(`Boundary ${s}: Applying constant temperature of ${o} K (Dirichlet condition)`),this.boundaryElements[s].forEach((([s,i])=>{if("linear"===this.elementOrder){({0:[0],1:[1]})[i].forEach((i=>{const r=this.nop[s][i]-1;n(` - Applied fixed temperature to node ${r+1} (element ${s+1}, local node ${i+1})`),e[r]=o;for(let n=0;n{const r=this.nop[s][i]-1;n(` - Applied fixed temperature to node ${r+1} (element ${s+1}, local node ${i+1})`),e[r]=o;for(let n=0;n{if("constantTemp"===this.boundaryConditions[s][0]){const o=this.boundaryConditions[s][1];n(`Boundary ${s}: Applying constant temperature of ${o} K (Dirichlet condition)`),this.boundaryElements[s].forEach((([s,i])=>{if("linear"===this.elementOrder){({0:[0,2],1:[0,1],2:[1,3],3:[2,3]})[i].forEach((i=>{const r=this.nop[s][i]-1;n(` - Applied fixed temperature to node ${r+1} (element ${s+1}, local node ${i+1})`),e[r]=o;for(let n=0;n{const r=this.nop[s][i]-1;n(` - Applied fixed temperature to node ${r+1} (element ${s+1}, local node ${i+1})`),e[r]=o;for(let n=0;n{const t=this.boundaryConditions[e];"convection"===t[0]&&(d[e]=t[1],h[e]=t[2])})),"1D"===this.meshDimension?Object.keys(this.boundaryConditions).forEach((s=>{if("convection"===this.boundaryConditions[s][0]){const o=d[s],i=h[s];n(`Boundary ${s}: Applying convection with heat transfer coefficient h=${o} W/(m²·K) and external temperature T∞=${i} K`),this.boundaryElements[s].forEach((([s,r])=>{let a;"linear"===this.elementOrder?a=0===r?0:1:"quadratic"===this.elementOrder&&(a=0===r?0:2);const l=this.nop[s][a]-1;n(` - Applied convection boundary condition to node ${l+1} (element ${s+1}, local node ${a+1})`),e[l]+=-o*i,t[l][l]+=o}))}})):"2D"===this.meshDimension&&Object.keys(this.boundaryConditions).forEach((s=>{if("convection"===this.boundaryConditions[s][0]){const m=d[s],u=h[s];n(`Boundary ${s}: Applying convection with heat transfer coefficient h=${m} W/(m²·K) and external temperature T∞=${u} K`),this.boundaryElements[s].forEach((([s,d])=>{if("linear"===this.elementOrder){let h,c,f,p,y;0===d?(h=o[0],c=0,f=0,p=3,y=2):1===d?(h=0,c=o[0],f=0,p=2,y=1):2===d?(h=o[0],c=1,f=1,p=4,y=2):3===d&&(h=1,c=o[0],f=2,p=4,y=1);let g=l.getBasisFunctions(h,c),b=g.basisFunction,E=g.basisFunctionDerivKsi,M=g.basisFunctionDerivEta,$=0,v=0,w=0,C=0;const N=this.nop[s].length;for(let e=0;e"object"==typeof e&&null!==e||"function"==typeof e,f=new Map([["proxy",{canHandle:e=>c(e)&&e[l],serialize(e){const{port1:t,port2:n}=new MessageChannel;return p(e,t),[n,[n]]},deserialize:e=>(e.start(),g(e))}],["throw",{canHandle:e=>c(e)&&u in e,serialize({value:e}){let t;return t=e instanceof Error?{isError:!0,value:{message:e.message,name:e.name,stack:e.stack}}:{isError:!1,value:e},[t,[]]},deserialize(e){if(e.isError)throw Object.assign(new Error(e.value.message),e.value);throw e.value}}]]);function p(e,t=globalThis,n=["*"]){t.addEventListener("message",(function s(o){if(!o||!o.data)return;if(!function(e,t){for(const n of e){if(t===n||"*"===n)return!0;if(n instanceof RegExp&&n.test(t))return!0}return!1}(n,o.origin))return void console.warn(`Invalid origin '${o.origin}' for comlink proxy`);const{id:i,type:r,path:a}=Object.assign({path:[]},o.data),d=(o.data.argumentList||[]).map(S);let h;try{const t=a.slice(0,-1).reduce(((e,t)=>e[t]),e),n=a.reduce(((e,t)=>e[t]),e);switch(r){case"GET":h=n;break;case"SET":t[a.slice(-1)[0]]=S(o.data.value),h=!0;break;case"APPLY":h=n.apply(t,d);break;case"CONSTRUCT":h=function(e){return Object.assign(e,{[l]:!0})}(new n(...d));break;case"ENDPOINT":{const{port1:t,port2:n}=new MessageChannel;p(e,n),h=function(e,t){return C.set(e,t),e}(t,[t])}break;case"RELEASE":h=void 0;break;default:return}}catch(e){h={value:e,[u]:0}}Promise.resolve(h).catch((e=>({value:e,[u]:0}))).then((n=>{const[o,a]=N(n);t.postMessage(Object.assign(Object.assign({},o),{id:i}),a),"RELEASE"===r&&(t.removeEventListener("message",s),y(t),m in e&&"function"==typeof e[m]&&e[m]())})).catch((e=>{const[n,s]=N({value:new TypeError("Unserializable return value"),[u]:0});t.postMessage(Object.assign(Object.assign({},n),{id:i}),s)}))})),t.start&&t.start()}function y(e){(function(e){return"MessagePort"===e.constructor.name})(e)&&e.close()}function g(e,t){const n=new Map;return e.addEventListener("message",(function(e){const{data:t}=e;if(!t||!t.id)return;const s=n.get(t.id);if(s)try{s(t)}finally{n.delete(t.id)}})),v(e,n,[],t)}function b(e){if(e)throw new Error("Proxy has been released and is not useable")}function E(e){return x(e,new Map,{type:"RELEASE"}).then((()=>{y(e)}))}const M=new WeakMap,$="FinalizationRegistry"in globalThis&&new FinalizationRegistry((e=>{const t=(M.get(e)||0)-1;M.set(e,t),0===t&&E(e)}));function v(e,t,n=[],s=function(){}){let o=!1;const i=new Proxy(s,{get(s,r){if(b(o),r===h)return()=>{!function(e){$&&$.unregister(e)}(i),E(e),t.clear(),o=!0};if("then"===r){if(0===n.length)return{then:()=>i};const s=x(e,t,{type:"GET",path:n.map((e=>e.toString()))}).then(S);return s.then.bind(s)}return v(e,t,[...n,r])},set(s,i,r){b(o);const[a,l]=N(r);return x(e,t,{type:"SET",path:[...n,i].map((e=>e.toString())),value:a},l).then(S)},apply(s,i,r){b(o);const a=n[n.length-1];if(a===d)return x(e,t,{type:"ENDPOINT"}).then(S);if("bind"===a)return v(e,t,n.slice(0,-1));const[l,h]=w(r);return x(e,t,{type:"APPLY",path:n.map((e=>e.toString())),argumentList:l},h).then(S)},construct(s,i){b(o);const[r,a]=w(i);return x(e,t,{type:"CONSTRUCT",path:n.map((e=>e.toString())),argumentList:r},a).then(S)}});return function(e,t){const n=(M.get(t)||0)+1;M.set(t,n),$&&$.register(e,t,e)}(i,e),i}function w(e){const t=e.map(N);return[t.map((e=>e[0])),(n=t.map((e=>e[1])),Array.prototype.concat.apply([],n))];var n}const C=new WeakMap;function N(e){for(const[t,n]of f)if(n.canHandle(e)){const[s,o]=n.serialize(e);return[{type:"HANDLER",name:t,value:s},o]}return[{type:"RAW",value:e},C.get(e)||[]]}function S(e){switch(e.type){case"HANDLER":return f.get(e.name).deserialize(e.value);case"RAW":return e.value}}function x(e,t,n,s){return new Promise((o=>{const i=new Array(4).fill(0).map((()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16))).join("-");t.set(i,o),e.start&&e.start(),e.postMessage(Object.assign({id:i},n),s)}))}exports.FEAScriptModel=class{constructor(){this.solverConfig=null,this.meshConfig={},this.boundaryConditions={},this.solverMethod="lusolve",s("FEAScriptModel instance created")}setSolverConfig(e){this.solverConfig=e,n(`Solver config set to: ${e}`)}setMeshConfig(e){this.meshConfig=e,n(`Mesh config set with dimensions: ${e.meshDimension}`)}addBoundaryCondition(e,t){this.boundaryConditions[e]=t,n(`Boundary condition added for boundary: ${e}, type: ${t[0]}`)}setSolverMethod(e){this.solverMethod=e,n(`Solver method set to: ${e}`)}solve(){if(!this.solverConfig||!this.meshConfig||!this.boundaryConditions){const e="Solver config, mesh config, and boundary conditions must be set before solving.";throw console.error(e),new Error(e)}let t=[],o=[],l=[],d={};if(s("Beginning matrix assembly..."),console.time("assemblyMatrices"),"solidHeatTransferScript"===this.solverConfig&&(s(`Using solver: ${this.solverConfig}`),({jacobianMatrix:t,residualVector:o,nodesCoordinates:d}=function(t,o){s("Starting solid heat transfer matrix assembly...");const{meshDimension:l,numElementsX:d,numElementsY:h,maxX:m,maxY:u,elementOrder:c,parsedMesh:f}=t;n("Generating mesh...");const p=new r({numElementsX:d,numElementsY:h,maxX:m,maxY:u,meshDimension:l,elementOrder:c,parsedMesh:f}).generateMesh();let y,g,b=p.nodesXCoordinates,E=p.nodesYCoordinates,M=p.totalNodesX,$=p.totalNodesY,v=p.nodalNumbering,w=p.boundaryElements;null!=f?(y=v.length,g=b.length,n(`Using parsed mesh with ${y} elements and ${g} nodes`)):(y=d*("2D"===l?h:1),g=M*("2D"===l?$:1),n(`Using mesh generated from geometry with ${y} elements and ${g} nodes`));let C,N,S,x,D,O,A,F=[],T=[],k=[],X=[],P=[],R=[],Y=[],W=[],I=[],j=[];for(let e=0;e{console.error("FEAScriptWorker: Worker error:",e)};const e=g(this.worker);this.feaWorker=await new e,this.isReady=!0}catch(e){throw console.error("Failed to initialize worker",e),e}}async _ensureReady(){return this.isReady?Promise.resolve():new Promise(((e,t)=>{let n=0;const s=()=>{n++,this.isReady?e():n>=50?t(new Error("Timeout waiting for worker to be ready")):setTimeout(s,1e3)};s()}))}async setSolverConfig(e){return await this._ensureReady(),s(`FEAScriptWorker: Setting solver config to: ${e}`),this.feaWorker.setSolverConfig(e)}async setMeshConfig(e){return await this._ensureReady(),s("FEAScriptWorker: Setting mesh config"),this.feaWorker.setMeshConfig(e)}async addBoundaryCondition(e,t){return await this._ensureReady(),s(`FEAScriptWorker: Adding boundary condition for boundary: ${e}`),this.feaWorker.addBoundaryCondition(e,t)}async setSolverMethod(e){return await this._ensureReady(),s(`FEAScriptWorker: Setting solver method to: ${e}`),this.feaWorker.setSolverMethod(e)}async solve(){await this._ensureReady(),s("FEAScriptWorker: Requesting solution from worker...");const e=performance.now(),t=await this.feaWorker.solve();return s(`FEAScriptWorker: Solution completed in ${((performance.now()-e)/1e3).toFixed(2)}s`),t}async getModelInfo(){return await this._ensureReady(),this.feaWorker.getModelInfo()}async ping(){return await this._ensureReady(),this.feaWorker.ping()}terminate(){this.worker&&(this.worker.terminate(),this.worker=null,this.feaWorker=null,this.isReady=!1)}},exports.VERSION="0.1.1",exports.importGmshQuadTri=async e=>{let t={nodesXCoordinates:[],nodesYCoordinates:[],nodalNumbering:{quadElements:[],triangleElements:[]},boundaryElements:[],boundaryConditions:[],boundaryNodePairs:{},gmshV:0,ascii:!1,fltBytes:"8",totalNodesX:0,totalNodesY:0,physicalPropMap:[],elementTypes:{}},s=(await e.text()).split("\n").map((e=>e.trim())).filter((e=>""!==e&&" "!==e)),o="",i=0,r=0,a=0,l=0,d={numNodes:0},h=0,m=[],u=0,c=0,f=0,p={dim:0,tag:0,elementType:0,numElements:0},y=0,g={};for(;i""!==e));if("meshFormat"===o)t.gmshV=parseFloat(n[0]),t.ascii="0"===n[1],t.fltBytes=n[2];else if("physicalNames"===o){if(n.length>=3){if(!/^\d+$/.test(n[0])){i++;continue}const e=parseInt(n[0],10),s=parseInt(n[1],10);let o=n.slice(2).join(" ");o=o.replace(/^"|"$/g,""),t.physicalPropMap.push({tag:s,dimension:e,name:o})}}else if("nodes"===o){if(0===r){r=parseInt(n[0],10),a=parseInt(n[1],10),t.nodesXCoordinates=new Array(a).fill(0),t.nodesYCoordinates=new Array(a).fill(0),i++;continue}if(lparseInt(e,10)));if(1===p.elementType||8===p.elementType){const n=p.tag;g[n]||(g[n]=[]),g[n].push(e),t.boundaryNodePairs[n]||(t.boundaryNodePairs[n]=[]),t.boundaryNodePairs[n].push(e)}else 2===p.elementType?t.nodalNumbering.triangleElements.push(e):(3===p.elementType||10===p.elementType)&&t.nodalNumbering.quadElements.push(e);y++,y===p.numElements&&(f++,p={numElements:0})}}i++}return t.physicalPropMap.forEach((e=>{if(1===e.dimension){const n=g[e.tag]||[];n.length>0&&t.boundaryConditions.push({name:e.name,tag:e.tag,nodes:n})}})),n(`Parsed boundary node pairs by physical tag: ${JSON.stringify(t.boundaryNodePairs)}. These pairs will be used to identify boundary elements in the mesh.`),t},exports.logSystem=function(e){"basic"!==e&&"debug"!==e?(console.log("%c[WARN] Invalid log level: "+e+". Using basic instead.","color: #FFC107; font-weight: bold;"),t="basic"):(t=e,s(`Log level set to: ${e}`))},exports.plotSolution=function(e,t,n,s,o,i,r="structured"){const{nodesXCoordinates:a,nodesYCoordinates:l}=t;if("1D"===s&&"line"===o){let t;t=e.length>0&&Array.isArray(e[0])?e.map((e=>e[0])):e;let s=Array.from(a),o={x:s,y:t,mode:"lines",type:"scatter",line:{color:"rgb(219, 64, 82)",width:2},name:"Solution"},r=Math.min(window.innerWidth,700),l=Math.max(...s),d=r/l,h={title:`line plot - ${n}`,width:Math.max(d*l,400),height:350,xaxis:{title:"x"},yaxis:{title:"Solution"},margin:{l:70,r:40,t:50,b:50}};Plotly.newPlot(i,[o],h,{responsive:!0})}else if("2D"===s&&"contour"===o){const t="structured"===r,s=new Set(a).size,d=new Set(l).size;let h=Array.isArray(e[0])?e.map((e=>e[0])):e,m=Math.min(window.innerWidth,700),u=Math.max(...a),c=Math.max(...l)/u,f=Math.min(m,600),p={title:`${o} plot - ${n}`,width:f,height:f*c*.8,xaxis:{title:"x"},yaxis:{title:"y"},margin:{l:50,r:50,t:50,b:50},hovermode:"closest"};if(t){const t=s,n=d;math.reshape(Array.from(a),[t,n]);let o=math.reshape(Array.from(l),[t,n]),r=math.reshape(Array.from(e),[t,n]),h=math.transpose(r),m=[];for(let e=0;e | |\n // 0 --- 1 0 --- 2\n\n feaScriptNodes[0] = gmshNodes[0]; // 0 -> 0\n feaScriptNodes[1] = gmshNodes[3]; // 3 -> 1\n feaScriptNodes[2] = gmshNodes[1]; // 1 -> 2\n feaScriptNodes[3] = gmshNodes[2]; // 2 -> 3\n } else if (gmshNodes.length === 9) {\n // Mapping for quadratic quad elements (9 nodes)\n // GMSH: FEAScript:\n // 3--6--2 2--5--8\n // | | | |\n // 7 8 5 --> 1 4 7\n // | | | |\n // 0--4--1 0--3--6\n\n feaScriptNodes[0] = gmshNodes[0]; // 0 -> 0\n feaScriptNodes[1] = gmshNodes[7]; // 7 -> 1\n feaScriptNodes[2] = gmshNodes[3]; // 3 -> 2\n feaScriptNodes[3] = gmshNodes[4]; // 4 -> 3\n feaScriptNodes[4] = gmshNodes[8]; // 8 -> 4\n feaScriptNodes[5] = gmshNodes[6]; // 6 -> 5\n feaScriptNodes[6] = gmshNodes[1]; // 1 -> 6\n feaScriptNodes[7] = gmshNodes[5]; // 5 -> 7\n feaScriptNodes[8] = gmshNodes[2]; // 2 -> 8\n }\n\n mappedNodalNumbering.push(feaScriptNodes);\n }\n\n this.parsedMesh.nodalNumbering = mappedNodalNumbering;\n } else if (this.parsedMesh.elementTypes[2]) {\n }\n\n debugLog(\n \"Nodal numbering after mapping from GMSH to FEAScript format: \" +\n JSON.stringify(this.parsedMesh.nodalNumbering)\n );\n\n // Process boundary elements if they exist and if physical property mapping exists\n if (this.parsedMesh.physicalPropMap && this.parsedMesh.boundaryElements) {\n // Check if boundary elements need to be processed\n if (\n Array.isArray(this.parsedMesh.boundaryElements) &&\n this.parsedMesh.boundaryElements.length > 0 &&\n this.parsedMesh.boundaryElements[0] === undefined\n ) {\n // Create a new array without the empty first element\n const fixedBoundaryElements = [];\n for (let i = 1; i < this.parsedMesh.boundaryElements.length; i++) {\n if (this.parsedMesh.boundaryElements[i]) {\n fixedBoundaryElements.push(this.parsedMesh.boundaryElements[i]);\n }\n }\n this.parsedMesh.boundaryElements = fixedBoundaryElements;\n }\n\n // If boundary node pairs exist but boundary elements haven't been processed\n if (this.parsedMesh.boundaryNodePairs && !this.parsedMesh.boundaryElementsProcessed) {\n // Reset boundary elements array\n this.parsedMesh.boundaryElements = [];\n\n // Process each physical property from the Gmsh file\n this.parsedMesh.physicalPropMap.forEach((prop) => {\n // Only process 1D physical entities (boundary lines)\n if (prop.dimension === 1) {\n // Get all node pairs for this boundary\n const boundaryNodePairs = this.parsedMesh.boundaryNodePairs[prop.tag] || [];\n\n if (boundaryNodePairs.length > 0) {\n // Initialize array for this boundary tag\n if (!this.parsedMesh.boundaryElements[prop.tag]) {\n this.parsedMesh.boundaryElements[prop.tag] = [];\n }\n\n // For each boundary line segment (defined by a pair of nodes)\n boundaryNodePairs.forEach((nodesPair) => {\n const node1 = nodesPair[0]; // First node in the pair\n const node2 = nodesPair[1]; // Second node in the pair\n\n debugLog(\n `Processing boundary node pair: [${node1}, ${node2}] for boundary ${prop.tag} (${\n prop.name || \"unnamed\"\n })`\n );\n\n // Search through all elements to find which one contains both nodes\n let foundElement = false;\n\n // Loop through all elements in the mesh\n for (let elemIdx = 0; elemIdx < this.parsedMesh.nodalNumbering.length; elemIdx++) {\n const elemNodes = this.parsedMesh.nodalNumbering[elemIdx];\n\n // For linear quadrilateral linear elements (4 nodes)\n if (elemNodes.length === 4) {\n // Check if both boundary nodes are in this element\n if (elemNodes.includes(node1) && elemNodes.includes(node2)) {\n // Find which side of the element these nodes form\n let side;\n\n const node1Index = elemNodes.indexOf(node1);\n const node2Index = elemNodes.indexOf(node2);\n\n debugLog(\n ` Found element ${elemIdx} containing boundary nodes. Element nodes: [${elemNodes.join(\n \", \"\n )}]`\n );\n debugLog(\n ` Node ${node1} is at index ${node1Index}, Node ${node2} is at index ${node2Index} in the element`\n );\n\n // Based on FEAScript linear quadrilateral numbering:\n // 1 --- 3\n // | |\n // 0 --- 2\n\n if (\n (node1Index === 0 && node2Index === 2) ||\n (node1Index === 2 && node2Index === 0)\n ) {\n side = 0; // Bottom side\n debugLog(` These nodes form the BOTTOM side (${side}) of element ${elemIdx}`);\n } else if (\n (node1Index === 0 && node2Index === 1) ||\n (node1Index === 1 && node2Index === 0)\n ) {\n side = 1; // Left side\n debugLog(` These nodes form the LEFT side (${side}) of element ${elemIdx}`);\n } else if (\n (node1Index === 1 && node2Index === 3) ||\n (node1Index === 3 && node2Index === 1)\n ) {\n side = 2; // Top side\n debugLog(` These nodes form the TOP side (${side}) of element ${elemIdx}`);\n } else if (\n (node1Index === 2 && node2Index === 3) ||\n (node1Index === 3 && node2Index === 2)\n ) {\n side = 3; // Right side\n debugLog(` These nodes form the RIGHT side (${side}) of element ${elemIdx}`);\n }\n\n // Add the element and side to the boundary elements array\n this.parsedMesh.boundaryElements[prop.tag].push([elemIdx, side]);\n debugLog(\n ` Added element-side pair [${elemIdx}, ${side}] to boundary tag ${prop.tag}`\n );\n foundElement = true;\n break;\n }\n } else if (elemNodes.length === 9) {\n // For quadratic quadrilateral elements (9 nodes)\n // Check if both boundary nodes are in this element\n if (elemNodes.includes(node1) && elemNodes.includes(node2)) {\n // Find which side of the element these nodes form\n let side;\n\n const node1Index = elemNodes.indexOf(node1);\n const node2Index = elemNodes.indexOf(node2);\n\n debugLog(\n ` Found element ${elemIdx} containing boundary nodes. Element nodes: [${elemNodes.join(\n \", \"\n )}]`\n );\n debugLog(\n ` Node ${node1} is at index ${node1Index}, Node ${node2} is at index ${node2Index} in the element`\n );\n\n // Based on FEAScript quadratic quadrilateral numbering:\n // 2--5--8\n // | |\n // 1 4 7\n // | |\n // 0--3--6\n\n if (\n (node1Index === 0 && node2Index === 6) ||\n (node1Index === 6 && node2Index === 0) ||\n (node1Index === 0 && node2Index === 3) ||\n (node1Index === 3 && node2Index === 0) ||\n (node1Index === 3 && node2Index === 6) ||\n (node1Index === 6 && node2Index === 3)\n ) {\n side = 0; // Bottom side (nodes 0, 3, 6)\n debugLog(` These nodes form the BOTTOM side (${side}) of element ${elemIdx}`);\n } else if (\n (node1Index === 0 && node2Index === 2) ||\n (node1Index === 2 && node2Index === 0) ||\n (node1Index === 0 && node2Index === 1) ||\n (node1Index === 1 && node2Index === 0) ||\n (node1Index === 1 && node2Index === 2) ||\n (node1Index === 2 && node2Index === 1)\n ) {\n side = 1; // Left side (nodes 0, 1, 2)\n debugLog(` These nodes form the LEFT side (${side}) of element ${elemIdx}`);\n } else if (\n (node1Index === 2 && node2Index === 8) ||\n (node1Index === 8 && node2Index === 2) ||\n (node1Index === 2 && node2Index === 5) ||\n (node1Index === 5 && node2Index === 2) ||\n (node1Index === 5 && node2Index === 8) ||\n (node1Index === 8 && node2Index === 5)\n ) {\n side = 2; // Top side (nodes 2, 5, 8)\n debugLog(` These nodes form the TOP side (${side}) of element ${elemIdx}`);\n } else if (\n (node1Index === 6 && node2Index === 8) ||\n (node1Index === 8 && node2Index === 6) ||\n (node1Index === 6 && node2Index === 7) ||\n (node1Index === 7 && node2Index === 6) ||\n (node1Index === 7 && node2Index === 8) ||\n (node1Index === 8 && node2Index === 7)\n ) {\n side = 3; // Right side (nodes 6, 7, 8)\n debugLog(` These nodes form the RIGHT side (${side}) of element ${elemIdx}`);\n }\n\n // Add the element and side to the boundary elements array\n this.parsedMesh.boundaryElements[prop.tag].push([elemIdx, side]);\n debugLog(\n ` Added element-side pair [${elemIdx}, ${side}] to boundary tag ${prop.tag}`\n );\n foundElement = true;\n break;\n }\n }\n }\n\n if (!foundElement) {\n errorLog(\n `Could not find element containing boundary nodes ${node1} and ${node2}. Boundary may be incomplete.`\n );\n }\n });\n }\n }\n });\n\n // Mark as processed\n this.parsedMesh.boundaryElementsProcessed = true;\n\n // Fix boundary elements array - remove undefined entries\n if (\n this.parsedMesh.boundaryElements.length > 0 &&\n this.parsedMesh.boundaryElements[0] === undefined\n ) {\n const fixedBoundaryElements = [];\n for (let i = 1; i < this.parsedMesh.boundaryElements.length; i++) {\n if (this.parsedMesh.boundaryElements[i]) {\n fixedBoundaryElements.push(this.parsedMesh.boundaryElements[i]);\n }\n }\n this.parsedMesh.boundaryElements = fixedBoundaryElements;\n }\n }\n }\n }\n }\n\n debugLog(\"Processed boundary elements by tag: \" + JSON.stringify(this.parsedMesh.boundaryElements));\n\n return this.parsedMesh;\n } else {\n // Validate required geometry parameters based on mesh dimension\n if (this.meshDimension === \"1D\") {\n if (this.numElementsX === null || this.maxX === null) {\n errorLog(\"numElementsX and maxX are required parameters when generating a 1D mesh from geometry\");\n }\n } else if (this.meshDimension === \"2D\") {\n if (\n this.numElementsX === null ||\n this.maxX === null ||\n this.numElementsY === null ||\n this.maxY === null\n ) {\n errorLog(\n \"numElementsX, maxX, numElementsY, and maxY are required parameters when generating a 2D mesh from geometry\"\n );\n }\n }\n\n // Generate mesh based on dimension\n return this.generateMeshFromGeometry();\n }\n }\n\n /**\n * Function to generate a structured mesh based on the geometry configuration\n * @returns {object} An object containing the coordinates of nodes,\n * total number of nodes, nodal numbering (NOP) array, and boundary elements\n */\n generateMeshFromGeometry() {\n let nodesXCoordinates = [];\n let nodesYCoordinates = [];\n const xStart = 0;\n const yStart = 0;\n let totalNodesX, totalNodesY, deltaX, deltaY;\n\n if (this.meshDimension === \"1D\") {\n if (this.elementOrder === \"linear\") {\n totalNodesX = this.numElementsX + 1;\n deltaX = (this.maxX - xStart) / this.numElementsX;\n\n nodesXCoordinates[0] = xStart;\n for (let nodeIndex = 1; nodeIndex < totalNodesX; nodeIndex++) {\n nodesXCoordinates[nodeIndex] = nodesXCoordinates[nodeIndex - 1] + deltaX;\n }\n } else if (this.elementOrder === \"quadratic\") {\n totalNodesX = 2 * this.numElementsX + 1;\n deltaX = (this.maxX - xStart) / this.numElementsX;\n\n nodesXCoordinates[0] = xStart;\n for (let nodeIndex = 1; nodeIndex < totalNodesX; nodeIndex++) {\n nodesXCoordinates[nodeIndex] = nodesXCoordinates[nodeIndex - 1] + deltaX / 2;\n }\n }\n // Generate nodal numbering (NOP) array\n const nodalNumbering = this.generateNodalNumbering(\n this.numElementsX,\n null, // numElementsY (not used in 1D)\n totalNodesX,\n null, // totalNodesY (not used in 1D)\n this.elementOrder\n );\n // Find boundary elements\n const boundaryElements = this.findBoundaryElements();\n\n debugLog(\"Generated node X coordinates: \" + JSON.stringify(nodesXCoordinates));\n\n // Return x coordinates of nodes, total nodes, NOP array, and boundary elements\n return {\n nodesXCoordinates,\n totalNodesX,\n nodalNumbering,\n boundaryElements,\n };\n } else if (this.meshDimension === \"2D\") {\n if (this.elementOrder === \"linear\") {\n totalNodesX = this.numElementsX + 1;\n totalNodesY = this.numElementsY + 1;\n deltaX = (this.maxX - xStart) / this.numElementsX;\n deltaY = (this.maxY - yStart) / this.numElementsY;\n\n nodesXCoordinates[0] = xStart;\n nodesYCoordinates[0] = yStart;\n for (let nodeIndexY = 1; nodeIndexY < totalNodesY; nodeIndexY++) {\n nodesXCoordinates[nodeIndexY] = nodesXCoordinates[0];\n nodesYCoordinates[nodeIndexY] = nodesYCoordinates[0] + nodeIndexY * deltaY;\n }\n for (let nodeIndexX = 1; nodeIndexX < totalNodesX; nodeIndexX++) {\n const nnode = nodeIndexX * totalNodesY;\n nodesXCoordinates[nnode] = nodesXCoordinates[0] + nodeIndexX * deltaX;\n nodesYCoordinates[nnode] = nodesYCoordinates[0];\n for (let nodeIndexY = 1; nodeIndexY < totalNodesY; nodeIndexY++) {\n nodesXCoordinates[nnode + nodeIndexY] = nodesXCoordinates[nnode];\n nodesYCoordinates[nnode + nodeIndexY] = nodesYCoordinates[nnode] + nodeIndexY * deltaY;\n }\n }\n } else if (this.elementOrder === \"quadratic\") {\n totalNodesX = 2 * this.numElementsX + 1;\n totalNodesY = 2 * this.numElementsY + 1;\n deltaX = (this.maxX - xStart) / this.numElementsX;\n deltaY = (this.maxY - yStart) / this.numElementsY;\n\n nodesXCoordinates[0] = xStart;\n nodesYCoordinates[0] = yStart;\n for (let nodeIndexY = 1; nodeIndexY < totalNodesY; nodeIndexY++) {\n nodesXCoordinates[nodeIndexY] = nodesXCoordinates[0];\n nodesYCoordinates[nodeIndexY] = nodesYCoordinates[0] + (nodeIndexY * deltaY) / 2;\n }\n for (let nodeIndexX = 1; nodeIndexX < totalNodesX; nodeIndexX++) {\n const nnode = nodeIndexX * totalNodesY;\n nodesXCoordinates[nnode] = nodesXCoordinates[0] + (nodeIndexX * deltaX) / 2;\n nodesYCoordinates[nnode] = nodesYCoordinates[0];\n for (let nodeIndexY = 1; nodeIndexY < totalNodesY; nodeIndexY++) {\n nodesXCoordinates[nnode + nodeIndexY] = nodesXCoordinates[nnode];\n nodesYCoordinates[nnode + nodeIndexY] = nodesYCoordinates[nnode] + (nodeIndexY * deltaY) / 2;\n }\n }\n }\n // Generate nodal numbering (NOP) array\n const nodalNumbering = this.generateNodalNumbering(\n this.numElementsX,\n this.numElementsY,\n totalNodesX,\n totalNodesY,\n this.elementOrder\n );\n // Find boundary elements\n const boundaryElements = this.findBoundaryElements();\n\n debugLog(\"Generated node X coordinates: \" + JSON.stringify(nodesXCoordinates));\n debugLog(\"Generated node Y coordinates: \" + JSON.stringify(nodesYCoordinates));\n\n // Return x and y coordinates of nodes, total nodes, NOP array, and boundary elements\n return {\n nodesXCoordinates,\n nodesYCoordinates,\n totalNodesX,\n totalNodesY,\n nodalNumbering,\n boundaryElements,\n };\n }\n }\n\n /**\n * Function to find the elements that belong to each boundary of a domain\n * @returns {array} An array containing arrays of elements and their adjacent boundary side for each boundary\n * Each element in the array is of the form [elementIndex, side], where 'side' indicates which side\n * of the reference element is in contact with the physical boundary:\n *\n * For 1D domains (line segments):\n * 0 - Left node of reference element (maps to physical left endpoint)\n * 1 - Right node of reference element (maps to physical right endpoint)\n *\n * For 2D domains (rectangular):\n * 0 - Bottom side of reference element (maps to physical bottom boundary)\n * 1 - Left side of reference element (maps to physical left boundary)\n * 2 - Top side of reference element (maps to physical top boundary)\n * 3 - Right side of reference element (maps to physical right boundary)\n */\n findBoundaryElements() {\n const boundaryElements = [];\n const maxSides = this.meshDimension === \"1D\" ? 2 : 4; // Number of element sides based on mesh dimension\n for (let sideIndex = 0; sideIndex < maxSides; sideIndex++) {\n boundaryElements.push([]);\n }\n\n if (this.meshDimension === \"1D\") {\n // Left boundary (element 0, side 0)\n boundaryElements[0].push([0, 0]);\n\n // Right boundary (last element, side 1)\n boundaryElements[1].push([this.numElementsX - 1, 1]);\n } else if (this.meshDimension === \"2D\") {\n for (let elementIndexX = 0; elementIndexX < this.numElementsX; elementIndexX++) {\n for (let elementIndexY = 0; elementIndexY < this.numElementsY; elementIndexY++) {\n const elementIndex = elementIndexX * this.numElementsY + elementIndexY;\n\n // Bottom boundary\n if (elementIndexY === 0) {\n boundaryElements[0].push([elementIndex, 0]);\n }\n\n // Left boundary\n if (elementIndexX === 0) {\n boundaryElements[1].push([elementIndex, 1]);\n }\n\n // Top boundary\n if (elementIndexY === this.numElementsY - 1) {\n boundaryElements[2].push([elementIndex, 2]);\n }\n\n // Right boundary\n if (elementIndexX === this.numElementsX - 1) {\n boundaryElements[3].push([elementIndex, 3]);\n }\n }\n }\n }\n\n debugLog(\"Identified boundary elements by side: \" + JSON.stringify(boundaryElements));\n return boundaryElements;\n }\n\n /**\n * Function to generate the nodal numbering (NOP) array for a structured mesh\n * This array represents the connectivity between elements and their corresponding nodes\n * @param {number} numElementsX - Number of elements along the x-axis\n * @param {number} [numElementsY] - Number of elements along the y-axis (optional for 1D)\n * @param {number} totalNodesX - Total number of nodes along the x-axis\n * @param {number} [totalNodesY] - Total number of nodes along the y-axis (optional for 1D)\n * @param {string} elementOrder - The order of elements, either 'linear' or 'quadratic'\n * @returns {array} NOP - A two-dimensional array which represents the element-to-node connectivity for the entire mesh\n */\n generateNodalNumbering(numElementsX, numElementsY, totalNodesX, totalNodesY, elementOrder) {\n let elementIndex = 0;\n let nop = [];\n\n if (this.meshDimension === \"1D\") {\n if (elementOrder === \"linear\") {\n /**\n * Linear 1D elements with the following nodes representation:\n *\n * 1 --- 2\n *\n */\n for (let elementIndex = 0; elementIndex < numElementsX; elementIndex++) {\n nop[elementIndex] = [];\n for (let nodeIndex = 1; nodeIndex <= 2; nodeIndex++) {\n nop[elementIndex][nodeIndex - 1] = elementIndex + nodeIndex;\n }\n }\n } else if (elementOrder === \"quadratic\") {\n /**\n * Quadratic 1D elements with the following nodes representation:\n *\n * 1--2--3\n *\n */\n let columnCounter = 0;\n for (let elementIndex = 0; elementIndex < numElementsX; elementIndex++) {\n nop[elementIndex] = [];\n for (let nodeIndex = 1; nodeIndex <= 3; nodeIndex++) {\n nop[elementIndex][nodeIndex - 1] = elementIndex + nodeIndex + columnCounter;\n }\n columnCounter += 1;\n }\n }\n } else if (this.meshDimension === \"2D\") {\n if (elementOrder === \"linear\") {\n /**\n * Linear rectangular elements with the following nodes representation:\n *\n * 1 --- 3\n * | |\n * 0 --- 2\n *\n */\n let rowCounter = 0;\n let columnCounter = 2;\n for (let elementIndex = 0; elementIndex < numElementsX * numElementsY; elementIndex++) {\n rowCounter += 1;\n nop[elementIndex] = [];\n nop[elementIndex][0] = elementIndex + columnCounter - 1;\n nop[elementIndex][1] = elementIndex + columnCounter;\n nop[elementIndex][2] = elementIndex + columnCounter + numElementsY;\n nop[elementIndex][3] = elementIndex + columnCounter + numElementsY + 1;\n if (rowCounter === numElementsY) {\n columnCounter += 1;\n rowCounter = 0;\n }\n }\n } else if (elementOrder === \"quadratic\") {\n /**\n * Quadratic rectangular elements with the following nodes representation:\n *\n * 2--5--8\n * | |\n * 1 4 7\n * | |\n * 0--3--6\n *\n */\n for (let elementIndexX = 1; elementIndexX <= numElementsX; elementIndexX++) {\n for (let elementIndexY = 1; elementIndexY <= numElementsY; elementIndexY++) {\n nop[elementIndex] = [];\n for (let nodeIndex1 = 1; nodeIndex1 <= 3; nodeIndex1++) {\n let nodeIndex2 = 3 * nodeIndex1 - 2;\n nop[elementIndex][nodeIndex2 - 1] =\n totalNodesY * (2 * elementIndexX + nodeIndex1 - 3) + 2 * elementIndexY - 1;\n nop[elementIndex][nodeIndex2] = nop[elementIndex][nodeIndex2 - 1] + 1;\n nop[elementIndex][nodeIndex2 + 1] = nop[elementIndex][nodeIndex2 - 1] + 2;\n }\n elementIndex = elementIndex + 1;\n }\n }\n }\n }\n\n return nop;\n }\n}\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\n// Internal imports\nimport { basicLog, debugLog, errorLog } from \"../utilities/loggingScript.js\";\n\n/**\n * Class to handle thermal boundary conditions application\n */\nexport class ThermalBoundaryConditions {\n /**\n * Constructor to initialize the ThermalBoundaryConditions class\n * @param {object} boundaryConditions - Object containing boundary conditions for the finite element analysis\n * @param {array} boundaryElements - Array containing elements that belong to each boundary\n * @param {array} nop - Nodal numbering (NOP) array representing the connectivity between elements and nodes\n * @param {string} meshDimension - The dimension of the mesh (e.g., \"2D\")\n * @param {string} elementOrder - The order of elements (e.g., \"linear\", \"quadratic\")\n */\n constructor(boundaryConditions, boundaryElements, nop, meshDimension, elementOrder) {\n this.boundaryConditions = boundaryConditions;\n this.boundaryElements = boundaryElements;\n this.nop = nop;\n this.meshDimension = meshDimension;\n this.elementOrder = elementOrder;\n }\n\n /**\n * Function to impose constant temperature boundary conditions (Dirichlet type)\n * @param {array} residualVector - The residual vector to be modified\n * @param {array} jacobianMatrix - The Jacobian matrix to be modified\n */\n imposeConstantTempBoundaryConditions(residualVector, jacobianMatrix) {\n basicLog(\"Applying constant temperature boundary conditions (Dirichlet type)\");\n if (this.meshDimension === \"1D\") {\n Object.keys(this.boundaryConditions).forEach((boundaryKey) => {\n if (this.boundaryConditions[boundaryKey][0] === \"constantTemp\") {\n const tempValue = this.boundaryConditions[boundaryKey][1];\n debugLog(\n `Boundary ${boundaryKey}: Applying constant temperature of ${tempValue} K (Dirichlet condition)`\n );\n this.boundaryElements[boundaryKey].forEach(([elementIndex, side]) => {\n if (this.elementOrder === \"linear\") {\n const boundarySides = {\n 0: [0], // Node at the left side of the reference element\n 1: [1], // Node at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied fixed temperature to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n // Set the residual vector to the ConstantTemp value\n residualVector[globalNodeIndex] = tempValue;\n // Set the Jacobian matrix row to zero\n for (let colIndex = 0; colIndex < residualVector.length; colIndex++) {\n jacobianMatrix[globalNodeIndex][colIndex] = 0;\n }\n // Set the diagonal entry of the Jacobian matrix to one\n jacobianMatrix[globalNodeIndex][globalNodeIndex] = 1;\n });\n } else if (this.elementOrder === \"quadratic\") {\n const boundarySides = {\n 0: [0], // Node at the left side of the reference element\n 2: [2], // Node at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied fixed temperature to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n // Set the residual vector to the ConstantTemp value\n residualVector[globalNodeIndex] = tempValue;\n // Set the Jacobian matrix row to zero\n for (let colIndex = 0; colIndex < residualVector.length; colIndex++) {\n jacobianMatrix[globalNodeIndex][colIndex] = 0;\n }\n // Set the diagonal entry of the Jacobian matrix to one\n jacobianMatrix[globalNodeIndex][globalNodeIndex] = 1;\n });\n }\n });\n }\n });\n } else if (this.meshDimension === \"2D\") {\n Object.keys(this.boundaryConditions).forEach((boundaryKey) => {\n if (this.boundaryConditions[boundaryKey][0] === \"constantTemp\") {\n const tempValue = this.boundaryConditions[boundaryKey][1];\n debugLog(\n `Boundary ${boundaryKey}: Applying constant temperature of ${tempValue} K (Dirichlet condition)`\n );\n this.boundaryElements[boundaryKey].forEach(([elementIndex, side]) => {\n if (this.elementOrder === \"linear\") {\n const boundarySides = {\n 0: [0, 2], // Nodes at the bottom side of the reference element\n 1: [0, 1], // Nodes at the left side of the reference element\n 2: [1, 3], // Nodes at the top side of the reference element\n 3: [2, 3], // Nodes at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied fixed temperature to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n // Set the residual vector to the ConstantTemp value\n residualVector[globalNodeIndex] = tempValue;\n // Set the Jacobian matrix row to zero\n for (let colIndex = 0; colIndex < residualVector.length; colIndex++) {\n jacobianMatrix[globalNodeIndex][colIndex] = 0;\n }\n // Set the diagonal entry of the Jacobian matrix to one\n jacobianMatrix[globalNodeIndex][globalNodeIndex] = 1;\n });\n } else if (this.elementOrder === \"quadratic\") {\n const boundarySides = {\n 0: [0, 3, 6], // Nodes at the bottom side of the reference element\n 1: [0, 1, 2], // Nodes at the left side of the reference element\n 2: [2, 5, 8], // Nodes at the top side of the reference element\n 3: [6, 7, 8], // Nodes at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied fixed temperature to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n // Set the residual vector to the ConstantTemp value\n residualVector[globalNodeIndex] = tempValue;\n // Set the Jacobian matrix row to zero\n for (let colIndex = 0; colIndex < residualVector.length; colIndex++) {\n jacobianMatrix[globalNodeIndex][colIndex] = 0;\n }\n // Set the diagonal entry of the Jacobian matrix to one\n jacobianMatrix[globalNodeIndex][globalNodeIndex] = 1;\n });\n }\n });\n }\n });\n }\n }\n\n /**\n * Function to impose convection boundary conditions (Robin type)\n * @param {array} residualVector - The residual vector to be modified\n * @param {array} jacobianMatrix - The Jacobian matrix to be modified\n * @param {array} gaussPoints - Array of Gauss points for numerical integration\n * @param {array} gaussWeights - Array of Gauss weights for numerical integration\n * @param {array} nodesXCoordinates - Array of x-coordinates of nodes\n * @param {array} nodesYCoordinates - Array of y-coordinates of nodes\n * @param {object} basisFunctionsData - Object containing basis functions and their derivatives\n */\n imposeConvectionBoundaryConditions(\n residualVector,\n jacobianMatrix,\n gaussPoints,\n gaussWeights,\n nodesXCoordinates,\n nodesYCoordinates,\n basisFunctionsData\n ) {\n basicLog(\"Applying convection boundary conditions (Robin type)\");\n // Extract convection parameters from boundary conditions\n let convectionHeatTranfCoeff = [];\n let convectionExtTemp = [];\n Object.keys(this.boundaryConditions).forEach((key) => {\n const boundaryCondition = this.boundaryConditions[key];\n if (boundaryCondition[0] === \"convection\") {\n convectionHeatTranfCoeff[key] = boundaryCondition[1];\n convectionExtTemp[key] = boundaryCondition[2];\n }\n });\n\n if (this.meshDimension === \"1D\") {\n Object.keys(this.boundaryConditions).forEach((boundaryKey) => {\n if (this.boundaryConditions[boundaryKey][0] === \"convection\") {\n const convectionCoeff = convectionHeatTranfCoeff[boundaryKey];\n const extTemp = convectionExtTemp[boundaryKey];\n debugLog(\n `Boundary ${boundaryKey}: Applying convection with heat transfer coefficient h=${convectionCoeff} W/(m²·K) and external temperature T∞=${extTemp} K`\n );\n this.boundaryElements[boundaryKey].forEach(([elementIndex, side]) => {\n let nodeIndex;\n if (this.elementOrder === \"linear\") {\n if (side === 0) {\n // Node at the left side of the reference element\n nodeIndex = 0;\n } else {\n // Node at the right side of the reference element\n nodeIndex = 1;\n }\n } else if (this.elementOrder === \"quadratic\") {\n if (side === 0) {\n // Node at the left side of the reference element\n nodeIndex = 0;\n } else {\n // Node at the right side of the reference element\n nodeIndex = 2;\n }\n }\n\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied convection boundary condition to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n residualVector[globalNodeIndex] += -convectionCoeff * extTemp;\n jacobianMatrix[globalNodeIndex][globalNodeIndex] += convectionCoeff;\n });\n }\n });\n } else if (this.meshDimension === \"2D\") {\n Object.keys(this.boundaryConditions).forEach((boundaryKey) => {\n if (this.boundaryConditions[boundaryKey][0] === \"convection\") {\n const convectionCoeff = convectionHeatTranfCoeff[boundaryKey];\n const extTemp = convectionExtTemp[boundaryKey];\n debugLog(\n `Boundary ${boundaryKey}: Applying convection with heat transfer coefficient h=${convectionCoeff} W/(m²·K) and external temperature T∞=${extTemp} K`\n );\n this.boundaryElements[boundaryKey].forEach(([elementIndex, side]) => {\n if (this.elementOrder === \"linear\") {\n let gaussPoint1, gaussPoint2, firstNodeIndex, lastNodeIndex, nodeIncrement;\n if (side === 0) {\n // Nodes at the bottom side of the reference element\n gaussPoint1 = gaussPoints[0];\n gaussPoint2 = 0;\n firstNodeIndex = 0;\n lastNodeIndex = 3;\n nodeIncrement = 2;\n } else if (side === 1) {\n // Nodes at the left side of the reference element\n gaussPoint1 = 0;\n gaussPoint2 = gaussPoints[0];\n firstNodeIndex = 0;\n lastNodeIndex = 2;\n nodeIncrement = 1;\n } else if (side === 2) {\n // Nodes at the top side of the reference element\n gaussPoint1 = gaussPoints[0];\n gaussPoint2 = 1;\n firstNodeIndex = 1;\n lastNodeIndex = 4;\n nodeIncrement = 2;\n } else if (side === 3) {\n // Nodes at the right side of the reference element\n gaussPoint1 = 1;\n gaussPoint2 = gaussPoints[0];\n firstNodeIndex = 2;\n lastNodeIndex = 4;\n nodeIncrement = 1;\n }\n\n let basisFunctionsAndDerivatives = basisFunctionsData.getBasisFunctions(\n gaussPoint1,\n gaussPoint2\n );\n let basisFunction = basisFunctionsAndDerivatives.basisFunction;\n let basisFunctionDerivKsi = basisFunctionsAndDerivatives.basisFunctionDerivKsi;\n let basisFunctionDerivEta = basisFunctionsAndDerivatives.basisFunctionDerivEta;\n\n let ksiDerivX = 0;\n let ksiDerivY = 0;\n let etaDerivX = 0;\n let etaDerivY = 0;\n const numNodes = this.nop[elementIndex].length;\n for (let nodeIndex = 0; nodeIndex < numNodes; nodeIndex++) {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n\n // For boundaries along Ksi (horizontal), use Ksi derivatives\n if (side === 0 || side === 2) {\n ksiDerivX += nodesXCoordinates[globalNodeIndex] * basisFunctionDerivKsi[nodeIndex];\n ksiDerivY += nodesYCoordinates[globalNodeIndex] * basisFunctionDerivKsi[nodeIndex];\n }\n // For boundaries along Eta (vertical), use Eta derivatives\n else if (side === 1 || side === 3) {\n etaDerivX += nodesXCoordinates[globalNodeIndex] * basisFunctionDerivEta[nodeIndex];\n etaDerivY += nodesYCoordinates[globalNodeIndex] * basisFunctionDerivEta[nodeIndex];\n }\n }\n\n // Compute the length of tangent vector\n const tangentVectorLength =\n side === 0 || side === 2\n ? Math.sqrt(ksiDerivX ** 2 + ksiDerivY ** 2)\n : Math.sqrt(etaDerivX ** 2 + etaDerivY ** 2);\n\n for (\n let localNodeIndex = firstNodeIndex;\n localNodeIndex < lastNodeIndex;\n localNodeIndex += nodeIncrement\n ) {\n let globalNodeIndex = this.nop[elementIndex][localNodeIndex] - 1;\n debugLog(\n ` - Applied convection boundary condition to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${localNodeIndex + 1})`\n );\n\n // Apply boundary condition with proper Jacobian for all sides\n residualVector[globalNodeIndex] +=\n -gaussWeights[0] * tangentVectorLength * basisFunction[localNodeIndex] * convectionCoeff * extTemp;\n\n for (\n let localNodeIndex2 = firstNodeIndex;\n localNodeIndex2 < lastNodeIndex;\n localNodeIndex2 += nodeIncrement\n ) {\n let globalNodeIndex2 = this.nop[elementIndex][localNodeIndex2] - 1;\n jacobianMatrix[globalNodeIndex][globalNodeIndex2] +=\n -gaussWeights[0] *\n tangentVectorLength *\n basisFunction[localNodeIndex] *\n basisFunction[localNodeIndex2] *\n convectionCoeff;\n }\n }\n } else if (this.elementOrder === \"quadratic\") {\n for (let gaussPointIndex = 0; gaussPointIndex < 3; gaussPointIndex++) {\n let gaussPoint1, gaussPoint2, firstNodeIndex, lastNodeIndex, nodeIncrement;\n if (side === 0) {\n // Nodes at the bottom side of the reference element\n gaussPoint1 = gaussPoints[gaussPointIndex];\n gaussPoint2 = 0;\n firstNodeIndex = 0;\n lastNodeIndex = 7;\n nodeIncrement = 3;\n } else if (side === 1) {\n // Nodes at the left side of the reference element\n gaussPoint1 = 0;\n gaussPoint2 = gaussPoints[gaussPointIndex];\n firstNodeIndex = 0;\n lastNodeIndex = 3;\n nodeIncrement = 1;\n } else if (side === 2) {\n // Nodes at the top side of the reference element\n gaussPoint1 = gaussPoints[gaussPointIndex];\n gaussPoint2 = 1;\n firstNodeIndex = 2;\n lastNodeIndex = 9;\n nodeIncrement = 3;\n } else if (side === 3) {\n // Nodes at the right side of the reference element\n gaussPoint1 = 1;\n gaussPoint2 = gaussPoints[gaussPointIndex];\n firstNodeIndex = 6;\n lastNodeIndex = 9;\n nodeIncrement = 1;\n }\n let basisFunctionsAndDerivatives = basisFunctionsData.getBasisFunctions(\n gaussPoint1,\n gaussPoint2\n );\n let basisFunction = basisFunctionsAndDerivatives.basisFunction;\n let basisFunctionDerivKsi = basisFunctionsAndDerivatives.basisFunctionDerivKsi;\n let basisFunctionDerivEta = basisFunctionsAndDerivatives.basisFunctionDerivEta;\n\n let ksiDerivX = 0;\n let ksiDerivY = 0;\n let etaDerivX = 0;\n let etaDerivY = 0;\n const numNodes = this.nop[elementIndex].length;\n for (let nodeIndex = 0; nodeIndex < numNodes; nodeIndex++) {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n\n // For boundaries along Ksi (horizontal), use Ksi derivatives\n if (side === 0 || side === 2) {\n ksiDerivX += nodesXCoordinates[globalNodeIndex] * basisFunctionDerivKsi[nodeIndex];\n ksiDerivY += nodesYCoordinates[globalNodeIndex] * basisFunctionDerivKsi[nodeIndex];\n }\n // For boundaries along Eta (vertical), use Eta derivatives\n else if (side === 1 || side === 3) {\n etaDerivX += nodesXCoordinates[globalNodeIndex] * basisFunctionDerivEta[nodeIndex];\n etaDerivY += nodesYCoordinates[globalNodeIndex] * basisFunctionDerivEta[nodeIndex];\n }\n }\n\n // Compute the length of tangent vector\n const tangentVectorLength =\n side === 0 || side === 2\n ? Math.sqrt(ksiDerivX ** 2 + ksiDerivY ** 2)\n : Math.sqrt(etaDerivX ** 2 + etaDerivY ** 2);\n\n for (\n let localNodeIndex = firstNodeIndex;\n localNodeIndex < lastNodeIndex;\n localNodeIndex += nodeIncrement\n ) {\n let globalNodeIndex = this.nop[elementIndex][localNodeIndex] - 1;\n debugLog(\n ` - Applied convection boundary condition to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${localNodeIndex + 1})`\n );\n\n // Apply boundary condition with proper Jacobian for all sides\n residualVector[globalNodeIndex] +=\n -gaussWeights[gaussPointIndex] *\n tangentVectorLength *\n basisFunction[localNodeIndex] *\n convectionCoeff *\n extTemp;\n\n for (\n let localNodeIndex2 = firstNodeIndex;\n localNodeIndex2 < lastNodeIndex;\n localNodeIndex2 += nodeIncrement\n ) {\n let globalNodeIndex2 = this.nop[elementIndex][localNodeIndex2] - 1;\n jacobianMatrix[globalNodeIndex][globalNodeIndex2] +=\n -gaussWeights[gaussPointIndex] *\n tangentVectorLength *\n basisFunction[localNodeIndex] *\n basisFunction[localNodeIndex2] *\n convectionCoeff;\n }\n }\n }\n }\n });\n }\n });\n }\n }\n}\n","/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\nconst proxyMarker = Symbol(\"Comlink.proxy\");\nconst createEndpoint = Symbol(\"Comlink.endpoint\");\nconst releaseProxy = Symbol(\"Comlink.releaseProxy\");\nconst finalizer = Symbol(\"Comlink.finalizer\");\nconst throwMarker = Symbol(\"Comlink.thrown\");\nconst isObject = (val) => (typeof val === \"object\" && val !== null) || typeof val === \"function\";\n/**\n * Internal transfer handle to handle objects marked to proxy.\n */\nconst proxyTransferHandler = {\n canHandle: (val) => isObject(val) && val[proxyMarker],\n serialize(obj) {\n const { port1, port2 } = new MessageChannel();\n expose(obj, port1);\n return [port2, [port2]];\n },\n deserialize(port) {\n port.start();\n return wrap(port);\n },\n};\n/**\n * Internal transfer handler to handle thrown exceptions.\n */\nconst throwTransferHandler = {\n canHandle: (value) => isObject(value) && throwMarker in value,\n serialize({ value }) {\n let serialized;\n if (value instanceof Error) {\n serialized = {\n isError: true,\n value: {\n message: value.message,\n name: value.name,\n stack: value.stack,\n },\n };\n }\n else {\n serialized = { isError: false, value };\n }\n return [serialized, []];\n },\n deserialize(serialized) {\n if (serialized.isError) {\n throw Object.assign(new Error(serialized.value.message), serialized.value);\n }\n throw serialized.value;\n },\n};\n/**\n * Allows customizing the serialization of certain values.\n */\nconst transferHandlers = new Map([\n [\"proxy\", proxyTransferHandler],\n [\"throw\", throwTransferHandler],\n]);\nfunction isAllowedOrigin(allowedOrigins, origin) {\n for (const allowedOrigin of allowedOrigins) {\n if (origin === allowedOrigin || allowedOrigin === \"*\") {\n return true;\n }\n if (allowedOrigin instanceof RegExp && allowedOrigin.test(origin)) {\n return true;\n }\n }\n return false;\n}\nfunction expose(obj, ep = globalThis, allowedOrigins = [\"*\"]) {\n ep.addEventListener(\"message\", function callback(ev) {\n if (!ev || !ev.data) {\n return;\n }\n if (!isAllowedOrigin(allowedOrigins, ev.origin)) {\n console.warn(`Invalid origin '${ev.origin}' for comlink proxy`);\n return;\n }\n const { id, type, path } = Object.assign({ path: [] }, ev.data);\n const argumentList = (ev.data.argumentList || []).map(fromWireValue);\n let returnValue;\n try {\n const parent = path.slice(0, -1).reduce((obj, prop) => obj[prop], obj);\n const rawValue = path.reduce((obj, prop) => obj[prop], obj);\n switch (type) {\n case \"GET\" /* MessageType.GET */:\n {\n returnValue = rawValue;\n }\n break;\n case \"SET\" /* MessageType.SET */:\n {\n parent[path.slice(-1)[0]] = fromWireValue(ev.data.value);\n returnValue = true;\n }\n break;\n case \"APPLY\" /* MessageType.APPLY */:\n {\n returnValue = rawValue.apply(parent, argumentList);\n }\n break;\n case \"CONSTRUCT\" /* MessageType.CONSTRUCT */:\n {\n const value = new rawValue(...argumentList);\n returnValue = proxy(value);\n }\n break;\n case \"ENDPOINT\" /* MessageType.ENDPOINT */:\n {\n const { port1, port2 } = new MessageChannel();\n expose(obj, port2);\n returnValue = transfer(port1, [port1]);\n }\n break;\n case \"RELEASE\" /* MessageType.RELEASE */:\n {\n returnValue = undefined;\n }\n break;\n default:\n return;\n }\n }\n catch (value) {\n returnValue = { value, [throwMarker]: 0 };\n }\n Promise.resolve(returnValue)\n .catch((value) => {\n return { value, [throwMarker]: 0 };\n })\n .then((returnValue) => {\n const [wireValue, transferables] = toWireValue(returnValue);\n ep.postMessage(Object.assign(Object.assign({}, wireValue), { id }), transferables);\n if (type === \"RELEASE\" /* MessageType.RELEASE */) {\n // detach and deactive after sending release response above.\n ep.removeEventListener(\"message\", callback);\n closeEndPoint(ep);\n if (finalizer in obj && typeof obj[finalizer] === \"function\") {\n obj[finalizer]();\n }\n }\n })\n .catch((error) => {\n // Send Serialization Error To Caller\n const [wireValue, transferables] = toWireValue({\n value: new TypeError(\"Unserializable return value\"),\n [throwMarker]: 0,\n });\n ep.postMessage(Object.assign(Object.assign({}, wireValue), { id }), transferables);\n });\n });\n if (ep.start) {\n ep.start();\n }\n}\nfunction isMessagePort(endpoint) {\n return endpoint.constructor.name === \"MessagePort\";\n}\nfunction closeEndPoint(endpoint) {\n if (isMessagePort(endpoint))\n endpoint.close();\n}\nfunction wrap(ep, target) {\n const pendingListeners = new Map();\n ep.addEventListener(\"message\", function handleMessage(ev) {\n const { data } = ev;\n if (!data || !data.id) {\n return;\n }\n const resolver = pendingListeners.get(data.id);\n if (!resolver) {\n return;\n }\n try {\n resolver(data);\n }\n finally {\n pendingListeners.delete(data.id);\n }\n });\n return createProxy(ep, pendingListeners, [], target);\n}\nfunction throwIfProxyReleased(isReleased) {\n if (isReleased) {\n throw new Error(\"Proxy has been released and is not useable\");\n }\n}\nfunction releaseEndpoint(ep) {\n return requestResponseMessage(ep, new Map(), {\n type: \"RELEASE\" /* MessageType.RELEASE */,\n }).then(() => {\n closeEndPoint(ep);\n });\n}\nconst proxyCounter = new WeakMap();\nconst proxyFinalizers = \"FinalizationRegistry\" in globalThis &&\n new FinalizationRegistry((ep) => {\n const newCount = (proxyCounter.get(ep) || 0) - 1;\n proxyCounter.set(ep, newCount);\n if (newCount === 0) {\n releaseEndpoint(ep);\n }\n });\nfunction registerProxy(proxy, ep) {\n const newCount = (proxyCounter.get(ep) || 0) + 1;\n proxyCounter.set(ep, newCount);\n if (proxyFinalizers) {\n proxyFinalizers.register(proxy, ep, proxy);\n }\n}\nfunction unregisterProxy(proxy) {\n if (proxyFinalizers) {\n proxyFinalizers.unregister(proxy);\n }\n}\nfunction createProxy(ep, pendingListeners, path = [], target = function () { }) {\n let isProxyReleased = false;\n const proxy = new Proxy(target, {\n get(_target, prop) {\n throwIfProxyReleased(isProxyReleased);\n if (prop === releaseProxy) {\n return () => {\n unregisterProxy(proxy);\n releaseEndpoint(ep);\n pendingListeners.clear();\n isProxyReleased = true;\n };\n }\n if (prop === \"then\") {\n if (path.length === 0) {\n return { then: () => proxy };\n }\n const r = requestResponseMessage(ep, pendingListeners, {\n type: \"GET\" /* MessageType.GET */,\n path: path.map((p) => p.toString()),\n }).then(fromWireValue);\n return r.then.bind(r);\n }\n return createProxy(ep, pendingListeners, [...path, prop]);\n },\n set(_target, prop, rawValue) {\n throwIfProxyReleased(isProxyReleased);\n // FIXME: ES6 Proxy Handler `set` methods are supposed to return a\n // boolean. To show good will, we return true asynchronously ¯\\_(ツ)_/¯\n const [value, transferables] = toWireValue(rawValue);\n return requestResponseMessage(ep, pendingListeners, {\n type: \"SET\" /* MessageType.SET */,\n path: [...path, prop].map((p) => p.toString()),\n value,\n }, transferables).then(fromWireValue);\n },\n apply(_target, _thisArg, rawArgumentList) {\n throwIfProxyReleased(isProxyReleased);\n const last = path[path.length - 1];\n if (last === createEndpoint) {\n return requestResponseMessage(ep, pendingListeners, {\n type: \"ENDPOINT\" /* MessageType.ENDPOINT */,\n }).then(fromWireValue);\n }\n // We just pretend that `bind()` didn’t happen.\n if (last === \"bind\") {\n return createProxy(ep, pendingListeners, path.slice(0, -1));\n }\n const [argumentList, transferables] = processArguments(rawArgumentList);\n return requestResponseMessage(ep, pendingListeners, {\n type: \"APPLY\" /* MessageType.APPLY */,\n path: path.map((p) => p.toString()),\n argumentList,\n }, transferables).then(fromWireValue);\n },\n construct(_target, rawArgumentList) {\n throwIfProxyReleased(isProxyReleased);\n const [argumentList, transferables] = processArguments(rawArgumentList);\n return requestResponseMessage(ep, pendingListeners, {\n type: \"CONSTRUCT\" /* MessageType.CONSTRUCT */,\n path: path.map((p) => p.toString()),\n argumentList,\n }, transferables).then(fromWireValue);\n },\n });\n registerProxy(proxy, ep);\n return proxy;\n}\nfunction myFlat(arr) {\n return Array.prototype.concat.apply([], arr);\n}\nfunction processArguments(argumentList) {\n const processed = argumentList.map(toWireValue);\n return [processed.map((v) => v[0]), myFlat(processed.map((v) => v[1]))];\n}\nconst transferCache = new WeakMap();\nfunction transfer(obj, transfers) {\n transferCache.set(obj, transfers);\n return obj;\n}\nfunction proxy(obj) {\n return Object.assign(obj, { [proxyMarker]: true });\n}\nfunction windowEndpoint(w, context = globalThis, targetOrigin = \"*\") {\n return {\n postMessage: (msg, transferables) => w.postMessage(msg, targetOrigin, transferables),\n addEventListener: context.addEventListener.bind(context),\n removeEventListener: context.removeEventListener.bind(context),\n };\n}\nfunction toWireValue(value) {\n for (const [name, handler] of transferHandlers) {\n if (handler.canHandle(value)) {\n const [serializedValue, transferables] = handler.serialize(value);\n return [\n {\n type: \"HANDLER\" /* WireValueType.HANDLER */,\n name,\n value: serializedValue,\n },\n transferables,\n ];\n }\n }\n return [\n {\n type: \"RAW\" /* WireValueType.RAW */,\n value,\n },\n transferCache.get(value) || [],\n ];\n}\nfunction fromWireValue(value) {\n switch (value.type) {\n case \"HANDLER\" /* WireValueType.HANDLER */:\n return transferHandlers.get(value.name).deserialize(value.value);\n case \"RAW\" /* WireValueType.RAW */:\n return value.value;\n }\n}\nfunction requestResponseMessage(ep, pendingListeners, msg, transfers) {\n return new Promise((resolve) => {\n const id = generateUUID();\n pendingListeners.set(id, resolve);\n if (ep.start) {\n ep.start();\n }\n ep.postMessage(Object.assign({ id }, msg), transfers);\n });\n}\nfunction generateUUID() {\n return new Array(4)\n .fill(0)\n .map(() => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16))\n .join(\"-\");\n}\n\nexport { createEndpoint, expose, finalizer, proxy, proxyMarker, releaseProxy, transfer, transferHandlers, windowEndpoint, wrap };\n//# sourceMappingURL=comlink.mjs.map\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\n// Internal imports\nimport { jacobiMethod } from \"./methods/jacobiMethodScript.js\";\nimport { assembleSolidHeatTransferMat } from \"./solvers/solidHeatTransferScript.js\";\nimport { basicLog, debugLog, errorLog } from \"./utilities/loggingScript.js\";\n\n/**\n * Class to implement finite element analysis in JavaScript\n * @param {string} solverConfig - Parameter specifying the type of solver\n * @param {object} meshConfig - Object containing computational mesh details\n * @param {object} boundaryConditions - Object containing boundary conditions for the finite element analysis\n * @returns {object} An object containing the solution vector and additional mesh information\n */\nexport class FEAScriptModel {\n constructor() {\n this.solverConfig = null;\n this.meshConfig = {};\n this.boundaryConditions = {};\n this.solverMethod = \"lusolve\"; // Default solver method\n basicLog(\"FEAScriptModel instance created\");\n }\n\n setSolverConfig(solverConfig) {\n this.solverConfig = solverConfig;\n debugLog(`Solver config set to: ${solverConfig}`);\n }\n\n setMeshConfig(meshConfig) {\n this.meshConfig = meshConfig;\n debugLog(\n `Mesh config set with dimensions: ${meshConfig.meshDimension}`\n );\n }\n\n addBoundaryCondition(boundaryKey, condition) {\n this.boundaryConditions[boundaryKey] = condition;\n debugLog(`Boundary condition added for boundary: ${boundaryKey}, type: ${condition[0]}`);\n }\n\n setSolverMethod(solverMethod) {\n this.solverMethod = solverMethod;\n debugLog(`Solver method set to: ${solverMethod}`);\n }\n\n solve() {\n if (!this.solverConfig || !this.meshConfig || !this.boundaryConditions) {\n const error = \"Solver config, mesh config, and boundary conditions must be set before solving.\";\n console.error(error);\n throw new Error(error);\n }\n\n let jacobianMatrix = [];\n let residualVector = [];\n let solutionVector = [];\n let nodesCoordinates = {};\n\n // Assembly matrices\n basicLog(\"Beginning matrix assembly...\");\n console.time(\"assemblyMatrices\");\n if (this.solverConfig === \"solidHeatTransferScript\") {\n basicLog(`Using solver: ${this.solverConfig}`);\n ({ jacobianMatrix, residualVector, nodesCoordinates } = assembleSolidHeatTransferMat(\n this.meshConfig,\n this.boundaryConditions\n ));\n }\n console.timeEnd(\"assemblyMatrices\");\n basicLog(\"Matrix assembly completed\");\n\n // System solving\n basicLog(`Solving system using ${this.solverMethod}...`);\n console.time(\"systemSolving\");\n if (this.solverMethod === \"lusolve\") {\n solutionVector = math.lusolve(jacobianMatrix, residualVector);\n } else if (this.solverMethod === \"jacobi\") {\n // Create initial guess of zeros\n const initialGuess = new Array(residualVector.length).fill(0);\n // Call Jacobi method with desired max iterations and tolerance\n const jacobiResult = jacobiMethod(jacobianMatrix, residualVector, initialGuess, 1000, 1e-6);\n\n // Log convergence information\n if (jacobiResult.converged) {\n debugLog(`Jacobi method converged in ${jacobiResult.iterations} iterations`);\n } else {\n debugLog(`Jacobi method did not converge after ${jacobiResult.iterations} iterations`);\n }\n\n solutionVector = jacobiResult.solution;\n }\n console.timeEnd(\"systemSolving\");\n basicLog(\"System solved successfully\");\n\n return { solutionVector, nodesCoordinates };\n }\n}\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\n// Internal imports\nimport { numericalIntegration } from \"../methods/numericalIntegrationScript.js\";\nimport { basisFunctions } from \"../mesh/basisFunctionsScript.js\";\nimport { meshGeneration } from \"../mesh/meshGenerationScript.js\";\nimport { ThermalBoundaryConditions } from \"./thermalBoundaryConditionsScript.js\";\nimport { basicLog, debugLog, errorLog } from \"../utilities/loggingScript.js\";\n\n/**\n * Function to assemble the solid heat transfer matrix\n * @param {object} meshConfig - Object containing computational mesh details\n * @param {object} boundaryConditions - Object containing boundary conditions for the finite element analysis\n * @returns {object} An object containing:\n * - jacobianMatrix: The assembled Jacobian matrix\n * - residualVector: The assembled residual vector\n * - nodesCoordinates: Object containing x and y coordinates of nodes\n */\nexport function assembleSolidHeatTransferMat(meshConfig, boundaryConditions) {\n basicLog(\"Starting solid heat transfer matrix assembly...\");\n\n // Extract mesh details from the configuration object\n const {\n meshDimension, // The dimension of the mesh\n numElementsX, // Number of elements in x-direction\n numElementsY, // Number of elements in y-direction (only for 2D)\n maxX, // Max x-coordinate (m) of the domain\n maxY, // Max y-coordinate (m) of the domain (only for 2D)\n elementOrder, // The order of elements\n parsedMesh, // The pre-parsed mesh data (if available)\n } = meshConfig;\n\n // Create a new instance of the meshGeneration class\n debugLog(\"Generating mesh...\");\n const meshGenerationData = new meshGeneration({\n numElementsX,\n numElementsY,\n maxX,\n maxY,\n meshDimension,\n elementOrder,\n parsedMesh, // Pass the parsed mesh to the mesh generator\n });\n\n // Generate the mesh\n const nodesCoordinatesAndNumbering = meshGenerationData.generateMesh();\n\n // Extract nodes coordinates and nodal numbering (NOP) from the mesh data\n let nodesXCoordinates = nodesCoordinatesAndNumbering.nodesXCoordinates;\n let nodesYCoordinates = nodesCoordinatesAndNumbering.nodesYCoordinates;\n let totalNodesX = nodesCoordinatesAndNumbering.totalNodesX;\n let totalNodesY = nodesCoordinatesAndNumbering.totalNodesY;\n let nop = nodesCoordinatesAndNumbering.nodalNumbering;\n let boundaryElements = nodesCoordinatesAndNumbering.boundaryElements;\n\n // Check the mesh type\n const isParsedMesh = parsedMesh !== undefined && parsedMesh !== null;\n\n // Calculate totalElements and totalNodes based on mesh type\n let totalElements, totalNodes;\n\n if (isParsedMesh) {\n totalElements = nop.length; // Number of elements is the length of the nodal numbering array\n totalNodes = nodesXCoordinates.length; // Number of nodes is the length of the coordinates array\n\n // Debug log for mesh size\n debugLog(`Using parsed mesh with ${totalElements} elements and ${totalNodes} nodes`);\n } else {\n // For structured mesh, calculate based on dimensions\n totalElements = numElementsX * (meshDimension === \"2D\" ? numElementsY : 1);\n totalNodes = totalNodesX * (meshDimension === \"2D\" ? totalNodesY : 1);\n // Debug log for mesh size\n debugLog(`Using mesh generated from geometry with ${totalElements} elements and ${totalNodes} nodes`);\n }\n\n // Initialize variables for matrix assembly\n let localToGlobalMap = []; // Maps local element node indices to global mesh node indices\n let gaussPoints = []; // Gauss points\n let gaussWeights = []; // Gauss weights\n let basisFunction = []; // Basis functions\n let basisFunctionDerivKsi = []; // Derivatives of basis functions with respect to ksi\n let basisFunctionDerivEta = []; // Derivatives of basis functions with respect to eta (only for 2D)\n let basisFunctionDerivX = []; // The x-derivative of the basis function\n let basisFunctionDerivY = []; // The y-derivative of the basis function (only for 2D)\n let residualVector = []; // Galerkin residuals\n let jacobianMatrix = []; // Jacobian matrix\n let xCoordinates; // x-coordinate (physical coordinates)\n let yCoordinates; // y-coordinate (physical coordinates) (only for 2D)\n let ksiDerivX; // ksi-derivative of xCoordinates\n let etaDerivX; // eta-derivative of xCoordinates (ksi and eta are natural coordinates that vary within a reference element) (only for 2D)\n let ksiDerivY; // ksi-derivative of yCoordinates (only for 2D)\n let etaDerivY; // eta-derivative of yCoordinates (only for 2D)\n let detJacobian; // The jacobian of the isoparametric mapping\n\n // Initialize jacobianMatrix and residualVector arrays\n for (let nodeIndex = 0; nodeIndex < totalNodes; nodeIndex++) {\n residualVector[nodeIndex] = 0;\n jacobianMatrix.push([]);\n for (let colIndex = 0; colIndex < totalNodes; colIndex++) {\n jacobianMatrix[nodeIndex][colIndex] = 0;\n }\n }\n\n // Initialize the basisFunctions class\n const basisFunctionsData = new basisFunctions({\n meshDimension,\n elementOrder,\n });\n\n // Initialize the numericalIntegration class\n const numIntegrationData = new numericalIntegration({\n meshDimension,\n elementOrder,\n });\n\n // Calculate Gauss points and weights\n let gaussPointsAndWeights = numIntegrationData.getGaussPointsAndWeights();\n gaussPoints = gaussPointsAndWeights.gaussPoints;\n gaussWeights = gaussPointsAndWeights.gaussWeights;\n\n // Determine the number of nodes in the reference element based on the first element in the nop array\n const numNodes = nop[0].length;\n\n // Matrix assembly\n for (let elementIndex = 0; elementIndex < totalElements; elementIndex++) {\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n // Subtract 1 from nop in order to start numbering from 0\n localToGlobalMap[localNodeIndex] = nop[elementIndex][localNodeIndex] - 1;\n }\n\n // Loop over Gauss points\n for (let gaussPointIndex1 = 0; gaussPointIndex1 < gaussPoints.length; gaussPointIndex1++) {\n // 1D solid heat transfer\n if (meshDimension === \"1D\") {\n let basisFunctionsAndDerivatives = basisFunctionsData.getBasisFunctions(\n gaussPoints[gaussPointIndex1]\n );\n basisFunction = basisFunctionsAndDerivatives.basisFunction;\n basisFunctionDerivKsi = basisFunctionsAndDerivatives.basisFunctionDerivKsi;\n xCoordinates = 0;\n ksiDerivX = 0;\n detJacobian = 0;\n\n // Isoparametric mapping\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n xCoordinates += nodesXCoordinates[localToGlobalMap[localNodeIndex]] * basisFunction[localNodeIndex];\n ksiDerivX +=\n nodesXCoordinates[localToGlobalMap[localNodeIndex]] * basisFunctionDerivKsi[localNodeIndex];\n detJacobian = ksiDerivX;\n }\n\n // Compute x-derivative of basis functions\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n basisFunctionDerivX[localNodeIndex] = basisFunctionDerivKsi[localNodeIndex] / detJacobian; // The x-derivative of the n basis function\n }\n\n // Computation of Galerkin's residuals and Jacobian matrix\n for (let localNodeIndex1 = 0; localNodeIndex1 < numNodes; localNodeIndex1++) {\n let localToGlobalMap1 = localToGlobalMap[localNodeIndex1];\n // residualVector is zero for this case\n\n for (let localNodeIndex2 = 0; localNodeIndex2 < numNodes; localNodeIndex2++) {\n let localToGlobalMap2 = localToGlobalMap[localNodeIndex2];\n jacobianMatrix[localToGlobalMap1][localToGlobalMap2] +=\n -gaussWeights[gaussPointIndex1] *\n detJacobian *\n (basisFunctionDerivX[localNodeIndex1] * basisFunctionDerivX[localNodeIndex2]);\n }\n }\n // 2D solid heat transfer\n } else if (meshDimension === \"2D\") {\n for (let gaussPointIndex2 = 0; gaussPointIndex2 < gaussPoints.length; gaussPointIndex2++) {\n // Initialise variables for isoparametric mapping\n let basisFunctionsAndDerivatives = basisFunctionsData.getBasisFunctions(\n gaussPoints[gaussPointIndex1],\n gaussPoints[gaussPointIndex2]\n );\n basisFunction = basisFunctionsAndDerivatives.basisFunction;\n basisFunctionDerivKsi = basisFunctionsAndDerivatives.basisFunctionDerivKsi;\n basisFunctionDerivEta = basisFunctionsAndDerivatives.basisFunctionDerivEta;\n xCoordinates = 0;\n yCoordinates = 0;\n ksiDerivX = 0;\n etaDerivX = 0;\n ksiDerivY = 0;\n etaDerivY = 0;\n detJacobian = 0;\n\n // Isoparametric mapping\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n xCoordinates +=\n nodesXCoordinates[localToGlobalMap[localNodeIndex]] * basisFunction[localNodeIndex];\n yCoordinates +=\n nodesYCoordinates[localToGlobalMap[localNodeIndex]] * basisFunction[localNodeIndex];\n ksiDerivX +=\n nodesXCoordinates[localToGlobalMap[localNodeIndex]] * basisFunctionDerivKsi[localNodeIndex];\n etaDerivX +=\n nodesXCoordinates[localToGlobalMap[localNodeIndex]] * basisFunctionDerivEta[localNodeIndex];\n ksiDerivY +=\n nodesYCoordinates[localToGlobalMap[localNodeIndex]] * basisFunctionDerivKsi[localNodeIndex];\n etaDerivY +=\n nodesYCoordinates[localToGlobalMap[localNodeIndex]] * basisFunctionDerivEta[localNodeIndex];\n detJacobian = meshDimension === \"2D\" ? ksiDerivX * etaDerivY - etaDerivX * ksiDerivY : ksiDerivX;\n }\n\n // Compute x-derivative and y-derivative of basis functions\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n basisFunctionDerivX[localNodeIndex] =\n (etaDerivY * basisFunctionDerivKsi[localNodeIndex] -\n ksiDerivY * basisFunctionDerivEta[localNodeIndex]) /\n detJacobian; // The x-derivative of the n basis function\n basisFunctionDerivY[localNodeIndex] =\n (ksiDerivX * basisFunctionDerivEta[localNodeIndex] -\n etaDerivX * basisFunctionDerivKsi[localNodeIndex]) /\n detJacobian; // The y-derivative of the n basis function\n }\n\n // Computation of Galerkin's residuals and Jacobian matrix\n for (let localNodeIndex1 = 0; localNodeIndex1 < numNodes; localNodeIndex1++) {\n let localToGlobalMap1 = localToGlobalMap[localNodeIndex1];\n // residualVector is zero for this case\n\n for (let localNodeIndex2 = 0; localNodeIndex2 < numNodes; localNodeIndex2++) {\n let localToGlobalMap2 = localToGlobalMap[localNodeIndex2];\n jacobianMatrix[localToGlobalMap1][localToGlobalMap2] +=\n -gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2] *\n detJacobian *\n (basisFunctionDerivX[localNodeIndex1] * basisFunctionDerivX[localNodeIndex2] +\n basisFunctionDerivY[localNodeIndex1] * basisFunctionDerivY[localNodeIndex2]);\n }\n }\n }\n }\n }\n }\n\n // Create an instance of ThermalBoundaryConditions\n debugLog(\"Applying thermal boundary conditions...\");\n const thermalBoundaryConditions = new ThermalBoundaryConditions(\n boundaryConditions,\n boundaryElements,\n nop,\n meshDimension,\n elementOrder\n );\n\n // Impose Convection boundary conditions\n thermalBoundaryConditions.imposeConvectionBoundaryConditions(\n residualVector,\n jacobianMatrix,\n gaussPoints,\n gaussWeights,\n nodesXCoordinates,\n nodesYCoordinates,\n basisFunctionsData\n );\n debugLog(\"Convection boundary conditions applied\");\n\n // Impose ConstantTemp boundary conditions\n thermalBoundaryConditions.imposeConstantTempBoundaryConditions(residualVector, jacobianMatrix);\n debugLog(\"Constant temperature boundary conditions applied\");\n\n basicLog(\"Solid heat transfer matrix assembly completed\");\n\n return {\n jacobianMatrix,\n residualVector,\n nodesCoordinates: {\n nodesXCoordinates,\n nodesYCoordinates,\n },\n };\n}\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\n/**\n * Function to solve a system of linear equations using the Jacobi iterative method\n * @param {array} A - The coefficient matrix (must be square)\n * @param {array} b - The right-hand side vector\n * @param {array} x0 - Initial guess for solution vector\n * @param {number} [maxIterations=100] - Maximum number of iterations\n * @param {number} [tolerance=1e-7] - Convergence tolerance\n * @returns {object} An object containing:\n * - solution: The solution vector\n * - iterations: The number of iterations performed\n * - converged: Boolean indicating whether the method converged\n */\nexport function jacobiMethod(A, b, x0, maxIterations = 100, tolerance = 1e-7) {\n const n = A.length; // Size of the square matrix\n let x = [...x0]; // Current solution (starts with initial guess)\n let xNew = new Array(n); // Next iteration's solution\n\n for (let iteration = 0; iteration < maxIterations; iteration++) {\n // Perform one iteration\n for (let i = 0; i < n; i++) {\n let sum = 0;\n // Calculate sum of A[i][j] * x[j] for j ≠i\n for (let j = 0; j < n; j++) {\n if (j !== i) {\n sum += A[i][j] * x[j];\n }\n }\n // Update xNew[i] using the Jacobi formula\n xNew[i] = (b[i] - sum) / A[i][i];\n }\n\n // Check convergence\n let maxDiff = 0;\n for (let i = 0; i < n; i++) {\n maxDiff = Math.max(maxDiff, Math.abs(xNew[i] - x[i]));\n }\n\n // Update x for next iteration\n x = [...xNew];\n\n // Successfully converged if maxDiff is less than tolerance\n if (maxDiff < tolerance) {\n return {\n solution: x,\n iterations: iteration + 1,\n converged: true,\n };\n }\n }\n\n // maxIterations were reached without convergence\n return {\n solution: x,\n iterations: maxIterations,\n converged: false,\n };\n}\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\n// External imports\nimport * as Comlink from \"../vendor/comlink.mjs\";\n\n// Internal imports\nimport { basicLog } from \"../utilities/loggingScript.js\";\n\n/**\n * Class to facilitate communication with web workers for FEAScript operations\n */\nexport class FEAScriptWorker {\n /**\n * Constructor to initialize the FEAScriptWorker class\n * Sets up the worker and initializes the workerWrapper.\n */\n constructor() {\n this.worker = null;\n this.feaWorker = null;\n this.isReady = false;\n\n this._initWorker();\n }\n\n /**\n * Function to initialize the web worker and wrap it using Comlink.\n * @private\n * @throws Will throw an error if the worker fails to initialize.\n */\n async _initWorker() {\n try {\n this.worker = new Worker(new URL(\"./wrapperScript.js\", import.meta.url), {\n type: \"module\",\n });\n\n this.worker.onerror = (event) => {\n console.error(\"FEAScriptWorker: Worker error:\", event);\n };\n const workerWrapper = Comlink.wrap(this.worker);\n\n this.feaWorker = await new workerWrapper();\n\n this.isReady = true;\n } catch (error) {\n console.error(\"Failed to initialize worker\", error);\n throw error;\n }\n }\n\n /**\n * Function to ensure that the worker is ready before performing any operations.\n * @private\n * @returns {Promise} Resolves when the worker is ready.\n * @throws Will throw an error if the worker is not ready within the timeout period.\n */\n async _ensureReady() {\n if (this.isReady) return Promise.resolve();\n\n return new Promise((resolve, reject) => {\n let attempts = 0;\n const maxAttempts = 50; // 5 seconds max\n\n const checkReady = () => {\n attempts++;\n if (this.isReady) {\n resolve();\n } else if (attempts >= maxAttempts) {\n reject(new Error(\"Timeout waiting for worker to be ready\"));\n } else {\n setTimeout(checkReady, 1000);\n }\n };\n checkReady();\n });\n }\n\n /**\n * Function to set the solver configuration in the worker.\n * @param {string} solverConfig - The solver configuration to set.\n * @returns {Promise} Resolves when the configuration is set.\n */\n async setSolverConfig(solverConfig) {\n await this._ensureReady();\n basicLog(`FEAScriptWorker: Setting solver config to: ${solverConfig}`);\n return this.feaWorker.setSolverConfig(solverConfig);\n }\n\n /**\n * Sets the mesh configuration in the worker.\n * @param {object} meshConfig - The mesh configuration to set.\n * @returns {Promise} Resolves when the configuration is set.\n */\n async setMeshConfig(meshConfig) {\n await this._ensureReady();\n basicLog(`FEAScriptWorker: Setting mesh config`);\n return this.feaWorker.setMeshConfig(meshConfig);\n }\n\n /**\n * Adds a boundary condition to the worker.\n * @param {string} boundaryKey - The key identifying the boundary.\n * @param {array} condition - The boundary condition to add.\n * @returns {Promise} Resolves when the boundary condition is added.\n */\n async addBoundaryCondition(boundaryKey, condition) {\n await this._ensureReady();\n basicLog(`FEAScriptWorker: Adding boundary condition for boundary: ${boundaryKey}`);\n return this.feaWorker.addBoundaryCondition(boundaryKey, condition);\n }\n\n /**\n * Sets the solver method in the worker.\n * @param {string} solverMethod - The solver method to set.\n * @returns {Promise} Resolves when the solver method is set.\n */\n async setSolverMethod(solverMethod) {\n await this._ensureReady();\n basicLog(`FEAScriptWorker: Setting solver method to: ${solverMethod}`);\n return this.feaWorker.setSolverMethod(solverMethod);\n }\n\n /**\n * Requests the worker to solve the problem.\n * @returns {Promise