Add more support

This commit is contained in:
Lol3rrr
2025-12-25 22:53:36 +01:00
parent 8f85bbe5ed
commit 60c55aee55
4 changed files with 1101 additions and 64 deletions

779
references/fluidsim.html Normal file
View File

@@ -0,0 +1,779 @@
<!--
Copyright 2022 Matthias Müller - Ten Minute Physics,
www.youtube.com/c/TenMinutePhysics
www.matthiasMueller.info/tenMinutePhysics
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-->
<!DOCTYPE html>
<html>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<title>Euler Fluid</title>
<style>
body {
font-family: verdana;
font-size: 15px;
}
.button {
background-color: #606060;
border: none;
color: white;
padding: 10px 10px;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
}
.slider {
-webkit-appearance: none;
width: 80px;
height: 6px;
border-radius: 5px;
background: #d3d3d3;
outline: none;
opacity: 0.7;
-webkit-transition: .2s;
transition: opacity .2s;
}
</style>
</head>
<body>
<button class="button" onclick="setupScene(1)">Wind Tunnel</button>
<button class="button" onclick="setupScene(3)">Hires Tunnel</button>
<button class="button" onclick="setupScene(0)">Tank</button>
<button class="button" onclick="setupScene(2)">Paint</button>
<input type = "checkbox" id = "streamButton" onclick = "scene.showStreamlines = !scene.showStreamlines">Streamlines
<input type = "checkbox" id = "velocityButton" onclick = "scene.showVelocities = !scene.showVelocities">Velocities
<input type = "checkbox" name = "field" id = "pressureButton" onclick = "scene.showPressure = !scene.showPressure;"> Pressure
<input type = "checkbox" name = "field" id = "smokeButton" onclick = "scene.showSmoke = !scene.showSmoke;" checked>Smoke
<input type = "checkbox" id = "overrelaxButton" onclick = "scene.overRelaxation = scene.overRelaxation == 1.0 ? 1.9 : 1.0" checked>Overrelax
<br>
<canvas id="myCanvas" style="border:2px solid"></canvas>
<script>
var canvas = document.getElementById("myCanvas");
var c = canvas.getContext("2d");
canvas.width = window.innerWidth - 20;
canvas.height = window.innerHeight - 100;
canvas.focus();
var simHeight = 1.1;
var cScale = canvas.height / simHeight;
var simWidth = canvas.width / cScale;
var U_FIELD = 0;
var V_FIELD = 1;
var S_FIELD = 2;
var cnt = 0;
function cX(x) {
return x * cScale;
}
function cY(y) {
return canvas.height - y * cScale;
}
// ----------------- start of simulator ------------------------------
class Fluid {
constructor(density, numX, numY, h) {
this.density = density;
this.numX = numX + 2;
this.numY = numY + 2;
this.numCells = this.numX * this.numY;
this.h = h;
this.u = new Float32Array(this.numCells);
this.v = new Float32Array(this.numCells);
this.newU = new Float32Array(this.numCells);
this.newV = new Float32Array(this.numCells);
this.p = new Float32Array(this.numCells);
this.s = new Float32Array(this.numCells);
this.m = new Float32Array(this.numCells);
this.newM = new Float32Array(this.numCells);
this.m.fill(1.0)
var num = numX * numY;
}
integrate(dt, gravity) {
var n = this.numY;
for (var i = 1; i < this.numX; i++) {
for (var j = 1; j < this.numY-1; j++) {
if (this.s[i*n + j] != 0.0 && this.s[i*n + j-1] != 0.0)
this.v[i*n + j] += gravity * dt;
}
}
}
solveIncompressibility(numIters, dt) {
var n = this.numY;
var cp = this.density * this.h / dt;
for (var iter = 0; iter < numIters; iter++) {
for (var i = 1; i < this.numX-1; i++) {
for (var j = 1; j < this.numY-1; j++) {
if (this.s[i*n + j] == 0.0)
continue;
var s = this.s[i*n + j];
var sx0 = this.s[(i-1)*n + j];
var sx1 = this.s[(i+1)*n + j];
var sy0 = this.s[i*n + j-1];
var sy1 = this.s[i*n + j+1];
var s = sx0 + sx1 + sy0 + sy1;
if (s == 0.0)
continue;
var div = this.u[(i+1)*n + j] - this.u[i*n + j] +
this.v[i*n + j+1] - this.v[i*n + j];
var p = -div / s;
p *= scene.overRelaxation;
this.p[i*n + j] += cp * p;
this.u[i*n + j] -= sx0 * p;
this.u[(i+1)*n + j] += sx1 * p;
this.v[i*n + j] -= sy0 * p;
this.v[i*n + j+1] += sy1 * p;
}
}
}
}
extrapolate() {
var n = this.numY;
for (var i = 0; i < this.numX; i++) {
this.u[i*n + 0] = this.u[i*n + 1];
this.u[i*n + this.numY-1] = this.u[i*n + this.numY-2];
}
for (var j = 0; j < this.numY; j++) {
this.v[0*n + j] = this.v[1*n + j];
this.v[(this.numX-1)*n + j] = this.v[(this.numX-2)*n + j]
}
}
sampleField(x, y, field) {
var n = this.numY;
var h = this.h;
var h1 = 1.0 / h;
var h2 = 0.5 * h;
x = Math.max(Math.min(x, this.numX * h), h);
y = Math.max(Math.min(y, this.numY * h), h);
var dx = 0.0;
var dy = 0.0;
var f;
switch (field) {
case U_FIELD: f = this.u; dy = h2; break;
case V_FIELD: f = this.v; dx = h2; break;
case S_FIELD: f = this.m; dx = h2; dy = h2; break
}
var x0 = Math.min(Math.floor((x-dx)*h1), this.numX-1);
var tx = ((x-dx) - x0*h) * h1;
var x1 = Math.min(x0 + 1, this.numX-1);
var y0 = Math.min(Math.floor((y-dy)*h1), this.numY-1);
var ty = ((y-dy) - y0*h) * h1;
var y1 = Math.min(y0 + 1, this.numY-1);
var sx = 1.0 - tx;
var sy = 1.0 - ty;
var val = sx*sy * f[x0*n + y0] +
tx*sy * f[x1*n + y0] +
tx*ty * f[x1*n + y1] +
sx*ty * f[x0*n + y1];
return val;
}
avgU(i, j) {
var n = this.numY;
var u = (this.u[i*n + j-1] + this.u[i*n + j] +
this.u[(i+1)*n + j-1] + this.u[(i+1)*n + j]) * 0.25;
return u;
}
avgV(i, j) {
var n = this.numY;
var v = (this.v[(i-1)*n + j] + this.v[i*n + j] +
this.v[(i-1)*n + j+1] + this.v[i*n + j+1]) * 0.25;
return v;
}
advectVel(dt) {
this.newU.set(this.u);
this.newV.set(this.v);
var n = this.numY;
var h = this.h;
var h2 = 0.5 * h;
for (var i = 1; i < this.numX; i++) {
for (var j = 1; j < this.numY; j++) {
cnt++;
// u component
if (this.s[i*n + j] != 0.0 && this.s[(i-1)*n + j] != 0.0 && j < this.numY - 1) {
var x = i*h;
var y = j*h + h2;
var u = this.u[i*n + j];
var v = this.avgV(i, j);
// var v = this.sampleField(x,y, V_FIELD);
x = x - dt*u;
y = y - dt*v;
u = this.sampleField(x,y, U_FIELD);
this.newU[i*n + j] = u;
}
// v component
if (this.s[i*n + j] != 0.0 && this.s[i*n + j-1] != 0.0 && i < this.numX - 1) {
var x = i*h + h2;
var y = j*h;
var u = this.avgU(i, j);
// var u = this.sampleField(x,y, U_FIELD);
var v = this.v[i*n + j];
x = x - dt*u;
y = y - dt*v;
v = this.sampleField(x,y, V_FIELD);
this.newV[i*n + j] = v;
}
}
}
this.u.set(this.newU);
this.v.set(this.newV);
}
advectSmoke(dt) {
this.newM.set(this.m);
var n = this.numY;
var h = this.h;
var h2 = 0.5 * h;
for (var i = 1; i < this.numX-1; i++) {
for (var j = 1; j < this.numY-1; j++) {
if (this.s[i*n + j] != 0.0) {
var u = (this.u[i*n + j] + this.u[(i+1)*n + j]) * 0.5;
var v = (this.v[i*n + j] + this.v[i*n + j+1]) * 0.5;
var x = i*h + h2 - dt*u;
var y = j*h + h2 - dt*v;
this.newM[i*n + j] = this.sampleField(x,y, S_FIELD);
}
}
}
this.m.set(this.newM);
}
// ----------------- end of simulator ------------------------------
simulate(dt, gravity, numIters) {
this.integrate(dt, gravity);
this.p.fill(0.0);
this.solveIncompressibility(numIters, dt);
this.extrapolate();
this.advectVel(dt);
this.advectSmoke(dt);
}
}
var scene =
{
gravity : -9.81,
dt : 1.0 / 120.0,
numIters : 100,
frameNr : 0,
overRelaxation : 1.9,
obstacleX : 0.0,
obstacleY : 0.0,
obstacleRadius: 0.15,
paused: false,
sceneNr: 0,
showObstacle: false,
showStreamlines: false,
showVelocities: false,
showPressure: false,
showSmoke: true,
fluid: null
};
function setupScene(sceneNr = 0)
{
scene.sceneNr = sceneNr;
scene.obstacleRadius = 0.15;
scene.overRelaxation = 1.9;
scene.dt = 1.0 / 60.0;
scene.numIters = 40;
var res = 100;
if (sceneNr == 0)
res = 50;
else if (sceneNr == 3)
res = 200;
var domainHeight = 1.0;
var domainWidth = domainHeight / simHeight * simWidth;
var h = domainHeight / res;
var numX = Math.floor(domainWidth / h);
var numY = Math.floor(domainHeight / h);
var density = 1000.0;
f = scene.fluid = new Fluid(density, numX, numY, h);
var n = f.numY;
if (sceneNr == 0) { // tank
for (var i = 0; i < f.numX; i++) {
for (var j = 0; j < f.numY; j++) {
var s = 1.0; // fluid
if (i == 0 || i == f.numX-1 || j == 0)
s = 0.0; // solid
f.s[i*n + j] = s
}
}
scene.gravity = -9.81;
scene.showPressure = true;
scene.showSmoke = false;
scene.showStreamlines = false;
scene.showVelocities = false;
}
else if (sceneNr == 1 || sceneNr == 3) { // vortex shedding
var inVel = 2.0;
for (var i = 0; i < f.numX; i++) {
for (var j = 0; j < f.numY; j++) {
var s = 1.0; // fluid
if (i == 0 || j == 0 || j == f.numY-1)
s = 0.0; // solid
f.s[i*n + j] = s
if (i == 1) {
f.u[i*n + j] = inVel;
}
}
}
var pipeH = 0.1 * f.numY;
var minJ = Math.floor(0.5 * f.numY - 0.5*pipeH);
var maxJ = Math.floor(0.5 * f.numY + 0.5*pipeH);
for (var j = minJ; j < maxJ; j++)
f.m[j] = 0.0;
setObstacle(0.4, 0.5, true)
scene.gravity = 0.0;
scene.showPressure = false;
scene.showSmoke = true;
scene.showStreamlines = false;
scene.showVelocities = false;
if (sceneNr == 3) {
scene.dt = 1.0 / 120.0;
scene.numIters = 100;
scene.showPressure = true;
}
}
else if (sceneNr == 2) { // paint
scene.gravity = 0.0;
scene.overRelaxation = 1.0;
scene.showPressure = false;
scene.showSmoke = true;
scene.showStreamlines = false;
scene.showVelocities = false;
scene.obstacleRadius = 0.1;
}
document.getElementById("streamButton").checked = scene.showStreamlines;
document.getElementById("velocityButton").checked = scene.showVelocities;
document.getElementById("pressureButton").checked = scene.showPressure;
document.getElementById("smokeButton").checked = scene.showSmoke;
document.getElementById("overrelaxButton").checked = scene.overRelaxation > 1.0;
}
// draw -------------------------------------------------------
function setColor(r,g,b) {
c.fillStyle = `rgb(
${Math.floor(255*r)},
${Math.floor(255*g)},
${Math.floor(255*b)})`
c.strokeStyle = `rgb(
${Math.floor(255*r)},
${Math.floor(255*g)},
${Math.floor(255*b)})`
}
function getSciColor(val, minVal, maxVal) {
val = Math.min(Math.max(val, minVal), maxVal- 0.0001);
var d = maxVal - minVal;
val = d == 0.0 ? 0.5 : (val - minVal) / d;
var m = 0.25;
var num = Math.floor(val / m);
var s = (val - num * m) / m;
var r, g, b;
switch (num) {
case 0 : r = 0.0; g = s; b = 1.0; break;
case 1 : r = 0.0; g = 1.0; b = 1.0-s; break;
case 2 : r = s; g = 1.0; b = 0.0; break;
case 3 : r = 1.0; g = 1.0 - s; b = 0.0; break;
}
return[255*r,255*g,255*b, 255]
}
function draw()
{
c.clearRect(0, 0, canvas.width, canvas.height);
c.fillStyle = "#FF0000";
f = scene.fluid;
n = f.numY;
var cellScale = 1.1;
var h = f.h;
minP = f.p[0];
maxP = f.p[0];
for (var i = 0; i < f.numCells; i++) {
minP = Math.min(minP, f.p[i]);
maxP = Math.max(maxP, f.p[i]);
}
id = c.getImageData(0,0, canvas.width, canvas.height)
var color = [255, 255, 255, 255]
for (var i = 0; i < f.numX; i++) {
for (var j = 0; j < f.numY; j++) {
if (scene.showPressure) {
var p = f.p[i*n + j];
var s = f.m[i*n + j];
color = getSciColor(p, minP, maxP);
if (scene.showSmoke) {
color[0] = Math.max(0.0, color[0] - 255*s);
color[1] = Math.max(0.0, color[1] - 255*s);
color[2] = Math.max(0.0, color[2] - 255*s);
}
}
else if (scene.showSmoke) {
var s = f.m[i*n + j];
color[0] = 255*s;
color[1] = 255*s;
color[2] = 255*s;
if (scene.sceneNr == 2)
color = getSciColor(s, 0.0, 1.0);
}
else if (f.s[i*n + j] == 0.0) {
color[0] = 0;
color[1] = 0;
color[2] = 0;
}
var x = Math.floor(cX(i * h));
var y = Math.floor(cY((j+1) * h));
var cx = Math.floor(cScale * cellScale * h) + 1;
var cy = Math.floor(cScale * cellScale * h) + 1;
r = color[0];
g = color[1];
b = color[2];
for (var yi = y; yi < y + cy; yi++) {
var p = 4 * (yi * canvas.width + x)
for (var xi = 0; xi < cx; xi++) {
id.data[p++] = r;
id.data[p++] = g;
id.data[p++] = b;
id.data[p++] = 255;
}
}
}
}
c.putImageData(id, 0, 0);
if (scene.showVelocities) {
c.strokeStyle = "#000000";
scale = 0.02;
for (var i = 0; i < f.numX; i++) {
for (var j = 0; j < f.numY; j++) {
var u = f.u[i*n + j];
var v = f.v[i*n + j];
c.beginPath();
x0 = cX(i * h);
x1 = cX(i * h + u * scale);
y = cY((j + 0.5) * h );
c.moveTo(x0, y);
c.lineTo(x1, y);
c.stroke();
x = cX((i + 0.5) * h);
y0 = cY(j * h );
y1 = cY(j * h + v * scale)
c.beginPath();
c.moveTo(x, y0);
c.lineTo(x, y1);
c.stroke();
}
}
}
if (scene.showStreamlines) {
var segLen = f.h * 0.2;
var numSegs = 15;
c.strokeStyle = "#000000";
for (var i = 1; i < f.numX - 1; i += 5) {
for (var j = 1; j < f.numY - 1; j += 5) {
var x = (i + 0.5) * f.h;
var y = (j + 0.5) * f.h;
c.beginPath();
c.moveTo(cX(x), cY(y));
for (var n = 0; n < numSegs; n++) {
var u = f.sampleField(x, y, U_FIELD);
var v = f.sampleField(x, y, V_FIELD);
l = Math.sqrt(u*u + v*v);
// x += u/l * segLen;
// y += v/l * segLen;
x += u * 0.01;
y += v * 0.01;
if (x > f.numX * f.h)
break;
c.lineTo(cX(x), cY(y));
}
c.stroke();
}
}
}
if (scene.showObstacle) {
c.strokeW
r = scene.obstacleRadius + f.h;
if (scene.showPressure)
c.fillStyle = "#000000";
else
c.fillStyle = "#DDDDDD";
c.beginPath();
c.arc(
cX(scene.obstacleX), cY(scene.obstacleY), cScale * r, 0.0, 2.0 * Math.PI);
c.closePath();
c.fill();
c.lineWidth = 3.0;
c.strokeStyle = "#000000";
c.beginPath();
c.arc(
cX(scene.obstacleX), cY(scene.obstacleY), cScale * r, 0.0, 2.0 * Math.PI);
c.closePath();
c.stroke();
c.lineWidth = 1.0;
}
if (scene.showPressure) {
var s = "pressure: " + minP.toFixed(0) + " - " + maxP.toFixed(0) + " N/m";
c.fillStyle ="#000000";
c.font = "16px Arial";
c.fillText(s, 10, 35);
}
}
function setObstacle(x, y, reset) {
var vx = 0.0;
var vy = 0.0;
if (!reset) {
vx = (x - scene.obstacleX) / scene.dt;
vy = (y - scene.obstacleY) / scene.dt;
}
scene.obstacleX = x;
scene.obstacleY = y;
var r = scene.obstacleRadius;
var f = scene.fluid;
var n = f.numY;
var cd = Math.sqrt(2) * f.h;
for (var i = 1; i < f.numX-2; i++) {
for (var j = 1; j < f.numY-2; j++) {
f.s[i*n + j] = 1.0;
dx = (i + 0.5) * f.h - x;
dy = (j + 0.5) * f.h - y;
if (dx * dx + dy * dy < r * r) {
f.s[i*n + j] = 0.0;
if (scene.sceneNr == 2)
f.m[i*n + j] = 0.5 + 0.5 * Math.sin(0.1 * scene.frameNr)
else
f.m[i*n + j] = 1.0;
f.u[i*n + j] = vx;
f.u[(i+1)*n + j] = vx;
f.v[i*n + j] = vy;
f.v[i*n + j+1] = vy;
}
}
}
scene.showObstacle = true;
}
// interaction -------------------------------------------------------
var mouseDown = false;
function startDrag(x, y) {
let bounds = canvas.getBoundingClientRect();
let mx = x - bounds.left - canvas.clientLeft;
let my = y - bounds.top - canvas.clientTop;
mouseDown = true;
x = mx / cScale;
y = (canvas.height - my) / cScale;
setObstacle(x,y, true);
}
function drag(x, y) {
if (mouseDown) {
let bounds = canvas.getBoundingClientRect();
let mx = x - bounds.left - canvas.clientLeft;
let my = y - bounds.top - canvas.clientTop;
x = mx / cScale;
y = (canvas.height - my) / cScale;
setObstacle(x,y, false);
}
}
function endDrag() {
mouseDown = false;
}
canvas.addEventListener('mousedown', event => {
startDrag(event.x, event.y);
});
canvas.addEventListener('mouseup', event => {
endDrag();
});
canvas.addEventListener('mousemove', event => {
drag(event.x, event.y);
});
canvas.addEventListener('touchstart', event => {
startDrag(event.touches[0].clientX, event.touches[0].clientY)
});
canvas.addEventListener('touchend', event => {
endDrag()
});
canvas.addEventListener('touchmove', event => {
event.preventDefault();
event.stopImmediatePropagation();
drag(event.touches[0].clientX, event.touches[0].clientY)
}, { passive: false});
document.addEventListener('keydown', event => {
switch(event.key) {
case 'p': scene.paused = !scene.paused; break;
case 'm': scene.paused = false; simulate(); scene.paused = true; break;
}
});
function toggleStart()
{
var button = document.getElementById('startButton');
if (scene.paused)
button.innerHTML = "Stop";
else
button.innerHTML = "Start";
scene.paused = !scene.paused;
}
// main -------------------------------------------------------
function simulate()
{
if (!scene.paused)
scene.fluid.simulate(scene.dt, scene.gravity, scene.numIters)
scene.frameNr++;
}
function update() {
simulate();
draw();
requestAnimationFrame(update);
}
setupScene(1);
update();
</script>
</body>
</html>

