文章目录
-
- 每日一句正能量
- 前言
- 一、前言:建筑设计评审的交互范式革新
- 二、核心特性解析与技术选型
-
- [2.1 沉浸光感在建筑评审中的价值](#2.1 沉浸光感在建筑评审中的价值)
- [2.2 Face AR在评审中的创新应用](#2.2 Face AR在评审中的创新应用)
- [2.3 Body AR在空间漫游中的创新应用](#2.3 Body AR在空间漫游中的创新应用)
- 三、环境配置与权限声明
-
- [3.1 模块依赖配置](#3.1 模块依赖配置)
- [3.2 权限声明(module.json5)](#3.2 权限声明(module.json5))
- 四、核心代码实战
-
- [4.1 建筑类型光感引擎(ArchitectureLightEngine.ets)](#4.1 建筑类型光感引擎(ArchitectureLightEngine.ets))
- [4.2 Face AR评审专注度与情绪分析系统(ReviewAttentionSystem.ets)](#4.2 Face AR评审专注度与情绪分析系统(ReviewAttentionSystem.ets))
- [4.3 Body AR建筑漫游操控系统(ArchitectureRoamSystem.ets)](#4.3 Body AR建筑漫游操控系统(ArchitectureRoamSystem.ets))
- [4.4 沉浸光感建筑标题栏(ImmersiveArchitectureTitleBar.ets)](#4.4 沉浸光感建筑标题栏(ImmersiveArchitectureTitleBar.ets))
- [4.5 悬浮阶段导航面板(FloatPhaseNav.ets)](#4.5 悬浮阶段导航面板(FloatPhaseNav.ets))
- [4.6 主建筑评审页面(ArchitectureMainPage.ets)](#4.6 主建筑评审页面(ArchitectureMainPage.ets))
- 五、关键技术总结
-
- [5.1 Face AR在评审中的适配清单](#5.1 Face AR在评审中的适配清单)
- [5.2 Body AR在漫游中的最佳实践](#5.2 Body AR在漫游中的最佳实践)
- [5.3 沉浸光感建筑适配要点](#5.3 沉浸光感建筑适配要点)
- 六、调试与测试建议
-
- [6.1 AR性能监控](#6.1 AR性能监控)
- [6.2 多窗口测试矩阵](#6.2 多窗口测试矩阵)
- [6.3 常见问题排查](#6.3 常见问题排查)
- 七、总结与展望

每日一句正能量
我们终此一生就是要走出别人的期待,活成真正的自己。
别人的期待 包括父母的、社会的、伴侣的、朋友的。它们常常以"为你好"的名义植入我们内心。走出 不是对抗,而是辨别------哪些期待我愿意接纳,哪些必须放下。 "真正的自己" 不是一个固定终点,而是一个不断探索、选择、承担的过程。
前言
摘要 :HarmonyOS 6(API 23)带来的悬浮导航、沉浸光感与Face AR & Body AR特性,为建筑设计评审开辟了全新的交互维度。本文将实战开发一款面向HarmonyOS PC的"灵犀筑境"建筑空间评审系统,展示如何利用
systemMaterialEffect构建随设计主题动态变化的评审环境光感,通过悬浮导航实现设计阶段与评审视角的快速切换,基于Face AR实现评审专注度与情绪反馈的实时捕捉,基于Body AR实现手势操控的3D建筑模型与虚拟漫游,以及基于多窗口架构构建方案展示、技术图纸、材料清单和协作批注的评审界面。
一、前言:建筑设计评审的交互范式革新
传统建筑设计评审依赖二维图纸、静态效果图和物理模型,评审者难以直观感知空间尺度、光照氛围和人流动线。HarmonyOS 6(API 23)引入的悬浮导航(Float Navigation) 、沉浸光感(Immersive Light Effects)与Face AR & Body AR特性,为建筑评审带来了"情绪即反馈、肢体即标尺"的全新可能。
本文核心亮点:
- 主题感知光效:根据建筑类型(住宅/商业/文化/教育/医疗/景观)动态切换评审环境光色与材质氛围
- 评审专注度监测:通过Face AR实时捕捉评审者眼神方向、表情变化,生成专注度热力图
- 空间手势漫游:Body AR手势实现虚拟行走、视角旋转、尺度测量、剖切分析
- 悬浮阶段导航:底部悬浮页签切换概念/方案/扩初/施工图阶段,支持透明度调节
- 多窗口评审协作:主3D视图 + 浮动平面图 + 浮动立面图 + 浮动材料面板 + 浮动批注列表
二、核心特性解析与技术选型
2.1 沉浸光感在建筑评审中的价值
HarmonyOS 6的systemMaterialEffect通过模拟物理光照模型,为UI组件带来细腻的光晕与反射效果。在建筑评审场景中:
- 增强空间感知:住宅评审时呈现温馨暖光、商业评审时呈现冷峻白光、文化建筑评审时呈现典雅金光
- 材质模拟反馈:玻璃幕墙的反射光效、木质材料的温润光晕、混凝土的冷峻质感
- 日照分析可视化:通过光效强弱模拟不同时段的日照条件
- 情绪调节:检测到评审者困惑时切换为柔和光效,帮助聚焦
2.2 Face AR在评审中的创新应用
- 专注度追踪:通过眼球追踪判断评审者是否注视关键设计区域
- 情绪反馈识别:识别满意、困惑、惊喜等情绪,辅助设计师理解评审意见
- 疲劳度监测:长时间评审时自动建议休息,降低认知负荷
- 决策记录:记录评审过程中的表情变化,生成"决策情绪档案"
2.3 Body AR在空间漫游中的创新应用
- 虚拟行走:原地踏步手势触发虚拟漫游,双手摆动控制行进速度
- 视角操控:单手画圈旋转视角、双手张开缩放模型、双手平移平移视角
- 剖切分析:单手横切手势触发建筑剖面展示,纵向切分手势展示楼层剖面
- 尺度测量:双手框选区域自动计算面积/体积,单指指向测量距离
三、环境配置与权限声明
3.1 模块依赖配置
json
{
"dependencies": {
"@hms.core.ar.engine": "^6.1.0",
"@hms.core.graphics.3d": "^6.0.0",
"@hms.core.arkui.design": "^6.0.0",
"@hms.core.ai.vision": "^6.0.0",
"@hms.core.distributed.device": "^6.0.0"
}
}
3.2 权限声明(module.json5)
json
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:ar_camera_permission",
"usedScene": {
"abilities": ["ArchitectureAbility"],
"when": "always"
}
},
{
"name": "ohos.permission.INTERNET",
"reason": "$string:network_permission"
},
{
"name": "ohos.permission.DISTRIBUTED_DATASYNC",
"reason": "$string:team_sync"
}
]
}
}
四、核心代码实战
4.1 建筑类型光感引擎(ArchitectureLightEngine.ets)
typescript
// engines/ArchitectureLightEngine.ets
import { ColorUtils } from '@hms.core.graphics.2d';
export enum BuildingType {
RESIDENTIAL = 'residential', // 住宅 - 温馨暖光
COMMERCIAL = 'commercial', // 商业 - 冷峻白光
CULTURAL = 'cultural', // 文化 - 典雅金光
EDUCATION = 'education', // 教育 - 明亮绿光
MEDICAL = 'medical', // 医疗 - 洁净蓝光
LANDSCAPE = 'landscape' // 景观 - 自然绿光
}
export enum DesignPhase {
CONCEPT = 'concept', // 概念阶段
SCHEMATIC = 'schematic', // 方案阶段
DD = 'dd', // 扩初阶段
CD = 'cd' // 施工图阶段
}
export interface StudioLightTheme {
primaryColor: ResourceColor;
secondaryColor: ResourceColor;
ambientColor: ResourceColor;
materialTint: ResourceColor; // 材质色调
glowIntensity: number;
pulseSpeed: number;
colorTemperature: number;
brightness: number;
shadowSoftness: number; // 阴影柔和度
}
export class ArchitectureLightEngine {
private static themes: Map<BuildingType, StudioLightTheme> = new Map([
[BuildingType.RESIDENTIAL, {
primaryColor: '#D4A574', // 暖木色
secondaryColor: '#8B7355', // 深木色
ambientColor: 'rgba(212, 165, 116, 0.08)',
materialTint: '#F5DEB3', // 小麦色
glowIntensity: 0.6,
pulseSpeed: 5000,
colorTemperature: 3200,
brightness: 0.85,
shadowSoftness: 0.8
}],
[BuildingType.COMMERCIAL, {
primaryColor: '#E2E8F0', // 冷灰白
secondaryColor: '#64748B', // 石板灰
ambientColor: 'rgba(226, 232, 240, 0.1)',
materialTint: '#CBD5E1', // 淡蓝灰
glowIntensity: 0.7,
pulseSpeed: 4000,
colorTemperature: 6500,
brightness: 0.9,
shadowSoftness: 0.4
}],
[BuildingType.CULTURAL, {
primaryColor: '#F59E0B', // 典雅金
secondaryColor: '#B45309', // 深琥珀
ambientColor: 'rgba(245, 158, 11, 0.08)',
materialTint: '#FEF3C7', // 古纸黄
glowIntensity: 0.65,
pulseSpeed: 4500,
colorTemperature: 3800,
brightness: 0.8,
shadowSoftness: 0.6
}],
[BuildingType.EDUCATION, {
primaryColor: '#22C55E', // 教育绿
secondaryColor: '#15803D', // 深绿
ambientColor: 'rgba(34, 197, 94, 0.08)',
materialTint: '#DCFCE7', // 淡绿
glowIntensity: 0.55,
pulseSpeed: 5000,
colorTemperature: 4500,
brightness: 0.9,
shadowSoftness: 0.7
}],
[BuildingType.MEDICAL, {
primaryColor: '#38BDF8', // 医疗蓝
secondaryColor: '#0284C7', // 深蓝
ambientColor: 'rgba(56, 189, 248, 0.08)',
materialTint: '#E0F2FE', // 淡蓝
glowIntensity: 0.5,
pulseSpeed: 6000,
colorTemperature: 5500,
brightness: 0.95,
shadowSoftness: 0.3
}],
[BuildingType.LANDSCAPE, {
primaryColor: '#4ADE80', // 自然绿
secondaryColor: '#166534', // 森林绿
ambientColor: 'rgba(74, 222, 128, 0.1)',
materialTint: '#F0FDF4', // 淡绿白
glowIntensity: 0.6,
pulseSpeed: 4000,
colorTemperature: 4200,
brightness: 0.85,
shadowSoftness: 0.9
}]
]);
static getTheme(type: BuildingType): StudioLightTheme {
return this.themes.get(type) || this.themes.get(BuildingType.RESIDENTIAL)!;
}
// 根据设计阶段调整光效
static adjustByPhase(theme: StudioLightTheme, phase: DesignPhase): StudioLightTheme {
const adjusted = { ...theme };
switch (phase) {
case DesignPhase.CONCEPT:
adjusted.glowIntensity *= 1.2; // 概念阶段更梦幻
adjusted.shadowSoftness *= 1.2;
break;
case DesignPhase.SCHEMATIC:
adjusted.brightness *= 1.05;
break;
case DesignPhase.DD:
adjusted.colorTemperature = Math.min(6500, adjusted.colorTemperature + 200);
break;
case DesignPhase.CD:
adjusted.shadowSoftness *= 0.8; // 施工图阶段更精确
break;
}
return adjusted;
}
// 根据评审情绪调整
static adjustByReviewMood(theme: StudioLightTheme, mood: ReviewMood): StudioLightTheme {
const adjusted = { ...theme };
switch (mood) {
case ReviewMood.FOCUSED:
adjusted.brightness *= 1.05;
break;
case ReviewMood.CONFUSED:
adjusted.primaryColor = '#FBBF24'; // 暖黄提示
adjusted.glowIntensity *= 1.1;
break;
case ReviewMood.IMPRESSED:
adjusted.glowIntensity *= 1.3;
adjusted.pulseSpeed *= 0.8;
break;
case ReviewMood.CRITICAL:
adjusted.primaryColor = '#FCA5A5'; // 淡红警示
break;
}
return adjusted;
}
}
export enum ReviewMood {
FOCUSED = 'focused',
CONFUSED = 'confused',
IMPRESSED = 'impressed',
CRITICAL = 'critical',
NEUTRAL = 'neutral'
}
4.2 Face AR评审专注度与情绪分析系统(ReviewAttentionSystem.ets)
typescript
// systems/ReviewAttentionSystem.ets
import { ARSession, ARFaceTrack, ARBlendShapes } from '@hms.core.ar.engine';
export interface ReviewerState {
reviewerId: string;
name: string;
attentionScore: number; // 专注度 0-100
gazeTarget: { x: number; y: number }; // 注视目标
mood: ReviewMood;
interestAreas: string[]; // 感兴趣区域
fatigueLevel: number; // 疲劳度 0-1
lastUpdate: number;
}
export interface ReviewAnalytics {
totalReviewers: number;
averageAttention: number;
dominantMood: ReviewMood;
hotAreas: Map<string, number>; // 热点区域
confusionAlerts: string[]; // 困惑区域告警
}
export class ReviewAttentionSystem {
private session: ARSession | null = null;
private faceTrack: ARFaceTrack | null = null;
private reviewerStates: Map<string, ReviewerState> = new Map();
private attentionHistory: number[] = [];
async initialize(): Promise<void> {
this.session = await ARSession.create({
featureTypes: [ARFeatureType.FACE],
cameraConfig: {
facing: CameraFacing.FRONT,
resolution: CameraResolution.HD_720P
}
});
this.faceTrack = this.session.getFaceTrack();
await this.session.start();
}
update(frameData: ARFrame, buildingAreas: string[]): ReviewAnalytics | null {
if (!this.faceTrack) return null;
const faces = this.faceTrack.getTrackedFaces(frameData);
if (faces.length === 0) return null;
faces.forEach((face, index) => {
const blendshapes = face.getBlendShapes();
const reviewerId = `reviewer_${index}`;
const state = this.analyzeReviewer(reviewerId, blendshapes, face, buildingAreas);
this.reviewerStates.set(reviewerId, state);
});
return this.generateReviewAnalytics(buildingAreas);
}
private analyzeReviewer(
reviewerId: string,
blendshapes: ARBlendShapes,
face: ARFace,
areas: string[]
): ReviewerState {
// 专注度分析
const attentionScore = this.calculateAttentionScore(blendshapes, face);
// 注视方向
const gazeTarget = this.estimateGazeTarget(face, areas);
// 情绪识别
const mood = this.recognizeReviewMood(blendshapes);
// 疲劳度
const fatigueLevel = this.calculateFatigue(blendshapes);
// 感兴趣区域
const interestAreas = this.trackInterestAreas(reviewerId, gazeTarget, areas);
return {
reviewerId,
name: `评审者${reviewerId.split('_')[1]}`,
attentionScore,
gazeTarget,
mood,
interestAreas,
fatigueLevel,
lastUpdate: Date.now()
};
}
private calculateAttentionScore(blendshapes: ARBlendShapes, face: ARFace): number {
let score = 100;
// 眼球追踪
const eyeLookLeft = blendshapes.getValue('EYE_LOOK_IN_LEFT') || 0;
const eyeLookRight = blendshapes.getValue('EYE_LOOK_IN_RIGHT') || 0;
const eyeLookUp = blendshapes.getValue('EYE_LOOK_UP_LEFT') || 0;
if (eyeLookLeft > 0.5 || eyeLookRight > 0.5) score -= 25;
if (eyeLookUp > 0.6) score -= 15; // 可能在看其他地方
// 头部姿态
const headYaw = face.getPose()?.yaw || 0;
if (Math.abs(headYaw) > 25) score -= 20;
// 表情分析
const browLower = blendshapes.getValue('BROW_LOWERER') || 0;
const jawOpen = blendshapes.getValue('JAW_OPEN') || 0;
if (browLower > 0.5 && jawOpen > 0.3) score -= 10; // 困惑
return Math.max(0, Math.min(100, score));
}
private estimateGazeTarget(face: ARFace, areas: string[]): { x: number; y: number } {
const pose = face.getPose();
if (!pose) return { x: 0.5, y: 0.5 };
// 根据头部姿态估算注视目标
const x = 0.5 + (pose.yaw / 50) * 0.5;
const y = 0.5 + (pose.pitch / 40) * 0.5;
return {
x: Math.max(0, Math.min(1, x)),
y: Math.max(0, Math.min(1, y))
};
}
private recognizeReviewMood(blendshapes: ARBlendShapes): ReviewMood {
const smileLeft = blendshapes.getValue('MOUTH_SMILE_LEFT') || 0;
const smileRight = blendshapes.getValue('MOUTH_SMILE_RIGHT') || 0;
const browLower = blendshapes.getValue('BROW_LOWERER') || 0;
const browRaise = blendshapes.getValue('BROW_RAISE') || 0;
const eyeWideLeft = blendshapes.getValue('EYE_WIDE_LEFT') || 0;
const lipCornerDepress = blendshapes.getValue('MOUTH_CORNER_DEPRESS_LEFT') || 0;
// 惊喜/赞赏:大笑 + 眼睛睁大
if (smileLeft > 0.7 && smileRight > 0.7 && eyeWideLeft > 0.5) {
return ReviewMood.IMPRESSED;
}
// 困惑:皱眉 + 嘴角下垂
if (browLower > 0.5 && lipCornerDepress > 0.3) {
return ReviewMood.CONFUSED;
}
// 批评:紧咬牙关 + 皱眉
if (blendshapes.getValue('JAW_CLENCH') > 0.4 && browLower > 0.6) {
return ReviewMood.CRITICAL;
}
// 专注:轻微皱眉 + 眼神集中
if (browLower > 0.2 && browLower < 0.5 && browRaise < 0.3) {
return ReviewMood.FOCUSED;
}
return ReviewMood.NEUTRAL;
}
private calculateFatigue(blendshapes: ARBlendShapes): number {
const blinkRate = blendshapes.getValue('EYE_BLINK_LEFT') || 0;
const browLower = blendshapes.getValue('BROW_LOWERER') || 0;
// 频繁眨眼 + 眉毛下垂 = 疲劳
let fatigue = 0;
if (blinkRate > 0.7) fatigue += 0.4;
if (browLower < 0.1) fatigue += 0.3;
return Math.min(1, fatigue);
}
private trackInterestAreas(
reviewerId: string,
gaze: { x: number; y: number },
areas: string[]
): string[] {
// 根据注视位置映射到建筑区域
const areaIndex = Math.floor(gaze.x * areas.length);
const currentArea = areas[Math.min(areaIndex, areas.length - 1)];
const existing = this.reviewerStates.get(reviewerId)?.interestAreas || [];
if (!existing.includes(currentArea)) {
return [...existing, currentArea].slice(-5);
}
return existing;
}
private generateReviewAnalytics(areas: string[]): ReviewAnalytics {
const states = Array.from(this.reviewerStates.values());
const totalReviewers = states.length;
if (totalReviewers === 0) {
return {
totalReviewers: 0,
averageAttention: 0,
dominantMood: ReviewMood.NEUTRAL,
hotAreas: new Map(),
confusionAlerts: []
};
}
const averageAttention = states.reduce((sum, s) => sum + s.attentionScore, 0) / totalReviewers;
this.attentionHistory.push(averageAttention);
if (this.attentionHistory.length > 60) this.attentionHistory.shift();
// 主导情绪
const moodCounts = new Map<ReviewMood, number>();
states.forEach(s => {
const count = moodCounts.get(s.mood) || 0;
moodCounts.set(s.mood, count + 1);
});
let dominantMood = ReviewMood.NEUTRAL;
let maxCount = 0;
moodCounts.forEach((count, mood) => {
if (count > maxCount) {
maxCount = count;
dominantMood = mood;
}
});
// 热点区域
const hotAreas = new Map<string, number>();
states.forEach(s => {
s.interestAreas.forEach(area => {
const count = hotAreas.get(area) || 0;
hotAreas.set(area, count + 1);
});
});
// 困惑告警
const confusionAlerts = states
.filter(s => s.mood === ReviewMood.CONFUSED)
.flatMap(s => s.interestAreas)
.filter((v, i, a) => a.indexOf(v) === i);
return {
totalReviewers,
averageAttention,
dominantMood,
hotAreas,
confusionAlerts
};
}
release(): void {
this.session?.stop();
this.session?.release();
}
}
4.3 Body AR建筑漫游操控系统(ArchitectureRoamSystem.ets)
typescript
// systems/ArchitectureRoamSystem.ets
import { ARSession, ARBodyTrack, ARBodySkeleton, KeyPointType } from '@hms.core.ar.engine';
export enum RoamGesture {
WALK = 'walk', // 原地踏步 → 虚拟行走
ROTATE = 'rotate', // 单手画圈 → 视角旋转
ZOOM = 'zoom', // 双手张合 → 缩放
PAN = 'pan', // 双手平移 → 平移视角
SECTION_H = 'section_h', // 单手横切 → 水平剖切
SECTION_V = 'section_v', // 单手竖切 → 垂直剖切
MEASURE = 'measure', // 双手框选 → 测量
RESET = 'reset' // 双手合十 → 重置视角
}
export interface CameraTransform {
position: { x: number; y: number; z: number };
rotation: { yaw: number; pitch: number };
zoom: number;
}
export interface RoamCommand {
gesture: RoamGesture;
value: number;
direction: { x: number; y: number };
confidence: number;
}
export class ArchitectureRoamSystem {
private session: ARSession | null = null;
private bodyTrack: ARBodyTrack | null = null;
private rightHandHistory: HandPosition[] = [];
private leftHandHistory: HandPosition[] = [];
private cameraTransform: CameraTransform = {
position: { x: 0, y: 1.6, z: 0 },
rotation: { yaw: 0, pitch: 0 },
zoom: 1
};
async initialize(): Promise<void> {
this.session = await ARSession.create({
featureTypes: [ARFeatureType.BODY],
cameraConfig: {
facing: CameraFacing.FRONT,
resolution: CameraResolution.HD_720P
}
});
this.bodyTrack = this.session.getBodyTrack();
await this.session.start();
}
update(frameData: ARFrame): RoamCommand | null {
if (!this.bodyTrack) return null;
const bodies = this.bodyTrack.getTrackedBodies(frameData);
if (bodies.length === 0) return null;
const body = bodies[0];
const skeleton = body.getSkeleton();
const keypoints = skeleton.getKeyPoints();
const leftWrist = keypoints.find(kp => kp.type === KeyPointType.LEFT_WRIST);
const rightWrist = keypoints.find(kp => kp.type === KeyPointType.RIGHT_WRIST);
const leftAnkle = keypoints.find(kp => kp.type === KeyPointType.LEFT_ANKLE);
const rightAnkle = keypoints.find(kp => kp.type === KeyPointType.RIGHT_ANKLE);
const leftKnee = keypoints.find(kp => kp.type === KeyPointType.LEFT_KNEE);
const rightKnee = keypoints.find(kp => kp.type === KeyPointType.RIGHT_KNEE);
if (!leftWrist || !rightWrist) return null;
this.recordHandTrajectory(leftWrist, rightWrist);
// 检测行走:膝盖上下运动
if (leftKnee && rightKnee && leftAnkle && rightAnkle) {
const walkGesture = this.detectWalking(leftKnee, rightKnee, leftAnkle, rightAnkle);
if (walkGesture) return walkGesture;
}
// 检测其他手势
return this.detectRoamGestures(leftWrist, rightWrist);
}
private detectWalking(
leftKnee: KeyPoint,
rightKnee: KeyPoint,
leftAnkle: KeyPoint,
rightAnkle: KeyPoint
): RoamCommand | null {
// 检测原地踏步:膝盖交替上下运动
const leftLegLength = this.calculateDistance(leftKnee, leftAnkle);
const rightLegLength = this.calculateDistance(rightKnee, rightAnkle);
const leftKneeHeight = leftKnee.y;
const rightKneeHeight = rightKnee.y;
const kneeDiff = Math.abs(leftKneeHeight - rightKneeHeight);
// 如果膝盖高度差显著,说明在行走
if (kneeDiff > 30 && leftLegLength > 50 && rightLegLength > 50) {
const speed = kneeDiff / 100; // 行走速度
// 更新相机位置
const forwardX = Math.sin(this.cameraTransform.rotation.yaw * Math.PI / 180) * speed;
const forwardZ = Math.cos(this.cameraTransform.rotation.yaw * Math.PI / 180) * speed;
this.cameraTransform.position.x += forwardX;
this.cameraTransform.position.z += forwardZ;
return {
gesture: RoamGesture.WALK,
value: speed,
direction: { x: forwardX, y: 0 },
confidence: Math.min(kneeDiff / 100, 1)
};
}
return null;
}
private detectRoamGestures(leftWrist: KeyPoint, rightWrist: KeyPoint): RoamCommand | null {
const handDistance = this.calculateDistance(leftWrist, rightWrist);
const handCenterX = (leftWrist.x + rightWrist.x) / 2;
const handCenterY = (leftWrist.y + rightWrist.y) / 2;
// 双手张合 → 缩放
if (handDistance > 300 || handDistance < 80) {
const zoom = Math.min(3, Math.max(0.3, handDistance / 200));
this.cameraTransform.zoom = zoom;
return {
gesture: RoamGesture.ZOOM,
value: zoom,
direction: { x: 0, y: 0 },
confidence: handDistance > 300
? Math.min((handDistance - 300) / 200, 1)
: 1 - (handDistance / 80)
};
}
// 单手画圈 → 旋转
const circle = this.detectCircularMotion(this.rightHandHistory, 'clockwise');
if (circle && circle.confidence > 0.7) {
const rotation = circle.totalAngle * (180 / Math.PI) * 0.5;
this.cameraTransform.rotation.yaw += rotation;
return {
gesture: RoamGesture.ROTATE,
value: rotation,
direction: { x: Math.sin(rotation * Math.PI / 180), y: 0 },
confidence: circle.confidence
};
}
// 双手平移 → 平移
const leftVelocity = this.calculateVelocity(this.leftHandHistory);
const rightVelocity = this.calculateVelocity(this.rightHandHistory);
if (leftVelocity && rightVelocity &&
Math.abs(leftVelocity.dx - rightVelocity.dx) < 50) {
const avgDx = (leftVelocity.dx + rightVelocity.dx) / 2;
const avgDy = (leftVelocity.dy + rightVelocity.dy) / 2;
this.cameraTransform.position.x -= avgDx * 0.01;
this.cameraTransform.position.y += avgDy * 0.01;
return {
gesture: RoamGesture.PAN,
value: Math.sqrt(avgDx * avgDx + avgDy * avgDy),
direction: { x: avgDx, y: avgDy },
confidence: 0.8
};
}
// 单手横切 → 水平剖切
const horizontalSlice = this.detectHorizontalSlice(this.rightHandHistory);
if (horizontalSlice) {
return {
gesture: RoamGesture.SECTION_H,
value: handCenterY,
direction: { x: 0, y: handCenterY },
confidence: horizontalSlice.confidence
};
}
// 单手竖切 → 垂直剖切
const verticalSlice = this.detectVerticalSlice(this.rightHandHistory);
if (verticalSlice) {
return {
gesture: RoamGesture.SECTION_V,
value: handCenterX,
direction: { x: handCenterX, y: 0 },
confidence: verticalSlice.confidence
};
}
// 双手合十 → 重置
const isPraying = handDistance < 60 &&
Math.abs(leftWrist.y - rightWrist.y) < 30;
if (isPraying) {
this.cameraTransform = {
position: { x: 0, y: 1.6, z: 0 },
rotation: { yaw: 0, pitch: 0 },
zoom: 1
};
return {
gesture: RoamGesture.RESET,
value: 1,
direction: { x: 0, y: 0 },
confidence: 1 - (handDistance / 60)
};
}
return null;
}
private detectHorizontalSlice(history: HandPosition[]): { confidence: number } | null {
if (history.length < 8) return null;
// 检测水平切割动作:手从左到右或从右到左快速移动
const recent = history.slice(-8);
const startX = recent[0].x;
const endX = recent[recent.length - 1].x;
const yVariation = Math.max(...recent.map(h => h.y)) - Math.min(...recent.map(h => h.y));
const isHorizontal = Math.abs(endX - startX) > 150 && yVariation < 50;
if (!isHorizontal) return null;
return { confidence: Math.min(Math.abs(endX - startX) / 300, 1) };
}
private detectVerticalSlice(history: HandPosition[]): { confidence: number } | null {
if (history.length < 8) return null;
const recent = history.slice(-8);
const startY = recent[0].y;
const endY = recent[recent.length - 1].y;
const xVariation = Math.max(...recent.map(h => h.x)) - Math.min(...recent.map(h => h.x));
const isVertical = Math.abs(endY - startY) > 150 && xVariation < 50;
if (!isVertical) return null;
return { confidence: Math.min(Math.abs(endY - startY) / 300, 1) };
}
private detectCircularMotion(
history: HandPosition[],
direction: 'clockwise' | 'counterclockwise'
): { confidence: number; totalAngle: number } | null {
if (history.length < 15) return null;
let totalAngle = 0;
let validSegments = 0;
for (let i = 1; i < history.length; i++) {
const prev = history[i - 1];
const curr = history[i];
const angle1 = Math.atan2(prev.y - 540, prev.x - 960);
const angle2 = Math.atan2(curr.y - 540, curr.x - 960);
let diff = angle2 - angle1;
if (diff > Math.PI) diff -= 2 * Math.PI;
if (diff < -Math.PI) diff += 2 * Math.PI;
const expectedSign = direction === 'clockwise' ? -1 : 1;
if (Math.sign(diff) === expectedSign && Math.abs(diff) > 0.05) {
totalAngle += Math.abs(diff);
validSegments++;
}
}
if (validSegments < 10) return null;
const confidence = Math.min(totalAngle / Math.PI, 1);
return { confidence, totalAngle };
}
private calculateVelocity(history: HandPosition[]): { dx: number; dy: number } | null {
if (history.length < 3) return null;
const recent = history.slice(-3);
const dx = recent[recent.length - 1].x - recent[0].x;
const dy = recent[recent.length - 1].y - recent[0].y;
const dt = (recent[recent.length - 1].timestamp - recent[0].timestamp) / 1000;
if (dt === 0) return null;
return { dx: dx / dt, dy: dy / dt };
}
private recordHandTrajectory(left: KeyPoint, right: KeyPoint): void {
const now = Date.now();
this.leftHandHistory.push({ x: left.x, y: left.y, timestamp: now });
this.rightHandHistory.push({ x: right.x, y: right.y, timestamp: now });
const cutoff = now - 2000;
this.leftHandHistory = this.leftHandHistory.filter(h => h.timestamp > cutoff);
this.rightHandHistory = this.rightHandHistory.filter(h => h.timestamp > cutoff);
}
private calculateDistance(p1: KeyPoint, p2: KeyPoint): number {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
getCameraTransform(): CameraTransform {
return { ...this.cameraTransform };
}
release(): void {
this.session?.stop();
this.session?.release();
}
}
interface HandPosition {
x: number;
y: number;
timestamp: number;
}
4.4 沉浸光感建筑标题栏(ImmersiveArchitectureTitleBar.ets)
typescript
// components/ImmersiveArchitectureTitleBar.ets
import { ArchitectureLightEngine, BuildingType, DesignPhase, ReviewMood } from '../engines/ArchitectureLightEngine';
import { ReviewAnalytics } from '../systems/ReviewAttentionSystem';
@Component
export struct ImmersiveArchitectureTitleBar {
@Prop currentType: BuildingType;
@Prop currentPhase: DesignPhase;
@Prop projectName: string;
@Prop architectName: string;
@Prop reviewAnalytics: ReviewAnalytics;
@Prop buildingProgress: number;
@State theme = ArchitectureLightEngine.getTheme(BuildingType.RESIDENTIAL);
@State pulseAnimation: boolean = false;
aboutToAppear(): void {
this.theme = ArchitectureLightEngine.getTheme(this.currentType);
setInterval(() => {
this.pulseAnimation = !this.pulseAnimation;
}, this.theme.pulseSpeed / 2);
}
build() {
Row() {
// 左侧:项目与类型信息
Row({ space: 12 }) {
Stack() {
Circle()
.width(44)
.height(44)
.fill(this.theme.primaryColor)
.shadow({
radius: this.pulseAnimation ? 20 : 8,
color: this.theme.primaryColor,
offsetX: 0,
offsetY: 0
})
.animation({
duration: this.theme.pulseSpeed / 2,
curve: Curve.EaseInOut,
iterations: -1
})
Text(this.getTypeIcon())
.fontSize(22)
}
Column({ space: 4 }) {
Text(this.projectName)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text(`${this.architectName} | ${this.getPhaseName()}`)
.fontSize(12)
.fontColor('rgba(255,255,255,0.7)')
}
}
// 中间:评审数据
Row({ space: 20 }) {
Column({ space: 2 }) {
Row({ space: 4 }) {
Text('👁️')
.fontSize(12)
Text(`${this.reviewAnalytics.averageAttention.toFixed(0)}%`)
.fontSize(14)
.fontColor(this.getAttentionColor())
}
Progress({ value: this.reviewAnalytics.averageAttention, total: 100, type: ProgressType.Linear })
.width(80)
.height(4)
.color(this.getAttentionColor())
.backgroundColor('rgba(255,255,255,0.2)')
}
Column({ space: 2 }) {
Text('👥')
.fontSize(12)
Text(`${this.reviewAnalytics.totalReviewers}`)
.fontSize(14)
.fontColor('#FFFFFF')
}
Column({ space: 2 }) {
Text('🎯')
.fontSize(12)
Text(this.getDominantMoodLabel())
.fontSize(14)
.fontColor(this.getMoodColor())
}
Column({ space: 2 }) {
Text('📊')
.fontSize(12)
Text(`${this.buildingProgress.toFixed(0)}%`)
.fontSize(14)
.fontColor('#FFFFFF')
}
}
// 右侧:困惑告警
Row({ space: 12 }) {
if (this.reviewAnalytics.confusionAlerts.length > 0) {
Row({ space: 4 }) {
Text('❓')
.fontSize(14)
Text(`${this.reviewAnalytics.confusionAlerts.length}处困惑`)
.fontSize(11)
.fontColor('#F59E0B')
}
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor('rgba(245, 158, 11, 0.15)')
.borderRadius(12)
}
}
}
.width('100%')
.height(64)
.padding({ left: 24, right: 24 })
.backgroundColor(this.theme.ambientColor)
.backdropBlur(20)
.systemMaterialEffect(MaterialStyle.IMMERSIVE)
.borderRadius({ bottomLeft: 20, bottomRight: 20 })
.shadow({
radius: 25,
color: this.theme.primaryColor,
offsetX: 0,
offsetY: 6
})
.justifyContent(FlexAlign.SpaceBetween)
}
private getTypeIcon(): string {
const icons: Map<BuildingType, string> = new Map([
[BuildingType.RESIDENTIAL, '🏠'],
[BuildingType.COMMERCIAL, '🏢'],
[BuildingType.CULTURAL, '🏛️'],
[BuildingType.EDUCATION, '🏫'],
[BuildingType.MEDICAL, '🏥'],
[BuildingType.LANDSCAPE, '🌳']
]);
return icons.get(this.currentType) || '🏗️';
}
private getPhaseName(): string {
const names: Map<DesignPhase, string> = new Map([
[DesignPhase.CONCEPT, '概念设计'],
[DesignPhase.SCHEMATIC, '方案设计'],
[DesignPhase.DD, '扩初设计'],
[DesignPhase.CD, '施工图']
]);
return names.get(this.currentPhase) || '未知';
}
private getAttentionColor(): ResourceColor {
const score = this.reviewAnalytics.averageAttention;
if (score >= 80) return '#4ADE80';
if (score >= 60) return '#FBBF24';
return '#EF4444';
}
private getDominantMoodLabel(): string {
const labels: Map<ReviewMood, string> = new Map([
[ReviewMood.FOCUSED, '专注'],
[ReviewMood.CONFUSED, '困惑'],
[ReviewMood.IMPRESSED, '赞赏'],
[ReviewMood.CRITICAL, '批评'],
[ReviewMood.NEUTRAL, '平静']
]);
return labels.get(this.reviewAnalytics.dominantMood) || '未知';
}
private getMoodColor(): ResourceColor {
const colors: Map<ReviewMood, ResourceColor> = new Map([
[ReviewMood.FOCUSED, '#4ADE80'],
[ReviewMood.CONFUSED, '#F59E0B'],
[ReviewMood.IMPRESSED, '#EC4899'],
[ReviewMood.CRITICAL, '#EF4444'],
[ReviewMood.NEUTRAL, '#888888']
]);
return colors.get(this.reviewAnalytics.dominantMood) || '#888888';
}
}
4.5 悬浮阶段导航面板(FloatPhaseNav.ets)
typescript
// components/FloatPhaseNav.ets
import { HdsTabs, HdsTabBarStyle } from '@hms.core.arkui.design';
import { ArchitectureLightEngine, BuildingType, DesignPhase } from '../engines/ArchitectureLightEngine';
@Component
export struct FloatPhaseNav {
@Prop currentType: BuildingType;
@Prop currentPhase: DesignPhase;
@Prop transparencyLevel: number;
@State selectedIndex: number = 0;
@State theme = ArchitectureLightEngine.getTheme(BuildingType.RESIDENTIAL);
private phases: DesignPhase[] = [
DesignPhase.CONCEPT,
DesignPhase.SCHEMATIC,
DesignPhase.DD,
DesignPhase.CD
];
build() {
Column() {
HdsTabs({
barStyle: HdsTabBarStyle.FLOATING,
index: this.selectedIndex,
onChange: (index: number) => {
this.selectedIndex = index;
this.handlePhaseChange(this.phases[index]);
}
}) {
ForEach(this.phases, (phase: DesignPhase, index: number) => {
TabContent() {
Stack() {}
}
.tabBar(this.buildTabBar(phase, index))
})
}
.barBackgroundColor(`rgba(15, 15, 35, ${this.transparencyLevel})`)
.barActiveColor(this.theme.primaryColor)
.barInactiveColor('#666666')
.barHeight(72)
.barMargin({ left: 48, right: 48, bottom: 20 })
.barBorderRadius(36)
.systemMaterialEffect(MaterialStyle.IMMERSIVE)
.backdropBlur(20)
}
.width('100%')
.padding({ bottom: 16 })
}
@Builder
buildTabBar(phase: DesignPhase, index: number): void {
Column({ space: 4 }) {
Stack() {
Text(this.getPhaseIcon(phase))
.fontSize(26)
if (phase === this.currentPhase) {
Circle()
.width(10)
.height(10)
.fill('#00F0FF')
.position({ x: 20, y: -6 })
.shadow({ radius: 6, color: 'rgba(0, 240, 255, 0.6)' })
}
}
Text(this.getPhaseLabel(phase))
.fontSize(11)
.fontColor(index === this.selectedIndex ? this.theme.primaryColor : '#666666')
}
.width(68)
.height(60)
.justifyContent(FlexAlign.Center)
}
private handlePhaseChange(phase: DesignPhase): void {
this.theme = ArchitectureLightEngine.getTheme(this.currentType);
const adjustedTheme = ArchitectureLightEngine.adjustByPhase(this.theme, phase);
AppStorage.set('switch_phase', phase);
AppStorage.set('current_architecture_theme', adjustedTheme);
}
private getPhaseIcon(phase: DesignPhase): string {
const icons: Map<DesignPhase, string> = new Map([
[DesignPhase.CONCEPT, '💡'],
[DesignPhase.SCHEMATIC, '📐'],
[DesignPhase.DD, '📋'],
[DesignPhase.CD, '🔧']
]);
return icons.get(phase) || '❓';
}
private getPhaseLabel(phase: DesignPhase): string {
const labels: Map<DesignPhase, string> = new Map([
[DesignPhase.CONCEPT, '概念'],
[DesignPhase.SCHEMATIC, '方案'],
[DesignPhase.DD, '扩初'],
[DesignPhase.CD, '施工']
]);
return labels.get(phase) || '未知';
}
}
4.6 主建筑评审页面(ArchitectureMainPage.ets)
typescript
// pages/ArchitectureMainPage.ets
import { ReviewAttentionSystem, ReviewAnalytics, ReviewerState } from '../systems/ReviewAttentionSystem';
import { ArchitectureRoamSystem, RoamCommand, CameraTransform } from '../systems/ArchitectureRoamSystem';
import { ArchitectureLightEngine, BuildingType, DesignPhase, ReviewMood } from '../engines/ArchitectureLightEngine';
import { ImmersiveArchitectureTitleBar } from '../components/ImmersiveArchitectureTitleBar';
import { FloatPhaseNav } from '../components/FloatPhaseNav';
@Entry
@Component
struct ArchitectureMainPage {
// AR系统
private attentionSystem: ReviewAttentionSystem = new ReviewAttentionSystem();
private roamSystem: ArchitectureRoamSystem = new ArchitectureRoamSystem();
// 项目状态
@State currentType: BuildingType = BuildingType.RESIDENTIAL;
@State currentPhase: DesignPhase = DesignPhase.SCHEMATIC;
@State projectName: string = '湖畔花园住宅项目';
@State architectName: string = '李建筑师';
// 评审数据
@State reviewAnalytics: ReviewAnalytics = {
totalReviewers: 0,
averageAttention: 0,
dominantMood: ReviewMood.NEUTRAL,
hotAreas: new Map(),
confusionAlerts: []
};
@State buildingProgress: number = 65;
// 漫游状态
@State cameraTransform: CameraTransform = {
position: { x: 0, y: 1.6, z: 0 },
rotation: { yaw: 0, pitch: 0 },
zoom: 1
};
@State currentRoamGesture: RoamCommand | null = null;
// 多窗口
@State showFloorPlan: boolean = false;
@State showElevation: boolean = false;
@State showMaterial: boolean = false;
@State showAnnotation: boolean = false;
// 建筑区域定义
private buildingAreas: string[] = [
'入口大厅', '客厅', '餐厅', '厨房', '主卧', '次卧', '书房', '阳台', '卫生间'
];
aboutToAppear(): void {
this.setupImmersiveWindow();
this.initializeArchitectureSystems();
this.setupEventListeners();
}
private setupImmersiveWindow(): void {
const window = windowStage.getMainWindowSync();
window.setWindowLayoutFullScreen(true);
window.setWindowBackgroundColor('#0A0A0F');
}
private async initializeArchitectureSystems(): Promise<void> {
try {
await this.attentionSystem.initialize();
await this.roamSystem.initialize();
this.startArchitectureLoop();
} catch (err) {
console.error('建筑系统初始化失败:', err);
}
}
private async startArchitectureLoop(): Promise<void> {
const loop = async () => {
try {
const frame = await this.attentionSystem.session?.getCurrentFrame();
if (!frame) {
requestAnimationFrame(loop);
return;
}
// Face AR:评审专注度分析
const analytics = this.attentionSystem.update(frame, this.buildingAreas);
if (analytics) {
this.reviewAnalytics = analytics;
this.updateArchitectureLighting();
}
// Body AR:漫游操控
const roamCmd = this.roamSystem.update(frame);
if (roamCmd) {
this.currentRoamGesture = roamCmd;
this.cameraTransform = this.roamSystem.getCameraTransform();
} else {
this.currentRoamGesture = null;
}
} catch (err) {
console.error('建筑循环错误:', err);
}
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
}
private updateArchitectureLighting(): void {
const baseTheme = ArchitectureLightEngine.getTheme(this.currentType);
const phaseTheme = ArchitectureLightEngine.adjustByPhase(baseTheme, this.currentPhase);
const finalTheme = ArchitectureLightEngine.adjustByReviewMood(
phaseTheme,
this.reviewAnalytics.dominantMood
);
AppStorage.set('current_architecture_theme', finalTheme);
}
private setupEventListeners(): void {
AppStorage.watch('switch_phase', (phase: DesignPhase) => {
this.currentPhase = phase;
});
AppStorage.watch('show_floor_plan', (show: boolean) => {
this.showFloorPlan = show;
});
AppStorage.watch('show_elevation', (show: boolean) => {
this.showElevation = show;
});
}
build() {
Stack() {
// 背景环境光
Column()
.width('100%')
.height('100%')
.backgroundColor(ArchitectureLightEngine.getTheme(this.currentType).ambientColor)
.animation({
duration: 1500,
curve: Curve.EaseInOut
})
// 主3D视图
Column() {
Building3DView({
camera: this.cameraTransform,
buildingType: this.currentType,
phase: this.currentPhase,
hotAreas: Array.from(this.reviewAnalytics.hotAreas.keys())
})
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
// 漫游手势提示
if (this.currentRoamGesture) {
RoamGestureHint({
gesture: this.currentRoamGesture
})
.position({ x: '50%', y: '85%' })
.translate({ x: '-50%' })
}
// 沉浸光感标题栏
ImmersiveArchitectureTitleBar({
currentType: this.currentType,
currentPhase: this.currentPhase,
projectName: this.projectName,
architectName: this.architectName,
reviewAnalytics: this.reviewAnalytics,
buildingProgress: this.buildingProgress
})
.position({ x: 0, y: 0 })
.zIndex(100)
// 浮动平面图
if (this.showFloorPlan) {
FloatFloorPlan({
areas: this.buildingAreas,
hotAreas: this.reviewAnalytics.hotAreas,
onClose: () => {
this.showFloorPlan = false;
}
})
.position({ x: '2%', y: '15%' })
.width(320)
.height('70%')
.zIndex(90)
}
// 浮动立面图
if (this.showElevation) {
FloatElevationView({
onClose: () => {
this.showElevation = false;
}
})
.position({ x: '78%', y: '15%' })
.width(320)
.height('70%')
.zIndex(90)
}
// 底部悬浮阶段导航
FloatPhaseNav({
currentType: this.currentType,
currentPhase: this.currentPhase,
transparencyLevel: 0.65
})
.position({ x: 0, y: '100%' })
.translate({ y: -88 })
.zIndex(100)
}
.width('100%')
.height('100%')
.backgroundColor('#0A0A0F')
}
aboutToDisappear(): void {
this.attentionSystem.release();
this.roamSystem.release();
}
}
// 3D建筑视图组件
@Component
struct Building3DView {
@Prop camera: CameraTransform;
@Prop buildingType: BuildingType;
@Prop phase: DesignPhase;
@Prop hotAreas: string[];
build() {
Stack() {
Canvas(this.renderBuilding)
.width('100%')
.height('100%')
.backgroundColor('transparent')
// 热点区域高亮
ForEach(this.hotAreas, (area: string) => {
Text(`🔥 ${area}`)
.fontSize(12)
.fontColor('#F59E0B')
.padding(6)
.backgroundColor('rgba(245, 158, 11, 0.2)')
.borderRadius(8)
.position({ x: Math.random() * 80 + 10 + '%', y: Math.random() * 80 + 10 + '%' })
})
}
}
private renderBuilding = (context: CanvasRenderingContext2D) => {
const canvas = context.canvas;
const w = canvas.width;
const h = canvas.height;
context.clearRect(0, 0, w, h);
// 应用相机变换
context.save();
context.translate(w / 2, h / 2);
context.scale(this.camera.zoom, this.camera.zoom);
context.rotate(this.camera.rotation.yaw * Math.PI / 180);
// 绘制简化建筑模型
this.drawBuildingModel(context, this.buildingType, this.phase);
context.restore();
};
private drawBuildingModel(
ctx: CanvasRenderingContext2D,
type: BuildingType,
phase: DesignPhase
): void {
const theme = ArchitectureLightEngine.getTheme(type);
// 根据设计阶段调整绘制细节
const detailLevel = phase === DesignPhase.CD ? 1 : 0.6;
// 绘制建筑主体
ctx.fillStyle = theme.materialTint as string;
ctx.fillRect(-100, -50, 200, 150);
// 绘制窗户
if (detailLevel > 0.5) {
ctx.fillStyle = 'rgba(200, 220, 255, 0.6)';
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 2; j++) {
ctx.fillRect(-80 + i * 60, -30 + j * 60, 30, 40);
}
}
}
// 绘制屋顶
ctx.beginPath();
ctx.moveTo(-120, -50);
ctx.lineTo(0, -100);
ctx.lineTo(120, -50);
ctx.closePath();
ctx.fillStyle = theme.secondaryColor as string;
ctx.fill();
// 概念阶段添加草图效果
if (phase === DesignPhase.CONCEPT) {
ctx.strokeStyle = 'rgba(100, 100, 100, 0.3)';
ctx.lineWidth = 1;
ctx.setLineDash([5, 5]);
ctx.strokeRect(-110, -60, 220, 170);
ctx.setLineDash([]);
}
}
}
// 漫游手势提示组件
@Component
struct RoamGestureHint {
@Prop gesture: RoamCommand;
build() {
Column({ space: 8 }) {
Text(this.getGestureIcon(this.gesture.gesture))
.fontSize(32)
Text(this.getGestureLabel(this.gesture.gesture))
.fontSize(14)
.fontColor('#FFFFFF')
}
.padding(16)
.backgroundColor('rgba(0, 0, 0, 0.6)')
.borderRadius(16)
.backdropBlur(10)
}
private getGestureIcon(gesture: RoamGesture): string {
const icons: Map<RoamGesture, string> = new Map([
[RoamGesture.WALK, '🚶'],
[RoamGesture.ROTATE, '🔄'],
[RoamGesture.ZOOM, '🔍'],
[RoamGesture.PAN, '✋'],
[RoamGesture.SECTION_H, '➡️'],
[RoamGesture.SECTION_V, '⬇️'],
[RoamGesture.MEASURE, '📏'],
[RoamGesture.RESET, '🔄']
]);
return icons.get(gesture) || '👋';
}
private getGestureLabel(gesture: RoamCommand['gesture']): string {
const labels: Map<RoamGesture, string> = new Map([
[RoamGesture.WALK, '虚拟行走中'],
[RoamGesture.ROTATE, '旋转视角'],
[RoamGesture.ZOOM, `缩放: ${gesture.value.toFixed(1)}x`],
[RoamGesture.PAN, '平移视角'],
[RoamGesture.SECTION_H, '水平剖切'],
[RoamGesture.SECTION_V, '垂直剖切'],
[RoamGesture.MEASURE, '测量模式'],
[RoamGesture.RESET, '重置视角']
]);
return labels.get(gesture) || '手势操控';
}
}
// 浮动平面图组件
@Component
struct FloatFloorPlan {
@Prop areas: string[];
@Prop hotAreas: Map<string, number>;
@Prop onClose: () => void;
build() {
Column({ space: 16 }) {
Row() {
Text('📐 平面图')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Button('✕')
.fontSize(16)
.fontColor('#FFFFFF')
.backgroundColor('transparent')
.onClick(this.onClose)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
// 简化平面图
Canvas(this.drawFloorPlan)
.width('100%')
.height(300)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(12)
// 热点区域列表
List({ space: 8 }) {
ForEach(Array.from(this.hotAreas.entries()).sort((a, b) => b[1] - a[1]), ([area, count]: [string, number]) => {
ListItem() {
Row({ space: 12 }) {
Text('🔥')
.fontSize(14)
Text(area)
.fontSize(14)
.fontColor('#FFFFFF')
Text(`${count}次关注`)
.fontSize(12)
.fontColor('#F59E0B')
}
.width('100%')
.padding(10)
.backgroundColor('rgba(245, 158, 11, 0.1)')
.borderRadius(8)
}
})
}
.width('100%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('rgba(20, 20, 40, 0.85)')
.borderRadius(20)
.backdropBlur(20)
.systemMaterialEffect(MaterialStyle.IMMERSIVE)
}
private drawFloorPlan = (context: CanvasRenderingContext2D) => {
const canvas = context.canvas;
const w = canvas.width;
const h = canvas.height;
context.clearRect(0, 0, w, h);
// 绘制简化户型图
context.strokeStyle = 'rgba(255,255,255,0.5)';
context.lineWidth = 2;
// 外框
context.strokeRect(20, 20, w - 40, h - 40);
// 房间分隔
context.beginPath();
context.moveTo(w / 2, 20);
context.lineTo(w / 2, h - 20);
context.stroke();
context.beginPath();
context.moveTo(20, h / 2);
context.lineTo(w - 20, h / 2);
context.stroke();
// 标注房间
context.fillStyle = 'rgba(255,255,255,0.7)';
context.font = '12px sans-serif';
context.fillText('客厅', w / 4, h / 4);
context.fillText('主卧', w * 3 / 4, h / 4);
context.fillText('厨房', w / 4, h * 3 / 4);
context.fillText('卫生间', w * 3 / 4, h * 3 / 4);
};
}
// 浮动立面图组件
@Component
struct FloatElevationView {
@Prop onClose: () => void;
build() {
Column({ space: 16 }) {
Row() {
Text('🏢 立面图')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Button('✕')
.fontSize(16)
.fontColor('#FFFFFF')
.backgroundColor('transparent')
.onClick(this.onClose)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Canvas(this.drawElevation)
.width('100%')
.height(400)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(12)
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('rgba(20, 20, 40, 0.85)')
.borderRadius(20)
.backdropBlur(20)
.systemMaterialEffect(MaterialStyle.IMMERSIVE)
}
private drawElevation = (context: CanvasRenderingContext2D) => {
const canvas = context.canvas;
const w = canvas.width;
const h = canvas.height;
context.clearRect(0, 0, w, h);
// 绘制简化立面图
context.strokeStyle = 'rgba(255,255,255,0.5)';
context.lineWidth = 2;
// 建筑轮廓
context.strokeRect(40, 50, w - 80, h - 100);
// 楼层线
for (let i = 1; i < 4; i++) {
context.beginPath();
context.moveTo(40, 50 + (h - 100) * i / 4);
context.lineTo(w - 40, 50 + (h - 100) * i / 4);
context.stroke();
}
// 窗户
context.fillStyle = 'rgba(200, 220, 255, 0.4)';
for (let floor = 0; floor < 4; floor++) {
for (let win = 0; win < 3; win++) {
const x = 60 + win * ((w - 120) / 3);
const y = 60 + floor * ((h - 100) / 4);
context.fillRect(x, y, 40, 30);
}
}
};
}
五、关键技术总结
5.1 Face AR在评审中的适配清单
| 适配项 | 说明 | 代码位置 |
|---|---|---|
| 专注度追踪 | 眼球方向 + 头部姿态 + 表情分析 | ReviewAttentionSystem.calculateAttentionScore() |
| 情绪识别 | 赞赏/困惑/批评/专注/平静 | ReviewAttentionSystem.recognizeReviewMood() |
| 热点区域追踪 | 注视位置映射到建筑区域 | ReviewAttentionSystem.trackInterestAreas() |
| 疲劳预警 | 眨眼频率 + 眉毛下垂 | ReviewAttentionSystem.calculateFatigue() |
5.2 Body AR在漫游中的最佳实践
| 实践项 | 说明 | 代码位置 |
|---|---|---|
| 虚拟行走 | 膝盖交替运动检测 | ArchitectureRoamSystem.detectWalking() |
| 视角旋转 | 单手画圈识别 | ArchitectureRoamSystem.detectCircularMotion() |
| 剖切分析 | 水平/垂直切割手势 | ArchitectureRoamSystem.detectHorizontalSlice() |
| 视角重置 | 双手合十识别 | ArchitectureRoamSystem.detectRoamGestures() |
5.3 沉浸光感建筑适配要点
- 类型色动态切换:住宅暖木色、商业冷灰白、文化典雅金、教育明亮绿、医疗洁净蓝、景观自然绿
- 阶段光效差异:概念阶段更梦幻柔和、施工图阶段更精确冷峻
- 评审情绪响应:困惑时暖黄提示、赞赏时光效增强、批评时淡红警示
- 材质模拟 :通过
shadowSoftness模拟不同建筑材料的质感
六、调试与测试建议
6.1 AR性能监控
typescript
const startTime = performance.now();
// ... AR处理逻辑
const processTime = performance.now() - startTime;
if (processTime > 33) {
console.warn(`AR处理帧耗时${processTime.toFixed(1)}ms,存在掉帧风险`);
}
6.2 多窗口测试矩阵
| 测试场景 | 预期结果 |
|---|---|
| 主3D视图 + 浮动平面图 + 浮动立面图 | 标题栏光效同步,图纸不遮挡主视图 |
| 多人评审模式 | 各评审者专注度数据独立统计 |
| 虚拟漫游时 | 行走手势触发平滑移动,无眩晕感 |
| 剖切操作时 | 剖切面实时更新,建筑内部结构清晰展示 |
6.3 常见问题排查
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 专注度评估不准确 | 摄像头角度导致面部遮挡 | 调整摄像头至正面平视角度 |
| 漫游行走误触发 | 日常站立动作被识别为行走 | 增加膝盖高度差阈值 |
| 光效切换闪烁 | 动画时长过短 | 调整duration至1500ms以上 |
| 热点区域映射偏差 | 注视估算算法误差 | 校准头部姿态基准点 |
七、总结与展望
本文基于HarmonyOS 6(API 23)的悬浮导航 、沉浸光感 与Face AR & Body AR特性,完整实战了一款PC端"灵犀筑境"建筑空间评审系统。核心创新点总结:
- 建筑类型光效系统:住宅温馨暖木色、商业冷峻灰白、文化典雅金、教育明亮绿、医疗洁净蓝、景观自然绿
- Face AR评审分析:实时追踪评审者专注度、识别赞赏/困惑/批评情绪、生成热点区域热力图
- Body AR空间漫游:原地踏步虚拟行走、单手画圈旋转视角、双手张合缩放、横切/竖切剖面分析
- 设计阶段光效差异:概念阶段梦幻柔和、方案阶段明亮清晰、扩初阶段精确冷峻、施工图阶段严谨专业
- 悬浮导航自适应 :采用
HdsTabs悬浮样式,四周留白,支持透明度三档调节 - PC级多窗口协作:主3D视图 + 浮动平面图 + 浮动立面图 + 浮动材料面板 + 浮动批注列表
未来扩展方向:
- AI设计助手:结合评审情绪数据,AI自动生成设计优化建议
- 分布式协同评审:通过鸿蒙分布式软总线,实现多地设计师同步评审同一模型
- VR沉浸式评审:结合VR头显,实现1:1真实尺度的空间体验
- 日照模拟集成:根据真实地理位置和时间,实时模拟建筑日照光影
- 结构分析联动:与结构计算软件联动,实时显示受力分析结果
转载自:https://blog.csdn.net/u014727709/article/details/161053225
欢迎 👍点赞✍评论⭐收藏,欢迎指正