import { Canvas } from '@react-three/fiber'; import { OrbitControls, Edges, Grid, Html } from '@react-three/drei'; /* ========================================================= 双良节能大楼 · 3D 建筑模型(React Three Fiber) 按「详细的布局.png」像素级重建 —— 60×42 网格 -------------------------------------------------------- 设计原则: · 颜色仅供理解,渲染统一:所有地面 = 同一浅灰色 · 靠资产区分功能:办公室=办公桌 · 会议室=会议长桌+讲台 · 休息区=沙发 · 共享工位=工位桌 · 楼道=真实楼梯 · 一楼大厅(玻璃顶下方挑空)外墙 + 窗户 · 二楼楼板避开玻璃顶中庭(两块拼接,无覆盖/无悬空) · 相邻办公室共享一堵墙;过道两侧不设墙,门开向过道 ========================================================= */ const U = 0.5; // 网格单位 → 世界单位(60×42 = 30×21) const FLOOR2 = 1.9; // 二楼楼板高度 const ROOM_H = 1.5; // 二楼墙高 const HALL_H = 2.4; // 一楼大厅净高(至玻璃顶) const T = 0.14; // 墙厚 // 统一地面色(所有地面同一个颜色) const FLOOR_COLOR = '#e7ecf3'; const WALL_COLOR = '#eef3fb'; const WALL_EDGE = '#9db1cf'; /* ---- 房间矩形(60×42 坐标 [x0,x1,z0,z1,type,no])---- 编号规则:成长区 G01-G08 · 国际区 I01-I07 · 加速区 A01-A12 会议室 M · 楼道 S · 共享工位区 W · 休息区 L · 过道 H 注意:编号用于逐间修正定位,保持稳定 */ const ROOMS = [ // 玻璃顶(二楼中庭挑空) { x: [1, 44], z: [1, 27], type: 'glass', no: 'T' }, // 右上:成长区(左 8 间) | 过道 | 国际区(右 7 间) // 编号从底部开始:G1 在最下(行23-24),G8 在最上(行1-3) // 企业按 Excel 场地安排:成长1=Facebook 越南跨境电商 → 成长8=综合直播私域平台 { x: [45, 51], z: [1, 3], type: 'office', zone: '成长区', no: 'G8', company: '综合性云南直播私域平台' }, { x: [53, 54], z: [1, 3], type: 'hallway', no: 'H01' }, { x: [55, 58], z: [1, 3], type: 'stair', no: 'W01' }, { x: [45, 48], z: [5, 6], type: 'office', zone: '成长区', no: 'G7', company: '云南星瑞航空' }, { x: [45, 48], z: [8, 9], type: 'office', zone: '成长区', no: 'G6', company: '五华区丽裳文化' }, { x: [45, 48], z: [11, 12], type: 'office', zone: '成长区', no: 'G5', company: '南菌优培食用菌' }, { x: [45, 48], z: [14, 15], type: 'office', zone: '成长区', no: 'G4', company: '昆明云韵体育' }, { x: [45, 48], z: [17, 18], type: 'office', zone: '成长区', no: 'G3', company: '朵哈·玫瑰特色产业链' }, { x: [45, 48], z: [20, 21], type: 'office', zone: '成长区', no: 'G2', company: '研X同行者网络' }, { x: [45, 48], z: [23, 24], type: 'office', zone: '成长区', no: 'G1', company: 'Facebook越南跨境电商' }, { x: [50, 53], z: [5, 25], type: 'hallway', no: 'H02' }, // 国际区编号从底部开始:I1 在最下(行23-24),I7 在最上(行5-6) // 企业按 Excel:国际1=仰光客厅 → 国际7=达岸教育管理 { x: [55, 57], z: [5, 6], type: 'office', zone: '国际区', no: 'I7', company: '达岸教育管理' }, { x: [55, 57], z: [8, 9], type: 'office', zone: '国际区', no: 'I6', company: '昆明舒诺生物科技' }, { x: [55, 57], z: [11, 12], type: 'office', zone: '国际区', no: 'I5', company: '滇缅国际设计' }, { x: [55, 57], z: [14, 15], type: 'office', zone: '国际区', no: 'I4', company: '酷享野农AI农业' }, { x: [55, 57], z: [17, 18], type: 'office', zone: '国际区', no: 'I3', company: '中越生物医疗' }, { x: [55, 57], z: [20, 21], type: 'office', zone: '国际区', no: 'I2', company: '云南上古绝学文化' }, { x: [55, 57], z: [23, 24], type: 'office', zone: '国际区', no: 'I1', company: '仰光客厅' }, { x: [45, 58], z: [26, 27], type: 'hallway', no: 'H03' }, // 加速区:11 间办公室(下排6间 + 上排5间),编号从右下角开始 // 下排从右到左:A1-A5 + 左下角园区管理办公室 // 上排从右到左:A6-A10 // 企业按 Excel 场地安排:加速1=云南派音 → 加速10=云南大学AI平台 // ===== 上排(5 间 + 1 待入驻)===== { x: [1, 4], z: [28, 32], type: 'office', zone: '加速区', no: 'A11', company: '待入驻' }, { x: [6, 12], z: [28, 32], type: 'office', zone: '加速区', no: 'A10', company: '待入驻' }, { x: [14, 16], z: [28, 32], type: 'office', zone: '加速区', no: 'A9', company: '云南大学AI+创业平台' }, { x: [18, 21], z: [28, 32], type: 'office', zone: '加速区', no: 'A8', company: '瀚颖AI+教育信息咨询' }, { x: [23, 26], z: [28, 32], type: 'office', zone: '加速区', no: 'A7', company: '云南廷秀文旅康养' }, { x: [6, 12], z: [36, 40], type: 'office', zone: '加速区', no: 'A6', company: '人工智能机器人大模型训练' }, // x: [28, 31], z: [28, 32] { x: [32, 43], z: [28, 32], type: 'desk', no: 'W02' }, { x: [45, 48], z: [28, 32], type: 'lounge', no: 'L01' }, { x: [50, 58], z: [28, 32], type: 'hallway', no: 'H04' }, // 横向过道(行33-35) { x: [1, 58], z: [33, 35], type: 'hallway', no: 'H05' }, // ===== 下排(6 间:A1-A5 + 园区管理办公室)===== { x: [1, 4], z: [36, 40], type: 'office', zone: '管理', no: 'M', company: '园区管理办公室' }, // x: [1, 4], z: [36, 40], { x: [28, 31], z: [28, 32], type: 'office', zone: '加速区', no: '', company: '' }, // x: [6, 12], z: [36, 40] { x: [14, 16], z: [36, 40], type: 'office', zone: '加速区', no: 'A5', company: '昆明智海银高文化科技' }, { x: [18, 21], z: [36, 40], type: 'office', zone: '加速区', no: 'A4', company: '云南宸中低空经济' }, { x: [23, 26], z: [36, 40], type: 'office', zone: '加速区', no: 'A3', company: '中泰研学合作' }, { x: [28, 30], z: [36, 40], type: 'meeting', no: 'M02' }, // 小会议室(A12 位置) { x: [32, 33], z: [36, 40], type: 'stair', no: 'S01' }, // 楼道(真实楼梯通向一楼) { x: [35, 45], z: [36, 40], type: 'meeting', no: 'M01' }, // 大会议室(长桌+讲台) { x: [47, 49], z: [36, 40], type: 'office', zone: '加速区', no: 'A2', company: '米勒克尔蓝宝石珠宝' }, { x: [51, 53.5], z: [36, 40], type: 'office', zone: '加速区', no: 'A1', company: '云南派音人工智能科技' }, ]; /* ---- 水平墙段 [z, x0, x1](像素精确提取)---- 说明:行1-3 顶部 G01 南墙在行4;行4/7/10/13/16/19/22/25 为成长区/国际区段间墙; 过道(列50-53)处无墙;行27 玻璃顶南墙;行28-40 加速区内部墙(单格垂直墙点) */ const HWALLS = [ [0, 0.5, 57.5], // 北外墙 [4, 44.5, 48.5], [4, 51, 51], [4, 54, 57.5], // 顶部 G01 南墙(含列51 隔墙;列50 为过道口) [7, 44.5, 48.5], [7, 54, 57.5], [10, 44.5, 48.5], [10, 54, 57.5], [13, 44.5, 48.5], [13, 54, 57.5], [16, 44.5, 48.5], [16, 54, 57.5], [19, 44.5, 48.5], [19, 54, 57.5], [22, 44.5, 48.5], [22, 54, 57.5], [25, 44.5, 48.5], [25, 54, 57.5], [27, 0, 44], // 玻璃顶南墙(全高) // 过道上侧 [32.5, 0.5, 2.5], [32.5, 5, 10], [32.5, 13, 15], [32.5, 17.3, 19], [32.5, 21, 25], // z=36 横墙已去除;补相邻垂直墙(z36.6-40.5)间的横向短墙段(z=36.6,x 从第一堵起到第二堵-1) [36, 0.5, 2.5], [36, 5, 10], [36, 13, 14.5], [36, 17, 19], [36, 22, 24], [36, 27, 29], [36, 33.5, 34], // 过道 [36, 37, 44], [36, 46.3, 48.5], [36, 51, 53.1], // 派音右 [41, 0, 53], // 南外墙 ]; /* ---- 垂直墙段 [x, z0, z1](像素精确提取)---- */ const VWALLS = [ [0, 0.5, 41], // 西外墙 [5, 27, 32], [5, 36.6, 40.5], [13, 27, 32], [13, 36.6, 40.5], [17, 27, 32], [17, 36.6, 40.5], [22, 27, 32], [22, 36.6, 40.5], [27, 27, 32], [27, 36.6, 40.5], [31, 36.6, 40.5], // 小会议室与楼道之间 [34, 36.6, 40.5], // 楼道与大会议室之间 [44, 0.5, 32], [44, 36.6, 40.5], // 玻璃顶东墙 / 大会议室西墙 [49, 4, 5], [49, 7, 8], [49, 10, 11], [49, 13, 14], [49, 16, 17], [49, 19, 20], [49, 22, 23], // 成长区段间西侧墙 [53.5, 4, 5], [53.5, 7, 8], [53.5, 10, 11], [53.5, 13, 14], [53.5, 16, 17], [53.5, 19, 20], [53.5, 22, 23], // 成长区段间西侧墙 [48.5, 36.6, 40.5], // 底部 A12/A13 之间 [53.5, 36.6, 40.5], // 底部 A1 右墙(X-0.5=53.5,Z-0.5=[35.5,40.5]) [51, 0.5, 3.4], // 顶部共享工位西缘(G01 东墙) [58, 0.5, 24.7], // 东外墙(原 [58,0,25],延伸覆盖底部 A1 东侧) ]; const W = 60, D = 42; const cx = (x) => (x - W / 2) * U; const cz = (z) => (z - D / 2) * U; /* ---------- 基础组件 ---------- */ function Wall({ x, z, w, h, axis = 'z', color = WALL_COLOR, edge = WALL_EDGE, y = 0 }) { const size = axis === 'z' ? [w, h, T] : [T, h, w]; return ( ); } /* 窗(一楼外墙上的窗格:玻璃面板 + 窗框) */ function Window({ x, z, w, h = 1.0, axis = 'z', y = 0.85 }) { const size = axis === 'z' ? [w, h, 0.04] : [0.04, h, w]; return ( ); } /* 办公桌(办公桌 + 座椅) */ function Desk({ x, z, y = 0, rotY = 0 }) { return ( {/* 显示器 */} {/* 座椅 */} ); } /* 圆桌(茶几) */ function Table({ x, z, y = 0, s = 1 }) { return ( ); } /* 会议长桌 + 座椅 */ function MeetingTable({ x, z, y = 0, rotY = 0, len = 2.6 }) { return ( {[0, 1].map((i) => ( ))} {[-0.5, 0, 0.5].map((dx) => ( ))} ); } /* 会议室小办公桌(小型方桌 + 座椅,多张布置,不穿墙) */ function MeetingDesk({ x, z, y = 0, rotY = 0 }) { return ( {/* 桌面 */} {/* 桌腿 */} {/* 座椅(桌前) */} ); } /* 讲台(会议室讲台) */ function Podium({ x, z, y = 0 }) { return ( ); } /* 工位桌(共享工位区) */ function Workstation({ x, z, y = 0, rotY = 0 }) { return ( ); } /* 沙发 */ function Sofa({ x, z, y = 0, rotY = 0 }) { return ( ); } /* 绿植 */ function Plant({ x, z, y = 0, s = 1 }) { return ( ); } /* 楼梯(楼梯间:从二楼通向一楼 —— 踏步自二楼逐级向下延伸至地面,居中铺满房间) */ function Stairs({ x, z, y = 0, rotY = 0, steps = 9, stepW = 0.9, stepD = 0.24, rise }) { // 默认向下:最高一级落在 y(二楼楼面),最低一级落在 y - rise*(steps-1) = 0(一楼地面) const r = rise ?? (steps > 1 ? y / (steps - 1) : y); const len = steps * stepD; const els = []; for (let i = 0; i < steps; i++) { const dz = i * stepD - len / 2; // 居中铺满房间(避免穿墙) const h = y - r * i; // 从二楼向下逐级降低 els.push( ); } return ( {els} ); } function ZoneTag({ x, y, z, cls, label, sub, no }) { return (
{sub && {sub.text}} {no && {no}} {label}
); } /* ---------- 建筑主体 ---------- */ function Building() { const floors = []; const walls = []; const roofs = []; const furniture = []; const tags = []; /* ===== 1. 一楼大厅(玻璃顶下方挑空) ===== */ const hx0 = cx(0), hx1 = cx(44); const hz0 = cz(0), hz1 = cz(27); const hcX = (hx0 + hx1) / 2, hcZ = (hz0 + hz1) / 2; const hW = hx1 - hx0, hD = hz1 - hz0; // 一楼地面(统一色) floors.push( ); // 一楼大厅外墙(高度 = FLOOR2,不穿过二楼楼板)+ 窗户 const hallWalls = [ { axis: 'z', pos: 0, from: 0, to: 44 }, { axis: 'z', pos: 41.1, from: 0, to: 44 }, { axis: 'x', pos: 0, from: 0, to: 41 }, { axis: 'x', pos: 44, from: 0, to: 41 }, ]; hallWalls.forEach((wl, i) => { const posW = wl.axis === 'z' ? cz(wl.pos) : cx(wl.pos); const fromW = wl.axis === 'z' ? cx(wl.from) : cz(wl.from); const toW = wl.axis === 'z' ? cx(wl.to) : cz(wl.to); walls.push( ); // 窗(沿墙均匀分布) const len = Math.abs(toW - fromW); const winN = Math.max(2, Math.floor(len / 2.4)); for (let wi = 0; wi < winN; wi++) { const t = (wi + 0.5) / winN; const pos = fromW + len * t; if (wl.axis === 'z') { walls.push(); } else { walls.push(); } } }); // 大厅立柱 for (let i = 0; i <= 8; i++) { const t = i / 8; const px = hx0 + 0.8 + (hW - 1.6) * t; furniture.push( ); furniture.push( ); } // 吊灯(二楼楼板下方) [hcX - 4, hcX, hcX + 4].forEach((lx, li) => { furniture.push( ); }); // 大厅家具 furniture.push(); furniture.push(); furniture.push(); furniture.push(); furniture.push(); tags.push(); /* ===== 2. 二楼整体地面(两块拼接,避开玻璃顶中庭) ===== */ // 右上块:x[45,58] z[1,28] floors.push( ); // 加速块:x[1,58] z[28,41] floors.push( ); /* ===== 3. 房间家具 + 标签(地面统一色,不铺色块) ===== */ ROOMS.forEach((r, i) => { const x0 = cx(r.x[0]), x1 = cx(r.x[1]); const z0 = cz(r.z[0]), z1 = cz(r.z[1]); const cX = (x0 + x1) / 2, cZ = (z0 + z1) / 2; const w = x1 - x0, d = z1 - z0; if (r.type === 'glass') { // 玻璃顶:二楼中庭挑空;绿植放中庭平台 furniture.push(); furniture.push(); // 玻璃天窗(青绿半透明 + 框架梁) const gw = w + 0.4, gd = d + 0.4; roofs.push( ); for (let k = 0; k <= 8; k++) { const t = k / 8; const px = x0 - 0.2 + gw * t; roofs.push(); } for (let k = 0; k <= 4; k++) { const t = k / 4; const pz = z0 - 0.2 + gd * t; roofs.push(); } tags.push(); return; } // 过道不渲染顶棚(hallway:无顶棚,避免半透明色块干扰地面观感) // 家具按功能区分(地面统一色,靠资产区分) if (r.type === 'office') { // 办公室:办公桌按房间面积排布,留边距不穿墙 const roomW = r.x[1] - r.x[0]; const roomD = r.z[1] - r.z[0]; // 窄房间(宽 < 2.6):单桌且旋转 90°(桌+椅沿深度方向排布,避免横向穿墙) const narrow = roomW < 2.6; if (narrow) { furniture.push( ); } else { const nX = Math.max(1, Math.min(3, Math.floor(roomW / 2.4))); const nZ = Math.max(1, Math.min(2, Math.floor(roomD / 2.4))); const total = Math.min(4, nX * nZ); let idx = 0; for (let ix = 0; ix < nX && idx < total; ix++) { for (let iz = 0; iz < nZ && idx < total; iz++, idx++) { // 留边距居中,确保桌面/椅不穿墙 const dx = (ix + 0.5) / nX - 0.5; const dz = (iz + 0.5) / nZ - 0.5; furniture.push( ); } } } } else if (r.type === 'desk') { // 共享工位区:工位桌多张 const nX = Math.max(2, Math.floor((r.x[1] - r.x[0]) / 3)); const nZ = Math.max(1, Math.floor((r.z[1] - r.z[0]) / 3)); let idx = 0; for (let ix = 0; ix < nX; ix++) { for (let iz = 0; iz < nZ; iz++, idx++) { const dx = (ix + 0.5) / nX - 0.5; const dz = (iz + 0.5) / nZ - 0.5; furniture.push(); } } } else if (r.type === 'lounge') { // 休息区:多张沙发 + 茶几 furniture.push(); furniture.push(); furniture.push(); furniture.push(
); } else if (r.type === 'meeting') { // 会议室:多张小办公桌(MeetingDesk)按房间网格排布,不穿墙且满布 const roomW = r.x[1] - r.x[0]; const roomD = r.z[1] - r.z[0]; const nD = Math.max(1, Math.min(6, Math.floor((roomW - 0.8) / 1.2))); // 沿 X 列(留边距,桌宽0.9+隙) const nR = Math.max(1, Math.min(4, Math.floor((roomD - 0.8) / 1.2))); // 沿 Z 行 const total = Math.min(12, nD * nR); // 上限 12 张小桌 let idx = 0; for (let ix = 0; ix < nD && idx < total; ix++) { for (let iz = 0; iz < nR && idx < total; iz++, idx++) { // 留边距居中(每侧 0.5 单位),避免穿墙 const dx = (ix + 0.5) / nD - 0.5; const dz = (iz + 0.5) / nR - 0.5; furniture.push( ); } } // 大会议室额外加讲台 const aBig = roomW * roomD >= 40; if (aBig) furniture.push(); } else if (r.type === 'stair') { // 楼梯间:真实楼梯(二楼 → 一楼),自动按房间宽/深选择梯段方向与级数 const alongX = (x1 - x0) >= (z1 - z0); // 房间宽≥深 → 梯段沿 X 方向 const sX = (x0 + x1) / 2; const sZ = (z0 + z1) / 2; const sLen = alongX ? (x1 - x0) : (z1 - z0); const steps = Math.max(4, Math.min(11, Math.round(sLen / 0.45))); furniture.push( ); } // 标签:每个房间显示编号(编号 + 名称),办公室显示入驻企业名;过道不显示标签 if (r.type === 'hallway') return; const noLabel = r.no || ''; const typeLabel = r.type === 'desk' ? '共享工位区' : r.type === 'lounge' ? '休息区' : r.type === 'meeting' ? '会议室' : r.type === 'stair' ? '楼梯间' : r.type === 'office' ? (r.company || r.zone || '办公室') : '过道'; tags.push( ); }); /* ===== 4. 墙体(像素墙段精确生成,无交叉穿出) ===== */ HWALLS.forEach(([z, x0, x1], i) => { walls.push( ); }); VWALLS.forEach(([x, z0, z1], i) => { walls.push( ); }); // 玻璃顶中庭四周矮围栏(二楼,0.65 高) const gl = ROOMS.find((r) => r.type === 'glass'); const gx0 = cx(gl.x[0]), gx1 = cx(gl.x[1]); const gz0 = cz(gl.z[0]), gz1 = cz(gl.z[1]); const railH = 0.65; walls.push(); walls.push(); walls.push(); walls.push(); /* ===== 过道/大厅绿植(靠墙摆放 · 严禁穿墙 · 排列整齐) ===== 每盆绿植按【墙线 wall(x 或 z)+ 内侧方向 side(+1 向过道内侧 / -1)+ 沿墙间距】确定: 中心离墙 = 冠半径 + 固定余量 0.15,保证完全落在过道地面内、不越墙、不挡门 */ const plant = (s, wall, side, offset) => ({ s, x: wall.kind === 'x' ? wall.v + side * (0.28 * s + 0.18) : offset, z: wall.kind === 'z' ? wall.v + side * (0.28 * s + 0.18) : offset, }); // wall: 沿墙线;offset: 沿该墙线的间距(x 或 z 坐标) const corridorPlants = [ // —— H04 竖向过道西墙(x=50,过道 x50-58,内侧 = +x 方向)—— plant(0.9, { kind: 'x', v: 50 }, 1, 30), plant(0.8, { kind: 'x', v: 50 }, 1, 29.2), // —— H02 竖向过道(x50-53)绿植靠东内侧 x≈50.6,沿 z 等距 —— plant(0.85, { kind: 'x', v: 50.5 }, 1, 20), plant(0.85, { kind: 'x', v: 50.5 }, 1, 14), plant(0.85, { kind: 'x', v: 50.5 }, 1, 8), // —— H03 横向过道北墙(z=28,过道在 z<28,内侧 = -z)—— // plant(0.9, { kind: 'z', v: 28 }, -1, 48), // plant(0.9, { kind: 'z', v: 28 }, -1, 54), // plant(0.9, { kind: 'z', v: 28 }, -1, 56), // —— 中庭围栏(z=1 北外墙内侧 +z / z=27 南缘内侧 -z)—— // plant(0.85, { kind: 'z', v: 1 }, 1, 40), // plant(0.85, { kind: 'z', v: 27 }, -1, 40), // plant(0.85, { kind: 'z', v: 27 }, -1, 2), // —— W02 共享工位区南缘(z=32,内侧 = +z 回工位区)—— plant(0.8, { kind: 'z', v: 32 }, 1, 36), plant(0.8, { kind: 'z', v: 32 }, 1, 40), ]; corridorPlants.forEach((p, i) => { furniture.push(); }); return ( {/* 地基 */} {floors} {walls} {roofs} {furniture} {tags} {/* 底座平台 */} ); } /* ---------- 场景 ---------- */ export default function Building3D({ interactive = true }) { return (
); }