View File

@@ -1,3 +1,5 @@
pub mod ten_minute_physics;
#[derive(Debug, Clone)]
pub struct Domain {
pub width: usize,

View File

@@ -4,68 +4,58 @@ use ratatui::{text::Text, Frame, widgets::canvas::Canvas, widgets::canvas::Point
use fluidsim::{Domain, simulate};
fn main() {
let width = 100;
let width = 140;
let height = 60;
let mut domain = Domain {
width: width + 2,
height: height + 2,
cells: core::iter::repeat_n((0.0, 0.0), (width+2)*(height+2)).collect(),
enabled: core::iter::repeat_n(1.0, (width+2)*(height+2)).collect(),
};
let mut t_domain = fluidsim::ten_minute_physics::Domain::new(1000.0, width, height);
t_domain.set_s_with(|x, y| {
1.0
});
t_domain.set_u_with(|x, y| {
if x == 0 {
2.0
} else {
0.0
}
});
let boundary = 10;
for y in boundary..domain.height-boundary {
let cell = domain.cells.get_mut(y*domain.width + 1).unwrap();
cell.0 = 1.0;
}
for y in 0..domain.height {
*domain.enabled.get_mut(y*domain.width).unwrap() = 0.0;
*domain.enabled.get_mut(y*domain.width + domain.width-1).unwrap() = 0.0;
}
for x in 0..domain.width {
*domain.enabled.get_mut(0*domain.width + x).unwrap() = 0.0;
*domain.enabled.get_mut((domain.height-1)*domain.width + x).unwrap() = 0.0;
}
t_domain.set_s_with(|x, y| {
if x > 40 && x < 50 && y > 20 && y < 30 {
return 0.0;
}
let (domain_tx, domain_rx) = std::sync::mpsc::channel();
1.0
});
let domain_mtx_1 = std::sync::Arc::new(std::sync::Mutex::new(t_domain.clone()));
let domain_mtx = domain_mtx_1.clone();
std::thread::spawn(move || {
let mut start = domain;
domain_tx.send(Domain {
width: start.width,
height: start.height,
cells: start.cells.clone(),
enabled: start.enabled.clone(),
}).unwrap();
let mut start = t_domain;
for i in 0.. {
// println!("Iteration {}", i);
simulate(&mut start);
start.reset_pressure();
if i % 100 == 0 {
domain_tx.send(Domain {
width: start.width,
height: start.height,
cells: start.cells.clone(),
enabled: start.enabled.clone(),
}).unwrap();
start.solve_incompressibility(100, 0.01);
start.extrapolate();
start.advect_vel(0.01);
start.advect_smoke(0.01);
if let Ok(mut mtx) = domain_mtx.try_lock() {
*mtx = start.clone();
}
}
std::thread::sleep(std::time::Duration::from_millis(10));
std::thread::sleep(std::time::Duration::from_millis(10));
}
});
let mut domain = domain_rx.recv().unwrap();
let domain_mtx = domain_mtx_1;
let mut domain = domain_mtx.lock().unwrap().clone();
let mut terminal = ratatui::init();
loop {
domain = match domain_rx.try_recv() {
Ok(d) => d,
Err(_) => domain,
};
domain = domain_mtx.lock().unwrap().clone();
terminal.draw(|frame| {
draw(frame, &domain)
@@ -80,33 +70,23 @@ fn main() {
ratatui::restore();
}
fn draw(frame: &mut Frame, domain: &Domain) {
let canvas = Canvas::default().x_bounds([0.0, domain.width as f64 - 2.0]).y_bounds([0.0, domain.height as f64 - 2.0]).paint(|ctx| {
for y in 1..domain.height-1 {
for x in 1..domain.width-1 {
let own_cell_idx = y * domain.width + x;
let top_cell_idx = (y-1) * domain.width + x;
let right_cell_idx = y * domain.width + x + 1;
fn draw(frame: &mut Frame, domain: &fluidsim::ten_minute_physics::Domain) {
let (width, height) = domain.inner_dims();
let own_cell = domain.cells.get(own_cell_idx).unwrap();
let top_cell = domain.cells.get(top_cell_idx).unwrap();
let right_cell = domain.cells.get(right_cell_idx).unwrap();
let canvas = Canvas::default().x_bounds([0.0, width as f64]).y_bounds([0.0, height as f64]).paint(|ctx| {
let (min_v, max_v) = domain.smoke_iter().fold((f32::MAX, f32::MIN), |(mip, map), v| {
(mip.min(v.2), map.max(v.2))
});
let d = right_cell.0 - own_cell.0 + top_cell.1 - own_cell.1;
// let text = format!("{:.4}", d);
// ctx.print(-0.5 + x as f64, -0.5 + y as f64, text);
let color = match d.total_cmp(&0.0) {
core::cmp::Ordering::Less if d.abs() > 0.0001 => ratatui::style::Color::Rgb(255, 0, 0),
core::cmp::Ordering::Greater if d.abs() > 0.0001 => ratatui::style::Color::Rgb(0, 255, 0),
_ => ratatui::style::Color::White,
};
for (x, y, d) in domain.smoke_iter() {
let inter = ((d - min_v) / (max_v - min_v)).sqrt();
let color = ratatui::style::Color::Rgb((255.0*inter) as u8, (255.0*inter) as u8, 0);
ctx.draw(&Points {
coords: &[(-0.5 + x as f64, -0.5 + y as f64)],
color: color,
})
}
}
});
frame.render_widget(canvas, frame.area());

276
src/ten_minute_physics.rs Normal file
View File

@@ -0,0 +1,276 @@
#[derive(Clone)]
pub struct Domain {
density: f32,
/// The inner width (without the extra padding cells at the edge)
width: usize,
/// The inner height (without the extra padding cells at the edge)
height: usize,
u: Vec<f32>,
v: Vec<f32>,
s: Vec<f32>,
p: Vec<f32>,
/// Smoke field
m: Vec<f32>,
}
enum Field {
U,
V,
Smoke,
}
impl Domain {
pub fn new(density:f32, width: usize, height: usize) -> Self {
let num_cells = (width + 2) * (height + 2);
Self {
density,
width,
height,
u: vec![0.0; num_cells],
v: vec![0.0; num_cells],
s: vec![0.0; num_cells],
p: vec![0.0; num_cells],
m: vec![1.0; num_cells],
}
}
pub fn reset_pressure(&mut self) {
self.p.fill(0.0);
}
pub fn set_s_with<F>(&mut self, func: F) where F: Fn(usize, usize) -> f32 {
for i in 1..self.width+1 {
for j in 1..self.height+1 {
self.s[i*(self.height+2)+j] = func(i-1, j-1);
}
}
}
pub fn set_u_with<F>(&mut self, func: F) where F: Fn(usize, usize) -> f32 {
for i in 1..self.width+1 {
for j in 1..self.height+1 {
self.u[i*(self.height+2)+j] = func(i-1, j-1);
}
}
}
pub fn set_v_with<F>(&mut self, func: F) where F: Fn(usize, usize) -> f32 {
for i in 1..self.width+1 {
for j in 1..self.height+1 {
self.v[i*(self.height+2)+j] = func(i-1, j-1);
}
}
}
pub fn inner_dims(&self) -> (usize, usize) {
(self.width, self.height)
}
pub fn div_iter(&self) -> impl Iterator<Item = (usize, usize, f32)> {
let n = self.height + 2;
(1..self.width+1)
.flat_map(|x| core::iter::repeat(x).zip(1..self.height+1))
.map(move |(x, y)| {
let div = self.u[(x+1)*n + y] - self.u[x*n + y] + self.v[x*n + y + 1] - self.v[x*n+y];
(x - 1, y - 1, div)
})
}
pub fn vel_iter(&self) -> impl Iterator<Item = (usize, usize, f32)> {
let n = self.height + 2;
(1..self.width+1)
.flat_map(|x| core::iter::repeat(x).zip(1..self.height+1))
.map(move |(x, y)| {
let u = self.u[x*n+y];
let v = self.v[x*n+y];
(x - 1, y - 1, (u*u + v*v).sqrt())
})
}
pub fn pressure_iter(&self) -> impl Iterator<Item = (usize, usize, f32)> {
let n = self.height + 2;
(1..self.width+1)
.flat_map(|x| core::iter::repeat(x).zip(1..self.height+1))
.map(move |(i, j)| {
(i - 1, j - 1, self.p[i*n+j])
})
}
pub fn smoke_iter(&self) -> impl Iterator<Item = (usize, usize, f32)> {
let n = self.height + 2;
(1..self.width+1)
.flat_map(|x| core::iter::repeat(x).zip(1..self.height+1))
.map(move |(i, j)| {
(i - 1, j - 1, self.m[i*n+j])
})
}
pub fn solve_incompressibility(&mut self, num_iters: usize, dT: f32) {
let n = self.height + 2;
let cp = self.density * 1.0 / dT;
for _ in 0..num_iters {
for i in 1..self.width+1 {
for j in 1..self.height+1 {
if self.s[i*n + j] == 0.0 {
continue;
}
let s = self.s[i*n + j];
let sx0 = self.s[(i-1)*n + j];
let sx1 = self.s[(i+1)*n + j];
let sy0 = self.s[i*n + j - 1];
let sy1 = self.s[i*n + j + 1];
let s = sx0 + sx1 + sy0 + sy1;
if s == 0.0 {
continue;
}
let div = self.u[(i+1)*n + j] - self.u[i*n + j] + self.v[i*n + j + 1] - self.v[i*n+j];
let p = -div / s;
let p = p * 1.9;
self.p[i*n+j] += cp * p;
self.u[i*n + j] -= sx0 * p;
self.u[(i+1)*n + j] += sx1 * p;
self.v[i*n+j] -= sy0 * p;
self.v[i*n + j + 1] += sy1 * p;
}
}
}
}
pub fn extrapolate(&mut self) {
let n = self.height + 2;
for i in 0..self.width+2 {
self.u[i*n + 0] = self.u[i*n + 1];
self.u[i*n + self.height+1] = self.u[i*n + self.height];
}
for j in 0..self.height+2 {
self.v[0*n + j] = self.u[1*n + j];
self.v[(self.width+1)*n + j] = self.u[self.width*n + j];
}
}
fn sample_field(&self, x: f32, y: f32, field: Field) -> f32 {
let n = self.height + 2;
let h = 1.0;
let h1 = 1.0/h;
let h2 = 0.5*h;
let x = (x.min((self.width+2) as f32 * h)).max(h);
let y = (y.min((self.height+2) as f32 * h)).max(h);
let (f, dx, dy) = match field {
Field::U => (&self.u, 0.0, h2),
Field::V => (&self.v, h2, 0.0),
Field::Smoke => (&self.m, h2, h2),
};
let x0 = ((x-dx)*h1).floor().min(self.width as f32 + 1.0);
let tx = ((x-dx) - x0*h) * h1;
let x1 = (x0 + 1.0).min(self.width as f32 + 1.0);
let x0 = x0 as usize;
let x1 = x1 as usize;
let y0 = ((y-dy)*h1).floor().min(self.height as f32 + 1.0);
let ty = ((y-dy) - y0*h) * h1;
let y1 = (y0 + 1.0).min(self.height as f32 + 1.0);
let y0 = y0 as usize;
let y1 = y1 as usize;
let sx = 1.0 - tx;
let sy = 1.0 - ty;
let val = sx * sy * f[x0*n + y0]
+ tx*sy * f[x1*n + y0]
+ tx*ty * f[x1*n + y1]
+ sx*ty * f[x0*n + y1];
return val;
}
pub fn avg_u(&self, i: usize, j: usize) -> f32 {
let n = self.height + 2;
let u = (self.u[i*n + j-1] + self.u[i*n+j] + self.u[(i+1)*n + j-1] + self.u[(i+1)*n + j]) * 0.25;
return u;
}
pub fn avg_v(&self, i: usize, j: usize) -> f32 {
let n = self.height + 2;
let v = (self.v[(i-1)*n + j] + self.v[i*n+j] + self.v[(i-1)*n + j + 1] + self.v[i*n + j*1]) * 0.25;
return v;
}
pub fn advect_vel(&mut self, dt: f32) {
let mut newU = self.u.clone();
let mut newV = self.v.clone();
let n = self.height + 2;
let h = 1.0;
let h2 = 0.5 * h;
for i in 1..self.width+2 {
for j in 1..self.height+2 {
// u component
if self.s[i*n + j] != 0.0 && self.s[(i-1)*n + j] != 0.0 && j < self.height+1 {
let x = i as f32 * h;
let y = j as f32 *h + h2;
let u = self.u[i*n + j];
let v = self.avg_v(i, j);
let x = x - dt*u;
let y = y - dt*v;
let u = self.sample_field(x, y, Field::U);
newU[i*n + j] = u;
}
// v component
if self.s[i*n + j] != 0.0 && self.s[i*n + j - 1] != 0.0 && i < self.width + 1 {
let x = i as f32 * h;
let y = j as f32 *h + h2;
let u = self.avg_u(i, j);
let v = self.v[i*n+j];
let x = x - dt*u;
let y = y - dt*v;
let v = self.sample_field(x, y, Field::V);
newV[i*n + j] = v;
}
}
}
self.u = newU;
self.v = newV;
}
pub fn advect_smoke(&mut self, dt: f32) {
let mut newM = self.m.clone();
let n = self.height + 2;
let h = 1.0;
let h2 = 0.5*h;
for i in 1..self.width+1 {
for j in 1..self.height+1 {
let u = (self.u[i*n+j] + self.u[(i+1)*n + j]) * 0.5;
let v = (self.v[i*n+j] + self.v[i*n + j+1]) * 0.5;
let x = i as f32 *h + h2 - dt*u;
let y = j as f32 *h + h2 - dt*v;
newM[i*n+j] = self.sample_field(x, y, Field::Smoke);
}
}
self.m = newM;
}
}