feat(Building3D): 更新房间和墙体定义,添加临时坐标轴指示

This commit is contained in:
Pine
2026-08-18 05:34:02 +08:00
parent 7d3cdded9e
commit d00e9856cd
+121 -120
View File
@@ -37,7 +37,7 @@ const ROOMS = [
// 企业按 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: [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: '南菌优培食用菌' },
@@ -82,42 +82,68 @@ const ROOMS = [
{ 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: '云南派音人工智能科技' },
{ 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, 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],
[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, 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], // 大会议室东墙
[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], // 成长区段间西侧墙
[49, 36, 41], // 底部
[50, 36, 41], // 底部 A12/A13 之间
[51, 0, 4], // 顶部共享工位西缘(G01 东墙
[58, 0, 25], // 东外墙
[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.5Z-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;
@@ -126,14 +152,22 @@ const cz = (z) => (z - D / 2) * U;
/* ---------- 基础组件 ---------- */
function Wall({ x, z, w, h, axis = 'z', color = WALL_COLOR, edge = WALL_EDGE, y = 0 }) {
function Wall({ x, z, w, h, axis = 'z', color = WALL_COLOR, edge = WALL_EDGE, y = 0, gridLabel = '' }) {
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>
<group position={[x, y + h / 2, z]}>
<mesh castShadow receiveShadow>
<boxGeometry args={size} />
<meshStandardMaterial color={color} roughness={0.82} metalness={0.03} />
<Edges color={edge} threshold={20} />
</mesh>
{/* 临时调试:墙面数值标签 */}
{gridLabel && (
<Html position={[0, h / 2 + 0.15, 0]} center distanceFactor={16} zIndexRange={[35, 0]} style={{ pointerEvents: 'none' }}>
<div className="wall-lab">{gridLabel}</div>
</Html>
)}
</group>
);
}
@@ -148,25 +182,6 @@ function Window({ x, z, w, h = 1.0, axis = 'z', y = 0.85 }) {
);
}
/* 门(门框 + 门板 + 把手) */
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 }) {
@@ -322,36 +337,22 @@ function Plant({ x, z, y = 0, s = 1 }) {
);
}
/* 楼梯(楼:从二楼通向一楼,真实踏步+扶手 */
function Stairs({ x, z, y = 0, rotY = 0, steps = 9, stepW = 0.9, stepD = 0.24, rise = 0.21 }) {
/* 楼梯(楼梯间:从二楼通向一楼 —— 踏步自二楼逐级向下延伸至地面,居中铺满房间 */
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;
const h = y + rise * i;
const dz = i * stepD - len / 2; // 居中铺满房间(避免穿墙)
const h = y - r * i; // 从二楼向下逐级降低
els.push(
<mesh key={`st${i}`} position={[0, h + rise / 2, dz]} castShadow receiveShadow>
<boxGeometry args={[stepW, rise, stepD]} />
<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>
);
}
// 扶手(两侧)
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}
@@ -380,7 +381,6 @@ function Building() {
const roofs = [];
const furniture = [];
const tags = [];
const doors = [];
/* ===== 1. 一楼大厅(玻璃顶下方挑空) ===== */
const hx0 = cx(0), hx1 = cx(44);
@@ -560,12 +560,15 @@ function Building() {
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 alongX = (x1 - x0) >= (z1 - z0); // 房间宽≥深 → 梯段沿 X 方向
const sX = (x0 + x1) / 2;
const sZ = (z0 + z1) / 2;
const sLen = z1 - z0;
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={Math.PI} steps={9} stepW={Math.min(0.9, w - 0.1)} stepD={sLen / 9} rise={0.2} />
<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} />
);
}
// 标签:每个房间显示编号(编号 + 名称),办公室显示入驻企业名
@@ -574,7 +577,7 @@ function Building() {
r.type === 'desk' ? '共享工位区' :
r.type === 'lounge' ? '休息区' :
r.type === 'meeting' ? '会议室' :
r.type === 'stair' ? '楼' :
r.type === 'stair' ? '楼梯间' :
r.type === 'office' ? (r.company || r.zone || '办公室') :
'过道';
tags.push(
@@ -589,12 +592,14 @@ function Building() {
/* ===== 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} />
<Wall key={`h_${i}`} x={cx((x0 + x1) / 2)} z={cz(z)} w={(x1 - x0 + 1) * U} h={ROOM_H} axis="z" y={FLOOR2}
gridLabel={`z=${z} x${x0}-${x1}`} />
);
});
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} />
<Wall key={`v_${i}`} x={cx(x)} z={cz((z0 + z1) / 2)} w={(z1 - z0 + 1) * U} h={ROOM_H} axis="x" y={FLOOR2}
gridLabel={`x=${x} z${z0}-${z1}`} />
);
});
@@ -608,37 +613,6 @@ function Building() {
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>
@@ -646,19 +620,43 @@ function Building() {
<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} />
<Edges color="#eef2f8" 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} />
<meshStandardMaterial color="#eef2f8" roughness={0.75} metalness={0.02} />
<Edges color="#eef2f8" threshold={20} />
</mesh>
</group>
);
}
/* ---------- 临时坐标轴指示(XYZ,用于点位沟通调试) ---------- */
function TempAxis() {
return (
<group position={[0, 0, 0]}>
{/* 三色轴:X 红 → +x,Y 绿 ↑ +y,Z 蓝 → +z(长度 6) */}
<axesHelper args={[6]} />
{/* 轴端文字标签 */}
<Html position={[6.4, 0, 0]} center distanceFactor={20} zIndexRange={[30, 0]} style={{ pointerEvents: 'none' }}>
<div className="ax-lab ax-x">+X </div>
</Html>
<Html position={[0, 6.5, 0]} center distanceFactor={20} zIndexRange={[30, 0]} style={{ pointerEvents: 'none' }}>
<div className="ax-lab ax-y">+Y </div>
</Html>
<Html position={[0, 0, 6.4]} center distanceFactor={20} zIndexRange={[30, 0]} style={{ pointerEvents: 'none' }}>
<div className="ax-lab ax-z">+Z </div>
</Html>
{/* 原点 */}
<mesh position={[0, 0, 0]}>
<sphereGeometry args={[0.18, 12, 12]} />
<meshStandardMaterial color="#f04438" />
</mesh>
</group>
);
@@ -671,23 +669,26 @@ export default function Building3D({ interactive = true }) {
<Canvas
shadows
dpr={[1, 1.75]}
camera={{ position: [20, 16, 24], fov: 38 }}
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={['#ffffff', '#dfe9f7', 0.35]} />
<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="#bcd0e8" fadeDistance={40} fadeStrength={2.5} infiniteGrid />
sectionSize={4} sectionThickness={1} sectionColor="#eef2f8" fadeDistance={40} fadeStrength={2.5} infiniteGrid />
<OrbitControls makeDefault autoRotate={interactive} autoRotateSpeed={0.5} enablePan={false} enableZoom={interactive}
{/* 临时坐标轴指示(原点位于模型中心,X 红 / Y 绿 / Z 蓝) */}
<TempAxis />
<OrbitControls makeDefault autoRotate={false} 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>
);
}
}