hypercube-compute 2.0.0
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.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/demo/index.html +78 -0
- package/demo/package-lock.json +1016 -0
- package/demo/package.json +15 -0
- package/demo/src/main.ts +153 -0
- package/demo/vite.config.ts +9 -0
- package/dist/index.d.mts +321 -0
- package/dist/index.d.ts +321 -0
- package/dist/index.js +1392 -0
- package/dist/index.mjs +1351 -0
- package/docs/assets/index-BLyglqQr.js +1 -0
- package/docs/index.html +78 -0
- package/package.json +29 -0
- package/src/Triade.ts +45 -0
- package/src/addons/ocean-simulation/OceanEngine.ts +208 -0
- package/src/addons/ocean-simulation/OceanSimulatorAddon.ts +145 -0
- package/src/addons/ocean-simulation/OceanWebGLRenderer.ts +258 -0
- package/src/addons/ocean-simulation/OceanWorld.ts +280 -0
- package/src/core/TriadeCubeV2.ts +58 -0
- package/src/core/TriadeGrid.ts +119 -0
- package/src/core/TriadeMasterBuffer.ts +37 -0
- package/src/engines/AerodynamicsEngine.ts +134 -0
- package/src/engines/EcosystemEngineO1.ts +73 -0
- package/src/engines/GameOfLifeEngine.ts +61 -0
- package/src/engines/HeatmapEngine.ts +60 -0
- package/src/engines/ITriadeEngine.ts +29 -0
- package/src/index.ts +26 -0
- package/src/io/CanvasAdapter.ts +41 -0
- package/src/io/WebGLAdapter.ts +129 -0
- package/src/templates/BlankEngine.ts +48 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { TriadeMasterBuffer } from './TriadeMasterBuffer';
|
|
2
|
+
import { TriadeCubeV2 } from './TriadeCubeV2';
|
|
3
|
+
import type { ITriadeEngine } from '../engines/ITriadeEngine';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TriadeGrid gère un assemblage N x M de TriadeCubes adjacents.
|
|
7
|
+
* Il assure la communication "Boundary Exchange" (Ghost Cells) entre les cubes
|
|
8
|
+
* à la fin de chaque étape de calcul pour unifier la simulation.
|
|
9
|
+
*/
|
|
10
|
+
export class TriadeGrid {
|
|
11
|
+
public cubes: (TriadeCubeV2 | null)[][] = [];
|
|
12
|
+
public readonly cols: number;
|
|
13
|
+
public readonly rows: number;
|
|
14
|
+
public readonly cubeSize: number;
|
|
15
|
+
public isPeriodic: boolean;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
cols: number,
|
|
19
|
+
rows: number,
|
|
20
|
+
cubeSize: number,
|
|
21
|
+
masterBuffer: TriadeMasterBuffer,
|
|
22
|
+
engineFactory: () => ITriadeEngine,
|
|
23
|
+
numFaces: number = 6,
|
|
24
|
+
isPeriodic: boolean = true
|
|
25
|
+
) {
|
|
26
|
+
this.cols = cols;
|
|
27
|
+
this.rows = rows;
|
|
28
|
+
this.cubeSize = cubeSize;
|
|
29
|
+
this.isPeriodic = isPeriodic;
|
|
30
|
+
|
|
31
|
+
// Allocation de la grille de cubes
|
|
32
|
+
for (let y = 0; y < rows; y++) {
|
|
33
|
+
this.cubes[y] = [];
|
|
34
|
+
for (let x = 0; x < cols; x++) {
|
|
35
|
+
const cube = new TriadeCubeV2(cubeSize, masterBuffer, numFaces);
|
|
36
|
+
cube.setEngine(engineFactory());
|
|
37
|
+
this.cubes[y][x] = cube;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Calcule une étape complète de la grille.
|
|
44
|
+
* 1. Exécute "compute()" sur chaque cube
|
|
45
|
+
* 2. Synchronise les bords (Boundary Exchange) sur les faces demandées
|
|
46
|
+
*/
|
|
47
|
+
compute(facesToSynchronize: number | number[] = 0) {
|
|
48
|
+
// 1. Calcul (Intra-Cube)
|
|
49
|
+
for (let y = 0; y < this.rows; y++) {
|
|
50
|
+
for (let x = 0; x < this.cols; x++) {
|
|
51
|
+
this.cubes[y][x]?.compute();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. Synchronisation des bords O(1) Data Copy
|
|
56
|
+
const faces = Array.isArray(facesToSynchronize) ? facesToSynchronize : [facesToSynchronize];
|
|
57
|
+
for (const f of faces) {
|
|
58
|
+
this.synchronizeBoundaries(f);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Recopie les vecteurs périphériques (1 pixel de profondeur) vers les bords des voisins.
|
|
64
|
+
*/
|
|
65
|
+
private synchronizeBoundaries(f: number) {
|
|
66
|
+
const s = this.cubeSize;
|
|
67
|
+
const s_minus_1 = s - 1;
|
|
68
|
+
const s_minus_2 = s - 2;
|
|
69
|
+
|
|
70
|
+
// PASS 1: X-axis (Left/Right)
|
|
71
|
+
for (let y = 0; y < this.rows; y++) {
|
|
72
|
+
for (let x = 0; x < this.cols; x++) {
|
|
73
|
+
const cube = this.cubes[y][x]!;
|
|
74
|
+
const data = cube.faces[f];
|
|
75
|
+
|
|
76
|
+
// Right neighbor
|
|
77
|
+
if (x < this.cols - 1 || this.isPeriodic) {
|
|
78
|
+
const rightCube = this.cubes[y][(x + 1) % this.cols]!;
|
|
79
|
+
const rightData = rightCube.faces[f];
|
|
80
|
+
for (let row = 1; row < s_minus_1; row++) {
|
|
81
|
+
rightData[row * s + 0] = data[row * s + s_minus_2];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Left neighbor
|
|
86
|
+
if (x > 0 || this.isPeriodic) {
|
|
87
|
+
const leftCube = this.cubes[y][(x - 1 + this.cols) % this.cols]!;
|
|
88
|
+
const leftData = leftCube.faces[f];
|
|
89
|
+
for (let row = 1; row < s_minus_1; row++) {
|
|
90
|
+
leftData[row * s + s_minus_1] = data[row * s + 1];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// PASS 2: Y-axis (Top/Bottom) - Copying full rows which perfectly transfers the corners (ghost columns from PASS 1)
|
|
97
|
+
// to diagonal neighbors automatically!
|
|
98
|
+
for (let y = 0; y < this.rows; y++) {
|
|
99
|
+
for (let x = 0; x < this.cols; x++) {
|
|
100
|
+
const cube = this.cubes[y][x]!;
|
|
101
|
+
const data = cube.faces[f];
|
|
102
|
+
|
|
103
|
+
// Bottom neighbor
|
|
104
|
+
if (y < this.rows - 1 || this.isPeriodic) {
|
|
105
|
+
const bottomCube = this.cubes[(y + 1) % this.rows][x]!;
|
|
106
|
+
const bottomData = bottomCube.faces[f];
|
|
107
|
+
bottomData.set(data.subarray(s_minus_2 * s, s_minus_2 * s + s), 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Top neighbor
|
|
111
|
+
if (y > 0 || this.isPeriodic) {
|
|
112
|
+
const topCube = this.cubes[(y - 1 + this.rows) % this.rows][x]!;
|
|
113
|
+
const topData = topCube.faces[f];
|
|
114
|
+
topData.set(data.subarray(1 * s, 1 * s + s), s_minus_1 * s);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export class TriadeMasterBuffer {
|
|
2
|
+
public readonly buffer: ArrayBuffer;
|
|
3
|
+
private offset: number = 0;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Alloue un unique bloc de mémoire vive (ArrayBuffer) pour l'ensemble du système.
|
|
7
|
+
* @param totalBytes Taille totale de la RAM allouée (par défaut 100 MB).
|
|
8
|
+
*/
|
|
9
|
+
constructor(totalBytes: number = 100 * 1024 * 1024) {
|
|
10
|
+
this.buffer = new ArrayBuffer(totalBytes);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Alloue les octets nécessaires pour un Cube de 6 Faces en O(1) sans fragmentation.
|
|
15
|
+
* @param mapSize Résolution (ex: 400x400)
|
|
16
|
+
* @returns L'offset de départ dans l'ArrayBuffer
|
|
17
|
+
*/
|
|
18
|
+
allocateCube(mapSize: number, numFaces: number = 6): number {
|
|
19
|
+
const floatsPerFace = mapSize * mapSize;
|
|
20
|
+
const bytesPerFace = floatsPerFace * Float32Array.BYTES_PER_ELEMENT; // 4 octets
|
|
21
|
+
const totalCubeBytes = bytesPerFace * numFaces;
|
|
22
|
+
|
|
23
|
+
if (this.offset + totalCubeBytes > this.buffer.byteLength) {
|
|
24
|
+
throw new Error(`[TriadeMasterBuffer] Out Of Memory. Impossible d'allouer ${totalCubeBytes} bytes supplémentaires.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const startOffset = this.offset;
|
|
28
|
+
this.offset += totalCubeBytes;
|
|
29
|
+
|
|
30
|
+
return startOffset;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Retourne la quantité de RAM consommée */
|
|
34
|
+
getUsedMemoryInMB(): string {
|
|
35
|
+
return (this.offset / (1024 * 1024)).toFixed(2) + ' MB';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { ITriadeEngine } from "./ITriadeEngine";
|
|
2
|
+
|
|
3
|
+
export class AerodynamicsEngine implements ITriadeEngine {
|
|
4
|
+
public dragScore: number = 0;
|
|
5
|
+
private initialized: boolean = false;
|
|
6
|
+
|
|
7
|
+
public get name(): string {
|
|
8
|
+
return "Lattice Boltzmann D2Q9 (O(1))";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
public compute(faces: Float32Array[], mapSize: number): void {
|
|
12
|
+
const N = mapSize;
|
|
13
|
+
const obstacles = faces[18];
|
|
14
|
+
const ux_out = faces[19];
|
|
15
|
+
const uy_out = faces[20];
|
|
16
|
+
const curl_out = faces[21];
|
|
17
|
+
|
|
18
|
+
// Vecteurs du modèle D2Q9
|
|
19
|
+
const cx = [0, 1, 0, -1, 0, 1, -1, -1, 1];
|
|
20
|
+
const cy = [0, 0, 1, 0, -1, 1, 1, -1, -1];
|
|
21
|
+
const w = [4.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0, 1.0 / 36.0, 1.0 / 36.0, 1.0 / 36.0, 1.0 / 36.0];
|
|
22
|
+
const opp = [0, 3, 4, 1, 2, 7, 8, 5, 6]; // Rebonds opposés
|
|
23
|
+
|
|
24
|
+
const u0 = 0.12; // Vitesse de Mach 0.12 pour générer de sublimes tourbillons
|
|
25
|
+
const omega = 1.95; // Relaxation (Haute turbulence si proche de 2.0)
|
|
26
|
+
|
|
27
|
+
// 0. INITIALISATION (F_eq)
|
|
28
|
+
if (!this.initialized) {
|
|
29
|
+
for (let idx = 0; idx < N * N; idx++) {
|
|
30
|
+
const rho = 1.0;
|
|
31
|
+
const ux = u0; const uy = 0.0;
|
|
32
|
+
const u_sq = ux * ux + uy * uy;
|
|
33
|
+
for (let i = 0; i < 9; i++) {
|
|
34
|
+
const cu = cx[i] * ux + cy[i] * uy;
|
|
35
|
+
const feq = w[i] * rho * (1.0 + 3.0 * cu + 4.5 * cu * cu - 1.5 * u_sq);
|
|
36
|
+
faces[i][idx] = feq;
|
|
37
|
+
faces[i + 9][idx] = feq;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
this.initialized = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let frameDrag = 0;
|
|
44
|
+
|
|
45
|
+
// 1. LBM CORE (Collision & Streaming O(1))
|
|
46
|
+
for (let y = 1; y < N - 1; y++) {
|
|
47
|
+
for (let x = 1; x < N - 1; x++) {
|
|
48
|
+
const idx = y * N + x;
|
|
49
|
+
|
|
50
|
+
if (obstacles[idx] > 0) {
|
|
51
|
+
// BOUNCE BACK: L'air se cogne contre l'Aileron et repart en arrière
|
|
52
|
+
for (let i = 1; i < 9; i++) {
|
|
53
|
+
const originX = x - cx[i];
|
|
54
|
+
const originY = y - cy[i];
|
|
55
|
+
const originIdx = originY * N + originX;
|
|
56
|
+
// Renvoi de la distribution
|
|
57
|
+
faces[opp[i] + 9][originIdx] = faces[i][originIdx];
|
|
58
|
+
|
|
59
|
+
// On mesure la puissance de l'impact direct (Portance/Traînée)
|
|
60
|
+
if (i === 1) frameDrag += faces[1][originIdx];
|
|
61
|
+
}
|
|
62
|
+
ux_out[idx] = 0;
|
|
63
|
+
uy_out[idx] = 0;
|
|
64
|
+
continue; // Skip the fluid equations for solid material
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Macroscopique : Densité et Vitesse
|
|
68
|
+
let rho = 0;
|
|
69
|
+
let ux = 0;
|
|
70
|
+
let uy = 0;
|
|
71
|
+
for (let i = 0; i < 9; i++) {
|
|
72
|
+
const f_val = faces[i][idx];
|
|
73
|
+
rho += f_val;
|
|
74
|
+
ux += cx[i] * f_val;
|
|
75
|
+
uy += cy[i] * f_val;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// INLET : Vent continu forcé à gauche (Tunnel Aerodynamique)
|
|
79
|
+
if (x === 1) {
|
|
80
|
+
ux = u0 * rho;
|
|
81
|
+
uy = 0.0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (rho > 0) {
|
|
85
|
+
ux /= rho;
|
|
86
|
+
uy /= rho;
|
|
87
|
+
}
|
|
88
|
+
ux_out[idx] = ux;
|
|
89
|
+
uy_out[idx] = uy;
|
|
90
|
+
|
|
91
|
+
// BGK COLLISION EQUATION
|
|
92
|
+
const u_sq = ux * ux + uy * uy;
|
|
93
|
+
for (let i = 0; i < 9; i++) {
|
|
94
|
+
const cu = cx[i] * ux + cy[i] * uy;
|
|
95
|
+
const feq = w[i] * rho * (1.0 + 3.0 * cu + 4.5 * cu * cu - 1.5 * u_sq);
|
|
96
|
+
|
|
97
|
+
// Relaxation de Navier-Stokes (Le secret profond du gaz)
|
|
98
|
+
const f_post = faces[i][idx] * (1.0 - omega) + feq * omega;
|
|
99
|
+
|
|
100
|
+
// STREAMING : Propagation vers les 8 voisins (+ le centre)
|
|
101
|
+
let nx = x + cx[i];
|
|
102
|
+
let ny = y + cy[i];
|
|
103
|
+
|
|
104
|
+
// Effet de bord (Enroulement du tunnel en haut / en bas)
|
|
105
|
+
if (ny < 1) ny = N - 2;
|
|
106
|
+
else if (ny > N - 2) ny = 1;
|
|
107
|
+
|
|
108
|
+
// OUTLET : Sortie libre (Gradient zero)
|
|
109
|
+
if (nx > N - 2) nx = N - 2;
|
|
110
|
+
|
|
111
|
+
faces[i + 9][ny * N + nx] = f_post;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 2. SWAP BUFFERS
|
|
117
|
+
for (let i = 0; i < 9; i++) {
|
|
118
|
+
faces[i].set(faces[i + 9]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Exagération du calcul de drag pour UI
|
|
122
|
+
this.dragScore = this.dragScore * 0.9 + (frameDrag * 1000) * 0.1;
|
|
123
|
+
|
|
124
|
+
// 3. VORTICITY (Curl) POUR L'EFFET "WOW"
|
|
125
|
+
for (let y = 1; y < N - 1; y++) {
|
|
126
|
+
for (let x = 1; x < N - 1; x++) {
|
|
127
|
+
const idx = y * N + x;
|
|
128
|
+
const dUy_dx = uy_out[idx + 1] - uy_out[idx - 1];
|
|
129
|
+
const dUx_dy = ux_out[idx + N] - ux_out[idx - N];
|
|
130
|
+
curl_out[idx] = dUy_dx - dUx_dy;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ITriadeEngine } from "./ITriadeEngine";
|
|
2
|
+
|
|
3
|
+
export class EcosystemEngineO1 implements ITriadeEngine {
|
|
4
|
+
public get name(): string {
|
|
5
|
+
return "Guerre des Triades (Rouge vs Bleu)";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
public compute(faces: Float32Array[], mapSize: number): void {
|
|
9
|
+
const current = faces[1];
|
|
10
|
+
const next = faces[2];
|
|
11
|
+
|
|
12
|
+
// Automate Cellulaire Combat (Jeu de la vie + Bataille de Factions)
|
|
13
|
+
// STRICT O(1) : Sans mémoire asymétrique ni allocation.
|
|
14
|
+
for (let y = 0; y < mapSize; y++) {
|
|
15
|
+
for (let x = 0; x < mapSize; x++) {
|
|
16
|
+
const idx = y * mapSize + x;
|
|
17
|
+
const state = current[idx];
|
|
18
|
+
|
|
19
|
+
let blues = 0;
|
|
20
|
+
let reds = 0;
|
|
21
|
+
|
|
22
|
+
const yM = y > 0 ? y - 1 : mapSize - 1;
|
|
23
|
+
const yP = y < mapSize - 1 ? y + 1 : 0;
|
|
24
|
+
const xM = x > 0 ? x - 1 : mapSize - 1;
|
|
25
|
+
const xP = x < mapSize - 1 ? x + 1 : 0;
|
|
26
|
+
|
|
27
|
+
// Comptage de voisinage (Moore)
|
|
28
|
+
const n1 = current[yM * mapSize + xM]; if (n1 === 2) blues++; else if (n1 === 3) reds++;
|
|
29
|
+
const n2 = current[yM * mapSize + x]; if (n2 === 2) blues++; else if (n2 === 3) reds++;
|
|
30
|
+
const n3 = current[yM * mapSize + xP]; if (n3 === 2) blues++; else if (n3 === 3) reds++;
|
|
31
|
+
const n4 = current[y * mapSize + xM]; if (n4 === 2) blues++; else if (n4 === 3) reds++;
|
|
32
|
+
const n5 = current[y * mapSize + xP]; if (n5 === 2) blues++; else if (n5 === 3) reds++;
|
|
33
|
+
const n6 = current[yP * mapSize + xM]; if (n6 === 2) blues++; else if (n6 === 3) reds++;
|
|
34
|
+
const n7 = current[yP * mapSize + x]; if (n7 === 2) blues++; else if (n7 === 3) reds++;
|
|
35
|
+
const n8 = current[yP * mapSize + xP]; if (n8 === 2) blues++; else if (n8 === 3) reds++;
|
|
36
|
+
|
|
37
|
+
const total = blues + reds;
|
|
38
|
+
|
|
39
|
+
if (state !== 2 && state !== 3) { // Vide (ou anciens restes de map végétale)
|
|
40
|
+
// Règle de Naissance GOL (Exactement 3 parents vivants)
|
|
41
|
+
if (total === 3) {
|
|
42
|
+
next[idx] = (blues > reds) ? 2 : 3; // L'Allégeance de l'enfant va au camp majoritaire
|
|
43
|
+
}
|
|
44
|
+
// Renforts aéroportés aléatoires pour garantir l'agitation infinie (Noise factor)
|
|
45
|
+
else if (Math.random() < 0.0005) {
|
|
46
|
+
next[idx] = Math.random() < 0.5 ? 2 : 3;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
next[idx] = 0; // Reste un champ de bataille vide
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (state === 2) { // Troupe BLEUE
|
|
53
|
+
// Combat: Frappe fatale ! Si 2 Rouges sont au contact, le Bleu se fait annihiler
|
|
54
|
+
if (reds >= 2) next[idx] = 0;
|
|
55
|
+
// Règle de Survie GOL classique (2 ou 3 camarades de vie)
|
|
56
|
+
else if (total === 2 || total === 3) next[idx] = 2;
|
|
57
|
+
// Isolement, ou Étouffement par surpopulation GOL
|
|
58
|
+
else next[idx] = 0;
|
|
59
|
+
}
|
|
60
|
+
else if (state === 3) { // Troupe ROUGE
|
|
61
|
+
// Combat: Frappe fatale ! Si 2 Bleus sont au contact, le Rouge se fait annihilé
|
|
62
|
+
if (blues >= 2) next[idx] = 0;
|
|
63
|
+
// Règle de Survie GOL classique
|
|
64
|
+
else if (total === 2 || total === 3) next[idx] = 3;
|
|
65
|
+
// Isolement ou Surpopulation
|
|
66
|
+
else next[idx] = 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
current.set(next); // Synchronisation du Front O(1)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ITriadeEngine } from "./ITriadeEngine";
|
|
2
|
+
|
|
3
|
+
export class GameOfLifeEngine implements ITriadeEngine {
|
|
4
|
+
public get name(): string {
|
|
5
|
+
return "Ecosystème Tensoriel (Plantes, Herbivores, Carnivores)";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
public compute(faces: Float32Array[], mapSize: number): void {
|
|
9
|
+
const current = faces[1]; // Face 1: État actuel (t)
|
|
10
|
+
const next = faces[2]; // Face 2: État futur (t+1)
|
|
11
|
+
|
|
12
|
+
// Double boucle optimisée pour accès mémoires continus
|
|
13
|
+
for (let y = 0; y < mapSize; y++) {
|
|
14
|
+
|
|
15
|
+
const top = (y === 0) ? mapSize - 1 : y - 1;
|
|
16
|
+
const bottom = (y === mapSize - 1) ? 0 : y + 1;
|
|
17
|
+
|
|
18
|
+
const topRow = top * mapSize;
|
|
19
|
+
const midRow = y * mapSize;
|
|
20
|
+
const botRow = bottom * mapSize;
|
|
21
|
+
|
|
22
|
+
for (let x = 0; x < mapSize; x++) {
|
|
23
|
+
const left = (x === 0) ? mapSize - 1 : x - 1;
|
|
24
|
+
const right = (x === mapSize - 1) ? 0 : x + 1;
|
|
25
|
+
|
|
26
|
+
const idx = midRow + x;
|
|
27
|
+
const state = current[idx]; // 0: Vide, 1: Plante, 2: Herbi, 3: Carni
|
|
28
|
+
|
|
29
|
+
// Le prédateur / successeur de l'état actuel
|
|
30
|
+
let targetState = state + 1;
|
|
31
|
+
if (targetState > 3) targetState = 0;
|
|
32
|
+
|
|
33
|
+
let predators = 0;
|
|
34
|
+
|
|
35
|
+
// Von Neumann Neighborhood (Plus organique pour la croissance)
|
|
36
|
+
if (current[topRow + x] === targetState) predators++;
|
|
37
|
+
if (current[midRow + left] === targetState) predators++;
|
|
38
|
+
if (current[midRow + right] === targetState) predators++;
|
|
39
|
+
if (current[botRow + x] === targetState) predators++;
|
|
40
|
+
|
|
41
|
+
// Moore Neighborhood (Diagonales) avec moins de poids
|
|
42
|
+
if (current[topRow + left] === targetState) predators++;
|
|
43
|
+
if (current[topRow + right] === targetState) predators++;
|
|
44
|
+
if (current[botRow + left] === targetState) predators++;
|
|
45
|
+
if (current[botRow + right] === targetState) predators++;
|
|
46
|
+
|
|
47
|
+
// Seuil à 1 ou 2 selon l'état offre une dynamique d'essaim très organique
|
|
48
|
+
const threshold = (state === 0) ? 1 : 2; // Le vide est colonisé vite, les autres survivent plus
|
|
49
|
+
|
|
50
|
+
if (predators >= threshold) {
|
|
51
|
+
next[idx] = targetState;
|
|
52
|
+
} else {
|
|
53
|
+
next[idx] = state;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Swap / Recopie mémoire ultra-rapide de l'état (t+1) vers (t)
|
|
59
|
+
current.set(next);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ITriadeEngine } from './ITriadeEngine';
|
|
2
|
+
|
|
3
|
+
export class HeatmapEngine implements ITriadeEngine {
|
|
4
|
+
public readonly name = "Heatmap (O1 Spatial Convolution)";
|
|
5
|
+
public radius: number;
|
|
6
|
+
public weight: number;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param radius Rayon d'influence en cellules
|
|
10
|
+
* @param weight Coefficient multiplicateur à l'arrivée
|
|
11
|
+
*/
|
|
12
|
+
constructor(radius: number = 10, weight: number = 1.0) {
|
|
13
|
+
this.radius = radius;
|
|
14
|
+
this.weight = weight;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Exécute le Summed Area Table Algorithm (Face 5) suivi
|
|
19
|
+
* d'un Box Filter O(1) vers la Synthèse (Face 3).
|
|
20
|
+
*/
|
|
21
|
+
compute(faces: Float32Array[], mapSize: number): void {
|
|
22
|
+
const face2 = faces[1]; // Contexte Binaire d'entrée
|
|
23
|
+
const face3 = faces[2]; // Synthèse de Diffusion
|
|
24
|
+
const face5 = faces[4]; // Cheat-code O(1) SAT
|
|
25
|
+
|
|
26
|
+
// 1. O(N) : Génération Cristallisée (Integral Image)
|
|
27
|
+
for (let y = 0; y < mapSize; y++) {
|
|
28
|
+
for (let x = 0; x < mapSize; x++) {
|
|
29
|
+
const idx = y * mapSize + x;
|
|
30
|
+
const val = face2[idx];
|
|
31
|
+
const top = y > 0 ? face5[(y - 1) * mapSize + x] : 0;
|
|
32
|
+
const left = x > 0 ? face5[y * mapSize + (x - 1)] : 0;
|
|
33
|
+
const topLeft = (y > 0 && x > 0) ? face5[(y - 1) * mapSize + (x - 1)] : 0;
|
|
34
|
+
|
|
35
|
+
face5[idx] = val + top + left - topLeft;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. O(N) : Extraction d'Influence Indépendante du Rayon
|
|
40
|
+
for (let y = 0; y < mapSize; y++) {
|
|
41
|
+
for (let x = 0; x < mapSize; x++) {
|
|
42
|
+
// Clamping (Borne Map) rapide
|
|
43
|
+
const minX = Math.max(0, x - this.radius);
|
|
44
|
+
const minY = Math.max(0, y - this.radius);
|
|
45
|
+
const maxX = Math.min(mapSize - 1, x + this.radius);
|
|
46
|
+
const maxY = Math.min(mapSize - 1, y + this.radius);
|
|
47
|
+
|
|
48
|
+
// Récupération O(1) des Opcodes d'angles
|
|
49
|
+
const A = (minX > 0 && minY > 0) ? face5[(minY - 1) * mapSize + (minX - 1)] : 0;
|
|
50
|
+
const B = (minY > 0) ? face5[(minY - 1) * mapSize + maxX] : 0;
|
|
51
|
+
const C = (minX > 0) ? face5[maxY * mapSize + (minX - 1)] : 0;
|
|
52
|
+
const D = face5[maxY * mapSize + maxX];
|
|
53
|
+
|
|
54
|
+
// Différence des coins
|
|
55
|
+
const sum = D - B - C + A;
|
|
56
|
+
face3[y * mapSize + x] = sum * this.weight;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type alias to explicitly signal to AI that this array is a Flat Tensor (1D array simulating 2D/3D).
|
|
3
|
+
* @AI_RULE DO NOT iterate using functional array methods (map, forEach, reduce). DO NOT use `push` or `splice`.
|
|
4
|
+
*/
|
|
5
|
+
export type FlatTensorView = Float32Array;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Interface fondamentale (Le Contract) pour tout algorithme physique dans Triade Engine.
|
|
9
|
+
* Implémentée par les moteurs LBM, GameOfLife, SAT.
|
|
10
|
+
*/
|
|
11
|
+
export interface ITriadeEngine {
|
|
12
|
+
/**
|
|
13
|
+
* @description Exécute un pas de simulation mathématique à l'intérieur d'un TriadeCube.
|
|
14
|
+
* @param faces Les 6 (ou N) "Views" (FlatTensorView) pointant vers la VRAM continue.
|
|
15
|
+
* @param mapSize La dimension N (N x N) de ce chunk spatial.
|
|
16
|
+
*
|
|
17
|
+
* @AI_RULE CRITICAL_PERFORMANCE: This method runs inside a hot loop (like 60 FPS).
|
|
18
|
+
* @AI_RULE ZERO_ALLOCATION: DO NOT instantiate any object (`new Object()`, `[]`, `{}`) inside this scope to avoid Garbage Collection.
|
|
19
|
+
* @AI_RULE MEMORY_ACCESS: Use 1D sequential iterations `for (let i = 0; i < length; i++)`. Read/Write values via `faces[index][i]`. Map 2D coords using `index = y * mapSize + x`.
|
|
20
|
+
* @AI_RULE ERROR_HANDLING: If generating errors, be highly verbose for self-correction. Example: `throw new Error("[Triade] Out of bounds: index ${i} exceeds size ${length}")`
|
|
21
|
+
*/
|
|
22
|
+
compute(faces: FlatTensorView[], mapSize: number): void;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Identifiant explicite du moteur algorithmique.
|
|
26
|
+
* @AI_RULE This MUST match the semantic context of the Engine (e.g., 'Navier-Stokes-LBM-D2Q9', 'Game-Of-Life').
|
|
27
|
+
*/
|
|
28
|
+
get name(): string;
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export * from './Triade';
|
|
2
|
+
|
|
3
|
+
// Core
|
|
4
|
+
export * from './core/TriadeCubeV2';
|
|
5
|
+
export * from './core/TriadeGrid';
|
|
6
|
+
export * from './core/TriadeMasterBuffer';
|
|
7
|
+
|
|
8
|
+
// Engines
|
|
9
|
+
export * from './engines/ITriadeEngine';
|
|
10
|
+
export * from './engines/AerodynamicsEngine';
|
|
11
|
+
export * from './engines/EcosystemEngineO1';
|
|
12
|
+
export * from './engines/GameOfLifeEngine';
|
|
13
|
+
export * from './engines/HeatmapEngine';
|
|
14
|
+
|
|
15
|
+
// IO / Adapters
|
|
16
|
+
export * from './io/CanvasAdapter';
|
|
17
|
+
export * from './io/WebGLAdapter';
|
|
18
|
+
|
|
19
|
+
// Addons
|
|
20
|
+
export * from './addons/ocean-simulation/OceanEngine';
|
|
21
|
+
export { OceanSimulatorAddon } from './addons/ocean-simulation/OceanSimulatorAddon';
|
|
22
|
+
export * from './addons/ocean-simulation/OceanWebGLRenderer';
|
|
23
|
+
export * from './addons/ocean-simulation/OceanWorld';
|
|
24
|
+
|
|
25
|
+
// Templates
|
|
26
|
+
export * from './templates/BlankEngine';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class CanvasAdapter {
|
|
2
|
+
/**
|
|
3
|
+
* Lit un Tenseur Plat (Face de Float32Array) et le peint sur un contexte Canvas Native.
|
|
4
|
+
* Cette interface sépare la logique de rendu (UI) du moteur mathématique (Triade).
|
|
5
|
+
*/
|
|
6
|
+
static renderFaceToCanvas(
|
|
7
|
+
faceData: Float32Array,
|
|
8
|
+
mapSize: number,
|
|
9
|
+
ctx: CanvasRenderingContext2D,
|
|
10
|
+
options: { colorScheme: 'heat' | 'grayscale', normalizeMax?: number } = { colorScheme: 'grayscale' }
|
|
11
|
+
) {
|
|
12
|
+
const imgData = ctx.getImageData(0, 0, mapSize, mapSize);
|
|
13
|
+
const data = imgData.data;
|
|
14
|
+
|
|
15
|
+
// Auto-normalization si l'utilisateur ne connait pas le Max possible de sa matrice.
|
|
16
|
+
let max = options.normalizeMax || 0.0001;
|
|
17
|
+
if (!options.normalizeMax) {
|
|
18
|
+
for (let i = 0; i < faceData.length; i++) {
|
|
19
|
+
if (faceData[i] > max) max = faceData[i];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Colorisation O(N)
|
|
24
|
+
for (let i = 0; i < faceData.length; i++) {
|
|
25
|
+
const val = faceData[i] / max;
|
|
26
|
+
const p = i * 4;
|
|
27
|
+
|
|
28
|
+
if (options.colorScheme === 'heat') {
|
|
29
|
+
data[p] = val * 255; // R
|
|
30
|
+
data[p + 1] = (val > 0.5 ? (val - 0.5) * 510 : 0); // G (jaunit si forte intensité)
|
|
31
|
+
data[p + 2] = val * 50; // B
|
|
32
|
+
data[p + 3] = 255; // Alpha
|
|
33
|
+
} else {
|
|
34
|
+
const c = val * 255;
|
|
35
|
+
data[p] = c; data[p + 1] = c; data[p + 2] = c;
|
|
36
|
+
data[p + 3] = 255;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
ctx.putImageData(imgData, 0, 0);
|
|
40
|
+
}
|
|
41
|
+
}
|