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

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

相关推荐
xmdy58661 小时前
Flutter+开源鸿蒙全域智慧泊车调度管理平台 Day4 订单全流程闭环+支付核验+会员权益+个人中心开发
flutter·开源·harmonyos
虹科数字化与AR1 小时前
工业AR检测系统选型:关键技术指标解析
ar
前端不太难1 小时前
鸿蒙 App 多端 UI 不一致的原因
ui·状态模式·harmonyos
说再见再也见不到1 小时前
华为AC+AP旁挂二层组网+直接转发,配置实战
网络·华为·交换机·无线组网·无线ac
key_3_feng2 小时前
鸿蒙6.0电子手表高山攀登指标监测功能开发实战
华为·harmonyos
nashane2 小时前
HarmonyOS 6学习:超大分辨率图片压缩与长截图生成优化实践
学习·华为·harmonyos
xmdy58662 小时前
Flutter + 开源鸿蒙跨端实战|基于空间地理信息的城市全域智慧泊车调度与多维运维管理平台 Day3
flutter·华为·开源
shaodong11232 小时前
HarmonyOS NEXT 打印机开发实战:基于 Print Kit 全面解析
华为·harmonyos
三声三视2 小时前
Electron + 鸿蒙分布式投屏:PC 端一键推送画面到鸿蒙设备全实战
分布式·electron·harmonyos·鸿蒙·桌面