"use strict"; const RoomGeometryGame = (function(){ const RoomGeometryGame = function(hollow_bond){ const cache = {}; /** * @param {RoomModel} model * @param {Array} frames * @param {Array} holes */ this.get_for_room = (model, frames = [], holes = []) => { const pad = model.padding || 0; const w = Math.max(0.1, model.width - pad * 2); const h = Math.max(0.1, model.height - pad * 2); const d = Math.max(0.1, model.depth - pad * 2); const frame_ids = frames.map(f => f.id).sort().join(","); const hole_ids = holes.map(h => h.id).sort().join(","); // const key = `${w}_${h}_${d}_[F:${frame_ids}]_[H:${hole_ids}]`; const key = model.id; if(cache[key]){ return cache[key]; } const geometry = new THREE.BufferGeometry(); const hx = w / 2; const hy = h / 2; const hz = d / 2; const positions = []; const normals = []; const uvs = []; const indices = []; let index_offset = 0; const add_quad = (p1, p2, p3, p4, normal, uv_w, uv_h, uv_ox = 0, uv_oy = 0) => { positions.push(...p1, ...p2, ...p3, ...p4); normals.push(...normal, ...normal, ...normal, ...normal); uvs.push( uv_ox, uv_oy, uv_ox + uv_w, uv_oy, uv_ox + uv_w, uv_oy + uv_h, uv_ox, uv_oy + uv_h ); indices.push( index_offset, index_offset + 2, index_offset + 1, index_offset, index_offset + 3, index_offset + 2 ); index_offset += 4; }; // Obtenemos el índice del piso normalizado (0, 1, -1, -2...) const py = Math.round(model.y / model.height); // Filtrado de huecos activos en Suelo y Techo // Suelo perforado solo si el pozo continúa hacia abajo const floor_holes = holes.filter(hole => py > hole.yf && py < hole.yt); // Techo perforado solo si el pozo continúa hacia arriba const ceiling_holes = holes.filter(hole => py >= hole.yf && py < (hole.yt - 1)); // Subdivisión de un plano horizontal alrededor de 1 o más huecos const build_horizontal_plane = (hole_list, is_floor) => { const y_pos = is_floor ? -hy : hy; const norm = is_floor ? [0, 1, 0] : [0, -1, 0]; const add_surface_patch = (x1, x2, z1, z2) => { const pw = x2 - x1; const pd = z2 - z1; if(pw <= 0.001 || pd <= 0.001) return; const px1 = -hx + x1, px2 = -hx + x2; const pz1 = -hz + z1, pz2 = -hz + z2; if(is_floor){ add_quad( [px1, y_pos, pz1], [px2, y_pos, pz1], [px2, y_pos, pz2], [px1, y_pos, pz2], norm, pw, pd, x1, z1 ); } else { add_quad( [px1, y_pos, pz2], [px2, y_pos, pz2], [px2, y_pos, pz1], [px1, y_pos, pz1], norm, pw, pd, x1, z1 ); } }; // Si no hay huecos en este plano, generamos el plano entero de una pieza if(!hole_list || hole_list.length === 0){ add_surface_patch(0, w, 0, d); return; } // Lista de parches de superficie sólida inicial (toda la habitación) let patches = [{ x1: 0, x2: w, z1: 0, z2: d }]; for(const target_hole of hole_list){ const hx1 = Math.max(0, target_hole.xf - (model.x + pad)); const hx2 = Math.min(w, target_hole.xt - (model.x + pad)); const hz1 = Math.max(0, target_hole.zf - (model.z + pad)); const hz2 = Math.min(d, target_hole.zt - (model.z + pad)); // Si el hueco no solapa con el espacio útil de la sala, lo ignoramos if(hx2 <= hx1 || hz2 <= hz1) continue; const next_patches = []; for(const p of patches){ // Comprobar si el parche actual 'p' interseca con este hueco const ix1 = Math.max(p.x1, hx1); const ix2 = Math.min(p.x2, hx2); const iz1 = Math.max(p.z1, hz1); const iz2 = Math.min(p.z2, hz2); // Si no hay solape, el parche se conserva íntegro if(ix1 >= ix2 || iz1 >= iz2){ next_patches.push(p); continue; } // Sustracción 2D: dividimos el parche en las 4 regiones que rodean el hueco // 1. Franja Norte (Z entre p.z1 e iz1) if(iz1 > p.z1){ next_patches.push({ x1: p.x1, x2: p.x2, z1: p.z1, z2: iz1 }); } // 2. Franja Sur (Z entre iz2 y p.z2) if(p.z2 > iz2){ next_patches.push({ x1: p.x1, x2: p.x2, z1: iz2, z2: p.z2 }); } // 3. Franja Oeste (X entre p.x1 e ix1, Z recortado entre iz1 e iz2) if(ix1 > p.x1){ next_patches.push({ x1: p.x1, x2: ix1, z1: iz1, z2: iz2 }); } // 4. Franja Este (X entre ix2 y p.x2, Z recortado entre iz1 e iz2) if(p.x2 > ix2){ next_patches.push({ x1: ix2, x2: p.x2, z1: iz1, z2: iz2 }); } } patches = next_patches; } // Generamos la geometría de todos los parches sólidos resultantes for(const p of patches){ add_surface_patch(p.x1, p.x2, p.z1, p.z2); } }; // --- 1. SUELO (+Y) -> Grupo 0 --- const floor_start = indices.length; build_horizontal_plane(floor_holes, true); geometry.addGroup(floor_start, indices.length - floor_start, 0); // --- 2. TECHO (-Y) -> Grupo 1 --- const ceiling_start = indices.length; build_horizontal_plane(ceiling_holes, false); geometry.addGroup(ceiling_start, indices.length - ceiling_start, 1); // --- 3. PAREDES -> Grupo 2 --- const walls_start = indices.length; const wall_frames = { north: [], south: [], west: [], east: [] }; frames.forEach(f => { if(f.axis === "horizontal"){ const dist_north = Math.abs(f.z - model.z); const dist_south = Math.abs(f.z - (model.z + model.depth)); if(dist_north <= dist_south) wall_frames.north.push(f); else wall_frames.south.push(f); } else { const dist_west = Math.abs(f.x - model.x); const dist_east = Math.abs(f.x - (model.x + model.width)); if(dist_west <= dist_east) wall_frames.west.push(f); else wall_frames.east.push(f); } }); const build_segmented_wall = (wall_len, wall_h, frame_list, add_rect, normal) => { if(!frame_list || frame_list.length === 0){ add_rect(0, wall_len, 0, wall_h, normal); return; } const openings = frame_list.map(f => { const offset = (f.axis === "horizontal") ? (f.x - (model.x + pad)) : (f.z - (model.z + pad)); return { start: Math.max(0, offset), end: Math.min(wall_len, offset + f.width), height: Math.min(wall_h, f.height) }; }).sort((a, b) => a.start - b.start); let current_x = 0; openings.forEach(op => { if(op.start > current_x){ add_rect(current_x, op.start, 0, wall_h, normal); } if(op.height < wall_h){ add_rect(op.start, op.end, op.height, wall_h, normal); } current_x = Math.max(current_x, op.end); }); if(current_x < wall_len){ add_rect(current_x, wall_len, 0, wall_h, normal); } }; // Norte (-Z) build_segmented_wall(w, h, wall_frames.north, (x1, x2, y1, y2, norm) => { const px1 = -hx + x1, px2 = -hx + x2, py1 = -hy + y1, py2 = -hy + y2; add_quad([px2, py1, -hz], [px1, py1, -hz], [px1, py2, -hz], [px2, py2, -hz], norm, x2 - x1, y2 - y1, x1, y1); }, [0, 0, 1]); // Sur (+Z) build_segmented_wall(w, h, wall_frames.south, (x1, x2, y1, y2, norm) => { const px1 = -hx + x1, px2 = -hx + x2, py1 = -hy + y1, py2 = -hy + y2; add_quad([px1, py1, hz], [px2, py1, hz], [px2, py2, hz], [px1, py2, hz], norm, x2 - x1, y2 - y1, x1, y1); }, [0, 0, -1]); // Oeste (-X) build_segmented_wall(d, h, wall_frames.west, (z1, z2, y1, y2, norm) => { const pz1 = -hz + z1, pz2 = -hz + z2, py1 = -hy + y1, py2 = -hy + y2; add_quad([-hx, py1, pz1], [-hx, py1, pz2], [-hx, py2, pz2], [-hx, py2, pz1], norm, z2 - z1, y2 - y1, z1, y1); }, [1, 0, 0]); // Este (+X) build_segmented_wall(d, h, wall_frames.east, (z1, z2, y1, y2, norm) => { const pz1 = -hz + z1, pz2 = -hz + z2, py1 = -hy + y1, py2 = -hy + y2; add_quad([hx, py1, pz2], [hx, py1, pz1], [hx, py2, pz1], [hx, py2, pz2], norm, z2 - z1, y2 - y1, z1, y1); }, [-1, 0, 0]); geometry.addGroup(walls_start, indices.length - walls_start, 2); geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); geometry.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3)); geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2)); geometry.setIndex(indices); cache[key] = geometry; return geometry; }; this.reset = () => { for(const key in cache){ if(cache[key]){ cache[key].dispose(); delete cache[key]; } } }; }; return RoomGeometryGame; })();