729 lines
33 KiB
React
729 lines
33 KiB
React
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 (
|
||
<mesh position={[x, y + h / 2, z]} castShadow receiveShadow>
|
||
<boxGeometry args={size} />
|
||
<meshStandardMaterial color={color} roughness={0.82} metalness={0.03} />
|
||
<Edges color={edge} threshold={20} />
|
||
</mesh>
|
||
);
|
||
}
|
||
|
||
/* 窗(一楼外墙上的窗格:玻璃面板 + 窗框) */
|
||
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 (
|
||
<mesh position={[x, y + h / 2, z]} castShadow>
|
||
<boxGeometry args={size} />
|
||
<meshStandardMaterial color="#bfe0ff" transparent opacity={0.5} roughness={0.08} metalness={0.2} />
|
||
</mesh>
|
||
);
|
||
}
|
||
|
||
|
||
/* 办公桌(办公桌 + 座椅) */
|
||
function Desk({ x, z, y = 0, rotY = 0 }) {
|
||
return (
|
||
<group position={[x, y, z]} rotation={[0, rotY, 0]}>
|
||
<mesh position={[0, 0.4, 0]} castShadow>
|
||
<boxGeometry args={[1.1, 0.05, 0.55]} />
|
||
<meshStandardMaterial color="#f0e6d2" roughness={0.6} metalness={0.05} />
|
||
</mesh>
|
||
<mesh position={[0, 0.2, 0]} castShadow>
|
||
<boxGeometry args={[0.06, 0.4, 0.5]} />
|
||
<meshStandardMaterial color="#d8cbb2" roughness={0.7} />
|
||
</mesh>
|
||
{/* 显示器 */}
|
||
<mesh position={[0, 0.62, -0.1]} castShadow>
|
||
<boxGeometry args={[0.4, 0.3, 0.04]} />
|
||
<meshStandardMaterial color="#2e3a4e" roughness={0.3} metalness={0.2} />
|
||
</mesh>
|
||
{/* 座椅 */}
|
||
<mesh position={[0.5, 0.24, 0]} castShadow>
|
||
<boxGeometry args={[0.45, 0.08, 0.45]} />
|
||
<meshStandardMaterial color="#5c6b85" roughness={0.7} />
|
||
</mesh>
|
||
<mesh position={[0.5, 0.45, 0]} castShadow>
|
||
<boxGeometry args={[0.45, 0.4, 0.06]} />
|
||
<meshStandardMaterial color="#5c6b85" roughness={0.7} />
|
||
</mesh>
|
||
</group>
|
||
);
|
||
}
|
||
|
||
/* 圆桌(茶几) */
|
||
function Table({ x, z, y = 0, s = 1 }) {
|
||
return (
|
||
<group position={[x, y, z]}>
|
||
<mesh position={[0, 0.18 * s, 0]} castShadow receiveShadow>
|
||
<cylinderGeometry args={[0.22 * s, 0.26 * s, 0.34 * s, 24]} />
|
||
<meshStandardMaterial color="#e9eef7" roughness={0.6} metalness={0.05} />
|
||
</mesh>
|
||
<mesh position={[0, 0.38 * s, 0]} castShadow>
|
||
<cylinderGeometry args={[0.32 * s, 0.32 * s, 0.05 * s, 28]} />
|
||
<meshStandardMaterial color="#ffffff" roughness={0.45} metalness={0.06} />
|
||
</mesh>
|
||
</group>
|
||
);
|
||
}
|
||
|
||
/* 会议长桌 + 座椅 */
|
||
function MeetingTable({ x, z, y = 0, rotY = 0, len = 2.6 }) {
|
||
return (
|
||
<group position={[x, y, z]} rotation={[0, rotY, 0]}>
|
||
<mesh position={[0, 0.42, 0]} castShadow>
|
||
<boxGeometry args={[len, 0.05, 1.0]} />
|
||
<meshStandardMaterial color="#e8dcc4" roughness={0.55} metalness={0.05} />
|
||
</mesh>
|
||
{[0, 1].map((i) => (
|
||
<mesh key={`leg${i}`} position={[len / 2 - 0.15 - i * (len - 0.3), 0.2, 0]} castShadow>
|
||
<boxGeometry args={[0.07, 0.4, 0.9]} />
|
||
<meshStandardMaterial color="#d8cbb2" roughness={0.7} />
|
||
</mesh>
|
||
))}
|
||
{[-0.5, 0, 0.5].map((dx) => (
|
||
<mesh key={`chair${dx}`} position={[dx * (len / 2 - 0.4), 0.24, 0.75]} castShadow>
|
||
<boxGeometry args={[0.5, 0.08, 0.5]} />
|
||
<meshStandardMaterial color="#4f5f7a" roughness={0.7} />
|
||
</mesh>
|
||
))}
|
||
</group>
|
||
);
|
||
}
|
||
|
||
/* 会议室小办公桌(小型方桌 + 座椅,多张布置,不穿墙) */
|
||
function MeetingDesk({ x, z, y = 0, rotY = 0 }) {
|
||
return (
|
||
<group position={[x, y, z]} rotation={[0, rotY, 0]}>
|
||
{/* 桌面 */}
|
||
<mesh position={[0, 0.42, 0]} castShadow>
|
||
<boxGeometry args={[0.9, 0.05, 0.6]} />
|
||
<meshStandardMaterial color="#e8dcc4" roughness={0.55} metalness={0.05} />
|
||
</mesh>
|
||
{/* 桌腿 */}
|
||
<mesh position={[0, 0.2, 0]} castShadow>
|
||
<boxGeometry args={[0.06, 0.46, 0.5]} />
|
||
<meshStandardMaterial color="#d8cbb2" roughness={0.7} />
|
||
</mesh>
|
||
{/* 座椅(桌前) */}
|
||
<mesh position={[0, 0.24, 0.45]} castShadow>
|
||
<boxGeometry args={[0.45, 0.08, 0.45]} />
|
||
<meshStandardMaterial color="#4f5f7a" roughness={0.7} />
|
||
</mesh>
|
||
<mesh position={[0, 0.45, 0.45]} castShadow>
|
||
<boxGeometry args={[0.45, 0.4, 0.06]} />
|
||
<meshStandardMaterial color="#4f5f7a" roughness={0.7} />
|
||
</mesh>
|
||
</group>
|
||
);
|
||
}
|
||
|
||
/* 讲台(会议室讲台) */
|
||
function Podium({ x, z, y = 0 }) { return (
|
||
<group position={[x, y, z]}>
|
||
<mesh position={[0, 0.5, 0]} castShadow>
|
||
<boxGeometry args={[0.7, 1.0, 0.5]} />
|
||
<meshStandardMaterial color="#b9986a" roughness={0.6} metalness={0.1} />
|
||
</mesh>
|
||
<mesh position={[0, 1.02, 0]}>
|
||
<boxGeometry args={[0.76, 0.05, 0.56]} />
|
||
<meshStandardMaterial color="#a8875c" roughness={0.5} metalness={0.1} />
|
||
</mesh>
|
||
</group>
|
||
);
|
||
}
|
||
|
||
/* 工位桌(共享工位区) */
|
||
function Workstation({ x, z, y = 0, rotY = 0 }) {
|
||
return (
|
||
<group position={[x, y, z]} rotation={[0, rotY, 0]}>
|
||
<mesh position={[0, 0.4, 0]} castShadow>
|
||
<boxGeometry args={[1.0, 0.05, 0.6]} />
|
||
<meshStandardMaterial color="#e7dcc8" roughness={0.6} metalness={0.05} />
|
||
</mesh>
|
||
<mesh position={[0, 0.2, 0]} castShadow>
|
||
<boxGeometry args={[0.05, 0.4, 0.55]} />
|
||
<meshStandardMaterial color="#d0c3ab" roughness={0.7} />
|
||
</mesh>
|
||
<mesh position={[0, 0.6, 0]} castShadow>
|
||
<boxGeometry args={[0.5, 0.34, 0.04]} />
|
||
<meshStandardMaterial color="#33415c" roughness={0.3} metalness={0.2} />
|
||
</mesh>
|
||
<mesh position={[0.48, 0.24, 0]} castShadow>
|
||
<boxGeometry args={[0.4, 0.08, 0.4]} />
|
||
<meshStandardMaterial color="#6b7a94" roughness={0.7} />
|
||
</mesh>
|
||
</group>
|
||
);
|
||
}
|
||
|
||
/* 沙发 */
|
||
function Sofa({ x, z, y = 0, rotY = 0 }) {
|
||
return (
|
||
<group position={[x, y, z]} rotation={[0, rotY, 0]}>
|
||
<mesh position={[0, 0.2, 0]} castShadow>
|
||
<boxGeometry args={[0.9, 0.18, 0.4]} />
|
||
<meshStandardMaterial color="#8fbfa8" roughness={0.75} />
|
||
</mesh>
|
||
<mesh position={[0, 0.4, -0.15]} castShadow>
|
||
<boxGeometry args={[0.9, 0.42, 0.15]} />
|
||
<meshStandardMaterial color="#7fb39c" roughness={0.75} />
|
||
</mesh>
|
||
<mesh position={[-0.38, 0.33, 0.02]} castShadow>
|
||
<boxGeometry args={[0.14, 0.28, 0.38]} />
|
||
<meshStandardMaterial color="#7fb39c" roughness={0.75} />
|
||
</mesh>
|
||
<mesh position={[0.38, 0.33, 0.02]} castShadow>
|
||
<boxGeometry args={[0.14, 0.28, 0.38]} />
|
||
<meshStandardMaterial color="#7fb39c" roughness={0.75} />
|
||
</mesh>
|
||
<mesh position={[-0.3, 0.28, 0.22]} castShadow>
|
||
<boxGeometry args={[0.18, 0.14, 0.12]} />
|
||
<meshStandardMaterial color="#e8d9a8" roughness={0.8} />
|
||
</mesh>
|
||
</group>
|
||
);
|
||
}
|
||
|
||
/* 绿植 */
|
||
function Plant({ x, z, y = 0, s = 1 }) {
|
||
return (
|
||
<group position={[x, y, z]}>
|
||
<mesh position={[0, 0.2 * s, 0]} castShadow>
|
||
<cylinderGeometry args={[0.08 * s, 0.11 * s, 0.4 * s, 16]} />
|
||
<meshStandardMaterial color="#c9b28a" roughness={0.8} />
|
||
</mesh>
|
||
<mesh position={[0, 0.56 * s, 0]} castShadow>
|
||
<sphereGeometry args={[0.3 * s, 20, 20]} />
|
||
<meshStandardMaterial color="#3fae7f" roughness={0.7} />
|
||
</mesh>
|
||
</group>
|
||
);
|
||
}
|
||
|
||
/* 楼梯(楼梯间:从二楼通向一楼 —— 踏步自二楼逐级向下延伸至地面,居中铺满房间) */
|
||
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(
|
||
<mesh key={`st${i}`} position={[0, h - r / 2, dz]} castShadow receiveShadow>
|
||
<boxGeometry args={[stepW, r, stepD]} />
|
||
<meshStandardMaterial color="#e8eef5" roughness={0.6} metalness={0.02} />
|
||
</mesh>
|
||
);
|
||
}
|
||
return (
|
||
<group position={[x, y, z]} rotation={[0, rotY, 0]}>
|
||
{els}
|
||
</group>
|
||
);
|
||
}
|
||
|
||
function ZoneTag({ x, y, z, cls, label, sub, no }) {
|
||
return (
|
||
<Html position={[x, y, z]} center distanceFactor={13} zIndexRange={[40, 0]} style={{ pointerEvents: 'none' }}>
|
||
<div className={`bd3d-tag${cls ? ` ${cls}` : ''}`}>
|
||
{sub && <span className={`bd3d-bubble ${sub.cls}`}>{sub.text}</span>}
|
||
<span className="bd3d-roomname">
|
||
{no && <b className="bd3d-no">{no}</b>}
|
||
{label}
|
||
</span>
|
||
</div>
|
||
</Html>
|
||
);
|
||
}
|
||
|
||
/* ---------- 建筑主体 ---------- */
|
||
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(
|
||
<mesh key="hf" position={[hcX, 0.02, hcZ]} receiveShadow>
|
||
<boxGeometry args={[hW - 0.3, 0.04, hD - 0.3]} />
|
||
<meshStandardMaterial color={FLOOR_COLOR} roughness={0.9} metalness={0} />
|
||
</mesh>
|
||
);
|
||
|
||
// 一楼大厅外墙(高度 = 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(
|
||
<Wall key={`hw${i}`} x={wl.axis === 'z' ? (fromW + toW) / 2 : posW} z={wl.axis === 'z' ? posW : (fromW + toW) / 2}
|
||
w={Math.abs(toW - fromW)} h={FLOOR2} axis={wl.axis} color="#e9f0f8" edge="#9db1cf" y={0} />
|
||
);
|
||
// 窗(沿墙均匀分布)
|
||
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(<Window key={`win${i}_${wi}`} x={pos} z={posW + (wl.pos === 27 ? -0.03 : 0.03)} w={len / winN - 0.4} axis="z" />);
|
||
} else {
|
||
walls.push(<Window key={`win${i}_${wi}`} x={posW + (wl.pos === 44 ? -0.03 : 0.03)} z={pos} w={len / winN - 0.4} axis="x" />);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 大厅立柱
|
||
for (let i = 0; i <= 8; i++) {
|
||
const t = i / 8;
|
||
const px = hx0 + 0.8 + (hW - 1.6) * t;
|
||
furniture.push(<mesh key={`c1${i}`} position={[px, FLOOR2 / 2, hz0 + 1.5]} castShadow>
|
||
<cylinderGeometry args={[0.09, 0.12, FLOOR2, 12]} />
|
||
<meshStandardMaterial color="#eef2f9" roughness={0.5} metalness={0.1} />
|
||
</mesh>);
|
||
furniture.push(<mesh key={`c2${i}`} position={[px, FLOOR2 / 2, hz1 - 1.5]} castShadow>
|
||
<cylinderGeometry args={[0.09, 0.12, FLOOR2, 12]} />
|
||
<meshStandardMaterial color="#eef2f9" roughness={0.5} metalness={0.1} />
|
||
</mesh>);
|
||
}
|
||
// 吊灯(二楼楼板下方)
|
||
[hcX - 4, hcX, hcX + 4].forEach((lx, li) => {
|
||
furniture.push(
|
||
<group key={`hl${li}`} position={[lx, 0, hcZ]}>
|
||
<mesh position={[0, FLOOR2 - 0.4, 0]}><cylinderGeometry args={[0.04, 0.04, 0.35, 10]} /><meshStandardMaterial color="#b9c8de" roughness={0.3} metalness={0.5} /></mesh>
|
||
<mesh position={[0, FLOOR2 - 0.75, 0]}><sphereGeometry args={[0.18, 24, 24]} /><meshStandardMaterial color="#fff3cf" emissive="#ffd97a" emissiveIntensity={1.1} roughness={0.15} /></mesh>
|
||
</group>
|
||
);
|
||
});
|
||
// 大厅家具
|
||
furniture.push(<Sofa key="hs1" x={hcX - 2} z={hcZ + 1.5} />);
|
||
furniture.push(<Sofa key="hs2" x={hcX + 2} z={hcZ - 1.5} />);
|
||
furniture.push(<Table key="ht1" x={hcX} z={hcZ} s={1.2} />);
|
||
furniture.push(<Plant key="hp1" x={hx0 + 2.4} z={hcZ} s={1.3} />);
|
||
furniture.push(<Plant key="hp2" x={hx1 - 2.4} z={hcZ} s={1.3} />);
|
||
tags.push(<ZoneTag key="hall" x={hcX} y={FLOOR2 + 0.4} z={hcZ} cls="glass" label="一楼大厅" />);
|
||
|
||
/* ===== 2. 二楼整体地面(两块拼接,避开玻璃顶中庭) ===== */
|
||
// 右上块:x[45,58] z[1,28]
|
||
floors.push(
|
||
<mesh key="f2r" position={[(cx(45) + cx(58)) / 2, FLOOR2, (cz(1) + cz(28)) / 2]} receiveShadow castShadow>
|
||
<boxGeometry args={[cx(58) - cx(45) + 0.2, 0.05, cz(28) - cz(1) + 0.2]} />
|
||
<meshStandardMaterial color={FLOOR_COLOR} roughness={0.88} metalness={0} />
|
||
</mesh>
|
||
);
|
||
// 加速块:x[1,58] z[28,41]
|
||
floors.push(
|
||
<mesh key="f2a" position={[(cx(1) + cx(58)) / 2, FLOOR2, (cz(28) + cz(41)) / 2]} receiveShadow castShadow>
|
||
<boxGeometry args={[cx(58) - cx(1) + 0.2, 0.05, cz(41) - cz(28) + 0.2]} />
|
||
<meshStandardMaterial color={FLOOR_COLOR} roughness={0.88} metalness={0} />
|
||
</mesh>
|
||
);
|
||
|
||
/* ===== 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(<Plant key={`g1${i}`} x={x0 + 1.8} z={z0 + 1.8} y={FLOOR2} s={1} />);
|
||
furniture.push(<Plant key={`g2${i}`} x={x1 - 1.8} z={z1 - 1.8} y={FLOOR2} s={1} />);
|
||
// 玻璃天窗(青绿半透明 + 框架梁)
|
||
const gw = w + 0.4, gd = d + 0.4;
|
||
roofs.push(
|
||
<mesh key="growf" position={[cX, FLOOR2 + ROOM_H + 0.25, cZ]} castShadow>
|
||
<boxGeometry args={[gw, 0.05, gd]} />
|
||
<meshStandardMaterial color="#8fd8c0" transparent opacity={0.28} roughness={0.05} metalness={0.25} />
|
||
</mesh>
|
||
);
|
||
for (let k = 0; k <= 8; k++) {
|
||
const t = k / 8;
|
||
const px = x0 - 0.2 + gw * t;
|
||
roofs.push(<mesh key={`gx${i}_${k}`} position={[px, FLOOR2 + ROOM_H + 0.28, cZ]}><boxGeometry args={[0.06, 0.09, gd + 0.4]} /><meshStandardMaterial color="#dbe7f5" roughness={0.4} metalness={0.2} /></mesh>);
|
||
}
|
||
for (let k = 0; k <= 4; k++) {
|
||
const t = k / 4;
|
||
const pz = z0 - 0.2 + gd * t;
|
||
roofs.push(<mesh key={`gz${i}_${k}`} position={[cX, FLOOR2 + ROOM_H + 0.28, pz]}><boxGeometry args={[gw + 0.4, 0.09, 0.06]} /><meshStandardMaterial color="#dbe7f5" roughness={0.4} metalness={0.2} /></mesh>);
|
||
}
|
||
tags.push(<ZoneTag key={`zt${i}`} x={cX} y={FLOOR2 + ROOM_H + 0.7} z={cZ} cls="glass" label="玻璃顶" />);
|
||
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(
|
||
<Desk key={`d${i}_0`} x={cX} z={cZ} y={FLOOR2} rotY={Math.PI / 2} />
|
||
);
|
||
} 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(
|
||
<Desk key={`d${i}_${idx}`} x={cX + dx * (w - 1.6)} z={cZ + dz * (d - 1.6)} y={FLOOR2} />
|
||
);
|
||
}
|
||
}
|
||
}
|
||
} 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(<Workstation key={`ws${i}_${idx}`} x={cX + dx * w} z={cZ + dz * d} y={FLOOR2} />);
|
||
}
|
||
}
|
||
} else if (r.type === 'lounge') {
|
||
// 休息区:多张沙发 + 茶几
|
||
furniture.push(<Sofa key={`l1${i}`} x={cX - 0.6} z={cZ - 0.5} y={FLOOR2} rotY={0.5} />);
|
||
furniture.push(<Sofa key={`l2${i}`} x={cX + 0.6} z={cZ - 0.5} y={FLOOR2} rotY={-0.5} />);
|
||
furniture.push(<Sofa key={`l3${i}`} x={cX} z={cZ + 0.7} y={FLOOR2} rotY={Math.PI} />);
|
||
furniture.push(<Table key={`l4${i}`} x={cX} z={cZ} y={FLOOR2} s={0.8} />);
|
||
} 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(
|
||
<MeetingDesk key={`md${i}_${idx}`}
|
||
x={cX + dx * (w - 1.0)} z={cZ + dz * (d - 1.0)} y={FLOOR2} />
|
||
);
|
||
}
|
||
}
|
||
// 大会议室额外加讲台
|
||
const aBig = roomW * roomD >= 40;
|
||
if (aBig) furniture.push(<Podium key={`m3${i}`} x={cX - (w - 1.0) / 2 + 0.6} z={cZ} y={FLOOR2} />);
|
||
} 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(
|
||
<Stairs key={`st${i}`} x={sX} z={sZ} y={FLOOR2} rotY={alongX ? Math.PI / 2 : Math.PI}
|
||
steps={steps} stepW={Math.min(0.9, (alongX ? d : w) - 0.1)} stepD={sLen / steps} />
|
||
);
|
||
}
|
||
// 标签:每个房间显示编号(编号 + 名称),办公室显示入驻企业名;过道不显示标签
|
||
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(
|
||
<ZoneTag key={`zt${i}`} x={cX} y={FLOOR2 + ROOM_H + 0.55} z={cZ}
|
||
cls={r.type === 'office' ? 'office' : r.type}
|
||
no={noLabel}
|
||
label={typeLabel}
|
||
/>
|
||
);
|
||
});
|
||
|
||
/* ===== 4. 墙体(像素墙段精确生成,无交叉穿出) ===== */
|
||
HWALLS.forEach(([z, x0, x1], i) => {
|
||
walls.push(
|
||
<Wall key={`h_${i}`} x={cx((x0 + x1) / 2)} z={cz(z)} w={(x1 - x0 + 1) * U} h={ROOM_H} axis="z" y={FLOOR2} />
|
||
);
|
||
});
|
||
VWALLS.forEach(([x, z0, z1], i) => {
|
||
walls.push(
|
||
<Wall key={`v_${i}`} x={cx(x)} z={cz((z0 + z1) / 2)} w={(z1 - z0 + 1) * U} h={ROOM_H} axis="x" y={FLOOR2} />
|
||
);
|
||
});
|
||
|
||
// 玻璃顶中庭四周矮围栏(二楼,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(<Wall key="railN" x={(gx0 + gx1) / 2} z={gz0} w={gx1 - gx0} h={railH} axis="z" y={FLOOR2} />);
|
||
walls.push(<Wall key="railS" x={(gx0 + gx1) / 2} z={gz1} w={gx1 - gx0} h={railH} axis="z" y={FLOOR2} />);
|
||
walls.push(<Wall key="railW" x={gx0} z={(gz0 + gz1) / 2} w={gz1 - gz0} h={railH} axis="x" y={FLOOR2} />);
|
||
walls.push(<Wall key="railE" x={gx1} z={(gz0 + gz1) / 2} w={gz1 - gz0} h={railH} axis="x" y={FLOOR2} />);
|
||
|
||
/* ===== 过道/大厅绿植(靠墙摆放 · 严禁穿墙 · 排列整齐) =====
|
||
每盆绿植按【墙线 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(<Plant key={`cp${i}`} x={cx(p.x)} z={cz(p.z)} y={FLOOR2} s={p.s} />);
|
||
});
|
||
|
||
return (
|
||
<group>
|
||
{/* 地基 */}
|
||
<mesh position={[0, -0.09, 0]} receiveShadow castShadow>
|
||
<boxGeometry args={[W * U + 0.4, 0.18, D * U + 0.4]} />
|
||
<meshStandardMaterial color="#eef2f8" roughness={0.92} metalness={0} />
|
||
<Edges color="#eef2f8" threshold={20} />
|
||
</mesh>
|
||
{floors}
|
||
{walls}
|
||
{roofs}
|
||
{furniture}
|
||
{tags}
|
||
{/* 底座平台 */}
|
||
<mesh position={[0, -0.38, 0]} receiveShadow castShadow>
|
||
<boxGeometry args={[W * U + 0.8, 0.44, D * U + 0.8]} />
|
||
<meshStandardMaterial color="#eef2f8" roughness={0.75} metalness={0.02} />
|
||
<Edges color="#eef2f8" threshold={20} />
|
||
</mesh>
|
||
</group>
|
||
);
|
||
}
|
||
|
||
/* ---------- 场景 ---------- */
|
||
export default function Building3D({ interactive = true }) {
|
||
return (
|
||
<div className="bd3d-wrap">
|
||
<Canvas
|
||
shadows
|
||
dpr={[1, 1.75]}
|
||
camera={{ position: [22.2, 17.6, 26.7], fov: 38 }}
|
||
gl={{ antialias: true, alpha: true }}
|
||
>
|
||
<ambientLight intensity={0.55} />
|
||
<directionalLight position={[14, 22, 12]} intensity={1.1} castShadow shadow-mapSize={[2048, 2048]}
|
||
shadow-camera-left={-26} shadow-camera-right={26} shadow-camera-top={26} shadow-camera-bottom={-26} />
|
||
<directionalLight position={[-14, 10, -10]} intensity={0.3} />
|
||
<hemisphereLight args={['#d3e0f0', '#d3e0f0', 0.35]} />
|
||
|
||
<Building />
|
||
|
||
<Grid position={[0, -0.62, 0]} args={[80, 80]} cellSize={0.8} cellThickness={0.6} cellColor="#d3e0f0"
|
||
sectionSize={4} sectionThickness={1} sectionColor="#eef2f8" fadeDistance={40} fadeStrength={2.5} infiniteGrid />
|
||
|
||
<OrbitControls makeDefault autoRotate={interactive} autoRotateSpeed={0.5} enablePan={false} enableZoom={interactive}
|
||
minDistance={10} maxDistance={55} maxPolarAngle={Math.PI / 2.05} minPolarAngle={Math.PI / 7} target={[0, 1.6, 0]} />
|
||
</Canvas>
|
||
</div>
|
||
);
|
||
} |