feat: 3D 园区建筑模型(像素级重建)
- Building3D.jsx:React Three Fiber 双良节能大楼 3D 模型 - 按平面图 60×42 网格重建:玻璃顶中庭/成长区/国际区/加速区/楼道/会议室 - 房间编号 A1-A9/I1-I7/G1-G8 + 入驻企业标签 + 门开向过道 - 共享墙/整体地面/一楼挑空大厅 - scripts/parse_layout.py:纯标准库 PNG 像素解析脚本
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
按像素精确解析 PNG 平面图 —— 精确颜色分类 + 高分辨率网格化
|
||||
颜色规则(依据实际采样校准):
|
||||
W 黑色(0,0,0) = 墙壁
|
||||
O 灰色(176,176,176) = 办公室
|
||||
P 蓝色(64,176,240) = 过道
|
||||
R 绿色(80,176,96) = 休息厅
|
||||
Y 橙色(240,112,16) = 共享工位区 / 楼道
|
||||
C 青绿(80,224,192) = 玻璃顶
|
||||
. 白色(240,240,240) = 空白/底
|
||||
"""
|
||||
import zlib, struct, sys, collections
|
||||
|
||||
def read_png(path):
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
assert data[:8] == b'\x89PNG\r\n\x1a\n', 'Not a PNG'
|
||||
pos = 8
|
||||
width = height = None
|
||||
bit_depth = color_type = None
|
||||
idat = b''
|
||||
while pos < len(data):
|
||||
ln = struct.unpack('>I', data[pos:pos+4])[0]
|
||||
typ = data[pos+4:pos+8]
|
||||
chunk = data[pos+8:pos+8+ln]
|
||||
pos += 12 + ln
|
||||
if typ == b'IHDR':
|
||||
width, height, bit_depth, color_type, _, _, _ = struct.unpack('>IIBBBBB', chunk)
|
||||
elif typ == b'IDAT':
|
||||
idat += chunk
|
||||
elif typ == b'IEND':
|
||||
break
|
||||
assert bit_depth == 8 and color_type in (2, 6) and width and height
|
||||
raw = zlib.decompress(idat)
|
||||
channels = 4 if color_type == 6 else 3
|
||||
bpp = channels
|
||||
stride = width * bpp
|
||||
rows = []
|
||||
off = 0
|
||||
prev = bytearray(stride)
|
||||
for y in range(height):
|
||||
ft = raw[off]; off += 1
|
||||
line = bytearray(raw[off:off+stride]); off += stride
|
||||
if ft == 1:
|
||||
for i in range(bpp, stride): line[i] = (line[i] + line[i-bpp]) & 0xFF
|
||||
elif ft == 2:
|
||||
for i in range(stride): line[i] = (line[i] + prev[i]) & 0xFF
|
||||
elif ft == 3:
|
||||
for i in range(stride):
|
||||
a = line[i-bpp] if i >= bpp else 0
|
||||
line[i] = (line[i] + ((a + prev[i]) >> 1)) & 0xFF
|
||||
elif ft == 4:
|
||||
for i in range(stride):
|
||||
a = line[i-bpp] if i >= bpp else 0
|
||||
b = prev[i]
|
||||
c = prev[i-bpp] if i >= bpp else 0
|
||||
p = a + b - c
|
||||
pa, pb, pc = abs(p-a), abs(p-b), abs(p-c)
|
||||
pr = a if (pa <= pb and pa <= pc) else (b if pb <= pc else c)
|
||||
line[i] = (line[i] + pr) & 0xFF
|
||||
rows.append(bytes(line))
|
||||
prev = line
|
||||
rgba = []
|
||||
for line in rows:
|
||||
if channels == 4:
|
||||
rgba.append([(line[i], line[i+1], line[i+2], line[i+3]) for i in range(0, stride, 4)])
|
||||
else:
|
||||
rgba.append([(line[i], line[i+1], line[i+2], 255) for i in range(0, stride, 3)])
|
||||
return width, height, rgba
|
||||
|
||||
# 精确色板(中心色 + 容差)
|
||||
PALETTE = [
|
||||
('W', (0, 0, 0), 70), # 黑 = 墙壁
|
||||
('O', (176, 176, 176), 40), # 灰 = 办公室
|
||||
('P', (64, 176, 240), 45), # 蓝 = 过道
|
||||
('R', (80, 176, 96), 40), # 绿 = 休息厅
|
||||
('Y', (240, 112, 16), 55), # 橙 = 共享工位/楼道
|
||||
('C', (80, 224, 192), 55), # 青绿 = 玻璃顶
|
||||
('.', (240, 240, 240), 40), # 白 = 空白
|
||||
]
|
||||
|
||||
def classify(rgb):
|
||||
r, g, b = rgb[:3]
|
||||
best, best_d = '?', 1e9
|
||||
for tag, (cr, cg, cb), tol in PALETTE:
|
||||
d = abs(r-cr) + abs(g-cg) + abs(b-cb)
|
||||
if d <= tol * 3 and d < best_d:
|
||||
best, best_d = tag, d
|
||||
return best
|
||||
|
||||
def main(path, grid_cols=48, grid_rows=28, sub=3):
|
||||
w, h, rgba = read_png(path)
|
||||
print(f'PNG: {w}x{h} 网格: {grid_cols}x{grid_rows}')
|
||||
cw, ch = w / grid_cols, h / grid_rows
|
||||
grid = []
|
||||
for gy in range(grid_rows):
|
||||
row = []
|
||||
for gx in range(grid_cols):
|
||||
cx = int((gx + 0.5) * cw)
|
||||
cy = int((gy + 0.5) * ch)
|
||||
votes = collections.Counter()
|
||||
rad_x, rad_y = max(2, int(cw*0.3)), max(2, int(ch*0.3))
|
||||
for dy in range(-rad_y, rad_y+1, sub):
|
||||
for dx in range(-rad_x, rad_x+1, sub):
|
||||
yy, xx = cy+dy, cx+dx
|
||||
if 0 <= yy < h and 0 <= xx < w:
|
||||
votes[classify(rgba[yy][xx])] += 1
|
||||
row.append(votes.most_common(1)[0][0])
|
||||
grid.append(row)
|
||||
# 打印
|
||||
print(' ' + ''.join(str(c % 10) for c in range(grid_cols)))
|
||||
for gy, row in enumerate(grid):
|
||||
print(f'{gy:2d} ' + ''.join(row))
|
||||
return grid
|
||||
|
||||
if __name__ == '__main__':
|
||||
path = sys.argv[1] if len(sys.argv) > 1 else '详细的布局.png'
|
||||
main(path)
|
||||
@@ -0,0 +1,693 @@
|
||||
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: 'desk', 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: [28, 31], z: [28, 32], type: 'office', zone: '加速区', no: 'A6', company: '人工智能机器人大模型训练' },
|
||||
{ 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: [6, 12], z: [36, 40], type: 'office', zone: '加速区', no: 'A12', company: '待入驻' },
|
||||
{ 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, 54], 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, 58], // 北外墙
|
||||
[4, 44, 49], [4, 51, 51], [4, 54, 58], // 顶部 G01 南墙(含列51 隔墙;列50 为过道口)
|
||||
[7, 44, 49], [7, 54, 58],
|
||||
[10, 44, 49], [10, 54, 58],
|
||||
[13, 44, 49], [13, 54, 58],
|
||||
[16, 44, 49], [16, 54, 58],
|
||||
[19, 44, 49], [19, 54, 58],
|
||||
[22, 44, 49], [22, 54, 58],
|
||||
[25, 44, 49], [25, 54, 58],
|
||||
[27, 0, 44], // 玻璃顶南墙(全高)
|
||||
[41, 0, 53], // 南外墙
|
||||
];
|
||||
/* ---- 垂直墙段 [x, z0, z1](像素精确提取)---- */
|
||||
const VWALLS = [
|
||||
[0, 0, 41], // 西外墙
|
||||
[5, 27, 32], [5, 36, 41],
|
||||
[13, 27, 32], [13, 36, 41],
|
||||
[17, 27, 32], [17, 36, 41],
|
||||
[22, 27, 32], [22, 36, 41],
|
||||
[27, 27, 32], [27, 36, 41],
|
||||
[31, 36, 41], // 小会议室与楼道之间
|
||||
[34, 36, 41], // 楼道与大会议室之间
|
||||
[44, 0, 32], [44, 36, 41], // 玻璃顶东墙 / 大会议室西墙
|
||||
[46, 36, 41], // 大会议室东墙
|
||||
[49, 4, 5], [49, 7, 8], [49, 10, 11], [49, 13, 14], [49, 16, 17], [49, 19, 20], [49, 22, 23], // 成长区段间西侧墙
|
||||
[49, 36, 41], // 底部
|
||||
[50, 36, 41], // 底部 A12/A13 之间
|
||||
[51, 0, 4], // 顶部共享工位西缘(G01 东墙)
|
||||
[58, 0, 25], // 东外墙
|
||||
];
|
||||
|
||||
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 Door({ x, z, rotY = 0, y = 0 }) {
|
||||
return (
|
||||
<group position={[x, y, z]} rotation={[0, rotY, 0]}>
|
||||
<mesh position={[0, 0.62, 0]} castShadow>
|
||||
<boxGeometry args={[0.8, 1.24, 0.06]} />
|
||||
<meshStandardMaterial color="#8b9bb5" roughness={0.55} metalness={0.1} />
|
||||
</mesh>
|
||||
<mesh position={[0, 0.62, 0.05]}>
|
||||
<boxGeometry args={[0.84, 1.28, 0.02]} />
|
||||
<meshStandardMaterial color="#c4d0e2" roughness={0.5} metalness={0.15} />
|
||||
</mesh>
|
||||
<mesh position={[0.29, 0.56, 0.11]}>
|
||||
<sphereGeometry args={[0.035, 10, 10]} />
|
||||
<meshStandardMaterial color="#e8c46a" roughness={0.35} metalness={0.6} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/* 办公桌(办公桌 + 座椅) */
|
||||
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 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 = 0.21 }) {
|
||||
const els = [];
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const dz = i * stepD;
|
||||
const h = y + rise * i;
|
||||
els.push(
|
||||
<mesh key={`st${i}`} position={[0, h + rise / 2, dz]} castShadow receiveShadow>
|
||||
<boxGeometry args={[stepW, rise, stepD]} />
|
||||
<meshStandardMaterial color="#e8eef5" roughness={0.6} metalness={0.02} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
// 扶手(两侧)
|
||||
const railLen = steps * stepD + 0.3;
|
||||
const railH = y + rise * (steps - 1) + 0.95;
|
||||
[-stepW / 2 - 0.06, stepW / 2 + 0.06].forEach((rx) => {
|
||||
els.push(
|
||||
<mesh key={`rp${rx}`} position={[rx, railH - 0.05, railLen / 2 - 0.15]} castShadow>
|
||||
<boxGeometry args={[0.04, 1.0, 0.04]} />
|
||||
<meshStandardMaterial color="#a9bccf" roughness={0.4} metalness={0.4} />
|
||||
</mesh>
|
||||
);
|
||||
els.push(
|
||||
<mesh key={`rl${rx}`} position={[rx, railH, railLen / 2 - 0.15]} castShadow>
|
||||
<boxGeometry args={[0.04, 0.06, railLen]} />
|
||||
<meshStandardMaterial color="#c4d2e2" roughness={0.4} metalness={0.4} />
|
||||
</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 = [];
|
||||
const doors = [];
|
||||
|
||||
/* ===== 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: 27, from: 0, to: 44 },
|
||||
{ axis: 'x', pos: 0, from: 0, to: 27 },
|
||||
{ axis: 'x', pos: 44, from: 0, to: 27 },
|
||||
];
|
||||
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;
|
||||
}
|
||||
|
||||
// 过道顶棚(右侧竖过道、横向过道)
|
||||
if (r.type === 'hallway') {
|
||||
roofs.push(
|
||||
<mesh key={`hr${i}`} position={[cX, FLOOR2 + 0.5, cZ]}>
|
||||
<boxGeometry args={[w - 0.1, 0.05, d - 0.1]} />
|
||||
<meshStandardMaterial color="#c3d8f2" transparent opacity={0.3} roughness={0.1} metalness={0.1} />
|
||||
</mesh>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 家具按功能区分(地面统一色,靠资产区分)
|
||||
if (r.type === 'office') {
|
||||
// 办公室:办公桌(按面积放置多张)
|
||||
const nX = Math.max(1, Math.floor((r.x[1] - r.x[0]) / 3.5));
|
||||
const nZ = Math.max(1, Math.floor((r.z[1] - r.z[0]) / 3.5));
|
||||
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} z={cZ + dz * d} 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') {
|
||||
// 会议室:按面积区分——大会议室(长桌+讲台),小会议室(单张会议桌)
|
||||
const area = (r.x[1] - r.x[0]) * (r.z[1] - r.z[0]);
|
||||
if (area >= 40) {
|
||||
// 大会议室(M01):两张会议长桌 + 讲台
|
||||
furniture.push(<MeetingTable key={`m1${i}`} x={cX - 0.5} z={cZ} y={FLOOR2} rotY={Math.PI / 2} len={Math.min(3.2, d - 0.8)} />);
|
||||
furniture.push(<MeetingTable key={`m2${i}`} x={cX + 1.8} z={cZ} y={FLOOR2} rotY={Math.PI / 2} len={2.4} />);
|
||||
furniture.push(<Podium key={`m3${i}`} x={cX - 2.6} z={cZ} y={FLOOR2} />);
|
||||
} else {
|
||||
// 小会议室(M02):单张会议长桌 + 四周座椅
|
||||
furniture.push(<MeetingTable key={`m1${i}`} x={cX} z={cZ} y={FLOOR2} rotY={Math.PI / 2} len={Math.min(2.0, w - 0.6)} />);
|
||||
}
|
||||
} else if (r.type === 'stair') {
|
||||
// 楼道:真实楼梯(二楼 → 一楼)
|
||||
const sX = (x0 + x1) / 2;
|
||||
const sZ = (z0 + z1) / 2;
|
||||
const sLen = z1 - z0;
|
||||
furniture.push(
|
||||
<Stairs key={`st${i}`} x={sX} z={sZ} y={FLOOR2} rotY={Math.PI} steps={9} stepW={Math.min(0.9, w - 0.1)} stepD={sLen / 9} rise={0.2} />
|
||||
);
|
||||
}
|
||||
// 标签:每个房间显示编号(编号 + 名称),办公室显示入驻企业名
|
||||
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} />);
|
||||
|
||||
/* ===== 5. 办公室门:全部开向过道一侧(门嵌于墙段位置) ===== */
|
||||
ROOMS.forEach((r, i) => {
|
||||
if (r.type !== 'office') return;
|
||||
const z0 = cz(r.z[0]), z1 = cz(r.z[1]);
|
||||
const cZ = (z0 + z1) / 2;
|
||||
const cX = (cx(r.x[0]) + cx(r.x[1])) / 2;
|
||||
|
||||
if (r.zone === '成长区') {
|
||||
// 成长区东侧是过道(列50-53),门开在 x=49 墙段位置(列49 是墙)
|
||||
doors.push(<Door key={`d${i}e`} x={cx(49)} z={cZ} rotY={Math.PI / 2} y={FLOOR2} />);
|
||||
} else if (r.zone === '国际区') {
|
||||
// 国际区西侧是过道,门开在 x=54 墙段位置(列54 是墙)
|
||||
doors.push(<Door key={`d${i}w`} x={cx(54)} z={cZ} rotY={-Math.PI / 2} y={FLOOR2} />);
|
||||
} else if (r.zone === '加速区' || r.zone === '管理') {
|
||||
if (r.z[1] <= 32) {
|
||||
// 上排:南侧是横向过道(行33-35),门开在 z=32
|
||||
doors.push(<Door key={`d${i}s`} x={cX} z={cz(32)} rotY={Math.PI} y={FLOOR2} />);
|
||||
} else {
|
||||
// 下排:北侧是横向过道,门开在 z=36
|
||||
doors.push(<Door key={`d${i}n`} x={cX} z={cz(36)} rotY={0} y={FLOOR2} />);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* ===== 6. 区域标签 ===== */
|
||||
tags.push(<ZoneTag key="zc" x={cx(47)} y={FLOOR2 + ROOM_H + 1.3} z={cz(2)} cls="office" no="G" label="成长区" />);
|
||||
tags.push(<ZoneTag key="zi" x={cx(56)} y={FLOOR2 + ROOM_H + 1.3} z={cz(2)} cls="office" no="I" label="国际区" />);
|
||||
tags.push(<ZoneTag key="za" x={cx(18)} y={FLOOR2 + ROOM_H + 0.9} z={cz(30)} cls="office" no="A" label="加速区" />);
|
||||
tags.push(<ZoneTag key="t1" x={cx(48)} y={FLOOR2 + ROOM_H + 1.1} z={cz(14)} cls="office" no="G4" label="成长区" sub={{ text: 'AI 工具调用 128 次/h', cls: 'ok' }} />);
|
||||
tags.push(<ZoneTag key="t2" x={cx(3)} y={FLOOR2 + ROOM_H + 0.6} z={cz(30)} cls="office" no="A12" label="加速区" sub={{ text: '今日营收 +¥1.6万', cls: 'cold' }} />);
|
||||
tags.push(<ZoneTag key="t3" x={cx(57)} y={FLOOR2 + ROOM_H + 0.6} z={cz(30)} cls="office" no="I1" label="国际区" sub={{ text: '工位使用率 92%', cls: 'hot' }} />);
|
||||
|
||||
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="#cfdced" threshold={20} />
|
||||
</mesh>
|
||||
{floors}
|
||||
{walls}
|
||||
{doors}
|
||||
{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="#e4eaf3" roughness={0.75} metalness={0.02} />
|
||||
<Edges color="#c3d3e8" threshold={20} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 场景 ---------- */
|
||||
export default function Building3D({ interactive = true }) {
|
||||
return (
|
||||
<div className="bd3d-wrap">
|
||||
<Canvas
|
||||
shadows
|
||||
dpr={[1, 1.75]}
|
||||
camera={{ position: [20, 16, 24], 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={['#ffffff', '#dfe9f7', 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="#bcd0e8" 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user