在这里插入图片描述

每日一句正能量

别因几粒尘埃迷了眼,就错过整个天空的晴朗。
我们要有宏大的视野和格局,不为琐事困扰。
做不了决定的时候,让时间帮你决定。如果还是无法决定,做了再说。宁愿犯错,不留遗憾。早安!

前言

摘要:HarmonyOS 6(API 23)带来的悬浮导航、沉浸光感与Body AR特性,为建筑设计与空间展示领域提供了全新的交互范式。本文将实战开发一款面向HarmonyOS PC的"筑境空间"应用,展示如何利用systemMaterialEffect打造沉浸式建筑浏览环境,通过悬浮导航实现多项目快速切换,基于Body AR实现手势操控3D建筑模型漫游,以及基于多窗口架构构建浮动户型详情、材质库和日照分析窗口的协作设计体验。


一、前言:建筑设计的交互革新需求

传统的建筑设计软件往往采用复杂的工具栏和固定的面板布局,在HarmonyOS PC的大屏环境下显得臃肿且缺乏空间感。HarmonyOS 6(API 23)引入的悬浮导航(Float Navigation)沉浸光感(Immersive Light Effects)Body AR特性,为建筑可视化带来了"自然、沉浸、直观"的交互可能 。

本文核心亮点

  • 场景感知光效:根据当前建筑场景(日间/夜间/四季)动态切换环境光色与氛围
  • 悬浮项目导航:底部悬浮页签替代传统项目栏,支持拖拽排序与透明度调节
  • Body AR手势操控:通过人体运动捕捉实现手势缩放、旋转、平移3D建筑模型
  • 多窗口设计协作:主漫游窗口 + 浮动户型详情 + 材质库 + 日照分析窗口的光效联动

二、核心特性解析与技术选型

2.1 沉浸光感在建筑可视化中的价值

HarmonyOS 6的systemMaterialEffect通过模拟物理光照模型,为标题栏和导航组件带来细腻的光晕与反射效果 。在建筑漫游场景中,这种材质效果能够:

  • 增强空间氛围:玻璃拟态的半透明层让背景光效柔和过渡,模拟真实建筑的光影变化
  • 提升空间感知:动态环境光随场景时间变化(清晨暖黄、正午亮白、黄昏橙红、夜晚深蓝),增强空间沉浸感
  • 增强操作反馈:通过光效强弱区分窗口焦点状态,多窗口协作时视觉层级更清晰

2.2 Body AR在建筑漫游中的创新应用

HarmonyOS 6的Body AR能力支持实时精确捕捉人体运动信息 ,在建筑漫游中可以:

  • 自然手势操控:手掌张开放大模型、握拳抓取旋转、双手开合缩放
  • 体感漫游:身体前倾前进、后仰后退、左右侧身平移视角
  • 虚拟导览:通过手势触发热点标注、切换楼层、打开门窗动画

三、项目实战:"筑境空间"架构设计

3.1 应用场景与功能规划

面向HarmonyOS PC的建筑设计场景,核心功能包括:

功能模块 技术实现 沉浸光感/Body AR应用
主漫游窗口 XComponent + 3D引擎渲染 背景光效随场景时间变化
悬浮项目导航 HdsTabs + systemMaterialEffect 玻璃拟态页签,选中光晕反馈
Body AR手势操控 AR Engine + 骨骼跟踪 手势操控3D模型
浮动户型详情窗口 子窗口 + HdsNavigation 户型主题色光效同步
材质库窗口 子窗口 + Grid 材质预览光效
日照分析窗口 子窗口 + Canvas 日照角度光效模拟

3.2 技术架构图

┌─────────────────────────────────────────────────────────────┐
│                    UI Layer (ArkUI)                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
│  │ ImmersiveTitle│  │ BuildingView │  │ FloatTabBar      │  │
│  │ Bar (HDS)     │  │ (3D+AR)      │  │ (HdsTabs)        │  │
│  └──────────────┘  └──────────────┘  └──────────────────┘  │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│              AR Engine Layer (Body AR)                       │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
│  │ Skeleton     │  │ Gesture      │  │ Pose Control     │  │
│  │ Tracking     │  │ Recognition  │  │ (3D Model)       │  │
│  └──────────────┘  └──────────────┘  └──────────────────┘  │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│              Window Manager (PC Multi-Window)                │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
│  │ Main Window  │  │ Layout Win   │  │ Material Win     │  │
│  │ (FullScreen) │  │ (Floating)   │  │ (Floating)       │  │
│  └──────────────┘  └──────────────┘  └──────────────────┘  │
│  ┌──────────────┐                                            │
│  │ Sunlight Win │                                            │
│  │ (Floating)   │                                            │
│  └──────────────┘                                            │
└─────────────────────────────────────────────────────────────┘

四、环境配置与模块依赖

4.1 模块依赖配置

{
  "name": "zhujing-space",
  "version": "1.0.0",
  "description": "Immersive Architecture Walkthrough Platform for HarmonyOS PC",
  "dependencies": {
    "@kit.AbilityKit": "^6.1.0",
    "@kit.ArkUI": "^6.1.0",
    "@kit.UIDesignKit": "^6.1.0",
    "@kit.BasicServicesKit": "^6.1.0",
    "@kit.AREngineKit": "^6.1.0",
    "@kit.GraphicsKit": "^6.1.0"
  }
}

4.2 权限声明(module.json5)

{
  "module": {
    "name": "entry",
    "type": "entry",
    "mainElement": "ArchitectureAbility",
    "deviceTypes": [
      "2in1",
      "tablet",
      "default"
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:permission_camera_reason"
      },
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:permission_internet_reason"
      }
    ]
  }
}

五、核心组件实战

5.1 窗口沉浸配置(ArchitectureAbility.ets)

// entry/src/main/ets/ability/ArchitectureAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

export default class ArchitectureAbility extends UIAbility {
  private mainWindow: window.Window | null = null;

  onWindowStageCreate(windowStage: window.WindowStage): void {
    this.initializeArchitectureWindow(windowStage);
  }

  private async initializeArchitectureWindow(windowStage: window.WindowStage): Promise<void> {
    try {
      this.mainWindow = windowStage.getMainWindowSync();

      await this.mainWindow.setWindowSizeType(window.WindowSizeType.FREE);
      await this.mainWindow.setWindowMode(window.WindowMode.FULLSCREEN);
      await this.mainWindow.setWindowTitleBarEnable(false);
      await this.mainWindow.setWindowLayoutFullScreen(true);
      await this.mainWindow.setWindowShadowEnabled(true);
      await this.mainWindow.setWindowCornerRadius(12);
      await this.mainWindow.setWindowBackgroundColor('#00000000');

      AppStorage.setOrCreate('main_window', this.mainWindow);

      windowStage.loadContent('pages/ArchitecturePage', (err) => {
        if (err.code) {
          console.error('Failed to load architecture content:', JSON.stringify(err));
          return;
        }
        console.info('Zhujing Space main window initialized');
      });

    } catch (error) {
      console.error('Window initialization failed:', (error as BusinessError).message);
    }
  }

