/* Copyright (C) 2024 Sebastián Santisi , 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 = "

Aerogenerador Nº" + (i + 1) + "

" + (Math.round(this.simulationPot[i] * 100) / 100) + ' GWh
' + this.divPercentBar(perc, 50) + '
' + 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 = '

En un parque real tenemos que garantizar que la posición de los aerogeneradores sea la adecuada para todos los vientos posibles.

Este es el resultado de ensayar tu configuración con los 4 vientos disponibles en este simulador:

'; var pots = {0: 0, 225: 0, 270: 0, 315: 0} for(let i = 0; i < this.simulationPot.length; i++) { html += ''; [0, 315, 270, 225].forEach(pot => { var val = this.simulationPots[pot][i]; pots[pot] += val; var perc = val / 14.95 * 100; html += ''; }); html += ""; } html += ''; 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 += ''; }); html += "
AerogeneradorViento
Viento
Viento
Viento
Nº' + (i + 1) + '' + this.divPercentBar(perc, 50) + ' ' + Math.round(perc) + '%
TOTAL' + this.divPercentBar(perc, 80) + ' ' + Math.round(perc) + '%
"; if(notValid.length) { html += '
' + EXCLAMATION + '

Para ser una configuración correcta hay que superar el 80% de producción para todas las direcciones de vientos.

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 += '' + dirs[notValid[i]] + ''; } html += '.

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.

'; } else { html += '
' + PARTY + '

¡Felicitaciones! Tu configuración valida una producción de al menos el 80% para todas las direcciones de vientos del simulador.

'; if(this.simulationPot.length < 10) html += '

Utilizá el botón + para agregar más aerogeneradores y producir más energía en tu parque.

'; else html += '

¡SAPE! Lograste hacer funcionar tu parque con el máximo de aerogeneradores que permite este simulador.

'; } window.show(html); } divPercentBar(perc, limit) { return '
'; } 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 = '

Resultados de la simulación

' + (pasa ? '

' + PARTY + PARTY + PARTY + ' ¡Tu simulación está bien con este viento! Clickeá "¡validar!" para evaluar en los demás.

' : '

' + EXCLAMATION + EXCLAMATION + EXCLAMATION + ' ¡Tu simulación no funciona! Probá otras configuraciones.

') + ''; for(let i = 0; i < pot.length; i++) { var perc = Math.round(pot[i] / 14.95 * 100); innerHTML += '' } var perc = Math.round(sum / 14.95 / pot.length * 100); innerHTML += '
Nº' + (i + 1) + ':' + Math.round(pot[i] * 100) / 100 + ' GWh' + this.divPercentBar(perc, 50) + ' ' + perc + '% ' + (perc < 50 ? EXCLAMATION : '') + '
TOTAL:' + Math.round(sum * 100) / 100 + ' GWh' + this.divPercentBar(perc, 80) + ' ' + perc + '% ' + (perc < 80 ? EXCLAMATION : '') + '
'; 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 += '
' + (i + 1) + '
'; } 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();