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 (
);
}