  onWindowStageDestroy(): void {
    this.mainWindow = null;
  }
}

5.2 沉浸光感标题栏(ImmersiveTitleBar.ets)

// entry/src/main/ets/components/ImmersiveTitleBar.ets
import { HdsNavigation, SystemMaterialEffect } from '@kit.UIDesignKit';

export enum SceneTime {
  DAWN = 'dawn',      // 清晨
  NOON = 'noon',      // 正午
  DUSK = 'dusk',      // 黄昏
  NIGHT = 'night'     // 夜晚
}

@Component
export struct ImmersiveTitleBar {
  @Prop currentProject: string = '云栖别墅';
  @Prop currentFloor: string = '一层平面';
  @Prop sceneTime: SceneTime = SceneTime.NOON;
  @State isWindowFocused: boolean = true;
  @State titleBarHeight: number = 48;

  // 场景时间主题色映射
  private sceneColors: Map<SceneTime, string> = new Map([
    [SceneTime.DAWN, '#FFD93D'],    // 暖黄-清晨
    [SceneTime.NOON, '#FFFFFF'],     // 亮白-正午
    [SceneTime.DUSK, '#FF6B6B'],     // 橙红-黄昏
    [SceneTime.NIGHT, '#4A69BD']     // 深蓝-夜晚
  ]);

  aboutToAppear(): void {
    AppStorage.watch('window_focused', (focused: boolean) => {
      this.isWindowFocused = focused;
    });
  }

  private getThemeColor(): string {
    return this.sceneColors.get(this.sceneTime) || '#FFFFFF';
  }

  private getSceneText(): string {
    const texts: Map<SceneTime, string> = new Map([
      [SceneTime.DAWN, '清晨'],
      [SceneTime.NOON, '正午'],
      [SceneTime.DUSK, '黄昏'],
      [SceneTime.NIGHT, '夜晚']
    ]);
    return texts.get(this.sceneTime) || '正午';
  }

  build() {
    HdsNavigation({
      title: `筑境空间 - ${this.currentProject}`,
      subtitle: `${this.currentFloor} · ${this.getSceneText()}`,
      systemMaterialEffect: SystemMaterialEffect.IMMERSIVE,
      backgroundOpacity: this.isWindowFocused ? 0.85 : 0.55,
      height: this.titleBarHeight,
      leading: this.buildLeadingActions(),
      trailing: this.buildTrailingActions()
    })
    .width('100%')
    .border({
      width: { bottom: 1 },
      color: this.isWindowFocused 
        ? this.getThemeColor() 
        : 'rgba(255,255,255,0.1)'
    })
    .shadow({
      radius: this.isWindowFocused ? 15 : 5,
      color: this.getThemeColor(),
      offsetX: 0,
      offsetY: 2
    })
    .animation({
      duration: 300,
      curve: Curve.EaseInOut
    })
  }

  @Builder
  buildLeadingActions(): void {
    Row({ space: 12 }) {
      Button({ type: ButtonType.Circle }) {
        Image($r('app.media.ic_3d_view'))
          .width(18)
          .height(18)
          .fillColor('#FFFFFF')
      }
      .width(32)
      .height(32)
      .backgroundColor('rgba(255,255,255,0.1)')
      .onClick(() => {
        AppStorage.setOrCreate('view_action', 'toggle_3d');
      })

      Button({ type: ButtonType.Circle }) {
        Image($r('app.media.ic_floor'))
          .width(18)
          .height(18)
          .fillColor('#FFFFFF')
      }
      .width(32)
      .height(32)
      .backgroundColor('rgba(255,255,255,0.1)')
      .onClick(() => {
        AppStorage.setOrCreate('view_action', 'switch_floor');
      })

      Button({ type: ButtonType.Circle }) {
        Image($r('app.media.ic_sun'))
          .width(18)
          .height(18)
          .fillColor('#FFFFFF')
      }
      .width(32)
      .height(32)
      .backgroundColor('rgba(255,255,255,0.1)')
      .onClick(() => {
        AppStorage.setOrCreate('view_action', 'cycle_time');
      })
    }
    .padding({ left: 16 })
  }

  @Builder
  buildTrailingActions(): void {
    Row({ space: 12 }) {
      // AR状态指示
      Circle()
        .width(8)
        .height(8)
        .fill(AppStorage.get<boolean>('ar_active') ? '#27C93F' : '#95A5A6')
        .shadow({
          radius: 8,
          color: AppStorage.get<boolean>('ar_active') ? '#27C93F' : 'transparent'
        })

      Button({ type: ButtonType.Circle }) {
        Image($r('app.media.ic_layout'))
          .width(18)
          .height(18)
          .fillColor('#FFFFFF')
      }
      .width(32)
      .height(32)
      .backgroundColor('rgba(255,255,255,0.1)')
      .onClick(() => {
        AppStorage.setOrCreate('window_action', 'open_layout');
      })

      Button({ type: ButtonType.Circle }) {
        Image($r('app.media.ic_material'))
          .width(18)
          .height(18)
          .fillColor('#FFFFFF')
      }
      .width(32)
      .height(32)
      .backgroundColor('rgba(255,255,255,0.1)')
      .onClick(() => {
        AppStorage.setOrCreate('window_action', 'open_material');
      })

      Button({ type: ButtonType.Circle }) {
        Image($r('app.media.ic_sunlight'))
          .width(18)
          .height(18)
          .fillColor('#FFFFFF')
      }
      .width(32)
      .height(32)
      .backgroundColor('rgba(255,255,255,0.1)')
      .onClick(() => {
        AppStorage.setOrCreate('window_action', 'open_sunlight');
      })
    }
    .padding({ right: 16 })
  }
}

5.3 Body AR手势操控组件(BodyARController.ets)

核心创新组件,基于AR Engine实现人体骨骼跟踪与手势识别 。

// entry/src/main/ets/components/BodyARController.ets
import { arEngine } from '@kit.AREngineKit';
import { camera } from '@kit.CameraKit';

