UNPKG

aframe-babia-components

Version:

A data visualization set of components for A-Frame.

797 lines (683 loc) 29.6 kB
// let updateTitle = require('../others/common').updateTitle; // const colors = require('../others/common').colors; // let updateFunction = require('../others/common').updateFunction; // const NotiBuffer = require("../../common/noti-buffer").NotiBuffer; /* global AFRAME */ if (typeof AFRAME === 'undefined') { throw new Error('Component attempted to register before AFRAME was available.'); } /** * A-Charts component for A-Frame. */ AFRAME.registerComponent("babia-map", { dependencies: ['raycaster'], schema: { width: { type: "number", default: 1000 }, height: { type: "number", default: 1000 }, horTexture: { type: "number", default: 2 }, vertTexture: { type: "number", default: 2 }, widthSegments: { type: "int", default: 200 }, heightSegments: { type: "int", default: 200 }, bumpScale: { type: "number", default: 300 }, wireframe: { type: "boolean", default: false }, token: { type: "string" }, zoom: { type: "number", default: 4 }, x: { type: "number", default: 0 }, y: { type: "number", default: 0 }, subdomain: { type: "string", default: "a" }, // a, b, c textureType: { type: "string", default: "Standard" }, coordinates: { type: "vec2" }, handsUi: { type: "boolean", default: true}, entityRightButton: { type: 'string', default: JSON.stringify({ position: '0.25 201.5 -0.5', rotation: '-30 0 0' }) }, entityLeftButton: { type: 'string', default: JSON.stringify({ position: '-0.25 201.5 -0.5', rotation: '-30 0 0' }) }, rightButtons: { type: 'string', default: JSON.stringify([ { height: '0.075', width: '0.2', label: "Zoom In", color: "blue", position: "0 0.15 -0.3", rotation: "0 0 0", textSize: 0.6, className: "zoomInBtn" }, { height: '0.075', width: '0.2', label: "Zoom Out", color: "purple", position: "0 0.05 -0.3", rotation: "-0 0 0", textSize: 0.6, className: "zoomOutBtn" } ]) }, crossEntity: { type: 'string', default: JSON.stringify({ position: "0 -0.15 -0.25", rotation: "0 0 0" }) }, crossButtons: { type: 'string', default: JSON.stringify([ { height: '0.06', width: '0.06', label: "^", color: "darkgray", position: "0 0.1 0", textSize: 0.6, className: "dpadUp" }, { height: '0.06', width: '0.06', label: "v", color: "darkgray", position: "0 -0.1 0", textSize: 0.6, className: "dpadDown" }, { height: '0.06', width: '0.06', label: "<", color: "darkgray", position: "-0.125 0 0", textSize: 0.6, className: "dpadLeft" }, { height: '0.06', width: '0.06', label: ">", color: "darkgray", position: "0.125 0 0", textSize: 0.6, className: "dpadRight" } ]) }, leftButtons: { type: 'string', default: JSON.stringify([ { height: '0.075', width: '0.2', label: "Next Layer", color: "green", position: "0 0.05 -0.3", rotation: "0 0 0", textSize: 0.6, className: "layerUpBtn" }, { height: '0.075', width: '0.2', label: "Previous Layer", color: "orange", position: "0 -0.05 -0.3", rotation: "0 0 0", textSize: 0.6, className: "layerDownBtn" } ]) }, coordinatesText: { type: 'string', default: JSON.stringify({ label: 'Coordinates: ', color: 'black', position: '-0.15 0.1 -0.2', rotation: '0 0 0', textSize: 0.6 }) } }, init: function () { const data = this.data; const el = this.el; const layers = ['Coloured', 'Cycle', 'Transport', 'Standard', 'Satellite', 'Hot']; let currentLayerIndex = layers.indexOf(el.getAttribute('babia-map').textureType); if (currentLayerIndex === -1) { // El valor no está en la lista currentLayerIndex = 0; } const BABIA_MAP_PRESET_UI = { rightButtons: [ { height: '0.075', width: '0.2', label: 'Zoom In', color: 'blue', position: '-0.2 0.1 -0.3', rotation: '-30 0 0', textSize: 0.6, className: 'zoomInBtn' }, { height: '0.075', width: '0.2', label: 'Zoom Out', color: 'purple', position: '-0.2 0 -0.25', rotation: '-30 0 0', textSize: 0.6, className: 'zoomOutBtn' } ], crossEntity: { position: '0.2 0 -0.2', rotation: '-30 0 0' }, crossButtons: [ { height: '0.06', width: '0.06', label: '^', color: 'darkgray', position: '-0.125 0.175 0', textSize: 0.6, className: 'dpadUp' }, { height: '0.06', width: '0.06', label: 'v', color: 'darkgray', position: '-0.125 0.025 0', textSize: 0.6, className: 'dpadDown' }, { height: '0.06', width: '0.06', label: '<', color: 'darkgray', position: '-0.2 0.1 0', textSize: 0.6, className: 'dpadLeft' }, { height: '0.06', width: '0.06', label: '>', color: 'darkgray', position: '-0.05 0.1 0', textSize: 0.6, className: 'dpadRight' } ], leftButtons: [ { height: '0.075', width: '0.2', label: 'Next Layer', color: 'green', position: '0 0.2 -0.3', rotation: '-30 0 0', textSize: 0.6, className: 'layerUpBtn' }, { height: '0.075', width: '0.2', label: 'Previous Layer', color: 'orange', position: '0 0.1 -0.25', rotation: '-30 0 0', textSize: 0.6, className: 'layerDownBtn' } ], coordinatesText: { label: 'Coordinates: ', color: 'black', position: '-0.15 0 -0.2', rotation: '-30 0 0', textSize: 0.6 } }; let crossButtons = []; let entityRightButton = []; let entityLeftButton = []; let rightButtons = []; let crossEntity = []; let leftButtons = []; let coordinatesText = []; try { // BOTONES EN RV const scene = document.querySelector('a-scene'); function findHandEntity(handedness) { // 1. Buscar por ID directo const idSelector = `#${handedness}Hand`; let entity = document.querySelector(idSelector); if (entity) return entity; // 2. Buscar entidad laser-controls con hand: left o right entity = document.querySelector(`[laser-controls][hand="${handedness}"]`); if (entity) return entity; // 3. Buscar entidad oculus-touch-controls con hand: left o right entity = document.querySelector(`[oculus-touch-controls][hand="${handedness}"]`); if (entity) return entity; // Si no se encontró nada return null; } // Ejemplo de uso: const leftHand = findHandEntity("left"); const rightHand = findHandEntity("right"); let stateHands = false; const handlers = { layerUp: () => { if (currentLayerIndex < layers.length - 1) { currentLayerIndex++; updateLayer(); } }, layerDown: () => { if (currentLayerIndex > 0) { currentLayerIndex--; updateLayer(); } }, zoomIn: () => updateZoom(true), zoomOut: () => updateZoom(false), mouseEnter: (e) => { e.target.setAttribute('material', 'color', 'yellow'); }, mouseLeave: (e) => { const el = e.target; const defaultColor = el.classList.contains('zoomInBtn') ? 'blue' : el.classList.contains('zoomOutBtn') ? 'purple' : el.classList.contains('layerUpBtn') ? 'green' : el.classList.contains('layerDownBtn') ? 'orange' : 'darkgray'; el.setAttribute('material', 'color', defaultColor); }, moveUp: () => move('up'), moveDown: () => move('down'), moveRight: () => move('right'), moveLeft: () => move('left'), }; function toggleClickEvents(enable, ...parents) { const toggleEvent = (el, event, handler) => { el[enable ? 'addEventListener' : 'removeEventListener'](event, handler); }; parents.forEach(parent => { // Mapear clases a handlers de click const clickMap = { '.layerUpBtn': handlers.layerUp, '.layerDownBtn': handlers.layerDown, '.zoomInBtn': handlers.zoomIn, '.zoomOutBtn': handlers.zoomOut, '.dpadUp': handlers.moveUp, '.dpadDown': handlers.moveDown, '.dpadLeft': handlers.moveLeft, '.dpadRight': handlers.moveRight, }; const allContainers = [parent, ...parent.querySelectorAll('.cross')]; allContainers.forEach(container => { for (const [selector, handler] of Object.entries(clickMap)) { container.querySelectorAll(selector).forEach(btn => toggleEvent(btn, 'click', handler)); } }); // Para los eventos mouseenter/mouseleave en botones interactivos parent.querySelectorAll('.interactive-button').forEach(btn => { toggleEvent(btn, 'mouseenter', handlers.mouseEnter); toggleEvent(btn, 'mouseleave', handlers.mouseLeave); }); }); } let openCloseMenu = (entity) => { entity.addEventListener('gripdown', function () { const children = entity.children; // Alternar el estado stateHands = !stateHands; for (let i = 0; i < children.length; i++) { if (children[i].setAttribute) { children[i].setAttribute('visible', stateHands); } } }) } crossButtons = JSON.parse(data.crossButtons); entityRightButton = JSON.parse(data.entityRightButton); entityLeftButton = JSON.parse(data.entityLeftButton); rightButtons = JSON.parse(data.rightButtons); crossEntity = JSON.parse(data.crossEntity); leftButtons = JSON.parse(data.leftButtons); coordinatesText = JSON.parse(data.coordinatesText); let leftDesktopHolder = document.createElement('a-entity'); leftDesktopHolder.setAttribute('position', entityLeftButton.position); leftDesktopHolder.setAttribute('rotation', entityLeftButton.rotation); leftDesktopHolder.classList.add('left-desktop-ui-position'); el.appendChild(leftDesktopHolder); let rightDesktopHolder = document.createElement('a-entity'); rightDesktopHolder.setAttribute('position', entityRightButton.position); rightDesktopHolder.setAttribute('rotation', entityRightButton.rotation); rightDesktopHolder.classList.add('right-desktop-ui-position'); el.appendChild(rightDesktopHolder); addButtons(leftDesktopHolder, rightDesktopHolder, rightButtons, crossEntity, crossButtons, leftButtons, coordinatesText); if (data.handsUi) { addButtons(leftHand, rightHand, BABIA_MAP_PRESET_UI.rightButtons, BABIA_MAP_PRESET_UI.crossEntity, BABIA_MAP_PRESET_UI.crossButtons, BABIA_MAP_PRESET_UI.leftButtons, BABIA_MAP_PRESET_UI.coordinatesText); scene.addEventListener('enter-vr', () => { console.log("Conectado a VR"); stateHands = true; leftDesktopHolder.setAttribute('visible', false); rightDesktopHolder.setAttribute('visible', false); leftHand.setAttribute('visible', true); rightHand.setAttribute('visible', true); openCloseMenu(leftHand); openCloseMenu(rightHand); toggleClickEvents(true, leftHand, rightHand); toggleClickEvents(false, leftDesktopHolder, rightDesktopHolder); }); scene.addEventListener('exit-vr', () => { console.log("Saliendo de VR"); stateHands = false; leftDesktopHolder.setAttribute('visible', true); rightDesktopHolder.setAttribute('visible', true); leftHand.setAttribute('visible', false); rightHand.setAttribute('visible', false); toggleClickEvents(false, leftHand, rightHand); toggleClickEvents(true, leftDesktopHolder, rightDesktopHolder); }); } const updateLayer = () => { const newLayer = layers[currentLayerIndex]; console.log('Cambiando a capa:', newLayer); el.setAttribute('babia-map', 'textureType', newLayer); this.buildMesh(data, el); // Redibujar el mapa con la nueva capa }; const updateZoom = (zoomIn) => { const MIN_ZOOM = 0; const MAX_ZOOM = 19; let currentZoom = el.getAttribute('babia-map').zoom; let currentX = el.getAttribute('babia-map').x; let currentY = el.getAttribute('babia-map').y; if (zoomIn === true && currentZoom < MAX_ZOOM) { currentZoom += 1; currentX = Math.min(currentX * 2, Math.pow(2, currentZoom) - 1); currentY = Math.min(currentY * 2, Math.pow(2, currentZoom) - 1); } else if (!zoomIn && currentZoom > MIN_ZOOM) { currentZoom -= 1; currentX = Math.max(0, Math.trunc(currentX / 2)); currentY = Math.max(0, Math.trunc(currentY / 2)); } el.setAttribute('babia-map', 'zoom', currentZoom); el.setAttribute('babia-map', 'x', currentX); el.setAttribute('babia-map', 'y', currentY); this.buildMesh(data, el); }; const move = (where) => { let currentZoom = el.getAttribute('babia-map').zoom; let currentX = el.getAttribute('babia-map').x; let currentY = el.getAttribute('babia-map').y; const maxX = Math.pow(2, currentZoom) - 1; const maxY = maxX; console.log(where); switch (where) { case 'up': if (currentY > 0) currentY -= 1; break; case 'down': if (currentY < maxY) currentY += 1; break; case 'right': if (currentX < maxX) currentX += 1; break; case 'left': if (currentX > 0) currentX -= 1; break; default: console.error("Opción de movimiento no permitida."); } el.setAttribute('babia-map', 'x', currentX); el.setAttribute('babia-map', 'y', currentY); this.buildMesh(data, el); }; // Convierte coordenadas de tile a lat/lon (como las usa OSM) function localCoordsToLatLon(localX, localZ, el) { const zoom = el.getAttribute('babia-map').zoom; const tileX = el.getAttribute('babia-map').x; const tileY = el.getAttribute('babia-map').y; const width = el.getAttribute('babia-map').width; const height = el.getAttribute('babia-map').height; // Convertimos coordenadas locales en [0, 1] const u = (localX + width / 2) / width; const v = 1 - (localZ + height / 2) / height; // Coordenadas reales de tesela (decimales) const n = Math.pow(2, zoom); const globalTileX = tileX + u; const globalTileY = tileY + v; console.log(`Global: ${globalTileX}, ${globalTileY}`); // Conversión a lat/lon usando fórmula de Web Mercator const lon = globalTileX / n * 360 - 180; const latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * globalTileY / n))); const lat = latRad * (180 / Math.PI); return { lat, lon }; } leftHand.addEventListener('click', (evt) => { const intersection = evt.detail.intersection; if (!intersection) return; const targetEl = intersection.object.el; // Si no es el mapa, no sigue if (targetEl !== el) return; const point = intersection.point.clone(); const localPoint = el.object3D.worldToLocal(point); const localX = localPoint.x; const localY = localPoint.y; const localZ = localPoint.z; const geoCoords = localCoordsToLatLon(localX, localZ, el); el.setAttribute('babia-map', 'coordinates', { x: geoCoords.lat, y: geoCoords.lon }); let marker = el.querySelector('.marker'); if (!marker) { marker = document.createElement('a-entity'); marker.setAttribute('geometry', { primitive: 'sphere', radius: data.width/100 }); marker.setAttribute('material', { color: 'red' }); marker.setAttribute('position', { x: localX, y: localY+(data.bumpScale/1.5), z: localZ }); marker.classList.add('marker'); el.appendChild(marker); } else { marker.object3D.position.set(localX, localY+(data.bumpScale/1.5), localZ); marker.setAttribute('visible', 'true'); } const coords = el.getAttribute('babia-map').coordinates; console.log(`Coordenadas: ${coords.x.toFixed(2)}, ${coords.y.toFixed(2)}`) let textBlocks = el.querySelectorAll('.coordinates-text'); textBlocks.forEach (textBlock => { textBlock.setAttribute('value', `Coordenadas: ${coords.x.toFixed(2)}, ${coords.y.toFixed(2)}`); }) if(data.handsUi) { let textBlock = leftHand.querySelector('.coordinates-text'); textBlock.setAttribute('value', `Coordenadas: ${coords.x.toFixed(2)}, ${coords.y.toFixed(2)}`); } }); toggleClickEvents(true, leftDesktopHolder, rightDesktopHolder); // GUARDAR EL TOKEN const form = document.querySelector("#token-form"); const input = document.querySelector("#token-input"); const savedToken = localStorage.getItem("babia-map-token"); if (savedToken) { input.value = savedToken; data.token = savedToken; this.buildMesh(data, el); } if (form && input) { form.addEventListener("submit", (e) => { e.preventDefault(); // ← esto evita que la página se recargue const token = input.value.trim(); if (token) { data.token = token; localStorage.setItem("babia-map-token", token); console.log("Token actualizado:", token); this.buildMesh(data, el); } else { alert("Por favor, introduce un token válido."); } }); } else { console.warn("Formulario de token no encontrado en el DOM."); } } catch (e) { console.warn('Error:', e); } }, remove: function () { this.el.removeObject3D("mesh"); }, update: function (oldData) { const data = this.data; const el = this.el; if (data.token !== oldData.token || data.textureType !== oldData.textureType) { el.removeObject3D("mesh"); this.buildMesh(data, el); } }, buildMesh: function (data, el) { let minHeight; let maxHeight; const vertexShader1 = ` uniform sampler2D bumpTexture; uniform float bumpScale; varying float vAmount; void main() { vec4 bumpData = texture2D(bumpTexture, uv); vAmount = bumpData.r; vec3 newPosition = position + normal * bumpScale * vAmount; gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); } `; const fragmentShader1 = ` uniform float minHeight; uniform float maxHeight; varying float vAmount; void main() { float totalHeight = maxHeight - minHeight; float maxWater = minHeight + totalHeight * 0.55; float minSand = minHeight + totalHeight * 0.54; float maxSand = minHeight + totalHeight * 0.57; float minGrass = minHeight + totalHeight * 0.56; float maxGrass = minHeight + totalHeight * 0.7; float minRock = minHeight + totalHeight * 0.60; float maxRock = minHeight + totalHeight * 0.80; float minSnow = minHeight + totalHeight * 0.65; vec3 water = (smoothstep(0.00, maxWater, vAmount) - smoothstep(minSand, maxWater, vAmount)) * vec3(0.0, 0.0, 1.0); vec3 sand = (smoothstep(minSand, maxSand, vAmount) - smoothstep(minGrass, maxSand, vAmount)) * vec3(0.76, 0.7, 0.5); vec3 grass = (smoothstep(minGrass, maxGrass, vAmount) - smoothstep(minRock, maxGrass, vAmount)) * vec3(0.0, 0.6, 0.01); vec3 rock = (smoothstep(minRock, maxRock, vAmount) - smoothstep(minSnow, maxRock, vAmount)) * vec3(0.28, 0.25, 0.23); vec3 snow = (smoothstep(minSnow, 1.0, vAmount)) * vec3(1.0, 1.0, 1.0); gl_FragColor = vec4(water + sand + grass + rock + snow, 1.0); } `; const vertexShader2 = ` uniform sampler2D bumpTexture; uniform float bumpScale; varying vec2 vUv; varying float vAmount; void main() { vUv = uv; vec4 bumpData = texture2D(bumpTexture, uv); vAmount = bumpData.r; // Solo canal rojo vec3 newPosition = position + normal * bumpScale * vAmount; // Posición final del vértice con transformación del modelo, vista y proyección gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); } `; const fragmentShader2 = ` uniform sampler2D mapTexture; varying vec2 vUv; void main() { vec3 textureColor = texture2D(mapTexture, vUv).rgb; gl_FragColor = vec4(textureColor, 1.0); } `; // CREAR TEXTURA DESDE IMAGEN async function loadDisplacementMap(url) { let img = new Image(); img.src = url; img.crossOrigin = "Anonymous"; // Evitar problemas CORS return new Promise((resolve, reject) => { img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext("2d"); // Dibujar la imagen en el canvas de forma invertida (la orientación de la imagen es diferente en canvas respecto a WebGL) ctx.translate(0, canvas.height); ctx.scale(1, -1); ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, img.width, img.height); const data = imageData.data; // Crear un array para la textura de desplazamiento const displacement = new Float32Array(img.width * img.height); let elevation; let auxMinHeight = 1; let auxMaxHeight = 0; for (let i = 0; i < data.length; i += 4) { const R = data[i]; const G = data[i + 1]; const B = data[i + 2]; // Fórmula de Mapbox terrain-RGB -> Elevación en metros elevation = (R * 256 * 256 + G * 256 + B) * 0.1 - 10000; // Normalizar para Three.js (opcional, si necesitas valores entre 0 y 1) elevation = (elevation + 10000) / 20000; // Elevación entre 0 y 1 // Comprobar el valor más bajo y más alto if (elevation < auxMinHeight) { auxMinHeight = elevation; } if (elevation > auxMaxHeight) { auxMaxHeight = elevation; } displacement[i / 4] = elevation; } // Guardar el valor más bajo y más alto minHeight = auxMinHeight; maxHeight = auxMaxHeight; // Crear textura de desplazamiento const texture = new THREE.DataTexture( displacement, img.width, img.height, THREE.RedFormat, // Un canal para el desplazamiento THREE.FloatType // Usamos valores en coma flotante ); texture.needsUpdate = true; resolve(texture); }; img.onerror = reject; }); }; let urlMapbox = `https://api.mapbox.com/v4/mapbox.terrain-rgb/${data.zoom}/${data.x}/${data.y}@2x.pngraw?access_token=${data.token}` // CREAR MALLA loadDisplacementMap(urlMapbox).then((texture) => { console.log("Displacement map cargado:", urlMapbox); // Horizontal & vertical texture repetition texture.wrapS = texture.wrapT = THREE.RepeatWrapping; texture.repeat.set(data.horTexture, data.vertTexture); // Añadir uniformes let uniforms; let groundMaterial; // Si el tipo de textura es 0 (Openstreetmap) // Si el tipo de textura es 1 (Shaders) if (data.textureType === "Coloured") { uniforms = { bumpTexture: { type: "t", value: texture }, bumpScale: { type: "f", value: data.bumpScale }, minHeight: { value: minHeight }, maxHeight: { value: maxHeight }, }; console.log("Uniforms:", uniforms); // Añadir shader groundMaterial = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: vertexShader1, fragmentShader: fragmentShader1, }); } else { let url; switch (data.textureType) { case "Standard": // Si el tipo de textura es Openstreetmap Estándar url = `https://tile.openstreetmap.org/${data.zoom}/${data.x}/${data.y}.png`; break; case "Satellite": // Si el tipo de textura es Satélite url = `https://api.mapbox.com/styles/v1/mapbox/satellite-v9/tiles/256/${data.zoom}/${data.x}/${data.y}?access_token=${data.token}`; break; case "Hot": // Si el tipo de textura es Humanitaian OpenStreetMap Team url = `https://tile-a.openstreetmap.fr/hot/${data.zoom}/${data.x}/${data.y}.png`; break; case "Cycle": // Si el tipo de textura es OpenCycleMap url = `https://${data.subdomain}.tile.thunderforest.com/cycle/${data.zoom}/${data.x}/${data.y}.png`; break; case "Transport": // Si el tipo de textura es Thunderforest Transport url = `https://${data.subdomain}.tile.thunderforest.com/transport/${data.zoom}/${data.x}/${data.y}.png`; break; default: console.error("Tipo de textura no soportado"); }; const textureLoader = new THREE.TextureLoader(); const mapTexture = textureLoader.load(url); uniforms = { bumpTexture: { type: "t", value: texture }, bumpScale: { type: "f", value: data.bumpScale }, mapTexture: { value: mapTexture }, }; console.log("Uniforms:", uniforms); // Añadir shader groundMaterial = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: vertexShader2, fragmentShader: fragmentShader2, }); } // Crear geometría del terreno const groundGeo = new THREE.PlaneGeometry( data.width, data.height, data.widthSegments, data.heightSegments ); // Crear malla let groundMesh = new THREE.Mesh(groundGeo, groundMaterial); groundMesh.rotation.x = -Math.PI / 2; groundMesh.position.y = 0; // Añadir la malla a la entidad el.setObject3D("mesh", groundMesh); console.log(data) }, (error) => { console.error("Error al cargar el displacement map:", error); } ); } }); function addButtons(leftMenu, rightMenu, rightButtons, crossEntity, crossButtons, leftButtons, coordinatesText) { leftButtons.forEach(btnData => { const btn = createButton( `primitive: plane; height: ${btnData.height}; width: ${btnData.width}`, btnData.label, btnData.color, btnData.position, btnData.rotation, btnData.textSize, [btnData.className] ); leftMenu.appendChild(btn); }); rightButtons.forEach(btnData => { const btn = createButton( `primitive: plane; height: ${btnData.height}; width: ${btnData.width}`, btnData.label, btnData.color, btnData.position, btnData.rotation, btnData.textSize, [btnData.className] ); rightMenu.appendChild(btn); }); let cross = document.createElement('a-entity'); cross.setAttribute('rotation', crossEntity.rotation); // este es el tamaño del texto cross.setAttribute('position', crossEntity.position); cross.classList.add('cross'); rightMenu.appendChild(cross); crossButtons.forEach(btnData => { const btn = createButton( `primitive: plane; height: ${btnData.height}; width: ${btnData.width}`, btnData.label, btnData.color, btnData.position, btnData.rotation, btnData.textSize, [btnData.className] ); cross.appendChild(btn); }); let textBlock = document.createElement('a-text'); textBlock.setAttribute('value', coordinatesText.label); textBlock.setAttribute('color', coordinatesText.color); textBlock.setAttribute('position', coordinatesText.position); textBlock.setAttribute('rotation', coordinatesText.rotation); textBlock.setAttribute('width', coordinatesText.textSize); textBlock.classList.add('coordinates-text'); leftMenu.appendChild(textBlock); } function createButton(geometry, label, color, position, rotation = null, textSize = 0.5, classes = []) { const button = document.createElement('a-text'); button.setAttribute('geometry', geometry); button.setAttribute('material', `color: ${color}`); button.setAttribute('value', label); button.setAttribute('align', 'center'); button.setAttribute('width', textSize); button.setAttribute('position', position); if (rotation) { button.setAttribute('rotation', rotation); } button.classList.add('collidable', 'interactive-button', ...classes); return button; }