diff --git a/scripts/parse_layout.py b/scripts/parse_layout.py
new file mode 100644
index 0000000..7d2b046
--- /dev/null
+++ b/scripts/parse_layout.py
@@ -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)
diff --git a/src/components/Building3D.jsx b/src/components/Building3D.jsx
new file mode 100644
index 0000000..970ae94
--- /dev/null
+++ b/src/components/Building3D.jsx
@@ -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 (
+