@holoscript/engine 6.0.3 → 6.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/dist/AutoMesher-CK47F6AV.js +17 -0
  2. package/dist/GPUBuffers-2LHBCD7X.js +9 -0
  3. package/dist/WebGPUContext-TNEUYU2Y.js +11 -0
  4. package/dist/animation/index.cjs +38 -38
  5. package/dist/animation/index.d.cts +1 -1
  6. package/dist/animation/index.d.ts +1 -1
  7. package/dist/animation/index.js +1 -1
  8. package/dist/audio/index.cjs +16 -6
  9. package/dist/audio/index.d.cts +1 -1
  10. package/dist/audio/index.d.ts +1 -1
  11. package/dist/audio/index.js +1 -1
  12. package/dist/camera/index.cjs +23 -23
  13. package/dist/camera/index.d.cts +1 -1
  14. package/dist/camera/index.d.ts +1 -1
  15. package/dist/camera/index.js +1 -1
  16. package/dist/character/index.cjs +6 -4
  17. package/dist/character/index.js +1 -1
  18. package/dist/choreography/index.cjs +1194 -0
  19. package/dist/choreography/index.d.cts +687 -0
  20. package/dist/choreography/index.d.ts +687 -0
  21. package/dist/choreography/index.js +1156 -0
  22. package/dist/chunk-2CSNRI2N.js +217 -0
  23. package/dist/chunk-33T2WINR.js +266 -0
  24. package/dist/chunk-35R73OFM.js +1257 -0
  25. package/dist/chunk-4MMDSUNP.js +1256 -0
  26. package/dist/chunk-5V6HOU72.js +319 -0
  27. package/dist/chunk-6QOP6PYF.js +1038 -0
  28. package/dist/chunk-7KMJVHIL.js +8944 -0
  29. package/dist/chunk-7VPUC62U.js +1106 -0
  30. package/dist/chunk-A2Y6RCAT.js +1878 -0
  31. package/dist/chunk-AHM42MK6.js +8944 -0
  32. package/dist/chunk-BL7IDTHE.js +218 -0
  33. package/dist/chunk-CITOMSWL.js +10462 -0
  34. package/dist/chunk-CXDPKW2K.js +8944 -0
  35. package/dist/chunk-CXZPLD4S.js +223 -0
  36. package/dist/chunk-CZYJE7IH.js +5169 -0
  37. package/dist/chunk-D2OP7YC7.js +6325 -0
  38. package/dist/chunk-EDRVQHUU.js +1544 -0
  39. package/dist/chunk-EJSLOOW2.js +3589 -0
  40. package/dist/chunk-F53SFGW5.js +1878 -0
  41. package/dist/chunk-HCFPELPY.js +919 -0
  42. package/dist/chunk-HNEE36PY.js +93 -0
  43. package/dist/chunk-HYXNV36F.js +1256 -0
  44. package/dist/chunk-IB7KHVFY.js +821 -0
  45. package/dist/chunk-IBBO7YYG.js +690 -0
  46. package/dist/chunk-ILIBGINU.js +5470 -0
  47. package/dist/chunk-IS4MHLKN.js +5479 -0
  48. package/dist/chunk-JT2PFKWD.js +5479 -0
  49. package/dist/chunk-K4CUB4NY.js +1038 -0
  50. package/dist/chunk-KATDQXRJ.js +10462 -0
  51. package/dist/chunk-KBQE6ZFJ.js +8944 -0
  52. package/dist/chunk-KBVD5K7E.js +560 -0
  53. package/dist/chunk-KCDPVQRY.js +4088 -0
  54. package/dist/chunk-KN4QJPKN.js +8944 -0
  55. package/dist/chunk-KWJ3ROSI.js +8944 -0
  56. package/dist/chunk-L45VF6DD.js +919 -0
  57. package/dist/chunk-LY4T37YK.js +307 -0
  58. package/dist/chunk-MDN5WZXA.js +1544 -0
  59. package/dist/chunk-MGCDP6VU.js +928 -0
  60. package/dist/chunk-NCX7X6G2.js +8681 -0
  61. package/dist/chunk-OF54BPVD.js +913 -0
  62. package/dist/chunk-OWSN2Q3Q.js +690 -0
  63. package/dist/chunk-PRRB5TTA.js +406 -0
  64. package/dist/chunk-PXWVQF76.js +4086 -0
  65. package/dist/chunk-PYCOIDT2.js +812 -0
  66. package/dist/chunk-PZCSADOV.js +928 -0
  67. package/dist/chunk-Q2XBVS2K.js +1038 -0
  68. package/dist/chunk-QDZRXWN5.js +1776 -0
  69. package/dist/chunk-RNWOZ6WQ.js +913 -0
  70. package/dist/chunk-ROLFT4CJ.js +1693 -0
  71. package/dist/chunk-SLTJRZ2N.js +266 -0
  72. package/dist/chunk-SRUS5XSU.js +4088 -0
  73. package/dist/chunk-TKCA3WZ5.js +5409 -0
  74. package/dist/chunk-TNRMXYI2.js +1650 -0
  75. package/dist/chunk-TQB3GJGM.js +9763 -0
  76. package/dist/chunk-TUFGXG6K.js +510 -0
  77. package/dist/chunk-U6KMTGQJ.js +632 -0
  78. package/dist/chunk-VMGJQST6.js +8681 -0
  79. package/dist/chunk-X4F4TCG4.js +5470 -0
  80. package/dist/chunk-ZIFROE75.js +1544 -0
  81. package/dist/chunk-ZIJQYHSQ.js +1204 -0
  82. package/dist/combat/index.cjs +4 -4
  83. package/dist/combat/index.d.cts +1 -1
  84. package/dist/combat/index.d.ts +1 -1
  85. package/dist/combat/index.js +1 -1
  86. package/dist/ecs/index.cjs +1 -1
  87. package/dist/ecs/index.js +1 -1
  88. package/dist/environment/index.cjs +14 -14
  89. package/dist/environment/index.d.cts +1 -1
  90. package/dist/environment/index.d.ts +1 -1
  91. package/dist/environment/index.js +1 -1
  92. package/dist/gpu/index.cjs +4810 -0
  93. package/dist/gpu/index.js +3714 -0
  94. package/dist/hologram/index.cjs +27 -1
  95. package/dist/hologram/index.js +1 -1
  96. package/dist/index-B2PIsAmR.d.cts +2180 -0
  97. package/dist/index-B2PIsAmR.d.ts +2180 -0
  98. package/dist/index-BHySEPX7.d.cts +2921 -0
  99. package/dist/index-BJV21zuy.d.cts +341 -0
  100. package/dist/index-BJV21zuy.d.ts +341 -0
  101. package/dist/index-BQutTphC.d.cts +790 -0
  102. package/dist/index-ByIq2XrS.d.cts +3910 -0
  103. package/dist/index-BysHjDSO.d.cts +224 -0
  104. package/dist/index-BysHjDSO.d.ts +224 -0
  105. package/dist/index-CKwAJGck.d.ts +455 -0
  106. package/dist/index-CUl3QstQ.d.cts +3006 -0
  107. package/dist/index-CUl3QstQ.d.ts +3006 -0
  108. package/dist/index-CmYtNiI-.d.cts +953 -0
  109. package/dist/index-CmYtNiI-.d.ts +953 -0
  110. package/dist/index-CnRzWxi_.d.cts +522 -0
  111. package/dist/index-CnRzWxi_.d.ts +522 -0
  112. package/dist/index-CwRWbSC7.d.ts +2921 -0
  113. package/dist/index-CxKIBstO.d.ts +790 -0
  114. package/dist/index-DJ6-R8vh.d.cts +455 -0
  115. package/dist/index-DQKisbcI.d.cts +4968 -0
  116. package/dist/index-DQKisbcI.d.ts +4968 -0
  117. package/dist/index-DRT2zJez.d.ts +3910 -0
  118. package/dist/index-DfNLiAka.d.cts +192 -0
  119. package/dist/index-DfNLiAka.d.ts +192 -0
  120. package/dist/index-nMvkoRm8.d.cts +405 -0
  121. package/dist/index-nMvkoRm8.d.ts +405 -0
  122. package/dist/index-s9yOFU37.d.cts +604 -0
  123. package/dist/index-s9yOFU37.d.ts +604 -0
  124. package/dist/index.cjs +22966 -6960
  125. package/dist/index.d.cts +864 -20
  126. package/dist/index.d.ts +864 -20
  127. package/dist/index.js +3062 -48
  128. package/dist/input/index.cjs +1 -1
  129. package/dist/input/index.js +1 -1
  130. package/dist/orbital/index.cjs +3 -3
  131. package/dist/orbital/index.d.cts +1 -1
  132. package/dist/orbital/index.d.ts +1 -1
  133. package/dist/orbital/index.js +1 -1
  134. package/dist/particles/index.cjs +16 -16
  135. package/dist/particles/index.d.cts +1 -1
  136. package/dist/particles/index.d.ts +1 -1
  137. package/dist/particles/index.js +1 -1
  138. package/dist/physics/index.cjs +2377 -21
  139. package/dist/physics/index.d.cts +1 -1
  140. package/dist/physics/index.d.ts +1 -1
  141. package/dist/physics/index.js +35 -1
  142. package/dist/postfx/index.cjs +3491 -0
  143. package/dist/postfx/index.js +93 -0
  144. package/dist/procedural/index.cjs +1 -1
  145. package/dist/procedural/index.js +1 -1
  146. package/dist/puppeteer-5VF6KDVO.js +52197 -0
  147. package/dist/puppeteer-IZVZ3SG4.js +52197 -0
  148. package/dist/rendering/index.cjs +33 -32
  149. package/dist/rendering/index.d.cts +1 -1
  150. package/dist/rendering/index.d.ts +1 -1
  151. package/dist/rendering/index.js +8 -6
  152. package/dist/runtime/index.cjs +23 -13
  153. package/dist/runtime/index.d.cts +1 -1
  154. package/dist/runtime/index.d.ts +1 -1
  155. package/dist/runtime/index.js +8 -6
  156. package/dist/runtime/protocols/index.cjs +349 -0
  157. package/dist/runtime/protocols/index.js +15 -0
  158. package/dist/scene/index.cjs +8 -8
  159. package/dist/scene/index.d.cts +1 -1
  160. package/dist/scene/index.d.ts +1 -1
  161. package/dist/scene/index.js +1 -1
  162. package/dist/shader/index.cjs +3087 -0
  163. package/dist/shader/index.js +3044 -0
  164. package/dist/simulation/index.cjs +10680 -0
  165. package/dist/simulation/index.d.cts +3 -0
  166. package/dist/simulation/index.d.ts +3 -0
  167. package/dist/simulation/index.js +307 -0
  168. package/dist/spatial/index.cjs +2443 -0
  169. package/dist/spatial/index.d.cts +1545 -0
  170. package/dist/spatial/index.d.ts +1545 -0
  171. package/dist/spatial/index.js +2400 -0
  172. package/dist/terrain/index.cjs +1 -1
  173. package/dist/terrain/index.d.cts +1 -1
  174. package/dist/terrain/index.d.ts +1 -1
  175. package/dist/terrain/index.js +1 -1
  176. package/dist/transformers.node-4NKAPD5U.js +45620 -0
  177. package/dist/vm/index.cjs +7 -8
  178. package/dist/vm/index.d.cts +1 -1
  179. package/dist/vm/index.d.ts +1 -1
  180. package/dist/vm/index.js +1 -1
  181. package/dist/vm-bridge/index.cjs +2 -2
  182. package/dist/vm-bridge/index.d.cts +2 -2
  183. package/dist/vm-bridge/index.d.ts +2 -2
  184. package/dist/vm-bridge/index.js +1 -1
  185. package/dist/vr/index.cjs +6 -6
  186. package/dist/vr/index.js +1 -1
  187. package/dist/world/index.cjs +3 -3
  188. package/dist/world/index.d.cts +1 -1
  189. package/dist/world/index.d.ts +1 -1
  190. package/dist/world/index.js +1 -1
  191. package/package.json +53 -21
  192. package/LICENSE +0 -21
