HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与Face AR & Body AR的“灵犀筑境“——PC端沉浸式AR建筑空间评审系统

文章目录

    • 每日一句正能量
    • 前言
    • 一、前言:建筑设计评审的交互范式革新
    • 二、核心特性解析与技术选型
      • [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 沉浸光感建筑适配要点

  1. 类型色动态切换:住宅暖木色、商业冷灰白、文化典雅金、教育明亮绿、医疗洁净蓝、景观自然绿
  2. 阶段光效差异:概念阶段更梦幻柔和、施工图阶段更精确冷峻
  3. 评审情绪响应:困惑时暖黄提示、赞赏时光效增强、批评时淡红警示
  4. 材质模拟 :通过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端"灵犀筑境"建筑空间评审系统。核心创新点总结:

  1. 建筑类型光效系统:住宅温馨暖木色、商业冷峻灰白、文化典雅金、教育明亮绿、医疗洁净蓝、景观自然绿
  2. Face AR评审分析:实时追踪评审者专注度、识别赞赏/困惑/批评情绪、生成热点区域热力图
  3. Body AR空间漫游:原地踏步虚拟行走、单手画圈旋转视角、双手张合缩放、横切/竖切剖面分析
  4. 设计阶段光效差异:概念阶段梦幻柔和、方案阶段明亮清晰、扩初阶段精确冷峻、施工图阶段严谨专业
  5. 悬浮导航自适应 :采用HdsTabs悬浮样式,四周留白,支持透明度三档调节
  6. PC级多窗口协作:主3D视图 + 浮动平面图 + 浮动立面图 + 浮动材料面板 + 浮动批注列表

未来扩展方向:

  • AI设计助手:结合评审情绪数据,AI自动生成设计优化建议
  • 分布式协同评审:通过鸿蒙分布式软总线,实现多地设计师同步评审同一模型
  • VR沉浸式评审:结合VR头显,实现1:1真实尺度的空间体验
  • 日照模拟集成:根据真实地理位置和时间,实时模拟建筑日照光影
  • 结构分析联动:与结构计算软件联动,实时显示受力分析结果

转载自:https://blog.csdn.net/u014727709/article/details/161053225

欢迎 👍点赞✍评论⭐收藏,欢迎指正

相关推荐
fellow993 小时前
Electron + pnpm 在鸿蒙应用里跑起来
electron·harmonyos·openharmony·deepseek·deepseekharness
各类产品分享4 小时前
水务集团AR巡检系统选哪家公司好
ar·ar巡检
熊猫钓鱼>_>5 小时前
从闲置平板到家里的“控制大脑“:鸿蒙智慧中控面板完整实战
运维·人工智能·华为·自动化·电脑·ai编程·harmonyos
500845 小时前
React Native for OpenHarmony 实战:三方库 react-native-url-polyfill 的鸿蒙化适配指南
javascript·react native·react.js·性能优化·electron·harmonyos
熊猫钓鱼>_>8 小时前
从2D平铺到3D沉浸:我用HarmonyOS 7端侧AI做了一个全程数据不出设备的空间化私密相册
前端·人工智能·3d·华为·华为云·harmonyos·鸿蒙
m0_738185828 小时前
Flutter 鸿蒙化实战:foundation_fluttify 适配 OpenHarmony,Fluttify 桥接层
flutter·华为·harmonyos·鸿蒙
m0_738185829 小时前
Flutter 鸿蒙化实战:http_proxy 适配 OpenHarmony,HTTP 代理
flutter·http·华为·harmonyos·鸿蒙
m0_738185829 小时前
Flutter 鸿蒙化实战:headset_connection_event 适配 OpenHarmony,耳机插拔监听
flutter·华为·harmonyos·鸿蒙
视+老张9 小时前
商场AR导航为什么要把一个店铺拆成搜索点、入口点和到达区?
ar·增强现实·商场导航
视+老张9 小时前
AI导游怎样把一次问答转成可执行路线:地点实体与任务状态设计
人工智能·ar·增强现实