export enum GestureType {
  NONE = 'none',
  OPEN_PALM = 'open_palm',      // 张开手掌-放大
  CLOSED_FIST = 'closed_fist',   // 握拳-旋转
  PINCH = 'pinch',               // 捏合-缩放
  POINT = 'point',               // 指向-选择
  SWIPE_LEFT = 'swipe_left',     // 左滑-平移
  SWIPE_RIGHT = 'swipe_right'    // 右滑-平移
}

interface BodyPose {
  gesture: GestureType;
  handPosition: { x: number; y: number };
  handDistance: number;  // 双手距离(用于缩放)
  bodyLean: number;      // 身体倾斜角度
  confidence: number;
}

@Component
export struct BodyARController {
  @State isARActive: boolean = false;
  @State currentGesture: GestureType = GestureType.NONE;
  @State gestureConfidence: number = 0;
  @State trackingStatus: string = '未启动';

  private arSession: arEngine.ARSession | null = null;
  private bodyTracker: arEngine.BodyTracker | null = null;
  private gestureCallback: ((gesture: BodyPose) => void) | null = null;

  aboutToAppear(): void {
    this.initializeAR();
  }

  aboutToDisappear(): void {
    this.releaseAR();
  }

  setGestureCallback(callback: (gesture: BodyPose) => void): void {
    this.gestureCallback = callback;
  }

  private async initializeAR(): Promise<void> {
    try {
      this.arSession = arEngine.createARSession({
        mode: arEngine.ARMode.BODY,
        cameraConfig: {
          cameraFacing: camera.CameraFacing.CAMERA_FACING_FRONT
        }
      });

      this.bodyTracker = this.arSession.createBodyTracker({
        maxBodyCount: 1,
        enableSkeleton: true,
        enableGesture: true
      });

      await this.arSession.start();
      this.isARActive = true;
      this.trackingStatus = '跟踪中';

      this.startBodyTracking();
      console.info('Body AR initialized successfully');
    } catch (error) {
      console.error('Failed to initialize Body AR:', error);
      this.trackingStatus = '初始化失败';
    }
  }

  private startBodyTracking(): void {
    if (!this.arSession || !this.bodyTracker) return;

    this.arSession.on('frame', (frame: arEngine.ARFrame) => {
      const bodies = this.bodyTracker?.track(frame);
      if (bodies && bodies.length > 0) {
        const body = bodies[0];
        const pose = this.analyzeGesture(body);
        this.currentGesture = pose.gesture;
        this.gestureConfidence = pose.confidence;

        if (this.gestureCallback) {
          this.gestureCallback(pose);
        }

        AppStorage.setOrCreate('body_gesture', pose);
      }
    });
  }

  private analyzeGesture(body: arEngine.Body): BodyPose {
    const skeleton = body.getSkeleton();
    const leftHand = skeleton.getJoint(arEngine.BodyJointType.LEFT_HAND);
    const rightHand = skeleton.getJoint(arEngine.BodyJointType.RIGHT_HAND);
    const leftWrist = skeleton.getJoint(arEngine.BodyJointType.LEFT_WRIST);
    const rightWrist = skeleton.getJoint(arEngine.BodyJointType.RIGHT_WRIST);
    const nose = skeleton.getJoint(arEngine.BodyJointType.NOSE);
    const neck = skeleton.getJoint(arEngine.BodyJointType.NECK);

    // 计算双手距离(用于缩放判断)
    const handDistance = Math.sqrt(
      Math.pow(rightHand.x - leftHand.x, 2) + 
      Math.pow(rightHand.y - leftHand.y, 2)
    );

    // 计算身体倾斜(用于漫游判断)
    const bodyLean = (nose.x - neck.x) / (neck.confidence || 1);

    // 手势识别逻辑
    let gesture = GestureType.NONE;
    let confidence = 0;

    // 判断双手张开(放大)
    if (leftHand.confidence > 0.7 && rightHand.confidence > 0.7 && handDistance > 0.5) {
      gesture = GestureType.OPEN_PALM;
      confidence = (leftHand.confidence + rightHand.confidence) / 2;
    }
    // 判断单手挥舞(左滑/右滑)
    else if (leftWrist.confidence > 0.7 && Math.abs(leftWrist.velocityX) > 0.3) {
      gesture = leftWrist.velocityX > 0 ? GestureType.SWIPE_RIGHT : GestureType.SWIPE_LEFT;
      confidence = leftWrist.confidence;
    }
    // 判断指向(选择)
    else if (rightHand.confidence > 0.7 && rightWrist.confidence > 0.7) {
      const handAngle = Math.atan2(rightHand.y - rightWrist.y, rightHand.x - rightWrist.x);
      if (Math.abs(handAngle) < Math.PI / 4) {
        gesture = GestureType.POINT;
        confidence = rightHand.confidence;
      }
    }

    return {
      gesture,
      handPosition: { x: rightHand.x, y: rightHand.y },
      handDistance,
      bodyLean,
      confidence
    };
  }

  private releaseAR(): void {
    if (this.arSession) {
      this.arSession.stop();
      this.arSession = null;
    }
    this.bodyTracker = null;
    this.isARActive = false;
    this.trackingStatus = '已停止';
  }