@@ -0,0 +1,1693 @@
1
+ import {
2
+ __export
3
+ } from "./chunk-AKLW2MUS.js";
4
+
5
+ // src/simulation/index.ts
6
+ var simulation_exports = {};
7
+ __export(simulation_exports, {
8
+ CouplingManager: () => CouplingManager,
9
+ HydraulicSolver: () => HydraulicSolver,
10
+ RegularGrid3D: () => RegularGrid3D,
11
+ SaturationManager: () => SaturationManager,
12
+ StructuralSolver: () => StructuralSolver,
13
+ ThermalSolver: () => ThermalSolver,
14
+ applyBoundaryConditions: () => applyBoundaryConditions,
15
+ conjugateGradient: () => conjugateGradient,
16
+ findMaterial: () => findMaterial,
17
+ getMaterial: () => getMaterial,
18
+ jacobiIteration: () => jacobiIteration,
19
+ listMaterials: () => listMaterials,
20
+ registerMaterial: () => registerMaterial,
21
+ thermalDiffusivity: () => thermalDiffusivity
22
+ });
23
+
24
+ // src/simulation/RegularGrid3D.ts
25
+ var RegularGrid3D = class _RegularGrid3D {
26
+ nx;
27
+ ny;
28
+ nz;
29
+ components;
30
+ dx;
31
+ dy;
32
+ dz;
33
+ data;
34
+ constructor(resolution, domainSize, components = 1) {
35
+ this.nx = resolution[0];
36
+ this.ny = resolution[1];
37
+ this.nz = resolution[2];
38
+ this.components = components;
39
+ this.dx = domainSize[0] / (this.nx - 1);
40
+ this.dy = domainSize[1] / (this.ny - 1);
41
+ this.dz = domainSize[2] / (this.nz - 1);
42
+ this.data = new Float32Array(this.nx * this.ny * this.nz * components);
43
+ }
44
+ /** Flat index for cell (i, j, k) component c */
45
+ idx(i, j, k, c = 0) {
46
+ return ((k * this.ny + j) * this.nx + i) * this.components + c;
47
+ }
48
+ get(i, j, k, component = 0) {
49
+ return this.data[this.idx(i, j, k, component)];
50
+ }
51
+ set(i, j, k, value, component = 0) {
52
+ this.data[this.idx(i, j, k, component)] = value;
53
+ }
54
+ /** Total number of cells */
55
+ get cellCount() {
56
+ return this.nx * this.ny * this.nz;
57
+ }
58
+ // ── Stencil Operations ──────────────────────────────────────────────────
59
+ /**
60
+ * Discrete Laplacian ∇²f at (i,j,k) using 2nd-order central differences.
61
+ * Returns 0 at boundaries (Neumann-like default).
62
+ */
63
+ laplacian(i, j, k, component = 0) {
64
+ const c = this.get(i, j, k, component);
65
+ let lap = 0;
66
+ if (i > 0 && i < this.nx - 1) {
67
+ lap += (this.get(i + 1, j, k, component) - 2 * c + this.get(i - 1, j, k, component)) / (this.dx * this.dx);
68
+ }
69
+ if (j > 0 && j < this.ny - 1) {
70
+ lap += (this.get(i, j + 1, k, component) - 2 * c + this.get(i, j - 1, k, component)) / (this.dy * this.dy);
71
+ }
72
+ if (k > 0 && k < this.nz - 1) {
73
+ lap += (this.get(i, j, k + 1, component) - 2 * c + this.get(i, j, k - 1, component)) / (this.dz * this.dz);
74
+ }
75
+ return lap;
76
+ }
77
+ /**
78
+ * Gradient ∇f at (i,j,k) using central differences.
79
+ * Falls back to one-sided differences at boundaries.
80
+ */
81
+ gradient(i, j, k, component = 0) {
82
+ let gx, gy, gz;
83
+ if (i <= 0) {
84
+ gx = (this.get(i + 1, j, k, component) - this.get(i, j, k, component)) / this.dx;
85
+ } else if (i >= this.nx - 1) {
86
+ gx = (this.get(i, j, k, component) - this.get(i - 1, j, k, component)) / this.dx;
87
+ } else {
88
+ gx = (this.get(i + 1, j, k, component) - this.get(i - 1, j, k, component)) / (2 * this.dx);
89
+ }
90
+ if (j <= 0) {
91
+ gy = (this.get(i, j + 1, k, component) - this.get(i, j, k, component)) / this.dy;
92
+ } else if (j >= this.ny - 1) {
93
+ gy = (this.get(i, j, k, component) - this.get(i, j - 1, k, component)) / this.dy;
94
+ } else {
95
+ gy = (this.get(i, j + 1, k, component) - this.get(i, j - 1, k, component)) / (2 * this.dy);
96
+ }
97
+ if (k <= 0) {
98
+ gz = (this.get(i, j, k + 1, component) - this.get(i, j, k, component)) / this.dz;
99
+ } else if (k >= this.nz - 1) {
100
+ gz = (this.get(i, j, k, component) - this.get(i, j, k - 1, component)) / this.dz;
101
+ } else {
102
+ gz = (this.get(i, j, k + 1, component) - this.get(i, j, k - 1, component)) / (2 * this.dz);
103
+ }
104
+ return [gx, gy, gz];
105
+ }
106
+ /**
107
+ * Divergence ∇·F for a 3-component vector field at (i,j,k).
108
+ * Components 0,1,2 = x,y,z.
109
+ */
110
+ divergence(i, j, k) {
111
+ if (this.components < 3) return 0;
112
+ const [gx] = this.gradient(i, j, k, 0);
113
+ const gy = this.gradient(i, j, k, 1)[1];
114
+ const gz = this.gradient(i, j, k, 2)[2];
115
+ return gx + gy + gz;
116
+ }
117
+ // ── Bulk Operations ─────────────────────────────────────────────────────
118
+ fill(value) {
119
+ this.data.fill(value);
120
+ }
121
+ copy(other) {
122
+ this.data.set(other.data);
123
+ }
124
+ /** this.data += scale * other.data (element-wise) */
125
+ addScaled(other, scale) {
126
+ const d = this.data;
127
+ const o = other.data;
128
+ for (let n = 0; n < d.length; n++) {
129
+ d[n] += scale * o[n];
130
+ }
131
+ }
132
+ /** L∞ norm (max absolute value) */
133
+ maxAbs() {
134
+ let m = 0;
135
+ for (let n = 0; n < this.data.length; n++) {
136
+ const a = Math.abs(this.data[n]);
137
+ if (a > m) m = a;
138
+ }
139
+ return m;
140
+ }
141
+ /** L2 norm */
142
+ norm2() {
143
+ let s = 0;
144
+ for (let n = 0; n < this.data.length; n++) {
145
+ s += this.data[n] * this.data[n];
146
+ }
147
+ return Math.sqrt(s);
148
+ }
149
+ // ── Export for Rendering ─────────────────────────────────────────────────
150
+ /** Direct reference to the flat data buffer (for ScalarFieldOverlay) */
151
+ toFloat32Array() {
152
+ return this.data;
153
+ }
154
+ /**
155
+ * Sample grid values at mesh vertex positions via trilinear interpolation.
156
+ * @param positions Flat xyz vertex positions [x0,y0,z0, x1,y1,z1, ...]
157
+ * @param domainOrigin World-space origin of the grid domain
158
+ * @param component Which component to sample (default 0)
159
+ */
160
+ sampleAtPositions(positions, domainOrigin = [0, 0, 0], component = 0) {
161
+ const vertexCount = positions.length / 3;
162
+ const out = new Float32Array(vertexCount);
163
+ for (let v = 0; v < vertexCount; v++) {
164
+ const wx = positions[v * 3] - domainOrigin[0];
165
+ const wy = positions[v * 3 + 1] - domainOrigin[1];
166
+ const wz = positions[v * 3 + 2] - domainOrigin[2];
167
+ const fx = wx / this.dx;
168
+ const fy = wy / this.dy;
169
+ const fz = wz / this.dz;
170
+ const i0 = Math.max(0, Math.min(this.nx - 2, Math.floor(fx)));
171
+ const j0 = Math.max(0, Math.min(this.ny - 2, Math.floor(fy)));
172
+ const k0 = Math.max(0, Math.min(this.nz - 2, Math.floor(fz)));
173
+ const tx = Math.max(0, Math.min(1, fx - i0));
174
+ const ty = Math.max(0, Math.min(1, fy - j0));
175
+ const tz = Math.max(0, Math.min(1, fz - k0));
176
+ const c000 = this.get(i0, j0, k0, component);
177
+ const c100 = this.get(i0 + 1, j0, k0, component);
178
+ const c010 = this.get(i0, j0 + 1, k0, component);
179
+ const c110 = this.get(i0 + 1, j0 + 1, k0, component);
180
+ const c001 = this.get(i0, j0, k0 + 1, component);
181
+ const c101 = this.get(i0 + 1, j0, k0 + 1, component);
182
+ const c011 = this.get(i0, j0 + 1, k0 + 1, component);
183
+ const c111 = this.get(i0 + 1, j0 + 1, k0 + 1, component);
184
+ const c00 = c000 * (1 - tx) + c100 * tx;
185
+ const c10 = c010 * (1 - tx) + c110 * tx;
186
+ const c01 = c001 * (1 - tx) + c101 * tx;
187
+ const c11 = c011 * (1 - tx) + c111 * tx;
188
+ const c0 = c00 * (1 - ty) + c10 * ty;
189
+ const c1 = c01 * (1 - ty) + c11 * ty;
190
+ out[v] = c0 * (1 - tz) + c1 * tz;
191
+ }
192
+ return out;
193
+ }
194
+ /**
195
+ * Create a deep clone of this grid.
196
+ */
197
+ clone() {
198
+ const g = new _RegularGrid3D(
199
+ [this.nx, this.ny, this.nz],
200
+ [this.dx * (this.nx - 1), this.dy * (this.ny - 1), this.dz * (this.nz - 1)],
201
+ this.components
202
+ );
203
+ g.data.set(this.data);
204
+ return g;
205
+ }
206
+ };
207
+
208
+ // src/simulation/BoundaryConditions.ts
209
+ var VALID_FACES = /* @__PURE__ */ new Set(["x-", "x+", "y-", "y+", "z-", "z+"]);
210
+ function applyBoundaryConditions(grid, bcs, dt, component = 0) {
211
+ for (const bc of bcs) {
212
+ for (const face of bc.faces) {
213
+ if (!VALID_FACES.has(face)) continue;
214
+ applyToFace(grid, face, bc, dt, component);
215
+ }
216
+ }
217
+ }
218
+ function applyToFace(grid, face, bc, _dt, c) {
219
+ const { nx, ny, nz } = grid;
220
+ switch (face) {
221
+ case "x-":
222
+ for (let k = 0; k < nz; k++)
223
+ for (let j = 0; j < ny; j++) applyAtCell(grid, 0, j, k, face, bc, c);
224
+ break;
225
+ case "x+":
226
+ for (let k = 0; k < nz; k++)
227
+ for (let j = 0; j < ny; j++)
228
+ applyAtCell(grid, nx - 1, j, k, face, bc, c);
229
+ break;
230
+ case "y-":
231
+ for (let k = 0; k < nz; k++)
232
+ for (let i = 0; i < nx; i++) applyAtCell(grid, i, 0, k, face, bc, c);
233
+ break;
234
+ case "y+":
235
+ for (let k = 0; k < nz; k++)
236
+ for (let i = 0; i < nx; i++)
237
+ applyAtCell(grid, i, ny - 1, k, face, bc, c);
238
+ break;
239
+ case "z-":
240
+ for (let j = 0; j < ny; j++)
241
+ for (let i = 0; i < nx; i++) applyAtCell(grid, i, j, 0, face, bc, c);
242
+ break;
243
+ case "z+":
244
+ for (let j = 0; j < ny; j++)
245
+ for (let i = 0; i < nx; i++)
246
+ applyAtCell(grid, i, j, nz - 1, face, bc, c);
247
+ break;
248
+ }
249
+ }
250
+ function applyAtCell(grid, i, j, k, face, bc, c) {
251
+ switch (bc.type) {
252
+ case "dirichlet":
253
+ grid.set(i, j, k, bc.value, c);
254
+ break;
255
+ case "neumann": {
256
+ const [ni, nj, nk] = interiorNeighbor(i, j, k, face, grid);
257
+ const dn = normalSpacing(face, grid);
258
+ grid.set(i, j, k, grid.get(ni, nj, nk, c) + bc.value * dn, c);
259
+ break;
260
+ }
261
+ case "convection": {
262
+ const h = bc.coefficient ?? 10;
263
+ const Tamb = bc.ambient ?? bc.value;
264
+ const [ni, nj, nk] = interiorNeighbor(i, j, k, face, grid);
265
+ const dn = normalSpacing(face, grid);
266
+ const Bi = h * dn / 1;
267
+ const Tinterior = grid.get(ni, nj, nk, c);
268
+ grid.set(i, j, k, (Tinterior + Bi * Tamb) / (1 + Bi), c);
269
+ break;
270
+ }
271
+ case "robin": {
272
+ const alpha = bc.coefficient ?? 1;
273
+ const [ni, nj, nk] = interiorNeighbor(i, j, k, face, grid);
274
+ const dn = normalSpacing(face, grid);
275
+ const Tinterior = grid.get(ni, nj, nk, c);
276
+ grid.set(i, j, k, (bc.value + Tinterior / dn) / (alpha + 1 / dn), c);
277
+ break;
278
+ }
279
+ }
280
+ }
281
+ function interiorNeighbor(i, j, k, face, grid) {
282
+ switch (face) {
283
+ case "x-":
284
+ return [Math.min(i + 1, grid.nx - 1), j, k];
285
+ case "x+":
286
+ return [Math.max(i - 1, 0), j, k];
287
+ case "y-":
288
+ return [i, Math.min(j + 1, grid.ny - 1), k];
289
+ case "y+":
290
+ return [i, Math.max(j - 1, 0), k];
291
+ case "z-":
292
+ return [i, j, Math.min(k + 1, grid.nz - 1)];
293
+ case "z+":
294
+ return [i, j, Math.max(k - 1, 0)];
295
+ }
296
+ }
297
+ function normalSpacing(face, grid) {
298
+ switch (face) {
299
+ case "x-":
300
+ case "x+":
301
+ return grid.dx;
302
+ case "y-":
303
+ case "y+":
304
+ return grid.dy;
305
+ case "z-":
306
+ case "z+":
307
+ return grid.dz;
308
+ }
309
+ }
310
+
311
+ // src/simulation/MaterialDatabase.ts
312
+ var MATERIAL_DB = {
313
+ // From hvac-building.hsplus
314
+ concrete: { conductivity: 1.7, specific_heat: 880, density: 2300 },
315
+ glass: { conductivity: 1, specific_heat: 840, density: 2500 },
316
+ air: { conductivity: 0.026, specific_heat: 1005, density: 1.225 },
317
+ insulation: { conductivity: 0.04, specific_heat: 840, density: 50 },
318
+ // From bridge-load-test.hsplus
319
+ steel_a36: {
320
+ conductivity: 50,
321
+ specific_heat: 490,
322
+ density: 7850,
323
+ youngs_modulus: 2e11,
324
+ poisson_ratio: 0.3,
325
+ yield_strength: 25e7
326
+ },
327
+ structural_steel: {
328
+ conductivity: 50,
329
+ specific_heat: 490,
330
+ density: 7850,
331
+ youngs_modulus: 2e11,
332
+ poisson_ratio: 0.3,
333
+ yield_strength: 25e7
334
+ },
335
+ // From water-network.hsplus
336
+ ductile_iron: {
337
+ conductivity: 36,
338
+ specific_heat: 460,
339
+ density: 7100,
340
+ roughness: 0.015
341
+ },
342
+ pvc: {
343
+ conductivity: 0.16,
344
+ specific_heat: 900,
345
+ density: 1400,
346
+ roughness: 7e-3
347
+ },
348
+ // Common simulation materials
349
+ water: { conductivity: 0.6, specific_heat: 4186, density: 998 },
350
+ copper: {
351
+ conductivity: 385,
352
+ specific_heat: 385,
353
+ density: 8960,
354
+ youngs_modulus: 11e10,
355
+ poisson_ratio: 0.34,
356
+ yield_strength: 21e7
357
+ },
358
+ aluminum: {
359
+ conductivity: 205,
360
+ specific_heat: 900,
361
+ density: 2700,
362
+ youngs_modulus: 69e9,
363
+ poisson_ratio: 0.33,
364
+ yield_strength: 27e7
365
+ },
366
+ wood_oak: { conductivity: 0.17, specific_heat: 2e3, density: 700 },
367
+ brick: { conductivity: 0.72, specific_heat: 840, density: 1920 },
368
+ soil_dry: { conductivity: 0.25, specific_heat: 800, density: 1500 },
369
+ soil_wet: { conductivity: 1.5, specific_heat: 1480, density: 1800 },
370
+ granite: {
371
+ conductivity: 2.8,
372
+ specific_heat: 790,
373
+ density: 2750,
374
+ youngs_modulus: 5e10,
375
+ poisson_ratio: 0.25,
376
+ yield_strength: 13e7
377
+ }
378
+ };
379
+ var customMaterials = /* @__PURE__ */ new Map();
380
+ function getMaterial(name) {
381
+ const custom = customMaterials.get(name);
382
+ if (custom) return custom;
383
+ const builtin = MATERIAL_DB[name];
384
+ if (builtin) return builtin;
385
+ throw new Error(
386
+ `Unknown material: "${name}". Available: ${listMaterials().join(", ")}`
387
+ );
388
+ }
389
+ function findMaterial(name) {
390
+ return customMaterials.get(name) ?? MATERIAL_DB[name];
391
+ }
392
+ function registerMaterial(name, props) {
393
+ customMaterials.set(name, props);
394
+ }
395
+ function listMaterials() {
396
+ return [
397
+ .../* @__PURE__ */ new Set([...Object.keys(MATERIAL_DB), ...customMaterials.keys()])
398
+ ];
399
+ }
400
+ function thermalDiffusivity(mat) {
401
+ return mat.conductivity / (mat.density * mat.specific_heat);
402
+ }
403
+
404
+ // src/simulation/ConvergenceControl.ts
405
+ function conjugateGradient(applyA, b, x, maxIter, tol) {
406
+ const n = b.length;
407
+ const r = new Float32Array(n);
408
+ const p = new Float32Array(n);
409
+ const Ap = new Float32Array(n);
410
+ applyA(x, Ap);
411
+ for (let i = 0; i < n; i++) r[i] = b[i] - Ap[i];
412
+ p.set(r);
413
+ let rDotR = dot(r, r);
414
+ const bNorm = Math.sqrt(dot(b, b));
415
+ const threshold = Math.max(tol * bNorm, tol);
416
+ let maxChange = 0;
417
+ let iter = 0;
418
+ for (iter = 0; iter < maxIter; iter++) {
419
+ const rNorm = Math.sqrt(rDotR);
420
+ if (rNorm < threshold) {
421
+ return { converged: true, iterations: iter, residual: rNorm, maxChange };
422
+ }
423
+ applyA(p, Ap);
424
+ const pAp = dot(p, Ap);
425
+ if (Math.abs(pAp) < 1e-30) break;
426
+ const alpha = rDotR / pAp;
427
+ maxChange = 0;
428
+ for (let i = 0; i < n; i++) {
429
+ const dx = alpha * p[i];
430
+ x[i] += dx;
431
+ r[i] -= alpha * Ap[i];
432
+ if (Math.abs(dx) > maxChange) maxChange = Math.abs(dx);
433
+ }
434
+ const rDotRNew = dot(r, r);
435
+ const beta = rDotRNew / rDotR;
436
+ rDotR = rDotRNew;
437
+ for (let i = 0; i < n; i++) {
438
+ p[i] = r[i] + beta * p[i];
439
+ }
440
+ }
441
+ return {
442
+ converged: false,
443
+ iterations: iter,
444
+ residual: Math.sqrt(rDotR),
445
+ maxChange
446
+ };
447
+ }
448
+ function jacobiIteration(grid, rhs, alpha, beta, maxIter, tol, omega = 0.6667) {
449
+ const { nx, ny, nz } = grid;
450
+ const temp = grid.clone();
451
+ let maxChange = 0;
452
+ let iter = 0;
453
+ for (iter = 0; iter < maxIter; iter++) {
454
+ maxChange = 0;
455
+ for (let k = 1; k < nz - 1; k++) {
456
+ for (let j = 1; j < ny - 1; j++) {
457
+ for (let i = 1; i < nx - 1; i++) {
458
+ const neighbors = temp.get(i - 1, j, k) + temp.get(i + 1, j, k) + temp.get(i, j - 1, k) + temp.get(i, j + 1, k) + temp.get(i, j, k - 1) + temp.get(i, j, k + 1);
459
+ const newVal = (alpha * rhs.get(i, j, k) + neighbors) / beta;
460
+ const oldVal = temp.get(i, j, k);
461
+ const relaxed = oldVal + omega * (newVal - oldVal);
462
+ grid.set(i, j, k, relaxed);
463
+ const change = Math.abs(relaxed - oldVal);
464
+ if (change > maxChange) maxChange = change;
465
+ }
466
+ }
467
+ }
468
+ if (maxChange < tol) {
469
+ return { converged: true, iterations: iter + 1, residual: maxChange, maxChange };
470
+ }
471
+ temp.copy(grid);
472
+ }
473
+ return { converged: false, iterations: iter, residual: maxChange, maxChange };
474
+ }
475
+ function dot(a, b) {
476
+ let s = 0;
477
+ for (let i = 0; i < a.length; i++) s += a[i] * b[i];
478
+ return s;
479
+ }
480
+
481
+ // src/simulation/ThermalSolver.ts
482
+ var ThermalSolver = class {
483
+ temperature;
484
+ tempPrev;
485
+ sourceField;
486
+ config;
487
+ material;
488
+ alpha;
489
+ // thermal diffusivity
490
+ simulationTime = 0;
491
+ stepCount = 0;
492
+ useImplicit = false;
493
+ lastStepMs = 0;
494
+ constructor(config) {
495
+ this.config = config;
496
+ const matName = config.defaultMaterial;
497
+ const matOverride = config.materials[matName];
498
+ const matBase = getMaterial(matName);
499
+ this.material = { ...matBase, ...matOverride };
500
+ this.alpha = thermalDiffusivity(this.material);
501
+ this.temperature = new RegularGrid3D(config.gridResolution, config.domainSize);
502
+ this.tempPrev = new RegularGrid3D(config.gridResolution, config.domainSize);
503
+ this.sourceField = new RegularGrid3D(config.gridResolution, config.domainSize);
504
+ const T0 = config.initialTemperature ?? 20;
505
+ this.temperature.fill(T0);
506
+ this.tempPrev.fill(T0);
507
+ this.rebuildSourceField();
508
+ const dx = this.temperature.dx;
509
+ const dy = this.temperature.dy;
510
+ const dz = this.temperature.dz;
511
+ const dxMin = Math.min(dx, dy, dz);
512
+ const dtStable = dxMin * dxMin / (6 * this.alpha);
513
+ if (config.timeStep > dtStable) {
514
+ this.useImplicit = true;
515
+ }
516
+ }
517
+ /**
518
+ * Advance the thermal field by dt seconds.
519
+ */
520
+ step(dt) {
521
+ const t0 = performance.now();
522
+ const effectiveDt = dt > 0 ? dt : this.config.timeStep;
523
+ applyBoundaryConditions(
524
+ this.temperature,
525
+ this.config.boundaryConditions,
526
+ effectiveDt
527
+ );
528
+ if (this.useImplicit) {
529
+ this.stepImplicit(effectiveDt);
530
+ } else {
531
+ this.stepExplicit(effectiveDt);
532
+ }
533
+ this.simulationTime += effectiveDt;
534
+ this.stepCount++;
535
+ this.lastStepMs = performance.now() - t0;
536
+ }
537
+ /**
538
+ * Explicit forward Euler: T(n+1) = T(n) + dt * (α∇²T + Q/(ρcₚ))
539
+ */
540
+ stepExplicit(dt) {
541
+ const { nx, ny, nz } = this.temperature;
542
+ const rhoCp = this.material.density * this.material.specific_heat;
543
+ this.tempPrev.copy(this.temperature);
544
+ for (let k = 1; k < nz - 1; k++) {
545
+ for (let j = 1; j < ny - 1; j++) {
546
+ for (let i = 1; i < nx - 1; i++) {
547
+ const lap = this.tempPrev.laplacian(i, j, k);
548
+ const source = this.sourceField.get(i, j, k);
549
+ const dT = dt * (this.alpha * lap + source / rhoCp);
550
+ this.temperature.set(
551
+ i,
552
+ j,
553
+ k,
554
+ this.tempPrev.get(i, j, k) + dT
555
+ );
556
+ }
557
+ }
558
+ }
559
+ }
560
+ /**
561
+ * Implicit Jacobi: solve (I - dt·α·∇²)T(n+1) = T(n) + dt·Q/(ρcₚ)
562
+ */
563
+ stepImplicit(dt) {
564
+ const { nx, ny, nz } = this.temperature;
565
+ const rhoCp = this.material.density * this.material.specific_heat;
566
+ const rhs = this.tempPrev.clone();
567
+ for (let k = 0; k < nz; k++) {
568
+ for (let j = 0; j < ny; j++) {
569
+ for (let i = 0; i < nx; i++) {
570
+ const src = this.sourceField.get(i, j, k);
571
+ rhs.set(i, j, k, this.temperature.get(i, j, k) + dt * src / rhoCp);
572
+ }
573
+ }
574
+ }
575
+ const dx2 = this.temperature.dx * this.temperature.dx;
576
+ const alphaCoeff = dx2 / (dt * this.alpha);
577
+ const beta = 6 + alphaCoeff;
578
+ jacobiIteration(
579
+ this.temperature,
580
+ rhs,
581
+ alphaCoeff,
582
+ beta,
583
+ this.config.maxImplicitIterations ?? 100,
584
+ this.config.implicitTolerance ?? 1e-4
585
+ );
586
+ }
587
+ /**
588
+ * Rebuild the volumetric source field from config sources.
589
+ */
590
+ rebuildSourceField() {
591
+ this.sourceField.fill(0);
592
+ const { dx, dy, dz } = this.temperature;
593
+ const cellVolume = dx * dy * dz;
594
+ for (const src of this.config.sources) {
595
+ if (src.active === false) continue;
596
+ const [px, py, pz] = src.position;
597
+ const gi = Math.max(0, Math.min(this.temperature.nx - 1, Math.round(px / dx)));
598
+ const gj = Math.max(0, Math.min(this.temperature.ny - 1, Math.round(py / dy)));
599
+ const gk = Math.max(0, Math.min(this.temperature.nz - 1, Math.round(pz / dz)));
600
+ if (src.type === "point" || !src.radius) {
601
+ if (this.inBounds(gi, gj, gk)) {
602
+ this.sourceField.set(
603
+ gi,
604
+ gj,
605
+ gk,
606
+ this.sourceField.get(gi, gj, gk) + src.heat_output / cellVolume
607
+ );
608
+ }
609
+ } else {
610
+ const r = src.radius;
611
+ let totalCells = 0;
612
+ for (let dk = -r; dk <= r; dk++) {
613
+ for (let dj = -r; dj <= r; dj++) {
614
+ for (let di = -r; di <= r; di++) {
615
+ if (di * di + dj * dj + dk * dk <= r * r) {
616
+ if (this.inBounds(gi + di, gj + dj, gk + dk)) {
617
+ totalCells++;
618
+ }
619
+ }
620
+ }
621
+ }
622
+ }
623
+ if (totalCells === 0) continue;
624
+ const heatPerCell = src.heat_output / (totalCells * cellVolume);
625
+ for (let dk = -r; dk <= r; dk++) {
626
+ for (let dj = -r; dj <= r; dj++) {
627
+ for (let di = -r; di <= r; di++) {
628
+ if (di * di + dj * dj + dk * dk <= r * r) {
629
+ const ci = gi + di, cj = gj + dj, ck = gk + dk;
630
+ if (this.inBounds(ci, cj, ck)) {
631
+ this.sourceField.set(
632
+ ci,
633
+ cj,
634
+ ck,
635
+ this.sourceField.get(ci, cj, ck) + heatPerCell
636
+ );
637
+ }
638
+ }
639
+ }
640
+ }
641
+ }
642
+ }
643
+ }
644
+ }
645
+ inBounds(i, j, k) {
646
+ return i >= 0 && i < this.temperature.nx && j >= 0 && j < this.temperature.ny && k >= 0 && k < this.temperature.nz;
647
+ }
648
+ // ── Public API ──────────────────────────────────────────────────────────
649
+ /** Get the temperature field as Float32Array for ScalarFieldOverlay */
650
+ getTemperatureField() {
651
+ return this.temperature.toFloat32Array();
652
+ }
653
+ /** Get the underlying grid for coupling with other solvers */
654
+ getTemperatureGrid() {
655
+ return this.temperature;
656
+ }
657
+ /** Point query: temperature at world position via trilinear interpolation */
658
+ getTemperatureAt(x, y, z) {
659
+ const result = this.temperature.sampleAtPositions(
660
+ new Float32Array([x, y, z])
661
+ );
662
+ return result[0];
663
+ }
664
+ /** Update a heat source at runtime (e.g., HVAC on/off) */
665
+ setSource(id, heatOutput, active) {
666
+ const src = this.config.sources.find((s) => s.id === id);
667
+ if (src) {
668
+ src.heat_output = heatOutput;
669
+ if (active !== void 0) src.active = active;
670
+ this.rebuildSourceField();
671
+ }
672
+ }
673
+ /** Update boundary temperature (e.g., exterior weather change) */
674
+ setBoundaryValue(faceOrIndex, value) {
675
+ if (typeof faceOrIndex === "number") {
676
+ const bc = this.config.boundaryConditions[faceOrIndex];
677
+ if (bc) {
678
+ if (bc.type === "convection") bc.ambient = value;
679
+ else bc.value = value;
680
+ }
681
+ }
682
+ }
683
+ getStats() {
684
+ let min = Infinity, max = -Infinity, sum = 0;
685
+ const d = this.temperature.data;
686
+ for (let i = 0; i < d.length; i++) {
687
+ if (d[i] < min) min = d[i];
688
+ if (d[i] > max) max = d[i];
689
+ sum += d[i];
690
+ }
691
+ return {
692
+ minTemperature: min,
693
+ maxTemperature: max,
694
+ avgTemperature: sum / d.length,
695
+ simulationTime: this.simulationTime,
696
+ stepCount: this.stepCount,
697
+ isImplicit: this.useImplicit,
698
+ lastStepMs: this.lastStepMs
699
+ };
700
+ }
701
+ dispose() {
702
+ }
703
+ };
704
+
705
+ // src/simulation/StructuralSolver.ts
706
+ var StructuralSolver = class {
707
+ config;
708
+ material;
709
+ nodeCount;
710
+ elementCount;
711
+ dofCount;
712
+ // 3 DOF per node
713
+ displacements;
714
+ forces;
715
+ vonMisesStress;
716
+ // per-element
717
+ safetyFactors;
718
+ // per-element
719
+ constrainedDofs;
720
+ // Sparse stiffness: CSR-like via element assembly
721
+ elementStiffness;
722
+ // 12×12 per element
723
+ solveResult = null;
724
+ solveTimeMs = 0;
725
+ constructor(config) {
726
+ this.config = config;
727
+ this.material = typeof config.material === "string" ? getMaterial(config.material) : config.material;
728
+ this.nodeCount = config.vertices.length / 3;
729
+ this.elementCount = config.tetrahedra.length / 4;
730
+ this.dofCount = this.nodeCount * 3;
731
+ this.displacements = new Float32Array(this.dofCount);
732
+ this.forces = new Float32Array(this.dofCount);
733
+ this.vonMisesStress = new Float32Array(this.elementCount);
734
+ this.safetyFactors = new Float32Array(this.elementCount);
735
+ this.elementStiffness = [];
736
+ this.constrainedDofs = /* @__PURE__ */ new Set();
737
+ for (const c of config.constraints) {
738
+ for (const n of c.nodes) {
739
+ if (c.type === "fixed") {
740
+ this.constrainedDofs.add(n * 3);
741
+ this.constrainedDofs.add(n * 3 + 1);
742
+ this.constrainedDofs.add(n * 3 + 2);
743
+ } else if (c.type === "pinned") {
744
+ this.constrainedDofs.add(n * 3);
745
+ this.constrainedDofs.add(n * 3 + 1);
746
+ this.constrainedDofs.add(n * 3 + 2);
747
+ }
748
+ }
749
+ }
750
+ this.assembleStiffness();
751
+ this.assembleForces();
752
+ }
753
+ /**
754
+ * Solve the static equilibrium Ku = f.
755
+ */
756
+ solve() {
757
+ const t0 = performance.now();
758
+ const applyK = (x, out) => {
759
+ out.fill(0);
760
+ const tets = this.config.tetrahedra;
761
+ for (let e = 0; e < this.elementCount; e++) {
762
+ const ke = this.elementStiffness[e];
763
+ const nodes = [tets[e * 4], tets[e * 4 + 1], tets[e * 4 + 2], tets[e * 4 + 3]];
764
+ for (let a = 0; a < 4; a++) {
765
+ for (let ai = 0; ai < 3; ai++) {
766
+ const globalI = nodes[a] * 3 + ai;
767
+ const localI = a * 3 + ai;
768
+ let sum = 0;
769
+ for (let b = 0; b < 4; b++) {
770
+ for (let bi = 0; bi < 3; bi++) {
771
+ const globalJ = nodes[b] * 3 + bi;
772
+ const localJ = b * 3 + bi;
773
+ sum += ke[localI * 12 + localJ] * x[globalJ];
774
+ }
775
+ }
776
+ out[globalI] += sum;
777
+ }
778
+ }
779
+ }
780
+ for (const dof of this.constrainedDofs) {
781
+ out[dof] = x[dof];
782
+ }
783
+ };
784
+ const rhs = new Float32Array(this.forces);
785
+ for (const dof of this.constrainedDofs) {
786
+ rhs[dof] = 0;
787
+ }
788
+ this.displacements.fill(0);
789
+ this.solveResult = conjugateGradient(
790
+ applyK,
791
+ rhs,
792
+ this.displacements,
793
+ this.config.maxIterations ?? 1e3,
794
+ this.config.tolerance ?? 1e-8
795
+ );
796
+ for (const dof of this.constrainedDofs) {
797
+ this.displacements[dof] = 0;
798
+ }
799
+ this.recoverStress();
800
+ this.solveTimeMs = performance.now() - t0;
801
+ return this.solveResult;
802
+ }
803
+ /**
804
+ * Assemble element stiffness matrices using linear tetrahedral elements.
805
+ * Ke = V * Bᵀ * D * B where B is the strain-displacement matrix.
806
+ */
807
+ assembleStiffness() {
808
+ const verts = this.config.vertices;
809
+ const tets = this.config.tetrahedra;
810
+ const { youngs_modulus: E, poisson_ratio: nu } = this.material;
811
+ const lambda = E * nu / ((1 + nu) * (1 - 2 * nu));
812
+ const mu = E / (2 * (1 + nu));
813
+ const D = new Float64Array(36);
814
+ D[0] = D[7] = D[14] = lambda + 2 * mu;
815
+ D[1] = D[2] = D[6] = D[8] = D[12] = D[13] = lambda;
816
+ D[21] = D[28] = D[35] = mu;
817
+ this.elementStiffness = new Array(this.elementCount);
818
+ for (let e = 0; e < this.elementCount; e++) {
819
+ const n0 = tets[e * 4], n1 = tets[e * 4 + 1];
820
+ const n2 = tets[e * 4 + 2], n3 = tets[e * 4 + 3];
821
+ const x0 = verts[n0 * 3], y0 = verts[n0 * 3 + 1], z0 = verts[n0 * 3 + 2];
822
+ const x1 = verts[n1 * 3], y1 = verts[n1 * 3 + 1], z1 = verts[n1 * 3 + 2];
823
+ const x2 = verts[n2 * 3], y2 = verts[n2 * 3 + 1], z2 = verts[n2 * 3 + 2];
824
+ const x3 = verts[n3 * 3], y3 = verts[n3 * 3 + 1], z3 = verts[n3 * 3 + 2];
825
+ const J = [
826
+ x1 - x0,
827
+ y1 - y0,
828
+ z1 - z0,
829
+ x2 - x0,
830
+ y2 - y0,
831
+ z2 - z0,
832
+ x3 - x0,
833
+ y3 - y0,
834
+ z3 - z0
835
+ ];
836
+ const detJ = J[0] * (J[4] * J[8] - J[5] * J[7]) - J[1] * (J[3] * J[8] - J[5] * J[6]) + J[2] * (J[3] * J[7] - J[4] * J[6]);
837
+ const V = Math.abs(detJ) / 6;
838
+ if (V < 1e-20) {
839
+ this.elementStiffness[e] = new Float64Array(144);
840
+ continue;
841
+ }
842
+ const invDetJ = 1 / detJ;
843
+ const Ji = [
844
+ (J[4] * J[8] - J[5] * J[7]) * invDetJ,
845
+ (J[2] * J[7] - J[1] * J[8]) * invDetJ,
846
+ (J[1] * J[5] - J[2] * J[4]) * invDetJ,
847
+ (J[5] * J[6] - J[3] * J[8]) * invDetJ,
848
+ (J[0] * J[8] - J[2] * J[6]) * invDetJ,
849
+ (J[2] * J[3] - J[0] * J[5]) * invDetJ,
850
+ (J[3] * J[7] - J[4] * J[6]) * invDetJ,
851
+ (J[1] * J[6] - J[0] * J[7]) * invDetJ,
852
+ (J[0] * J[4] - J[1] * J[3]) * invDetJ
853
+ ];
854
+ const dN = new Float64Array(12);
855
+ dN[3] = Ji[0];
856
+ dN[4] = Ji[1];
857
+ dN[5] = Ji[2];
858
+ dN[6] = Ji[3];
859
+ dN[7] = Ji[4];
860
+ dN[8] = Ji[5];
861
+ dN[9] = Ji[6];
862
+ dN[10] = Ji[7];
863
+ dN[11] = Ji[8];
864
+ dN[0] = -(dN[3] + dN[6] + dN[9]);
865
+ dN[1] = -(dN[4] + dN[7] + dN[10]);
866
+ dN[2] = -(dN[5] + dN[8] + dN[11]);
867
+ const B = new Float64Array(72);
868
+ for (let a = 0; a < 4; a++) {
869
+ const dnx = dN[a * 3], dny = dN[a * 3 + 1], dnz = dN[a * 3 + 2];
870
+ const col = a * 3;
871
+ B[0 * 12 + col] = dnx;
872
+ B[1 * 12 + col + 1] = dny;
873
+ B[2 * 12 + col + 2] = dnz;
874
+ B[3 * 12 + col] = dny;
875
+ B[3 * 12 + col + 1] = dnx;
876
+ B[4 * 12 + col + 1] = dnz;
877
+ B[4 * 12 + col + 2] = dny;
878
+ B[5 * 12 + col] = dnz;
879
+ B[5 * 12 + col + 2] = dnx;
880
+ }
881
+ const ke = new Float64Array(144);
882
+ const DB = new Float64Array(72);
883
+ for (let i = 0; i < 6; i++) {
884
+ for (let j = 0; j < 12; j++) {
885
+ let sum = 0;
886
+ for (let k = 0; k < 6; k++) {
887
+ sum += D[i * 6 + k] * B[k * 12 + j];
888
+ }
889
+ DB[i * 12 + j] = sum;
890
+ }
891
+ }
892
+ for (let i = 0; i < 12; i++) {
893
+ for (let j = 0; j < 12; j++) {
894
+ let sum = 0;
895
+ for (let k = 0; k < 6; k++) {
896
+ sum += B[k * 12 + i] * DB[k * 12 + j];
897
+ }
898
+ ke[i * 12 + j] = V * sum;
899
+ }
900
+ }
901
+ this.elementStiffness[e] = ke;
902
+ }
903
+ }
904
+ /**
905
+ * Assemble the global force vector from loads.
906
+ */
907
+ assembleForces() {
908
+ this.forces.fill(0);
909
+ const { density } = this.material;
910
+ for (const load of this.config.loads) {
911
+ switch (load.type) {
912
+ case "gravity": {
913
+ const [ax, ay, az] = load.acceleration ?? [0, -9.81, 0];
914
+ const tets = this.config.tetrahedra;
915
+ const verts = this.config.vertices;
916
+ for (let e = 0; e < this.elementCount; e++) {
917
+ const nodes = [tets[e * 4], tets[e * 4 + 1], tets[e * 4 + 2], tets[e * 4 + 3]];
918
+ const n0 = nodes[0], n1 = nodes[1], n2 = nodes[2], n3 = nodes[3];
919
+ const dx1 = verts[n1 * 3] - verts[n0 * 3], dy1 = verts[n1 * 3 + 1] - verts[n0 * 3 + 1], dz1 = verts[n1 * 3 + 2] - verts[n0 * 3 + 2];
920
+ const dx2 = verts[n2 * 3] - verts[n0 * 3], dy2 = verts[n2 * 3 + 1] - verts[n0 * 3 + 1], dz2 = verts[n2 * 3 + 2] - verts[n0 * 3 + 2];
921
+ const dx3 = verts[n3 * 3] - verts[n0 * 3], dy3 = verts[n3 * 3 + 1] - verts[n0 * 3 + 1], dz3 = verts[n3 * 3 + 2] - verts[n0 * 3 + 2];
922
+ const vol = Math.abs(
923
+ dx1 * (dy2 * dz3 - dz2 * dy3) - dy1 * (dx2 * dz3 - dz2 * dx3) + dz1 * (dx2 * dy3 - dy2 * dx3)
924
+ ) / 6;
925
+ const massPerNode = density * vol / 4;
926
+ for (const n of nodes) {
927
+ this.forces[n * 3] += massPerNode * ax;
928
+ this.forces[n * 3 + 1] += massPerNode * ay;
929
+ this.forces[n * 3 + 2] += massPerNode * az;
930
+ }
931
+ }
932
+ break;
933
+ }
934
+ case "point": {
935
+ if (load.nodeIndex !== void 0 && load.force) {
936
+ const n = load.nodeIndex;
937
+ this.forces[n * 3] += load.force[0];
938
+ this.forces[n * 3 + 1] += load.force[1];
939
+ this.forces[n * 3 + 2] += load.force[2];
940
+ }
941
+ break;
942
+ }
943
+ case "distributed": {
944
+ if (load.surfaceElements && load.pressure) {
945
+ for (const faceIdx of load.surfaceElements) {
946
+ const tets = this.config.tetrahedra;
947
+ const verts = this.config.vertices;
948
+ const n0 = tets[faceIdx * 4], n1 = tets[faceIdx * 4 + 1], n2 = tets[faceIdx * 4 + 2];
949
+ const ax = verts[n1 * 3] - verts[n0 * 3], ay = verts[n1 * 3 + 1] - verts[n0 * 3 + 1], az = verts[n1 * 3 + 2] - verts[n0 * 3 + 2];
950
+ const bx = verts[n2 * 3] - verts[n0 * 3], by = verts[n2 * 3 + 1] - verts[n0 * 3 + 1], bz = verts[n2 * 3 + 2] - verts[n0 * 3 + 2];
951
+ const nx = ay * bz - az * by, ny = az * bx - ax * bz, nz = ax * by - ay * bx;
952
+ const area = 0.5 * Math.sqrt(nx * nx + ny * ny + nz * nz);
953
+ if (area < 1e-20) continue;
954
+ const scale = load.pressure * area / (3 * Math.sqrt(nx * nx + ny * ny + nz * nz));
955
+ for (const n of [n0, n1, n2]) {
956
+ this.forces[n * 3] += scale * nx;
957
+ this.forces[n * 3 + 1] += scale * ny;
958
+ this.forces[n * 3 + 2] += scale * nz;
959
+ }
960
+ }
961
+ }
962
+ break;
963
+ }
964
+ }
965
+ }
966
+ }
967
+ /**
968
+ * Recover Von Mises stress from displacements.
969
+ * σ = D * B * u → Von Mises = √(σxx²+σyy²+σzz²-σxx·σyy-σyy·σzz-σzz·σxx+3(τxy²+τyz²+τxz²))
970
+ */
971
+ recoverStress() {
972
+ const tets = this.config.tetrahedra;
973
+ const verts = this.config.vertices;
974
+ const { youngs_modulus: E, poisson_ratio: nu, yield_strength: Sy } = this.material;
975
+ const lambda = E * nu / ((1 + nu) * (1 - 2 * nu));
976
+ const mu = E / (2 * (1 + nu));
977
+ const D = new Float64Array(36);
978
+ D[0] = D[7] = D[14] = lambda + 2 * mu;
979
+ D[1] = D[2] = D[6] = D[8] = D[12] = D[13] = lambda;
980
+ D[21] = D[28] = D[35] = mu;
981
+ for (let e = 0; e < this.elementCount; e++) {
982
+ const n0 = tets[e * 4], n1 = tets[e * 4 + 1];
983
+ const n2 = tets[e * 4 + 2], n3 = tets[e * 4 + 3];
984
+ const x0 = verts[n0 * 3], y0 = verts[n0 * 3 + 1], z0 = verts[n0 * 3 + 2];
985
+ const J = [
986
+ verts[n1 * 3] - x0,
987
+ verts[n1 * 3 + 1] - y0,
988
+ verts[n1 * 3 + 2] - z0,
989
+ verts[n2 * 3] - x0,
990
+ verts[n2 * 3 + 1] - y0,
991
+ verts[n2 * 3 + 2] - z0,
992
+ verts[n3 * 3] - x0,
993
+ verts[n3 * 3 + 1] - y0,
994
+ verts[n3 * 3 + 2] - z0
995
+ ];
996
+ const detJ = J[0] * (J[4] * J[8] - J[5] * J[7]) - J[1] * (J[3] * J[8] - J[5] * J[6]) + J[2] * (J[3] * J[7] - J[4] * J[6]);
997
+ if (Math.abs(detJ) < 1e-20) {
998
+ this.vonMisesStress[e] = 0;
999
+ this.safetyFactors[e] = Sy > 0 ? Infinity : 0;
1000
+ continue;
1001
+ }
1002
+ const invDetJ = 1 / detJ;
1003
+ const Ji = [
1004
+ (J[4] * J[8] - J[5] * J[7]) * invDetJ,
1005
+ (J[2] * J[7] - J[1] * J[8]) * invDetJ,
1006
+ (J[1] * J[5] - J[2] * J[4]) * invDetJ,
1007
+ (J[5] * J[6] - J[3] * J[8]) * invDetJ,
1008
+ (J[0] * J[8] - J[2] * J[6]) * invDetJ,
1009
+ (J[2] * J[3] - J[0] * J[5]) * invDetJ,
1010
+ (J[3] * J[7] - J[4] * J[6]) * invDetJ,
1011
+ (J[1] * J[6] - J[0] * J[7]) * invDetJ,
1012
+ (J[0] * J[4] - J[1] * J[3]) * invDetJ
1013
+ ];
1014
+ const dN = new Float64Array(12);
1015
+ dN[3] = Ji[0];
1016
+ dN[4] = Ji[1];
1017
+ dN[5] = Ji[2];
1018
+ dN[6] = Ji[3];
1019
+ dN[7] = Ji[4];
1020
+ dN[8] = Ji[5];
1021
+ dN[9] = Ji[6];
1022
+ dN[10] = Ji[7];
1023
+ dN[11] = Ji[8];
1024
+ dN[0] = -(dN[3] + dN[6] + dN[9]);
1025
+ dN[1] = -(dN[4] + dN[7] + dN[10]);
1026
+ dN[2] = -(dN[5] + dN[8] + dN[11]);
1027
+ const nodes = [n0, n1, n2, n3];
1028
+ const strain = new Float64Array(6);
1029
+ for (let a = 0; a < 4; a++) {
1030
+ const dnx = dN[a * 3], dny = dN[a * 3 + 1], dnz = dN[a * 3 + 2];
1031
+ const ux = this.displacements[nodes[a] * 3];
1032
+ const uy = this.displacements[nodes[a] * 3 + 1];
1033
+ const uz = this.displacements[nodes[a] * 3 + 2];
1034
+ strain[0] += dnx * ux;
1035
+ strain[1] += dny * uy;
1036
+ strain[2] += dnz * uz;
1037
+ strain[3] += dny * ux + dnx * uy;
1038
+ strain[4] += dnz * uy + dny * uz;
1039
+ strain[5] += dnz * ux + dnx * uz;
1040
+ }
1041
+ const stress = new Float64Array(6);
1042
+ for (let i = 0; i < 6; i++) {
1043
+ for (let j = 0; j < 6; j++) {
1044
+ stress[i] += D[i * 6 + j] * strain[j];
1045
+ }
1046
+ }
1047
+ const sxx = stress[0], syy = stress[1], szz = stress[2];
1048
+ const txy = stress[3], tyz = stress[4], txz = stress[5];
1049
+ const vm = Math.sqrt(
1050
+ sxx * sxx + syy * syy + szz * szz - sxx * syy - syy * szz - szz * sxx + 3 * (txy * txy + tyz * tyz + txz * txz)
1051
+ );
1052
+ this.vonMisesStress[e] = vm;
1053
+ this.safetyFactors[e] = Sy > 0 ? Sy / Math.max(vm, 1e-20) : Infinity;
1054
+ }
1055
+ }
1056
+ // ── Public API ──────────────────────────────────────────────────────────
1057
+ getVonMisesStress() {
1058
+ return this.vonMisesStress;
1059
+ }
1060
+ getSafetyFactor() {
1061
+ return this.safetyFactors;
1062
+ }
1063
+ getDisplacements() {
1064
+ return this.displacements;
1065
+ }
1066
+ /** Update a load and re-solve */
1067
+ updateLoad(id, force) {
1068
+ const load = this.config.loads.find((l) => l.id === id);
1069
+ if (load) {
1070
+ load.force = force;
1071
+ this.assembleForces();
1072
+ }
1073
+ }
1074
+ getStats() {
1075
+ let maxVM = 0, minSF = Infinity;
1076
+ for (let e = 0; e < this.elementCount; e++) {
1077
+ if (this.vonMisesStress[e] > maxVM) maxVM = this.vonMisesStress[e];
1078
+ if (this.safetyFactors[e] < minSF) minSF = this.safetyFactors[e];
1079
+ }
1080
+ return {
1081
+ nodeCount: this.nodeCount,
1082
+ elementCount: this.elementCount,
1083
+ maxVonMises: maxVM,
1084
+ minSafetyFactor: minSF,
1085
+ solveResult: this.solveResult,
1086
+ solveTimeMs: this.solveTimeMs
1087
+ };
1088
+ }
1089
+ dispose() {
1090
+ this.elementStiffness = [];
1091
+ }
1092
+ };
1093
+
1094
+ // src/simulation/HydraulicSolver.ts
1095
+ var HydraulicSolver = class {
1096
+ config;
1097
+ pipes;
1098
+ nodes;
1099
+ nodeMap;
1100
+ pipeMap;
1101
+ loops;
1102
+ // arrays of pipe indices per loop
1103
+ pressures;
1104
+ flowRates;
1105
+ solveResult = null;
1106
+ viscosity;
1107
+ constructor(config) {
1108
+ this.config = config;
1109
+ this.viscosity = config.viscosity ?? 1004e-9;
1110
+ this.nodeMap = /* @__PURE__ */ new Map();
1111
+ this.nodes = config.nodes.map((n, i) => {
1112
+ this.nodeMap.set(n.id, i);
1113
+ return {
1114
+ index: i,
1115
+ config: n,
1116
+ head: n.type === "reservoir" ? n.head ?? 0 : n.elevation ?? 0
1117
+ };
1118
+ });
1119
+ this.pipeMap = /* @__PURE__ */ new Map();
1120
+ this.pipes = [];
1121
+ for (const conn of config.connections) {
1122
+ const [fromId, pipeId, toId] = conn;
1123
+ const pipeConfig = config.pipes.find((p) => p.id === pipeId);
1124
+ if (!pipeConfig) continue;
1125
+ const fromIdx = this.nodeMap.get(fromId);
1126
+ const toIdx = this.nodeMap.get(toId);
1127
+ if (fromIdx === void 0 || toIdx === void 0) continue;
1128
+ const idx = this.pipes.length;
1129
+ this.pipeMap.set(pipeId, idx);
1130
+ const valve = config.valves.find((v) => v.pipe === pipeId);
1131
+ const opening = valve?.opening ?? 1;
1132
+ const effectiveD = pipeConfig.diameter * Math.sqrt(Math.max(opening, 1e-3));
1133
+ this.pipes.push({
1134
+ index: idx,
1135
+ config: pipeConfig,
1136
+ fromNode: fromIdx,
1137
+ toNode: toIdx,
1138
+ flowRate: 1e-3,
1139
+ // initial guess
1140
+ effectiveDiameter: effectiveD
1141
+ });
1142
+ }
1143
+ this.pressures = new Float32Array(this.nodes.length);
1144
+ this.flowRates = new Float32Array(this.pipes.length);
1145
+ this.loops = this.findLoops();
1146
+ this.initialFlowGuess();
1147
+ }
1148
+ /**
1149
+ * Solve the pipe network for steady-state pressures and flow rates.
1150
+ */
1151
+ solve() {
1152
+ const maxIter = this.config.maxIterations;
1153
+ const tol = this.config.convergence;
1154
+ let maxCorrection = 0;
1155
+ let iter = 0;
1156
+ for (iter = 0; iter < maxIter; iter++) {
1157
+ maxCorrection = 0;
1158
+ for (const loop of this.loops) {
1159
+ let sumHf = 0;
1160
+ let sumDhf = 0;
1161
+ for (const pipeIdx of loop) {
1162
+ const pipe = this.pipes[pipeIdx];
1163
+ const hf = this.headLoss(pipe);
1164
+ const Q = pipe.flowRate;
1165
+ sumHf += hf;
1166
+ sumDhf += Math.abs(Q) > 1e-12 ? 2 * Math.abs(hf) / Math.abs(Q) : 0;
1167
+ }
1168
+ if (Math.abs(sumDhf) < 1e-20) continue;
1169
+ const dQ = -sumHf / sumDhf;
1170
+ maxCorrection = Math.max(maxCorrection, Math.abs(dQ));
1171
+ for (const pipeIdx of loop) {
1172
+ this.pipes[pipeIdx].flowRate += dQ;
1173
+ }
1174
+ }
1175
+ if (maxCorrection < tol) {
1176
+ this.computeNodePressures();
1177
+ this.updateOutputArrays();
1178
+ this.solveResult = { converged: true, iterations: iter + 1, residual: maxCorrection, maxChange: maxCorrection };
1179
+ return this.solveResult;
1180
+ }
1181
+ }
1182
+ this.computeNodePressures();
1183
+ this.updateOutputArrays();
1184
+ this.solveResult = { converged: false, iterations: iter, residual: maxCorrection, maxChange: maxCorrection };
1185
+ return this.solveResult;
1186
+ }
1187
+ /**
1188
+ * Head loss in a pipe using Darcy-Weisbach: hf = f * (L/D) * (V²/2g)
1189
+ * Sign follows flow direction.
1190
+ */
1191
+ headLoss(pipe) {
1192
+ const Q = pipe.flowRate;
1193
+ const D = pipe.effectiveDiameter;
1194
+ const L = pipe.config.length;
1195
+ const A = Math.PI / 4 * D * D;
1196
+ const V = Q / A;
1197
+ const Re = Math.abs(V) * D / this.viscosity;
1198
+ let f;
1199
+ if (Re < 2300) {
1200
+ f = Re > 0 ? 64 / Re : 0;
1201
+ } else {
1202
+ const eD = pipe.config.roughness / D;
1203
+ const logArg = eD / 3.7 + 5.74 / Math.pow(Re, 0.9);
1204
+ const logVal = Math.log10(logArg);
1205
+ f = 0.25 / (logVal * logVal);
1206
+ }
1207
+ const g = 9.81;
1208
+ return f * L * V * Math.abs(V) / (D * 2 * g);
1209
+ }
1210
+ /**
1211
+ * Compute node pressures by walking from known-head nodes.
1212
+ */
1213
+ computeNodePressures() {
1214
+ const visited = /* @__PURE__ */ new Set();
1215
+ const queue = [];
1216
+ for (const node of this.nodes) {
1217
+ if (node.config.type === "reservoir") {
1218
+ node.head = node.config.head ?? 0;
1219
+ visited.add(node.index);
1220
+ queue.push(node.index);
1221
+ }
1222
+ }
1223
+ while (queue.length > 0) {
1224
+ const nodeIdx = queue.shift();
1225
+ const nodeHead = this.nodes[nodeIdx].head;
1226
+ for (const pipe of this.pipes) {
1227
+ let neighborIdx = null;
1228
+ let sign = 1;
1229
+ if (pipe.fromNode === nodeIdx && !visited.has(pipe.toNode)) {
1230
+ neighborIdx = pipe.toNode;
1231
+ sign = 1;
1232
+ } else if (pipe.toNode === nodeIdx && !visited.has(pipe.fromNode)) {
1233
+ neighborIdx = pipe.fromNode;
1234
+ sign = -1;
1235
+ }
1236
+ if (neighborIdx !== null) {
1237
+ const hf = this.headLoss(pipe);
1238
+ this.nodes[neighborIdx].head = nodeHead - sign * hf;
1239
+ visited.add(neighborIdx);
1240
+ queue.push(neighborIdx);
1241
+ }
1242
+ }
1243
+ }
1244
+ }
1245
+ /**
1246
+ * Initial flow guess by distributing demand.
1247
+ */
1248
+ initialFlowGuess() {
1249
+ const totalDemand = this.nodes.reduce(
1250
+ (sum, n) => sum + (n.config.demand ?? 0),
1251
+ 0
1252
+ );
1253
+ const avgFlow = totalDemand / Math.max(this.pipes.length, 1);
1254
+ for (const pipe of this.pipes) {
1255
+ pipe.flowRate = avgFlow > 0 ? avgFlow : 1e-3;
1256
+ }
1257
+ }
1258
+ /**
1259
+ * Find independent loops using spanning tree fundamental cycles.
1260
+ *
1261
+ * Uses BFS to build a spanning tree, then each non-tree edge defines
1262
+ * exactly one fundamental cycle. This guarantees independent loops
1263
+ * (no duplicates or overlaps) which Hardy-Cross requires.
1264
+ */
1265
+ findLoops() {
1266
+ const loops = [];
1267
+ const n = this.nodes.length;
1268
+ if (n === 0) return loops;
1269
+ const adj = Array.from({ length: n }, () => []);
1270
+ for (const pipe of this.pipes) {
1271
+ adj[pipe.fromNode].push([pipe.toNode, pipe.index]);
1272
+ adj[pipe.toNode].push([pipe.fromNode, pipe.index]);
1273
+ }
1274
+ const treeParent = /* @__PURE__ */ new Map();
1275
+ const visited = /* @__PURE__ */ new Set();
1276
+ const treeEdges = /* @__PURE__ */ new Set();
1277
+ const bfs = (start) => {
1278
+ const queue = [start];
1279
+ visited.add(start);
1280
+ while (queue.length > 0) {
1281
+ const node = queue.shift();
1282
+ for (const [neighbor, pipeIdx] of adj[node]) {
1283
+ if (!visited.has(neighbor)) {
1284
+ visited.add(neighbor);
1285
+ treeParent.set(neighbor, { node, pipe: pipeIdx });
1286
+ treeEdges.add(pipeIdx);
1287
+ queue.push(neighbor);
1288
+ }
1289
+ }
1290
+ }
1291
+ };
1292
+ for (let i = 0; i < n; i++) {
1293
+ if (!visited.has(i)) bfs(i);
1294
+ }
1295
+ for (const pipe of this.pipes) {
1296
+ if (treeEdges.has(pipe.index)) continue;
1297
+ const pathA = this.traceToRoot(pipe.fromNode, treeParent);
1298
+ const pathB = this.traceToRoot(pipe.toNode, treeParent);
1299
+ const setA = new Set(pathA.map((p) => p.node));
1300
+ let lcaIdx = 0;
1301
+ for (let i = 0; i < pathB.length; i++) {
1302
+ if (setA.has(pathB[i].node)) {
1303
+ lcaIdx = i;
1304
+ break;
1305
+ }
1306
+ }
1307
+ const lcaNode = pathB[lcaIdx].node;
1308
+ const loopPipes = [pipe.index];
1309
+ for (const entry of pathA) {
1310
+ if (entry.node === lcaNode) break;
1311
+ if (entry.pipe >= 0) loopPipes.push(entry.pipe);
1312
+ }
1313
+ for (let i = 0; i < lcaIdx; i++) {
1314
+ if (pathB[i].pipe >= 0) loopPipes.push(pathB[i].pipe);
1315
+ }
1316
+ if (loopPipes.length > 1) loops.push(loopPipes);
1317
+ }
1318
+ return loops;
1319
+ }
1320
+ /** Trace a node to root of spanning tree, returning (node, pipe) pairs */
1321
+ traceToRoot(start, treeParent) {
1322
+ const path = [{ node: start, pipe: -1 }];
1323
+ let current = start;
1324
+ while (treeParent.has(current)) {
1325
+ const parent = treeParent.get(current);
1326
+ path.push({ node: parent.node, pipe: parent.pipe });
1327
+ current = parent.node;
1328
+ }
1329
+ return path;
1330
+ }
1331
+ updateOutputArrays() {
1332
+ for (const node of this.nodes) {
1333
+ this.pressures[node.index] = node.head;
1334
+ }
1335
+ for (const pipe of this.pipes) {
1336
+ this.flowRates[pipe.index] = pipe.flowRate;
1337
+ }
1338
+ }
1339
+ // ── Public API ──────────────────────────────────────────────────────────
1340
+ getPressureField() {
1341
+ return this.pressures;
1342
+ }
1343
+ getFlowRates() {
1344
+ return this.flowRates;
1345
+ }
1346
+ setValveOpening(id, opening) {
1347
+ const valve = this.config.valves.find((v) => v.id === id);
1348
+ if (valve) {
1349
+ valve.opening = opening;
1350
+ const pipeIdx = this.pipeMap.get(valve.pipe);
1351
+ if (pipeIdx !== void 0) {
1352
+ const pipe = this.pipes[pipeIdx];
1353
+ pipe.effectiveDiameter = pipe.config.diameter * Math.sqrt(Math.max(opening, 1e-3));
1354
+ }
1355
+ }
1356
+ }
1357
+ setDemand(nodeId, demand) {
1358
+ const idx = this.nodeMap.get(nodeId);
1359
+ if (idx !== void 0) {
1360
+ this.nodes[idx].config.demand = demand;
1361
+ }
1362
+ }
1363
+ setPumpPressure(nodeId, head) {
1364
+ const idx = this.nodeMap.get(nodeId);
1365
+ if (idx !== void 0 && this.nodes[idx].config.type === "reservoir") {
1366
+ this.nodes[idx].config.head = head;
1367
+ this.nodes[idx].head = head;
1368
+ }
1369
+ }
1370
+ getStats() {
1371
+ let maxP = -Infinity, minP = Infinity, totalD = 0;
1372
+ for (const node of this.nodes) {
1373
+ if (node.head > maxP) maxP = node.head;
1374
+ if (node.head < minP) minP = node.head;
1375
+ totalD += node.config.demand ?? 0;
1376
+ }
1377
+ return {
1378
+ nodeCount: this.nodes.length,
1379
+ pipeCount: this.pipes.length,
1380
+ loopCount: this.loops.length,
1381
+ maxPressure: maxP,
1382
+ minPressure: minP,
1383
+ totalDemand: totalD,
1384
+ solveResult: this.solveResult
1385
+ };
1386
+ }
1387
+ dispose() {
1388
+ this.pipes = [];
1389
+ this.nodes = [];
1390
+ }
1391
+ };
1392
+
1393
+ // src/simulation/SaturationManager.ts
1394
+ var SaturationManager = class {
1395
+ config;
1396
+ cellStates;
1397
+ // 0=normal, 1=warning, 2=critical
1398
+ phaseTransitionCells;
1399
+ cellCount;
1400
+ constructor(config) {
1401
+ this.config = config;
1402
+ this.cellCount = config.field instanceof RegularGrid3D ? config.field.cellCount : config.field.length;
1403
+ this.cellStates = new Uint8Array(this.cellCount);
1404
+ this.phaseTransitionCells = /* @__PURE__ */ new Set();
1405
+ }
1406
+ /**
1407
+ * Check all cells against thresholds. Returns events for cells that changed state.
1408
+ */
1409
+ update() {
1410
+ const events = [];
1411
+ const { warning, critical, recovery } = this.config.thresholds;
1412
+ const field = this.config.field instanceof RegularGrid3D ? this.config.field.data : this.config.field;
1413
+ for (let i = 0; i < this.cellCount; i++) {
1414
+ const value = field[i];
1415
+ const prevState = this.cellStates[i];
1416
+ let newState = prevState;
1417
+ if (prevState === 0) {
1418
+ if (value >= critical) newState = 2;
1419
+ else if (value >= warning) newState = 1;
1420
+ } else if (prevState === 1) {
1421
+ if (value >= critical) newState = 2;
1422
+ else if (value < recovery) newState = 0;
1423
+ } else {
1424
+ if (value < recovery) newState = 0;
1425
+ else if (value < warning) newState = 1;
1426
+ }
1427
+ if (newState !== prevState) {
1428
+ this.cellStates[i] = newState;
1429
+ let phaseTransition = false;
1430
+ if (this.config.phaseTransition) {
1431
+ const pt = this.config.phaseTransition;
1432
+ if (prevState < 2 && newState === 2 && value >= pt.transitionPoint || prevState === 2 && newState < 2 && value < pt.transitionPoint) {
1433
+ phaseTransition = true;
1434
+ if (newState === 2) {
1435
+ this.phaseTransitionCells.add(i);
1436
+ } else {
1437
+ this.phaseTransitionCells.delete(i);
1438
+ }
1439
+ }
1440
+ }
1441
+ events.push({
1442
+ index: i,
1443
+ from: stateLabel(prevState),
1444
+ to: stateLabel(newState),
1445
+ value,
1446
+ type: this.config.type,
1447
+ phaseTransition
1448
+ });
1449
+ }
1450
+ }
1451
+ return events;
1452
+ }
1453
+ /** Get per-cell state field: 0=normal, 1=warning, 2=critical */
1454
+ getStateField() {
1455
+ return this.cellStates;
1456
+ }
1457
+ /** Fraction of cells at or above warning level */
1458
+ getSaturationFraction() {
1459
+ let count = 0;
1460
+ for (let i = 0; i < this.cellCount; i++) {
1461
+ if (this.cellStates[i] > 0) count++;
1462
+ }
1463
+ return count / this.cellCount;
1464
+ }
1465
+ /** Whether any cell is currently undergoing a phase transition */
1466
+ isPhaseTransitionActive() {
1467
+ return this.phaseTransitionCells.size > 0;
1468
+ }
1469
+ getStats() {
1470
+ let normal = 0, warning = 0, critical = 0;
1471
+ for (let i = 0; i < this.cellCount; i++) {
1472
+ switch (this.cellStates[i]) {
1473
+ case 0:
1474
+ normal++;
1475
+ break;
1476
+ case 1:
1477
+ warning++;
1478
+ break;
1479
+ case 2:
1480
+ critical++;
1481
+ break;
1482
+ }
1483
+ }
1484
+ return {
1485
+ totalCells: this.cellCount,
1486
+ normalCount: normal,
1487
+ warningCount: warning,
1488
+ criticalCount: critical,
1489
+ saturationFraction: (warning + critical) / this.cellCount,
1490
+ phaseTransitionActive: this.phaseTransitionCells.size > 0,
1491
+ phaseTransitionCells: this.phaseTransitionCells.size
1492
+ };
1493
+ }
1494
+ /** Update the monitored field reference (e.g., after solver re-allocation) */
1495
+ setField(field) {
1496
+ this.config.field = field;
1497
+ const newCount = field instanceof RegularGrid3D ? field.cellCount : field.length;
1498
+ if (newCount !== this.cellCount) {
1499
+ this.cellCount = newCount;
1500
+ this.cellStates = new Uint8Array(newCount);
1501
+ this.phaseTransitionCells.clear();
1502
+ }
1503
+ }
1504
+ /** Update thresholds at runtime */
1505
+ setThresholds(thresholds) {
1506
+ Object.assign(this.config.thresholds, thresholds);
1507
+ }
1508
+ dispose() {
1509
+ this.phaseTransitionCells.clear();
1510
+ }
1511
+ };
1512
+ function stateLabel(s) {
1513
+ return s === 0 ? "normal" : s === 1 ? "warning" : "critical";
1514
+ }
1515
+
1516
+ // src/simulation/CouplingManager.ts
1517
+ var CouplingManager = class {
1518
+ solvers = /* @__PURE__ */ new Map();
1519
+ couplings = [];
1520
+ saturationManagers = [];
1521
+ lastEvents = [];
1522
+ totalEvents = 0;
1523
+ lastStepMs = 0;
1524
+ /**
1525
+ * Register a solver with a unique name.
1526
+ */
1527
+ registerSolver(name, type, solver) {
1528
+ this.solvers.set(name, { type, solver });
1529
+ }
1530
+ /**
1531
+ * Add a field coupling between two solvers.
1532
+ */
1533
+ addCoupling(coupling) {
1534
+ this.couplings.push({ enabled: true, ...coupling });
1535
+ }
1536
+ /**
1537
+ * Add a saturation monitor on a solver's field.
1538
+ */
1539
+ addSaturationMonitor(monitor) {
1540
+ this.saturationManagers.push(monitor);
1541
+ }
1542
+ /**
1543
+ * Step all solvers, transfer coupled fields, check saturation.
1544
+ *
1545
+ * Order:
1546
+ * 1. Step all time-dependent solvers (thermal)
1547
+ * 2. Transfer coupled fields (thermal → structural, etc.)
1548
+ * 3. Re-solve steady-state solvers if inputs changed (structural, hydraulic)
1549
+ * 4. Check saturation thresholds
1550
+ */
1551
+ step(dt) {
1552
+ const t0 = performance.now();
1553
+ this.lastEvents = [];
1554
+ for (const [, entry] of this.solvers) {
1555
+ if (entry.type === "thermal") {
1556
+ entry.solver.step(dt);
1557
+ }
1558
+ }
1559
+ for (const coupling of this.couplings) {
1560
+ if (coupling.enabled === false) continue;
1561
+ this.transferField(coupling);
1562
+ }
1563
+ for (const [, entry] of this.solvers) {
1564
+ if (entry.type === "structural") {
1565
+ entry.solver.solve();
1566
+ } else if (entry.type === "hydraulic") {
1567
+ entry.solver.solve();
1568
+ }
1569
+ }
1570
+ for (const monitor of this.saturationManagers) {
1571
+ const events = monitor.update();
1572
+ this.lastEvents.push(...events);
1573
+ }
1574
+ this.totalEvents += this.lastEvents.length;
1575
+ this.lastStepMs = performance.now() - t0;
1576
+ return this.lastEvents;
1577
+ }
1578
+ /**
1579
+ * Transfer a field value from source solver to target solver.
1580
+ */
1581
+ transferField(coupling) {
1582
+ const sourceEntry = this.solvers.get(coupling.source.solver);
1583
+ const targetEntry = this.solvers.get(coupling.target.solver);
1584
+ if (!sourceEntry || !targetEntry) return;
1585
+ const sourceField = this.getField(sourceEntry, coupling.source.field);
1586
+ if (!sourceField) return;
1587
+ if (sourceField instanceof RegularGrid3D) {
1588
+ const targetGrid = this.getField(targetEntry, coupling.target.field);
1589
+ if (targetGrid instanceof RegularGrid3D) {
1590
+ const d = sourceField.data;
1591
+ const td = targetGrid.data;
1592
+ if (d.length !== td.length) {
1593
+ console.warn(
1594
+ `CouplingManager: grid size mismatch in coupling ${coupling.source.solver}.${coupling.source.field} \u2192 ${coupling.target.solver}.${coupling.target.field} (${d.length} vs ${td.length})`
1595
+ );
1596
+ }
1597
+ const len = Math.min(d.length, td.length);
1598
+ for (let i = 0; i < len; i++) {
1599
+ td[i] = coupling.transform(d[i]);
1600
+ }
1601
+ }
1602
+ } else if (sourceField instanceof Float32Array) {
1603
+ const targetArray = this.getField(targetEntry, coupling.target.field);
1604
+ if (targetArray instanceof Float32Array) {
1605
+ if (sourceField.length !== targetArray.length) {
1606
+ console.warn(
1607
+ `CouplingManager: array size mismatch in coupling ${coupling.source.solver}.${coupling.source.field} \u2192 ${coupling.target.solver}.${coupling.target.field} (${sourceField.length} vs ${targetArray.length})`
1608
+ );
1609
+ }
1610
+ const len = Math.min(sourceField.length, targetArray.length);
1611
+ for (let i = 0; i < len; i++) {
1612
+ targetArray[i] = coupling.transform(sourceField[i]);
1613
+ }
1614
+ }
1615
+ }
1616
+ }
1617
+ /**
1618
+ * Get a named field from a solver.
1619
+ */
1620
+ getField(entry, fieldName) {
1621
+ switch (entry.type) {
1622
+ case "thermal": {
1623
+ const solver = entry.solver;
1624
+ if (fieldName === "temperature") return solver.getTemperatureGrid();
1625
+ if (fieldName === "temperature_flat") return solver.getTemperatureField();
1626
+ return null;
1627
+ }
1628
+ case "structural": {
1629
+ const solver = entry.solver;
1630
+ if (fieldName === "von_mises_stress") return solver.getVonMisesStress();
1631
+ if (fieldName === "safety_factor") return solver.getSafetyFactor();
1632
+ if (fieldName === "displacements") return solver.getDisplacements();
1633
+ return null;
1634
+ }
1635
+ case "hydraulic": {
1636
+ const solver = entry.solver;
1637
+ if (fieldName === "pressure") return solver.getPressureField();
1638
+ if (fieldName === "flow_rates") return solver.getFlowRates();
1639
+ return null;
1640
+ }
1641
+ default:
1642
+ return null;
1643
+ }
1644
+ }
1645
+ /** Get events from the last step */
1646
+ getLastEvents() {
1647
+ return this.lastEvents;
1648
+ }
1649
+ /** Enable/disable a coupling by index */
1650
+ setCouplingEnabled(index, enabled) {
1651
+ if (this.couplings[index]) {
1652
+ this.couplings[index].enabled = enabled;
1653
+ }
1654
+ }
1655
+ getStats() {
1656
+ return {
1657
+ solverCount: this.solvers.size,
1658
+ couplingCount: this.couplings.length,
1659
+ saturationMonitors: this.saturationManagers.length,
1660
+ totalEvents: this.totalEvents,
1661
+ lastStepMs: this.lastStepMs
1662
+ };
1663
+ }
1664
+ dispose() {
1665
+ for (const [, entry] of this.solvers) {
1666
+ entry.solver.dispose();
1667
+ }
1668
+ for (const monitor of this.saturationManagers) {
1669
+ monitor.dispose();
1670
+ }
1671
+ this.solvers.clear();
1672
+ this.couplings = [];
1673
+ this.saturationManagers = [];
1674
+ }
1675
+ };
1676
+
1677
+ export {
1678
+ RegularGrid3D,
1679
+ applyBoundaryConditions,
1680
+ getMaterial,
1681
+ findMaterial,
1682
+ registerMaterial,
1683
+ listMaterials,
1684
+ thermalDiffusivity,
1685
+ conjugateGradient,
1686
+ jacobiIteration,
1687
+ ThermalSolver,
1688
+ StructuralSolver,
1689
+ HydraulicSolver,
1690
+ SaturationManager,
1691
+ CouplingManager,
1692
+ simulation_exports
1693
+ };