You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
405 lines
16 KiB
JavaScript
405 lines
16 KiB
JavaScript
/* Copyright (C) 2024 Sebastián Santisi <ssantisi@fi.uba.ar>, CSC-CONICET */
|
|
|
|
import * as THREE from 'three';
|
|
import { STLLoader } from 'https://cdn.jsdelivr.net/npm/three@0.136.0/examples/jsm/loaders/STLLoader.js';
|
|
import { WindMill } from './windmill.js';
|
|
import { Screen } from './screen.js';
|
|
import { ArgentinianFlag } from './argentinianflag.js';
|
|
import { checkBoard } from './checkboard.js';
|
|
|
|
window.three = THREE;
|
|
|
|
const scene = new THREE.Scene();
|
|
const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
|
|
const renderer = new THREE.WebGLRenderer({antialias: true});
|
|
document.body.appendChild(renderer.domElement);
|
|
//renderer.setPixelRatio( window.devicePixelRatio );
|
|
|
|
const EXCLAMATION = '⚠️';
|
|
const PARTY = '🥳';
|
|
|
|
class WindSimulation {
|
|
constructor(width, height, mills) {
|
|
this.width = width;
|
|
this.height = height;
|
|
|
|
this.wms = [];
|
|
this.init_windmills(mills);
|
|
|
|
this.wm = null;
|
|
this.simulation = null;
|
|
this.simulationPots = null;
|
|
this.simulationPot = null;
|
|
this.xhr = null;
|
|
|
|
this.lastFrame = Date.now();
|
|
|
|
this.rotation = Math.PI;
|
|
this.targetRotation = Math.PI;
|
|
this.dir = 0;
|
|
|
|
this.checkBoard = checkBoard(width, height);
|
|
scene.add(this.checkBoard);
|
|
|
|
const loader = new STLLoader();
|
|
loader.load('assets/MonumentoBandera.stl', (geometry) => {
|
|
var material = new THREE.MeshStandardMaterial({color: 0xffffff});
|
|
var mesh = new THREE.Mesh(geometry, material);
|
|
mesh.castShadow = true;
|
|
mesh.receiveShadow = true;
|
|
var scale = 0.0093;
|
|
mesh.scale.setScalar(scale);
|
|
mesh.rotation.z = -Math.PI / 2;
|
|
mesh.position.x = 9.5;
|
|
mesh.position.y = 0.3;
|
|
scene.add(mesh);
|
|
this.monument = mesh;
|
|
});
|
|
|
|
this.flag = new ArgentinianFlag(0.3);
|
|
this.flag.position.set(9.5, 0.3, 0.3);
|
|
this.flag.rotation.z = Math.PI;
|
|
scene.add(this.flag);
|
|
}
|
|
|
|
init_windmills(mills) {
|
|
if(!WindMill.isReady()) {
|
|
setTimeout(() => { this.init_windmills(mills) }, 100);
|
|
return;
|
|
}
|
|
|
|
for(var i = 0; i < mills; i++)
|
|
this.addWindMill();
|
|
|
|
this.wm = null;
|
|
}
|
|
|
|
move(pos, ended=false, count=2) {
|
|
if(this.simulation)
|
|
this.resetSimulation();
|
|
|
|
if(! count)
|
|
return false;
|
|
|
|
var wm = this.wm;
|
|
|
|
if(ended) {
|
|
if(this.wm != null) this.wm.select(false);
|
|
this.wm = null;
|
|
}
|
|
|
|
if(wm == null) return false;
|
|
if(pos.x > this.width - 0.5 || pos.x < 0.5 || pos.y > this.height - 0.5 || pos.y < 0.5) {
|
|
if(pos.x > this.width - 0.5)
|
|
pos.x = this.width - 0.5;
|
|
if(pos.x < 0.5)
|
|
pos.x = 0.5;
|
|
if(pos.y > this.height - 0.5)
|
|
pos.y = this.height - 0.5;
|
|
if(pos.y < 0.5)
|
|
pos.y = 0.5;
|
|
return this.move(pos, ended, count - 1);
|
|
}
|
|
for(var i = 0; i < this.wms.length; i++) {
|
|
if(this.wms[i] == wm) continue;
|
|
if(pos.clone().sub(this.wms[i].position).length() < 1) {
|
|
return this.move(this.wms[i].position.clone().add(pos.clone().sub(this.wms[i].position).normalize()), ended, count - 1);
|
|
}
|
|
}
|
|
wm.position.x = pos.x;
|
|
wm.position.y = pos.y;
|
|
return true;
|
|
}
|
|
|
|
animate() {
|
|
requestAnimationFrame(() => { this.animate(); });
|
|
|
|
var now = Date.now();
|
|
var step = (now - this.lastFrame) / 1000;
|
|
|
|
if(!Screen.isMobile || (Screen.isMobile && screen.clicked)) {
|
|
const intersects = screen.raycaster.intersectObjects(this.wms);
|
|
var wm = null;
|
|
for(var i = 0; i < intersects.length; i++) {
|
|
if(intersects[i].object.constructor.name != "Spring") {
|
|
wm = intersects[i].object.parent;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!screen.clicked || Screen.isMobile) {
|
|
if(this.wm != wm) {
|
|
if(this.wm != null) {
|
|
this.wm.select(false);
|
|
document.getElementById("follower").style.display = "none";
|
|
}
|
|
if(wm != null) {
|
|
wm.select(true);
|
|
if(this.simulation) {
|
|
var i = this.wms.indexOf(wm);
|
|
var follower = document.getElementById("follower");
|
|
var perc = Math.round(this.simulationPot[i] * 100 / 14.95);
|
|
follower.innerHTML = "<h3>Aerogenerador Nº" + (i + 1) + "</h4>" + (Math.round(this.simulationPot[i] * 100) / 100) + ' GWh<br />' + this.divPercentBar(perc, 50) + '<br />' + perc + '% ' + (perc < 50 ? '⚠️' : '');
|
|
follower.style.display = "block";
|
|
}
|
|
}
|
|
}
|
|
this.wm = wm;
|
|
}
|
|
}
|
|
|
|
for(var i = 0; i < this.wms.length; i++)
|
|
this.wms[i].animate(step);
|
|
|
|
var speed = 2 * step;
|
|
if(Math.abs(this.targetRotation - this.rotation) > speed) {
|
|
var step = speed * Math.sign(this.targetRotation - this.rotation);
|
|
this.rotation += step;
|
|
for(var i = 0; i < this.wms.length; i++) {
|
|
this.wms[i].rotation.z += step;
|
|
}
|
|
this.flag.rotation.z += step;
|
|
}
|
|
|
|
this.flag.animate();
|
|
|
|
if(window.exclamation)
|
|
window.exclamation.rotation.y += 0.02;
|
|
|
|
this.lastFrame = now;
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
resetSimulation() {
|
|
scene.remove(this.simulation);
|
|
this.simulation = null;
|
|
this.simulationPots = null;
|
|
this.simulationPot = null;
|
|
for(var i = 0; i < this.wms.length; i++)
|
|
this.wms[i].lowPower(false);
|
|
document.getElementById("follower").style.display = "none";
|
|
document.getElementById("results").style.display = "none";
|
|
document.getElementById("numbers").innerHTML = "";
|
|
document.getElementById("button_validate").style.display = "none";
|
|
}
|
|
|
|
setRotation(dir) {
|
|
if(this.simulation)
|
|
this.resetSimulation();
|
|
|
|
var rotations = {};
|
|
rotations[0] = Math.PI;
|
|
rotations[270] = Math.PI / 2;
|
|
rotations[270 + 45] = Math.PI * 3 / 4;
|
|
rotations[270 - 45] = Math.PI / 4;
|
|
if(! (dir in rotations)) return false;
|
|
this.dir = dir;
|
|
this.targetRotation = rotations[dir];
|
|
}
|
|
|
|
positionValid(pos) {
|
|
for(let i = 0; i < this.wms.length; i++)
|
|
if(pos.clone().sub(this.wms[i].position).length() < 1)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
addWindMill() {
|
|
if(this.wms.length >= 10) return;
|
|
|
|
for(let x = 0; x < 5; x++)
|
|
for(let y = 0; y < 10; y++) {
|
|
var pos = new THREE.Vector3(4.5 + Math.ceil(x / 2) * (x % 2 ? 1 : -1), 4.5 + Math.ceil(y / 2) * (y % 2 ? 1 : -1), 0);
|
|
if(this.positionValid(pos)) {
|
|
var wm = new WindMill();
|
|
wm.position.set(pos.x, pos.y, 0);
|
|
wm.rotation.z = this.targetRotation;
|
|
scene.add(wm);
|
|
this.wms.push(wm);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
removeWindMill() {
|
|
if(this.wms.length <= 1) return;
|
|
|
|
if(this.wms[this.wms.length - 1] == this.wm) this.wm = null;
|
|
scene.remove(this.wms.pop());
|
|
}
|
|
|
|
simulate() {
|
|
if(this.xhr != null)
|
|
this.xhr.abort();
|
|
|
|
var xhr = new XMLHttpRequest();
|
|
this.xhr = xhr;
|
|
xhr.open("POST", "/solver/", true);
|
|
xhr.setRequestHeader("Content-Type", "application/json");
|
|
xhr.onreadystatechange = () => {
|
|
if(this.xhr != xhr) return;
|
|
if (xhr.readyState === 4 && xhr.status === 200) {
|
|
var json = JSON.parse(xhr.responseText);
|
|
this.add_simulation(json.ws, json.pot);
|
|
this.xhr = null;
|
|
}
|
|
};
|
|
var pos = [];
|
|
for(var i = 0; i < this.wms.length; i++)
|
|
pos.push([this.wms[i].position.y, this.wms[i].position.x]);
|
|
var data = JSON.stringify({"pos": pos, "dir": this.dir});
|
|
xhr.send(data);
|
|
}
|
|
|
|
validate() {
|
|
if(this.simulationPots == null) return;
|
|
|
|
var html = '<p>En un parque real tenemos que garantizar que la posición de los aerogeneradores sea la adecuada para todos los vientos posibles.</p><p>Este es el resultado de ensayar tu configuración con los 4 vientos disponibles en este simulador:</p><center><table><tr><th>Aerogenerador</th><th>Viento <div class="b">⬅</div></th><th>Viento <div class="b">⬉</div></th><th>Viento <div class="b">⬆</div></th><th>Viento <div class="b">⬈</div></th></tr>';
|
|
|
|
var pots = {0: 0, 225: 0, 270: 0, 315: 0}
|
|
|
|
for(let i = 0; i < this.simulationPot.length; i++) {
|
|
html += '<tr><td><b>Nº' + (i + 1) + '</b></td>';
|
|
[0, 315, 270, 225].forEach(pot => {
|
|
var val = this.simulationPots[pot][i];
|
|
pots[pot] += val;
|
|
var perc = val / 14.95 * 100;
|
|
html += '<td>' + this.divPercentBar(perc, 50) + ' ' + Math.round(perc) + '%</td>';
|
|
});
|
|
html += "</tr>";
|
|
}
|
|
html += '<tr class="last"><td><b>TOTAL</b></td>';
|
|
var notValid = [];
|
|
[0, 315, 270, 225].forEach(pot => {
|
|
var perc = pots[pot] / 14.95 * 100 / this.simulationPot.length;
|
|
if(perc < 80) notValid.push(pot);
|
|
html += '<td>' + this.divPercentBar(perc, 80) + ' ' + Math.round(perc) + '%</td>';
|
|
});
|
|
html += "</table></center>";
|
|
|
|
if(notValid.length) {
|
|
html += '<div style="float: left; font-size: 3em; padding-right: .3em;">' + EXCLAMATION + '</div><p>Para ser una configuración correcta hay que superar el 80% de producción para todas las direcciones de vientos.</p><p>Tu simulación no valida con ' + (notValid.length > 1 ? 'las direcciones ' : 'la dirección ');
|
|
var dirs = {0: '⬅', 315: '⬉', 270: '⬆', 225: '⬈'};
|
|
for(let i = 0; i < notValid.length; i++) {
|
|
html += '<span style="display: inline-block;" class="b">' + dirs[notValid[i]] + '</span>';
|
|
}
|
|
html += '.</p><p>Usá los botones de arriba a la derecha para simular tu parque con cada una de las direcciones asegurándote de cumplir con el objetivo.</p>';
|
|
}
|
|
else {
|
|
html += '<div style="float: left; font-size: 3em; padding-right: .3em;">' + PARTY + '</div><p>¡Felicitaciones! Tu configuración valida una producción de al menos el 80% para todas las direcciones de vientos del simulador.</p>';
|
|
if(this.simulationPot.length < 10)
|
|
html += '<p>Utilizá el botón <span class="b">+</span> para agregar más aerogeneradores y producir más energía en tu parque.</p>';
|
|
else
|
|
html += '<p>¡SAPE! Lograste hacer funcionar tu parque con el máximo de aerogeneradores que permite este simulador.</p>';
|
|
}
|
|
|
|
window.show(html);
|
|
}
|
|
|
|
divPercentBar(perc, limit) {
|
|
return '<div class="bar" style="background: linear-gradient(to right, ' + (perc >= limit ? '#fff' : '#f00') + ' ' + perc + '%, #0000 ' + perc + '%);"></div>';
|
|
}
|
|
|
|
add_simulation(ws, pots) {
|
|
var pot = pots[this.dir];
|
|
|
|
var sum = 0;
|
|
for(let i = 0; i < pot.length; i++) {
|
|
this.wms[i].lowPower(pot[i] < 14.95 * 0.5);
|
|
sum += pot[i];
|
|
}
|
|
var pasa = Math.round(sum / 14.95 / pot.length * 100) >= 80;
|
|
|
|
var results = document.getElementById("results")
|
|
var innerHTML = '<h3>Resultados de la simulación</h3>' + (pasa ?
|
|
'<p style="max-width: 16em">' + PARTY + PARTY + PARTY + ' ¡Tu simulación <b>está bien</b> con este viento! Clickeá "¡validar!" para evaluar en los demás.</p>' :
|
|
'<p style="max-width: 16em">' + EXCLAMATION + EXCLAMATION + EXCLAMATION + ' ¡Tu simulación <b>no</b> funciona! Probá otras configuraciones.</p>') + '<table>';
|
|
|
|
for(let i = 0; i < pot.length; i++) {
|
|
var perc = Math.round(pot[i] / 14.95 * 100);
|
|
innerHTML += '<tr><td><b>Nº' + (i + 1) + ':</b></td><td>' + Math.round(pot[i] * 100) / 100 + ' GWh</td><td>' + this.divPercentBar(perc, 50) + ' ' + perc + '% ' + (perc < 50 ? EXCLAMATION : '') + '</td></tr>'
|
|
}
|
|
var perc = Math.round(sum / 14.95 / pot.length * 100);
|
|
innerHTML += '<tr class="last"><td>TOTAL:</td><td>' + Math.round(sum * 100) / 100 + ' GWh</td><td>' + this.divPercentBar(perc, 80) + ' ' + perc + '% ' + (perc < 80 ? EXCLAMATION : '') + '</td></tr></table>';
|
|
|
|
results.innerHTML = innerHTML;
|
|
results.style.display = "block";
|
|
|
|
if(Screen.isMobile) {
|
|
var numbers = "";
|
|
for(let i = 0; i < pot.length; i++) {
|
|
var p = new THREE.Vector3(this.wms[i].position.x, this.wms[i].position.y, 1);
|
|
p.project(screen.camera);
|
|
p.x = (p.x + 1) / 2 * window.innerWidth;
|
|
p.y = -(p.y - 1) / 2 * window.innerHeight;
|
|
numbers += '<div style="left: ' + p.x + 'px; top: ' + p.y + 'px;">' + (i + 1) + '</div>';
|
|
}
|
|
document.getElementById("numbers").innerHTML = numbers;
|
|
}
|
|
|
|
var geometry = new THREE.PlaneGeometry(this.width, this.height, 10 * this.width, 10 * this.height);
|
|
var colors = new three.BufferAttribute(new Float32Array(geometry.attributes.position.count * 4), 4);
|
|
geometry.setAttribute('color', colors);
|
|
const material = new THREE.MeshBasicMaterial({vertexColors: true, transparent: true,});
|
|
var mesh = new THREE.Mesh(geometry, material);
|
|
mesh.position.set(this.width / 2, this.height / 2, 90/126);
|
|
|
|
for(var i = 0; i < colors.count; i++) {
|
|
var j = Math.trunc(i / 101);
|
|
colors.setXYZW(i, 1 - ws[i % 101][100 - j] / 8, 0, 0, 1 - ws[i % 101][100 - j] / 8);
|
|
}
|
|
|
|
if(this.simulation)
|
|
this.resetSimulation();
|
|
|
|
document.getElementById("button_validate").style.display = "block";
|
|
|
|
this.simulation = mesh;
|
|
this.simulationPots = pots;
|
|
this.simulationPot = pot;
|
|
scene.add(mesh);
|
|
}
|
|
}
|
|
|
|
var width = 10;
|
|
var height = 10;
|
|
var ws = new WindSimulation(width, height, 5);
|
|
|
|
var screen = new Screen(camera, renderer, new THREE.Vector3(width / 2 + 0.5, height / 2, -0.3));
|
|
screen.setMoveCallBack((pos, end) => { ws.move(pos, end); });
|
|
screen.onResize();
|
|
|
|
|
|
/*const light = new THREE.DirectionalLight("#FFFFFF");
|
|
light.position.set(5,5,100);
|
|
//scene.add(light);*/
|
|
const ambientLight = new THREE.AmbientLight("#999999", 0.6);
|
|
scene.add(ambientLight);
|
|
const light = new THREE.DirectionalLight( 0xfff8ee, 1 );
|
|
light.position.set(6, -10, 10); //default; light shining from top
|
|
//light.target.position.set(5, 5, 0);
|
|
light.castShadow = true; // default false
|
|
scene.add( light );
|
|
light.shadow.mapSize.width = 1024; // default 512
|
|
light.shadow.mapSize.height = 1024; // default 512
|
|
light.shadow.camera.near = 5;
|
|
light.shadow.camera.far = 25;
|
|
light.shadow.camera.right = 3.5 + 5;
|
|
light.shadow.camera.left = 3.5 - 5;
|
|
light.shadow.camera.top = 6 + 6;
|
|
light.shadow.camera.bottom = 6 - 6;
|
|
|
|
renderer.shadowMap.enabled = true;
|
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap
|
|
window.light = light;
|
|
|
|
//const helper = new THREE.CameraHelper( light.shadow.camera );
|
|
//scene.add( helper);
|
|
//window.helper = helper;
|
|
|
|
window.ws = ws;
|
|
window.screen = screen;
|
|
window.scene = scene;
|
|
ws.animate();
|