  build() {
    Stack() {
      // AR预览层
      Column() {
        if (this.isARActive) {
          XComponent({
            id: 'body_ar_preview',
            type: XComponentType.SURFACE,
            controller: new XComponentController()
          })
            .width('100%')
            .height('100%')
            .opacity(0.25)
        }
      }
      .width('100%')
      .height('100%')

      // 手势反馈层
      Column() {
        this.buildGestureFeedback()
      }
      .width('100%')
      .height('100%')

      // AR状态指示器
      this.buildARStatusIndicator()
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildGestureFeedback(): void {
    if (this.currentGesture !== GestureType.NONE) {
      Column() {
        // 手势图标
        Image(this.getGestureIcon())
          .width(64)
          .height(64)
          .fillColor('#4ECDC4')
          .opacity(this.gestureConfidence)

        Text(this.getGestureText())
          .fontSize(16)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .margin({ top: 8 })

        // 置信度进度条
        Progress({
          value: this.gestureConfidence * 100,
          total: 100,
          type: ProgressType.Linear
        })
          .width(120)
          .height(4)
          .color('#4ECDC4')
          .margin({ top: 8 })
      }
      .width(160)
      .height(140)
      .backgroundColor('rgba(0,0,0,0.6)')
      .borderRadius(16)
      .position({ x: '50%', y: '20%' })
      .anchor('50%')
      .backdropFilter($r('sys.blur.10'))
      .animation({
        duration: 200,
        curve: Curve.EaseOut
      })
    }
  }

  @Builder
  buildARStatusIndicator(): void {
    Row({ space: 6 }) {
      Circle()
        .width(8)
        .height(8)
        .fill(this.isARActive ? '#27C93F' : '#95A5A6')
        .shadow({
          radius: 6,
          color: this.isARActive ? '#27C93F' : 'transparent'
        })

      Text(`Body AR · ${this.trackingStatus}`)
        .fontSize(11)
        .fontColor(this.isARActive ? '#27C93F' : '#95A5A6')
    }
    .width('auto')
    .height(28)
    .padding({ left: 10, right: 10 })
    .backgroundColor('rgba(0,0,0,0.6)')
    .borderRadius(14)
    .position({ x: 16, y: 16 })
    .backdropFilter($r('sys.blur.10'))
  }

  private getGestureIcon(): Resource {
    const icons: Map<GestureType, Resource> = new Map([
      [GestureType.OPEN_PALM, $r('app.media.ic_open_palm')],
      [GestureType.CLOSED_FIST, $r('app.media.ic_fist')],
      [GestureType.PINCH, $r('app.media.ic_pinch')],
      [GestureType.POINT, $r('app.media.ic_point')],
      [GestureType.SWIPE_LEFT, $r('app.media.ic_swipe_left')],
      [GestureType.SWIPE_RIGHT, $r('app.media.ic_swipe_right')]
    ]);
    return icons.get(this.currentGesture) || $r('app.media.ic_hand');
  }

  private getGestureText(): string {
    const texts: Map<GestureType, string> = new Map([
      [GestureType.OPEN_PALM, '放大模型'],
      [GestureType.CLOSED_FIST, '旋转模型'],
      [GestureType.PINCH, '缩放模型'],
      [GestureType.POINT, '选择热点'],
      [GestureType.SWIPE_LEFT, '向左平移'],
      [GestureType.SWIPE_RIGHT, '向右平移']
    ]);
    return texts.get(this.currentGesture) || '未知手势';
  }
}

5.4 3D建筑漫游视图(BuildingViewer.ets)

// entry/src/main/ets/components/BuildingViewer.ets
import { BodyARController, GestureType, BodyPose } from './BodyARController';

@Component
export struct BuildingViewer {
  @Prop sceneTime: string = 'noon';
  @Prop themeColor: string = '#FFFFFF';
  @State modelScale: number = 1.0;
  @State modelRotationY: number = 0;
  @State modelOffsetX: number = 0;
  @State modelOffsetY: number = 0;
  @State currentFloor: number = 1;
  @State showHotspots: boolean = true;

  private arController: BodyARController | null = null;

  aboutToAppear(): void {
    // 监听Body AR手势
    AppStorage.watch('body_gesture', (pose: BodyPose) => {
      this.handleGesture(pose);
    });
  }

  private handleGesture(pose: BodyPose): void {
    switch (pose.gesture) {
      case GestureType.OPEN_PALM:
        // 双手张开-放大模型
        this.modelScale = Math.min(2.0, this.modelScale + 0.05);
        break;
      case GestureType.PINCH:
        // 捏合-缩小模型
        this.modelScale = Math.max(0.5, this.modelScale - 0.05);
        break;
      case GestureType.SWIPE_LEFT:
        // 左滑-向左平移
        this.modelOffsetX -= 10;
        break;
      case GestureType.SWIPE_RIGHT:
        // 右滑-向右平移
        this.modelOffsetX += 10;
        break;
      case GestureType.CLOSED_FIST:
        // 握拳-旋转模型
        this.modelRotationY += 2;
        break;
      case GestureType.POINT:
        // 指向-切换楼层
        if (pose.handPosition.y < 0.3) {
          this.currentFloor = Math.min(3, this.currentFloor + 1);
        } else if (pose.handPosition.y > 0.7) {
          this.currentFloor = Math.max(1, this.currentFloor - 1);
        }
        break;
    }
  }

  build() {
    Stack() {
      // 场景光效背景
      this.buildSceneLightBackground()

      // 3D建筑模型层
      Column() {
        // 建筑轮廓示意(实际项目中使用3D引擎渲染)
        this.buildBuildingModel()

        // 热点标注
        if (this.showHotspots) {
          this.buildHotspots()
        }
      }
      .width('100%')
      .height('100%')
      .scale({ x: this.modelScale, y: this.modelScale })
      .translate({ x: this.modelOffsetX, y: this.modelOffsetY })
      .rotate({ x: 0, y: 1, z: 0, angle: this.modelRotationY })
      .animation({
        duration: 300,
        curve: Curve.EaseInOut
      })

      // 楼层指示器
      this.buildFloorIndicator()

      // Body AR控制层
      BodyARController()
        .width('100%')
        .height('100%')
        .position({ x: 0, y: 0 })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildSceneLightBackground(): void {
    Column() {
      // 天空光效
      Column()
        .width('100%')
        .height('60%')
        .linearGradient({
          direction: GradientDirection.Bottom,
          colors: this.getSkyColors()
        })

      // 地面反射
      Column()
        .width('100%')
        .height('40%')
        .backgroundColor('rgba(0,0,0,0.3)')
        .linearGradient({
          direction: GradientDirection.Top,
          colors: [
            ['rgba(255,255,255,0.05)', 0.0],
            ['transparent', 1.0]
          ]
        })

      // 环境光晕
      Column()
        .width(800)
        .height(800)
        .backgroundColor(this.themeColor)
        .blur(200)
        .opacity(0.06)
        .position({ x: '50%', y: '30%' })
        .anchor('50%')
        .animation({
          duration: 10000,
          curve: Curve.EaseInOut,
          iterations: -1,
          playMode: PlayMode.Alternate
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0a0a0f')
  }

  private getSkyColors(): Array<[string, number]> {
    const colors: Map<string, Array<[string, number]>> = new Map([
      ['dawn', [['#FF9A76', 0.0], ['#FFD93D', 0.5], ['#87CEEB', 1.0]]],
      ['noon', [['#87CEEB', 0.0], ['#E0F7FA', 0.5], ['#FFFFFF', 1.0]]],
      ['dusk', [['#4A69BD', 0.0], ['#FF6B6B', 0.5], ['#FFD93D', 1.0]]],
      ['night', [['#0C1445', 0.0], ['#1E3A5F', 0.5], ['#2C3E50', 1.0]]]
    ]);
    return colors.get(this.sceneTime) || [['#87CEEB', 0.0], ['#FFFFFF', 1.0]];
  }

  @Builder
  buildBuildingModel(): void {
    Column() {
      // 建筑主体(简化示意)
      Column() {
        // 屋顶
        Triangle()
          .width(200)
          .height(100)
          .fill('#8B7355')
          .position({ x: '50%', y: 0 })
          .anchor('50%')

        // 楼层
        ForEach([1, 2, 3], (floor) => {
          Column() {
            Row({ space: 8 }) {
              ForEach([1, 2, 3, 4], (window) => {
                Rectangle()
                  .width(30)
                  .height(40)
                  .fill(floor === this.currentFloor ? '#FFEAA7' : '#4A5568')
                  .borderRadius(4)
                  .shadow({
                    radius: floor === this.currentFloor ? 8 : 0,
                    color: '#FFEAA7'
                  })
              })
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceEvenly)

            Text(`F${floor}`)
              .fontSize(10)
              .fontColor('#666666')
              .margin({ top: 4 })
          }
          .width(220)
          .height(100)
          .backgroundColor(floor === this.currentFloor ? 'rgba(255,234,167,0.1)' : 'rgba(74,85,104,0.3)')
          .borderRadius(8)
          .margin({ top: 4 })
          .border({
            width: floor === this.currentFloor ? 2 : 0,
            color: '#FFEAA7'
          })
        })
      }
      .width(240)
      .height(380)
      .position({ x: '50%', y: '50%' })
      .anchor('50%')
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildHotspots(): void {
    // 热点标注点
    ForEach([
      { x: 120, y: 150, label: '主卧', info: '32㎡ · 南向' },
      { x: 180, y: 200, label: '客厅', info: '45㎡ · 落地窗' },
      { x: 80, y: 250, label: '厨房', info: '15㎡ · 开放式' }
    ], (spot) => {
      Column() {
        Circle()
          .width(12)
          .height(12)
          .fill('#FF6B6B')
          .shadow({
            radius: 8,
            color: '#FF6B6B'
          })
          .animation({
            duration: 1500,
            curve: Curve.EaseInOut,
            iterations: -1,
            playMode: PlayMode.Alternate
          })
          .scale({ x: 1.2, y: 1.2 })

        Text(spot.label)
          .fontSize(11)
          .fontColor('#FFFFFF')
          .backgroundColor('rgba(0,0,0,0.6)')
          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          .borderRadius(4)
          .margin({ top: 4 })
      }
      .position({ x: spot.x, y: spot.y })
      .onClick(() => {
        AppStorage.setOrCreate('selected_hotspot', spot);
      })
    })
  }

  @Builder
  buildFloorIndicator(): void {
    Column({ space: 8 }) {
      ForEach([3, 2, 1], (floor) => {
        Button(`F${floor}`)
          .fontSize(12)
          .fontColor(floor === this.currentFloor ? '#FFFFFF' : '#666666')
          .backgroundColor(floor === this.currentFloor ? '#4ECDC4' : 'rgba(255,255,255,0.1)')
          .width(40)
          .height(40)
          .borderRadius(20)
          .onClick(() => {
            this.currentFloor = floor;
          })
      })
    }
    .width('auto')
    .height('auto')
    .padding(8)
    .backgroundColor('rgba(0,0,0,0.4)')
    .borderRadius(12)
    .position({ x: '95%', y: '50%' })
    .anchor('100% 50%')
    .backdropFilter($r('sys.blur.10'))
  }
}

5.5 悬浮项目导航页签(FloatTabNavigation.ets)

// entry/src/main/ets/components/FloatTabNavigation.ets
import { window } from '@kit.ArkUI';
import { HdsTabs, SystemMaterialEffect } from '@kit.UIDesignKit';

export enum TransparencyLevel {
  STRONG = 0.85,
  BALANCED = 0.70,
  WEAK = 0.55
}

interface ProjectTab {
  id: string;
  name: string;
  theme: string;
  type: string;
  area: string;
  icon: Resource;
}

@Component
export struct FloatTabNavigation {
  @Prop currentIndex: number = 0;
  @State navTransparency: number = TransparencyLevel.BALANCED;
  @State isExpanded: boolean = false;
  @State bottomAvoidHeight: number = 0;
  @State tabs: ProjectTab[] = [
    { id: '1', name: '云栖别墅', theme: '#4ECDC4', type: '别墅', area: '380㎡', icon: $r('app.media.ic_villa') },
    { id: '2', name: '湖畔公寓', theme: '#FF6B6B', type: '公寓', area: '120㎡', icon: $r('app.media.ic_apartment') },
    { id: '3', name: '商业综合体', theme: '#FFEAA7', type: '商业', area: '5000㎡', icon: $r('app.media.ic_commercial') },
    { id: '4', name: '文创园区', theme: '#96CEB4', type: '园区', area: '12000㎡', icon: $r('app.media.ic_park') }
  ];

  aboutToAppear(): void {
    this.getBottomAvoidArea();
  }

  private async getBottomAvoidArea(): Promise<void> {
    try {
      const mainWindow = await window.getLastWindow();
      const avoidArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
      this.bottomAvoidHeight = avoidArea.bottomRect.height;
    } catch (error) {
      console.error('Failed to get avoid area:', error);
    }
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column() {
        this.contentBuilder()
      }
      .padding({ bottom: this.bottomAvoidHeight + 88 })

      Column() {
        Stack() {
          Column()
            .width('100%')
            .height('100%')
            .backgroundBlurStyle(BlurStyle.REGULAR)
            .opacity(this.navTransparency)
            .backdropFilter($r('sys.blur.20'))

          Column()
            .width('100%')
            .height('100%')
            .linearGradient({
              direction: GradientDirection.Top,
              colors: [
                ['rgba(255,255,255,0.15)', 0.0],
                ['rgba(255,255,255,0.05)', 1.0]
              ]
            })
        }
        .width('100%')
        .height('100%')
        .borderRadius(20)
        .shadow({
          radius: 20,
          color: 'rgba(0,0,0,0.2)',
          offsetX: 0,
          offsetY: -4
        })

        Row() {
          ForEach(this.tabs, (tab: ProjectTab, index: number) => {
            this.buildProjectTab(tab, index)
          }, (tab: ProjectTab) => tab.id)
        }
        .width('100%')
        .height(64)
        .padding({ left: 16, right: 16 })
        .justifyContent(FlexAlign.Start)

        if (this.isExpanded) {
          this.buildTransparencyPanel()
        }
      }
      .width('96%')
      .height(this.isExpanded ? 108 : 64)
      .margin({ 
        bottom: this.bottomAvoidHeight + 12, 
        left: '2%', 
        right: '2%' 
      })
      .animation({
        duration: 300,
        curve: Curve.Spring,
        iterations: 1
      })
      .gesture(
        LongPressGesture({ duration: 500 })
          .onAction(() => {
            this.isExpanded = !this.isExpanded;
          })
      )
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildProjectTab(tab: ProjectTab, index: number): void {
    Row({ space: 6 }) {
      Image(tab.icon)
        .width(16)
        .height(16)
        .fillColor(tab.theme)

      Text(tab.name)
        .fontSize(13)
        .fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
        .fontColor(this.currentIndex === index ? '#FFFFFF' : '#AAAAAA')
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .width(80)

      Text(`${tab.type} · ${tab.area}`)
        .fontSize(10)
        .fontColor('#666666')
        .backgroundColor('rgba(255,255,255,0.1)')
        .borderRadius(8)
        .padding({ left: 6, right: 6, top: 2, bottom: 2 })

      if (this.currentIndex === index) {
        Button({ type: ButtonType.Circle }) {
          Text('×')
            .fontSize(14)
            .fontColor('#AAAAAA')
        }
        .width(20)
        .height(20)
        .backgroundColor('transparent')
        .onClick(() => {
          this.closeTab(index);
        })
      }
    }
    .height(40)
    .padding({ left: 12, right: 12 })
    .backgroundColor(this.currentIndex === index 
      ? 'rgba(255,255,255,0.15)' 
      : 'transparent')
    .borderRadius(12)
    .border({
      width: this.currentIndex === index ? 1 : 0,
      color: tab.theme
    })
    .onClick(() => {
      this.currentIndex = index;
      AppStorage.setOrCreate('current_project', tab.name);
      AppStorage.setOrCreate('current_theme', tab.theme);
    })
  }

  @Builder
  buildTransparencyPanel(): void {
    Row({ space: 12 }) {
      Text('透明度')
        .fontSize(12)
        .fontColor('#AAAAAA')

      Slider({
        value: this.navTransparency * 100,
        min: 55,
        max: 85,
        step: 15,
        style: SliderStyle.InSet
      })
        .width(120)
        .onChange((value: number) => {
          this.navTransparency = value / 100;
        })

      Text(`${Math.round(this.navTransparency * 100)}%`)
        .fontSize(12)
        .fontColor('#AAAAAA')

      Button('强')
        .fontSize(11)
        .backgroundColor(this.navTransparency === TransparencyLevel.STRONG 
          ? '#4ECDC4' 
          : 'rgba(255,255,255,0.1)')
        .onClick(() => { this.navTransparency = TransparencyLevel.STRONG; })

      Button('平衡')
        .fontSize(11)
        .backgroundColor(this.navTransparency === TransparencyLevel.BALANCED 
          ? '#4ECDC4' 
          : 'rgba(255,255,255,0.1)')
        .onClick(() => { this.navTransparency = TransparencyLevel.BALANCED; })

      Button('弱')
        .fontSize(11)
        .backgroundColor(this.navTransparency === TransparencyLevel.WEAK 
          ? '#4ECDC4' 
          : 'rgba(255,255,255,0.1)')
        .onClick(() => { this.navTransparency = TransparencyLevel.WEAK; })
    }
    .width('100%')
    .height(44)
    .justifyContent(FlexAlign.Center)
    .backgroundColor('rgba(255,255,255,0.05)')
    .borderRadius({ topLeft: 12, topRight: 12 })
  }

  private closeTab(index: number): void {
    if (this.tabs.length <= 1) return;
    this.tabs.splice(index, 1);
    if (this.currentIndex >= index && this.currentIndex > 0) {
      this.currentIndex--;
    }
  }

  @BuilderParam contentBuilder: () => void = this.defaultContentBuilder;

  @Builder
  defaultContentBuilder(): void {
    Column() {
      Text('建筑漫游区域')
        .fontSize(16)
        .fontColor('#999999')
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

5.6 多窗口光效同步管理器(WindowManager.ets)

// entry/src/main/ets/utils/WindowManager.ets
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

export interface ToolWindowConfig {
  name: string;
  title: string;
  width: number;
  height: number;
  x?: number;
  y?: number;
  followMainWindow?: boolean;
  themeColor?: string;
}

export class WindowManager {
  private static instance: WindowManager;
  private mainWindow: window.Window | null = null;
  private subWindows: Map<string, window.Window> = new Map();

  static getInstance(): WindowManager {
    if (!WindowManager.instance) {
      WindowManager.instance = new WindowManager();
    }
    return WindowManager.instance;
  }

  async initializeMainWindow(windowStage: window.WindowStage): Promise<void> {
    this.mainWindow = windowStage.getMainWindowSync();
    await this.mainWindow.setWindowSizeType(window.WindowSizeType.FREE);
    await this.mainWindow.setWindowMode(window.WindowMode.FULLSCREEN);
    await this.mainWindow.setWindowTitleBarEnable(false);
    await this.mainWindow.setWindowShadowEnabled(true);
    await this.mainWindow.setWindowCornerRadius(12);
    await this.mainWindow.setWindowBackgroundColor('#00000000');

    this.mainWindow.on('windowFocusChange', (isFocused: boolean) => {
      AppStorage.setOrCreate('window_focused', isFocused);
      if (isFocused) {
        this.syncGlobalLightEffect(AppStorage.get<string>('global_theme_color') || '#4ECDC4');
      }
    });

    console.info('Main window initialized for Zhujing Space');
  }

  async createToolWindow(config: ToolWindowConfig): Promise<window.Window | null> {
    try {
      if (!this.mainWindow) {
        throw new Error('Main window not initialized');
      }

      const subWindow = await this.mainWindow.createSubWindow(config.name);
      await subWindow.setWindowSizeType(window.WindowSizeType.FREE);
      await subWindow.moveWindowTo({ x: config.x ?? 100, y: config.y ?? 100 });
      await subWindow.resize(config.width, config.height);
      await subWindow.setWindowBackgroundColor('#00000000');
      await subWindow.setWindowShadowEnabled(true);
      await subWindow.setWindowCornerRadius(16);
      await subWindow.setWindowTopmost(true);

      this.subWindows.set(config.name, subWindow);
      await subWindow.setUIContent(`pages/${config.name}`);
      await subWindow.showWindow();

      if (config.followMainWindow) {
        this.setupWindowFollow(subWindow, config);
      }

      this.syncSubWindowLightEffect(subWindow, config.name, config.themeColor);
      return subWindow;
    } catch (error) {
      console.error(`Failed to create tool window:`, (error as BusinessError).message);
      return null;
    }
  }

  private setupWindowFollow(subWindow: window.Window, config: ToolWindowConfig): void {
    this.mainWindow?.on('windowRectChange', (data: window.RectChangeOptions) => {
      if (data.rectChangeReason === window.RectChangeReason.MOVE) {
        const mainRect = this.mainWindow?.getWindowProperties().windowRect;
        if (mainRect) {
          subWindow.moveWindowTo({
            x: mainRect.left + (config.x ?? 100),
            y: mainRect.top + (config.y ?? 100)
          });
        }
      }
    });
  }

  private syncSubWindowLightEffect(subWindow: window.Window, name: string, themeColor?: string): void {
    subWindow.on('windowFocusChange', (isFocused: boolean) => {
      AppStorage.setOrCreate(`window_${name}_focused`, isFocused);
      if (isFocused && themeColor) {
        AppStorage.setOrCreate('global_theme_color', themeColor);
      }
    });

    AppStorage.watch('global_theme_color', (color: string) => {
      console.info(`Syncing theme color ${color} to window ${name}`);
    });
  }

  async syncGlobalLightEffect(color: string): Promise<void> {
    AppStorage.setOrCreate('global_theme_color', color);
  }

  async openLayoutWindow(): Promise<void> {
    await this.createToolWindow({
      name: 'LayoutWindow',
      title: '户型详情',
      width: 400,
      height: 600,
      x: 1000,
      y: 100,
      themeColor: '#4ECDC4'
    });
  }

  async openMaterialWindow(): Promise<void> {
    await this.createToolWindow({
      name: 'MaterialWindow',
      title: '材质库',
      width: 350,
      height: 500,
      x: 200,
      y: 100,
      themeColor: '#FFEAA7'
    });
  }

  async openSunlightWindow(): Promise<void> {
    await this.createToolWindow({
      name: 'SunlightWindow',
      title: '日照分析',
      width: 500,
      height: 400,
      x: 50,
      y: 500,
      themeColor: '#FF6B6B'
    });
  }

  async closeToolWindow(name: string): Promise<void> {
    const subWindow = this.subWindows.get(name);
    if (subWindow) {
      await subWindow.destroyWindow();
      this.subWindows.delete(name);
    }
  }
}

5.7 浮动户型详情窗口(LayoutWindow.ets)

// entry/src/main/ets/pages/LayoutWindow.ets
@Entry
@Component
struct LayoutWindow {
  @State layoutInfo: string = '户型:四室两厅三卫\n建筑面积:380㎡\n套内面积:320㎡\n得房率:84.2%\n\n功能分区:\n- 一层:客厅、餐厅、厨房、老人房\n- 二层:主卧套房、儿童房、书房\n- 三层:露台、健身房、储藏室';
  @State isFocused: boolean = false;
  @State themeColor: string = '#4ECDC4';

  aboutToAppear(): void {
    AppStorage.watch('window_LayoutWindow_focused', (focused: boolean) => {
      this.isFocused = focused;
    });
    AppStorage.watch('current_theme', (color: string) => {
      this.themeColor = color;
    });
  }

  build() {
    Stack() {
      Column() {
        Column()
          .width(400)
          .height(400)
          .backgroundColor(this.themeColor)
          .blur(150)
          .opacity(this.isFocused ? 0.1 : 0.05)
          .position({ x: '50%', y: '30%' })
          .anchor('50%')
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#0f0f1a')

      Column() {
        Row() {
          Text('户型详情')
            .fontSize(14)
            .fontColor(this.themeColor)
            .fontWeight(FontWeight.Bold)
          
          Row({ space: 8 }) {
            Circle().width(12).height(12).fill('#FF5F56')
            Circle().width(12).height(12).fill('#FFBD2E')
            Circle().width(12).height(12).fill('#27C93F')
          }
        }
        .width('100%')
        .height(36)
        .padding({ left: 16, right: 16 })
        .justifyContent(FlexAlign.SpaceBetween)
        .backgroundColor(`rgba(${this.hexToRgb(this.themeColor)},0.05)`)

        Scroll() {
          Text(this.layoutInfo)
            .fontSize(14)
            .fontColor('#E0E0E0')
            .lineHeight(22)
            .width('100%')
        }
        .width('100%')
        .layoutWeight(1)
        .padding(16)

        Row({ space: 12 }) {
          Button('导出CAD')
            .type(ButtonType.Capsule)
            .fontSize(13)
            .backgroundColor(this.themeColor)
            .width(120)

          Button('VR漫游')
            .type(ButtonType.Capsule)
            .fontSize(13)
            .backgroundColor('rgba(255,255,255,0.1)')
            .width(120)
        }
        .width('100%')
        .height(56)
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
  }

  private hexToRgb(hex: string): string {
    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? 
      `${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}` 
      : '78,205,196';
  }
}

5.8 主页面集成(ArchitecturePage.ets)

// entry/src/main/ets/pages/ArchitecturePage.ets
import { ImmersiveTitleBar, SceneTime } from '../components/ImmersiveTitleBar';
import { FloatTabNavigation } from '../components/FloatTabNavigation';
import { BuildingViewer } from '../components/BuildingViewer';
import { WindowManager } from '../utils/WindowManager';

@Entry
@Component
struct ArchitecturePage {
  @State currentProject: number = 0;
  @State currentScene: SceneTime = SceneTime.NOON;
  @State currentTheme: string = '#4ECDC4';
  @State currentFloor: string = '一层平面';
  @State useBodyAR: boolean = true;
  @State lightIntensity: number = 0.6;

  private sceneCycle: SceneTime[] = [SceneTime.DAWN, SceneTime.NOON, SceneTime.DUSK, SceneTime.NIGHT];

  aboutToAppear(): void {
    AppStorage.watch('current_project', (project: string) => {
      // 项目切换逻辑
    });

    AppStorage.watch('view_action', (action: string) => {
      if (action === 'cycle_time') {
        this.cycleSceneTime();
      } else if (action === 'switch_floor') {
        // 切换楼层逻辑
      }
    });

    AppStorage.watch('window_action', (action: string) => {
      if (action === 'open_layout') {
        WindowManager.getInstance().openLayoutWindow();
      } else if (action === 'open_material') {
        WindowManager.getInstance().openMaterialWindow();
      } else if (action === 'open_sunlight') {
        WindowManager.getInstance().openSunlightWindow();
      }
    });
  }

  private cycleSceneTime(): void {
    const currentIndex = this.sceneCycle.indexOf(this.currentScene);
    const nextIndex = (currentIndex + 1) % this.sceneCycle.length;
    this.currentScene = this.sceneCycle[nextIndex];
    this.currentTheme = this.getSceneColor(this.currentScene);
    WindowManager.getInstance().syncGlobalLightEffect(this.currentTheme);
  }

  private getSceneColor(scene: SceneTime): string {
    const colors: Map<SceneTime, string> = new Map([
      [SceneTime.DAWN, '#FFD93D'],
      [SceneTime.NOON, '#FFFFFF'],
      [SceneTime.DUSK, '#FF6B6B'],
      [SceneTime.NIGHT, '#4A69BD']
    ]);
    return colors.get(scene) || '#FFFFFF';
  }

  build() {
    Stack() {
      this.buildAmbientLightLayer()

      Column() {
        ImmersiveTitleBar({ 
          currentProject: this.getProjectName(this.currentProject),
          currentFloor: this.currentFloor,
          sceneTime: this.currentScene 
        })

        BuildingViewer({
          sceneTime: this.currentScene,
          themeColor: this.currentTheme
        })
          .layoutWeight(1)
      }
      .width('100%')
      .height('100%')

      FloatTabNavigation({
        currentIndex: this.currentProject,
        onTabChange: (index: number) => {
          this.currentProject = index;
          this.currentTheme = this.getProjectTheme(index);
          WindowManager.getInstance().syncGlobalLightEffect(this.currentTheme);
        },
        contentBuilder: () => {}
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0a0a0f')
    .expandSafeArea(
      [SafeAreaType.SYSTEM],
      [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM, SafeAreaEdge.START, SafeAreaEdge.END]
    )
  }

  private getProjectName(index: number): string {
    const names = ['云栖别墅', '湖畔公寓', '商业综合体', '文创园区'];
    return names[index] || '云栖别墅';
  }

  private getProjectTheme(index: number): string {
    const themes = ['#4ECDC4', '#FF6B6B', '#FFEAA7', '#96CEB4'];
    return themes[index] || '#4ECDC4';
  }

  @Builder
  buildAmbientLightLayer(): void {
    Column() {
      Column()
        .width(600)
        .height(600)
        .backgroundColor(this.currentTheme)
        .blur(180)
        .opacity(this.lightIntensity * 0.3)
        .position({ x: '50%', y: '25%' })
        .anchor('50%')
        .animation({
          duration: 7000,
          curve: Curve.EaseInOut,
          iterations: -1,
          playMode: PlayMode.Alternate
        })
        .scale({ x: 1.4, y: 1.4 })

      Column()
        .width('100%')
        .height(250)
        .backgroundColor(this.currentTheme)
        .opacity(this.lightIntensity * 0.08)
        .blur(120)
        .position({ x: 0, y: '75%' })
        .linearGradient({
          direction: GradientDirection.Top,
          colors: [
            [this.currentTheme, 0.0],
            ['transparent', 1.0]
          ]
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#050508')
  }
}

六、关键技术总结

6.1 沉浸光感实现清单

技术点 API/方法 应用场景
系统材质效果 systemMaterialEffect: SystemMaterialEffect.IMMERSIVE HdsNavigation标题栏
背景模糊 backgroundBlurStyle(BlurStyle.REGULAR) 悬浮导航玻璃拟态
背景滤镜 backdropFilter($r('sys.blur.20')) 精细模糊控制
安全区扩展 expandSafeArea([SafeAreaType.SYSTEM], [...]) 全屏沉浸布局
窗口沉浸 setWindowLayoutFullScreen(true) 无边框模式
光效动画 animation({ duration, iterations: -1 }) 呼吸灯背景
动态透明度 backgroundOpacity 焦点感知降级

6.2 Body AR实现要点

技术点 API/方法 说明
AR会话创建 arEngine.createARSession({ mode: ARMode.BODY }) 启用人体模式
人体跟踪器 session.createBodyTracker({ enableSkeleton: true }) 启用骨骼跟踪
骨骼关节获取 skeleton.getJoint(BodyJointType.LEFT_HAND) 获取手部位置
手势识别 分析关节位置与速度 识别张开/握拳/滑动等
3D模型操控 绑定手势到模型变换属性 实时同步

6.3 PC端多窗口光效协同

  • 主窗口:全屏沉浸,环境光背景延伸至所有安全区边缘
  • 浮动工具窗口:置顶、圆角、阴影,跟随主窗口移动
  • 光效同步:通过AppStorage全局状态实现跨窗口主题色联动
  • 焦点感知:窗口激活时边缘发光增强,失活时自动降低光效强度

七、调试与性能优化

7.1 真机调试建议

  1. Body AR效果:需要支持AR Engine的真机设备,确保摄像头视野覆盖全身
  2. 手势识别校准:不同用户体型差异可能需要调整识别阈值
  3. 3D渲染性能:建议使用XComponent配合原生3D引擎进行高性能渲染

7.2 性能优化策略

// 1. AR性能优化
private optimizeARPerformance(): void {
  if (this.arSession) {
    this.arSession.setCameraConfig({
      fps: 15,
      resolution: { width: 640, height: 480 }
    });
  }
}

// 2. 3D渲染优化
aboutToDisappear(): void {
  this.pauseRendering = true;
}

// 3. 窗口创建优化
private async lazyLoadToolWindows(): Promise<void> {
  if (!this.layoutWindow) {
    this.layoutWindow = await WindowManager.getInstance().openLayoutWindow();
  }
}

八、总结与展望

本文基于HarmonyOS 6(API 23)的悬浮导航沉浸光感Body AR特性,完整实战了一款面向PC端的"筑境空间"沉浸式建筑漫游与设计评审平台。核心创新点总结:

  1. 场景时间感知光效系统:根据建筑场景时间(清晨/正午/黄昏/夜晚)动态切换主题色,从天空渐变到环境光晕形成统一的空间氛围

  2. Body AR手势操控:基于AR Engine人体骨骼跟踪,实现张开手掌放大、握拳旋转、身体倾斜漫游等自然交互,让设计师"身临其境"地操控建筑模型

  3. 悬浮项目导航:底部悬浮页签替代传统项目栏,玻璃拟态设计+三档透明度调节,在保持导航可达性的同时最大化漫游区域

  4. PC级多窗口设计协作:主漫游窗口 + 浮动户型详情 + 材质库 + 日照分析的四层架构,通过WindowManager实现跨窗口光效联动与焦点感知

未来扩展方向

  • 接入分布式软总线,实现跨设备协同设计(手机扫码查看、平板手绘批注、PC主控漫游)
  • AI辅助设计:基于当前建筑模型,AI推荐优化方案并以光效形式提示改进区域
  • VR/AR融合:支持VR头显接入,实现完全沉浸式的建筑漫游体验
  • 数字孪生:接入IoT传感器数据,实时展示建筑能耗、温湿度等运营指标

转载自:https://blog.csdn.net/u014727709/article/details/137143865
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