drone-camera 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -2
  3. package/index.d.ts +51 -0
  4. package/index.d.ts.map +1 -0
  5. package/index.js +20474 -0
  6. package/index.js.map +1 -0
  7. package/index.umd.cjs +1361 -0
  8. package/index.umd.cjs.map +1 -0
  9. package/lib/drone-capture-manifest.d.ts +169 -0
  10. package/lib/drone-capture-manifest.d.ts.map +1 -0
  11. package/lib/drone-capture-panel.d.ts +135 -0
  12. package/lib/drone-capture-panel.d.ts.map +1 -0
  13. package/lib/drone-capture.d.ts +380 -0
  14. package/lib/drone-capture.d.ts.map +1 -0
  15. package/lib/drone-cesium-mission.d.ts +120 -0
  16. package/lib/drone-cesium-mission.d.ts.map +1 -0
  17. package/lib/drone-cesium-rig.d.ts +50 -0
  18. package/lib/drone-cesium-rig.d.ts.map +1 -0
  19. package/lib/drone-cesium.d.ts +519 -0
  20. package/lib/drone-cesium.d.ts.map +1 -0
  21. package/lib/drone-control.d.ts +188 -0
  22. package/lib/drone-control.d.ts.map +1 -0
  23. package/lib/drone-engine.d.ts +70 -0
  24. package/lib/drone-engine.d.ts.map +1 -0
  25. package/lib/drone-exif.d.ts +64 -0
  26. package/lib/drone-exif.d.ts.map +1 -0
  27. package/lib/drone-fly.d.ts +545 -0
  28. package/lib/drone-fly.d.ts.map +1 -0
  29. package/lib/drone-gallery.d.ts +146 -0
  30. package/lib/drone-gallery.d.ts.map +1 -0
  31. package/lib/drone-husky-model.d.ts +22 -0
  32. package/lib/drone-husky-model.d.ts.map +1 -0
  33. package/lib/drone-image-utils.d.ts +67 -0
  34. package/lib/drone-image-utils.d.ts.map +1 -0
  35. package/lib/drone-image-viewer.d.ts +97 -0
  36. package/lib/drone-image-viewer.d.ts.map +1 -0
  37. package/lib/drone-lights.d.ts +194 -0
  38. package/lib/drone-lights.d.ts.map +1 -0
  39. package/lib/drone-minimap.d.ts +155 -0
  40. package/lib/drone-minimap.d.ts.map +1 -0
  41. package/lib/drone-panel.d.ts +265 -0
  42. package/lib/drone-panel.d.ts.map +1 -0
  43. package/lib/drone-panorama-viewer.d.ts +162 -0
  44. package/lib/drone-panorama-viewer.d.ts.map +1 -0
  45. package/lib/drone-panorama.d.ts +242 -0
  46. package/lib/drone-panorama.d.ts.map +1 -0
  47. package/lib/drone-photogrammetry.d.ts +207 -0
  48. package/lib/drone-photogrammetry.d.ts.map +1 -0
  49. package/lib/drone-radar.d.ts +136 -0
  50. package/lib/drone-radar.d.ts.map +1 -0
  51. package/lib/drone-reconstruction-preview.d.ts +84 -0
  52. package/lib/drone-reconstruction-preview.d.ts.map +1 -0
  53. package/lib/drone-rig.d.ts +385 -0
  54. package/lib/drone-rig.d.ts.map +1 -0
  55. package/lib/drone-sim.d.ts +883 -0
  56. package/lib/drone-sim.d.ts.map +1 -0
  57. package/lib/drone-stitch.d.ts +84 -0
  58. package/lib/drone-stitch.d.ts.map +1 -0
  59. package/lib/drone-world.d.ts +194 -0
  60. package/lib/drone-world.d.ts.map +1 -0
  61. package/lib/mission-editor.d.ts +116 -0
  62. package/lib/mission-editor.d.ts.map +1 -0
  63. package/lib/mission-panel.d.ts +173 -0
  64. package/lib/mission-panel.d.ts.map +1 -0
  65. package/package.json +61 -4
package/index.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["clamp","degToRad","normalizeHeading","DEFAULT_GIMBAL_RESPONSE","clamp","BATTERY_COLOR","AUTO_LIGHT_MAX_AGL_METERS","blinkPhase","DEG","DRAG_THRESHOLD_PX","SNAP_STEP","SNAP_STEP_FINE","clamp","snap","capturePointer","releasePointer","PHOTO_TIMEOUT_MS","RECORD_FPS","RECORD_BITRATE","RECORD_TIMESLICE_MS","RECORD_STOP_TIMEOUT_MS","clamp","ARM_POSITIONS","clamp","DEG","RAD","finite","STYLE_ID","PANEL_CSS","ensureStyles","escapeHtml","triggerDownload","stamp","STYLE_ID","clamp","escapeHtml","ensureStyles","PANEL_CSS","STYLE_ID","DEG","CSS","ensureStyles","STYLE_ID","DEFAULT_Z_INDEX","VIEWER_CSS","ensureStyles","DEG","STYLE_ID","DEFAULT_Z_INDEX","ensureStyles","STYLE_ID","ensureStyles"],"sources":["../src/lib/drone-camera/drone-sim.ts","../src/lib/drone-camera/drone-world.ts","../src/lib/drone-camera/drone-rig.ts","../src/lib/drone-camera/drone-lights.ts","../src/lib/drone-camera/drone-radar.ts","../src/lib/drone-camera/mission-editor.ts","../src/lib/drone-camera/drone-husky-model.ts","../src/lib/drone-camera/drone-fly.ts","../src/lib/drone-camera/drone-cesium-rig.ts","../src/lib/drone-camera/drone-cesium-mission.ts","../src/lib/drone-camera/drone-photogrammetry.ts","../src/lib/drone-camera/drone-cesium.ts","../src/lib/drone-camera/drone-engine.ts","../src/lib/drone-camera/drone-panorama.ts","../src/lib/drone-camera/drone-panel.ts","../src/lib/drone-camera/mission-panel.ts","../src/lib/drone-camera/drone-minimap.ts","../src/lib/drone-camera/drone-exif.ts","../src/lib/drone-camera/drone-capture-manifest.ts","../src/lib/drone-camera/drone-capture.ts","../src/lib/drone-camera/drone-image-utils.ts","../src/lib/drone-camera/drone-image-viewer.ts","../src/lib/drone-camera/drone-stitch.ts","../src/lib/drone-camera/drone-panorama-viewer.ts","../src/lib/drone-camera/drone-gallery.ts","../src/lib/drone-camera/drone-capture-panel.ts","../src/lib/drone-camera/drone-reconstruction-preview.ts"],"sourcesContent":["/**\n * DJI Mini 4 Pro 飞行仿真内核。\n *\n * 这里只做\"飞控 + 电池 + 定位\"的确定性逻辑,不引用 three,方便单独调参与验证:\n * - 状态机对齐真机流程:上电自检 → 传感器预热 → 地面待机(搜星) → 电机启动 → 自动起飞 →\n * 手动飞行 → 智能返航 / 自动降落 → 停桨;另含遥控失联失效保护、低电量返航。\n * ⚠️ 紧急停桨只在飞行器**落地后**可用(空中停桨会坠机,按需求禁用;空中调用会被拒绝并写警告日志);\n * 空中\"动力丧失\"状态仅由电池耗尽等真实故障触发。\n * - 摇杆在 GPS / 视觉定位下是\"速度指令\":松杆由飞控刹停并定点悬停;姿态模式下不保持位置,随风漂移。\n * - 电池按 18.96 Wh 真实容量与分项功耗积分(悬停 33 W → 约 34 分钟,与官方标称一致),\n * 低电量返航 / 严重低电量强制降落按百分比触发。\n * - 风按\"空气速度 = 指令 − 风\"建模:逆风顶得住就顶,顶不住(超过该档最大空速)才被吹跑。\n *\n * 坐标约定:+X 东、-Z 北、+Y 上(与模型一致,机头朝 -Z),航向 0° = 正北。\n */\n\n/** 机型公开规格(Mini 4 Pro) */\nexport const DRONE_SPEC = {\n name: 'DJI Mini 4 Pro',\n weightKg: 0.249,\n batteryWh: 18.96,\n batteryNominalVoltage: 7.32,\n batteryFullVoltage: 8.4,\n batteryEmptyVoltage: 6.9,\n batteryInternalResistance: 0.09,\n maxWindResistance: 10.7,\n hoverPowerWatts: 33,\n idlePowerWatts: 1.6,\n climbWattsPerMps: 3.6,\n levelSpeedPowerGain: 0.55,\n maxFlightTimeMin: 34,\n takeoffAltitude: 1.2,\n rthAltitudeDefault: 20,\n landingSlowAltitude: 2,\n gimbalPitchMin: -90,\n gimbalPitchMax: 60,\n gimbalYawRange: 5,\n} as const\n\n/** 飞行挡位:平稳 C / 普通 N / 运动 S */\nexport type FlightMode = 'cine' | 'normal' | 'sport'\n\n/** 单个挡位的性能参数:限速(水平/升降)、姿态倾角与加减速、偏航角速度 */\nexport interface ModeSpec {\n label: string\n horizontalSpeed: number\n climbSpeed: number\n descendSpeed: number\n maxTiltDeg: number\n accel: number\n brakeAccel: number\n yawRateDeg: number\n}\n\n/** N / S / C 三档飞行模式(对齐 DJI 飞行挡位参数) */\nexport const FLIGHT_MODES: Record<FlightMode, ModeSpec> = {\n cine: {\n label: '平稳挡 C',\n horizontalSpeed: 5,\n climbSpeed: 3,\n descendSpeed: 1.5,\n maxTiltDeg: 20,\n accel: 2,\n brakeAccel: 3,\n yawRateDeg: 60,\n },\n normal: {\n label: '普通挡 N',\n horizontalSpeed: 12,\n climbSpeed: 5,\n descendSpeed: 3,\n maxTiltDeg: 30,\n accel: 3.5,\n brakeAccel: 6,\n yawRateDeg: 100,\n },\n sport: {\n label: '运动挡 S',\n horizontalSpeed: 16,\n climbSpeed: 5,\n descendSpeed: 5,\n maxTiltDeg: 35,\n accel: 5,\n brakeAccel: 8,\n yawRateDeg: 150,\n },\n}\n\n/** 挡位列表(供下拉框渲染的稳定顺序) */\nexport const FLIGHT_MODE_LIST: Array<{ key: FlightMode; label: string }> = (\n Object.keys(FLIGHT_MODES) as FlightMode[]\n).map((key) => ({ key, label: FLIGHT_MODES[key].label }))\n\n/** 飞行速度倍率的取值区间(与面板滑杆一致):数值**本身**就是倍数,0 = 冻结、1 = 真机原速 */\nexport const SPEED_SCALE_MIN = 0\nexport const SPEED_SCALE_MAX = 1000\n/** 速度倍率缺省值:1 = 真机原速 */\nexport const DEFAULT_SPEED_SCALE = 1\n\n/**\n * 机身**原地自转**的默认角速度(度/秒)。\n *\n * 这是给\"机体自己转\"用的独立通道(见 `DroneSim.spinTo`):全景扫描的水平覆盖\n * 必须靠它,靠不了云台 —— Mini 4 Pro 云台 Pan 机械行程只有 ±30°\n * (`DRONE_SPEC.gimbalYawRange`),转不出 180°。90°/s 对齐真机全景的转速手感。\n */\nexport const BODY_SPIN_RATE_DEG = 90\n\n/** 判定\"机身自转已对准目标航向\"的容差(度):1 帧的转角远大于它,但航向是连续量 */\nexport const BODY_SPIN_TOLERANCE_DEG = 0.5\n\n/** 飞行阶段:上电自检 → 预热 → 待机 → 起飞 → 飞行/航线 → 返航/降落 → 停桨/失效 */\nexport type FlightPhase =\n | 'powerOff'\n | 'selfCheck'\n | 'warmingUp'\n | 'standby'\n | 'motorsOn'\n | 'takingOff'\n | 'flying'\n | 'waypoint'\n | 'rth'\n | 'landing'\n | 'emergency'\n | 'stopped'\n\n/** 阶段的中文标签(界面显示用) */\nexport const PHASE_LABELS: Record<FlightPhase, string> = {\n powerOff: '未上电',\n selfCheck: '开机自检',\n warmingUp: '传感器预热',\n standby: '地面待机',\n motorsOn: '电机已启动',\n takingOff: '自动起飞',\n flying: '飞行中',\n waypoint: '航线执行',\n rth: '智能返航',\n landing: '自动降落',\n emergency: '动力丧失 · 坠落',\n stopped: '已停桨',\n}\n\n/** 定位来源:GNSS / 视觉 / 姿态模式(无定位) */\nexport type PositionSource = 'gps' | 'vision' | 'atti'\n\n/** 定位来源的中文标签 */\nexport const POSITION_SOURCE_LABELS: Record<PositionSource, string> = {\n gps: 'GNSS 卫星定位',\n vision: '视觉定位',\n atti: '姿态模式(无定位)',\n}\n\n/** 四通道摇杆量,每通道取值 -1~1(松杆为 0) */\nexport interface StickState {\n /** 左杆上下:升降 -1~1(正 = 上升) */\n throttle: number\n /** 左杆左右:偏航 -1~1(正 = 右转) */\n yaw: number\n /** 右杆上下:俯仰 -1~1(正 = 前进) */\n pitch: number\n /** 右杆左右:横滚 -1~1(正 = 右移) */\n roll: number\n}\n\n/** 摇杆回中(全零) */\nexport const NEUTRAL_STICK: StickState = { throttle: 0, yaw: 0, pitch: 0, roll: 0 }\n\n/** 四个电机对应的机臂位置(与 DroneRig 的 ArmPosition 一致,模型节点名同款后缀) */\nexport type MotorPosition = 'FrontLeft' | 'FrontRight' | 'RearLeft' | 'RearRight'\n\n/** 四机臂的固定遍历顺序 */\nexport const MOTOR_POSITIONS: MotorPosition[] = ['FrontLeft', 'FrontRight', 'RearLeft', 'RearRight']\n\n/** 故障注入:测试沙盒里用来复现各种真机异常 */\nexport interface FaultFlags {\n /** GNSS 卫星丢失(强制进入视觉/姿态模式) */\n gnssLost: boolean\n /** 指南针受扰(需要校准,否则姿态模式) */\n compassError: boolean\n /** IMU 异常(禁止起飞) */\n imuError: boolean\n /** 电机异常:1 个电机动力衰减,导致偏航漂移 */\n motorFailure: boolean\n /** 遥控器失联(3 秒后触发失效返航) */\n rcLost: boolean\n /** 下视视觉失效(低空无法视觉定位) */\n visionLost: boolean\n /** 避障传感器关闭 */\n obstacleAvoidanceOff: boolean\n}\n\n/** 全部故障关闭(正常飞行) */\nexport const NO_FAULTS: FaultFlags = {\n gnssLost: false,\n compassError: false,\n imuError: false,\n motorFailure: false,\n rcLost: false,\n visionLost: false,\n obstacleAvoidanceOff: false,\n}\n\n/** 沙盒可调参数(限高限距、风、电量阈值、失效动作、时间倍速) */\nexport interface SimConfig {\n /** 限高(AGL,米) */\n maxAltitude: number\n /** 限距(米) */\n maxDistance: number\n /**\n * 取消高度与距离限制(演示 / 取景用),默认 `false`。\n *\n * `true` 时 `maxAltitude` / `maxDistance` 不再参与任何夹紧:\n * 摇杆升降与水平位移不被截断、航点不被夹回限高面/限距圆、航线校验不再拦超限航点,\n * 面板通过 `getLimits()` 拿到的高度/距离上限变成 `Infinity`。\n *\n * 改回 `false` 时若机体已在限制之外,下一帧就会被夹回边界(高度落到 `maxAltitude`,\n * 位置沿返航点方向缩到 `maxDistance` 圆上),所以这不是\"一次性开关\"。\n *\n * ⚠️ 只放开**限高 / 限距**这两条人为上限:电池、姿态、避障、GNSS 等物理与失效约束照旧生效。\n */\n unlimitedFlight: boolean\n /** 返航高度(米) */\n rthAltitude: number\n /** 风速 m/s */\n windSpeed: number\n /** 风\"吹向\"的罗盘方位角(0=北,90=东) */\n windDirection: number\n /** 低电量返航阈值 % */\n lowBatteryPercent: number\n /** 严重低电量阈值 % */\n criticalBatteryPercent: number\n /** 失效保护:遥控失联后的动作 */\n rcFailsafe: 'rth' | 'hover' | 'land'\n /** 时间倍速(1 = 实时) */\n timeScale: number\n /**\n * 飞行速度倍率(0 ~ 1000,数值本身就是倍数;1 = 真机原速)。\n *\n * 与 `timeScale` 不是一回事:时间倍速是\"整个仿真跑快\"(电池、自检、悬停计时一起加速),\n * 而速度倍率**只改机体自己的运动能力**,相当于换了一台\"几倍速\"的机器 ——\n * 手动摇杆速度、航线巡航速度、智能返航速度、起降速率与转向角速度整体乘这个系数,\n * 挡位表、加速度配平、倾角标定都跟着等比缩放(满舵仍是该挡位的最大倾角)。\n * 0 = 全部速度为 0(机体不动,连自动起飞/降落也会停住)。\n */\n speedScale: number\n /** 上电自检时长(秒):默认 0.3,整个上电流程控制在 1 秒内 */\n selfCheckSeconds: number\n /** 传感器预热时长(秒):默认 0.28 */\n warmupSeconds: number\n /** 搜星速率(颗/秒):默认 30,上电后约 0.6 秒即达满星 */\n gnssAcquireRate: number\n}\n\n/** 出厂默认参数:限高 120 m(合规上限)、限距 500 m、限高限距生效、无风、实时 */\nexport const DEFAULT_CONFIG: SimConfig = {\n maxAltitude: 120,\n maxDistance: 500,\n unlimitedFlight: false,\n rthAltitude: 20,\n windSpeed: 0,\n windDirection: 0,\n lowBatteryPercent: 20,\n criticalBatteryPercent: 10,\n rcFailsafe: 'rth',\n timeScale: 1,\n speedScale: DEFAULT_SPEED_SCALE,\n // 上电 → 可起飞总时长(自检 + 预热)≈ 0.58 s:演示/沙盒不需要等,想恢复\n // 真机的\"慢慢开机\"手感把两个值调回 2.6 / 2.4 即可\n selfCheckSeconds: 0.3,\n warmupSeconds: 0.28,\n gnssAcquireRate: 30,\n}\n\n// ————————————————————————————— 航线任务 —————————————————————————————\n\n/**\n * 单个航点(对齐真机航点任务的字段子集)。\n *\n * 坐标与遥测同一套约定:x 向东、z 向南(与模型坐标一致),altitude 是**相对起飞点高度(AGL)**。\n */\nexport interface MissionWaypoint {\n x: number\n z: number\n /** 目标高度(AGL,米) */\n altitude: number\n /** 过点速度(m/s);0 = 使用任务巡航速度 */\n speed: number\n /** 到点悬停时间(秒);0 = 不停留,直接飞下一个点 */\n hoverSeconds: number\n /** 到点后云台俯仰角(度);null = 不动云台 */\n gimbalPitch: number | null\n /** 到点动作 */\n action: 'none' | 'photo'\n}\n\n/** 机头朝向策略:auto = 始终机头指向下一航点;fixed = 保持任务开始时的航向 */\nexport type MissionHeadingMode = 'auto' | 'fixed'\n/** 过点方式:straight = 到点减速后再走;curved = 圆滑过点(不减速) */\nexport type MissionPathMode = 'straight' | 'curved'\n/** 任务结束动作,对齐真机 WaypointMissionFinishedAction */\nexport type MissionFinishAction = 'hover' | 'rth' | 'land'\n\n/** 航线任务的全局设置 */\nexport interface MissionConfig {\n /** 巡航速度(m/s):航点未单独指定速度时用它 */\n autoSpeed: number\n headingMode: MissionHeadingMode\n pathMode: MissionPathMode\n /** 全部航点完成后 */\n finishAction: MissionFinishAction\n /** 循环执行:完成后回到第一个航点继续 */\n loop: boolean\n}\n\n/** 任务状态:未执行 / 执行中 / 已暂停 */\nexport type MissionStatus = 'idle' | 'running' | 'paused'\n\n/** 任务状态的中文标签 */\nexport const MISSION_STATUS_LABELS: Record<MissionStatus, string> = {\n idle: '未执行',\n running: '执行中',\n paused: '已暂停',\n}\n\n/** 任务阶段的中文标签 */\nexport const MISSION_STAGE_LABELS: Record<MissionStage, string> = {\n idle: '待执行',\n 'depart-climb': '垂直调整到首航点高度',\n 'depart-cruise': '水平飞向首个航点',\n 'depart-settle': '调整到航点高度',\n 'depart-align': '对准航线方向',\n goto: '飞向航点',\n hover: '航点悬停',\n}\n\n/**\n * 任务执行阶段。\n *\n * 飞第一个航点前有一段「启航四步」,和智能返航同款的分段机动:\n * 先把高度调到首航点高度(**垂直段只动高度,不产生水平位移**,否则会斜着冲过去) →\n * 再保持该高度水平飞向首航点 → 到了把高度收干净 → 最后原地把机头转到与航线一致,\n * 之后才进入常规的 goto / hover 推进。\n */\nexport type MissionStage =\n 'idle' | 'depart-climb' | 'depart-cruise' | 'depart-settle' | 'depart-align' | 'goto' | 'hover'\n\n/** 是否为「启航段」阶段(首航点的分段机动) */\nexport function isMissionDepartStage(stage: MissionStage): boolean {\n return stage.startsWith('depart-')\n}\n\n/** 出厂默认任务设置:巡航 6 m/s、机头自动指向下一航点、直线过点、完成后悬停 */\nexport const DEFAULT_MISSION_CONFIG: MissionConfig = {\n autoSpeed: 6,\n headingMode: 'auto',\n pathMode: 'straight',\n finishAction: 'hover',\n loop: false,\n}\n\n/** 出厂示例航线:环绕起飞点一圈(最高 40 m,悬在 13 m 的建筑之上,不会触发避障刹停) */\nexport const DEFAULT_MISSION: MissionWaypoint[] = [\n { x: 0, z: -32, altitude: 35, speed: 6, hoverSeconds: 2, gimbalPitch: -40, action: 'photo' },\n { x: 32, z: -32, altitude: 40, speed: 6, hoverSeconds: 0, gimbalPitch: null, action: 'none' },\n { x: 32, z: 18, altitude: 40, speed: 6, hoverSeconds: 0, gimbalPitch: null, action: 'none' },\n { x: 0, z: 18, altitude: 30, speed: 6, hoverSeconds: 3, gimbalPitch: -60, action: 'photo' },\n { x: -28, z: 0, altitude: 35, speed: 6, hoverSeconds: 0, gimbalPitch: null, action: 'none' },\n]\n\n/**\n * 航点高度的**硬上限**(米)。\n *\n * 与 `SimConfig.maxAltitude`(可配置的限高)不是一回事:它是字段归一化的护栏,\n * 挡住外部灌进来的离谱值(比如 1e9),默认 500 m 就已高于合规限高。\n * `unlimitedFlight` 打开时内部会传 `Infinity` 把它一起放开。\n */\nexport const MISSION_ALTITUDE_HARD_LIMIT = 500\n\n/**\n * 单个航点字段归一化(手输/外部传入都可能越界)。\n *\n * @param maxAltitude 高度上限,默认 {@link MISSION_ALTITUDE_HARD_LIMIT};取消限高时传 `Infinity`\n */\nexport function normalizeMissionWaypoint(\n source: Partial<MissionWaypoint>,\n maxAltitude: number = MISSION_ALTITUDE_HARD_LIMIT,\n): MissionWaypoint {\n const number = (value: unknown, fallback: number): number =>\n typeof value === 'number' && Number.isFinite(value) ? value : fallback\n const pitch = source.gimbalPitch\n return {\n x: number(source.x, 0),\n z: number(source.z, 0),\n altitude: clamp(number(source.altitude, 30), 1, maxAltitude),\n speed: clamp(number(source.speed, 0), 0, 16),\n hoverSeconds: clamp(number(source.hoverSeconds, 0), 0, 60),\n gimbalPitch:\n typeof pitch === 'number' && Number.isFinite(pitch)\n ? clamp(pitch, DRONE_SPEC.gimbalPitchMin, DRONE_SPEC.gimbalPitchMax)\n : null,\n action: source.action === 'photo' ? 'photo' : 'none',\n }\n}\n\n// ——————————————————————————— 航线导出(序列化) ———————————————————————————\n//\n// 这一段是**纯逻辑、零依赖**(不引 three、不碰 DOM):三套消费方共用 ——\n// · three 后端 DroneFly.getMissionExport / exportMission(原实现在 drone-fly,后迁到此处)\n// · Cesium 后端 CesiumDroneAdapter 的同名方法\n// · 无头 MCP / CLI\n// 放 drone-sim 而不是 drone-fly,是为了让 Cesium 路径复用序列化时\n// **不会把 three 拖进它的运行时依赖**(drone-fly 是 three 装配层)。\n\n/** 航线导出格式 */\nexport type MissionExportFormat = 'json' | 'csv'\n\n/**\n * 航线导出数据(JSON 形态)。\n *\n * 坐标系沿用引擎内部的**局部 ENU**:原点为返航点(起飞点),x 向东、z 向南(负值即向北),\n * 单位米;高度为相对起飞点的 AGL。与面板列表里的 `E x / S z` 读数完全一致 ——\n * 想落到真实经纬度,由宿主拿到 home 的真实经纬度后自行平移。\n */\nexport interface MissionExport {\n /** 文件标识,便于导入方识别 */\n format: 'drone-camera/mission'\n version: number\n /** 导出时刻(ISO 8601) */\n exportedAt: string\n /** 坐标约定说明(写进文件,换个工具打开也不至于误读) */\n frame: { origin: string; unit: string; x: string; z: string; altitude: string }\n /** 返航点(局部坐标原点) */\n home: { x: number; z: number }\n /** 任务参数 */\n config: MissionConfig\n /** 概览统计:点数 / 航程 / 预计耗时 / 高度范围 */\n summary: {\n points: number\n distance: number\n duration: number\n altitudeMin: number\n altitudeMax: number\n }\n waypoints: MissionWaypoint[]\n}\n\n/**\n * 由「航点 + 任务参数 + 返航点」组出导出数据(纯函数,两套渲染后端共用)。\n *\n * 航程按\"返航点起飞 → 依次过点\"的三维折线累计,耗时按每点自己的速度(0 = 巡航速度)\n * 加上到点悬停折算,和引擎执行时的口径一致。\n */\nexport function buildMissionExport(\n waypoints: MissionWaypoint[],\n config: MissionConfig,\n home: { x: number; z: number },\n): MissionExport {\n const cruise = Math.max(0.5, config.autoSpeed)\n let distance = 0\n let duration = 0\n let previousX = home.x\n let previousZ = home.z\n let previousAltitude = 0\n let altitudeMin = Number.POSITIVE_INFINITY\n let altitudeMax = waypoints.length ? 0 : 0\n for (const waypoint of waypoints) {\n const segment = Math.hypot(\n waypoint.x - previousX,\n waypoint.z - previousZ,\n waypoint.altitude - previousAltitude,\n )\n distance += segment\n duration +=\n segment / Math.max(0.5, waypoint.speed > 0 ? waypoint.speed : cruise) +\n waypoint.hoverSeconds\n altitudeMin = Math.min(altitudeMin, waypoint.altitude)\n altitudeMax = Math.max(altitudeMax, waypoint.altitude)\n previousX = waypoint.x\n previousZ = waypoint.z\n previousAltitude = waypoint.altitude\n }\n return {\n format: 'drone-camera/mission',\n version: 1,\n exportedAt: new Date().toISOString(),\n frame: {\n origin: '返航点(起飞点)',\n unit: 'm',\n x: '东向偏移(向东为正)',\n z: '南向偏移(向南为正,负值表示偏北)',\n altitude: '相对起飞点的高度(AGL)',\n },\n home: { x: home.x, z: home.z },\n config,\n summary: {\n points: waypoints.length,\n distance: Number(distance.toFixed(2)),\n duration: Number(duration.toFixed(1)),\n altitudeMin: waypoints.length ? altitudeMin : 0,\n altitudeMax,\n },\n waypoints,\n }\n}\n\n/** 航点表 → CSV(不含 BOM;落盘时由调用方按需添加) */\nexport function missionToCsv(waypoints: MissionWaypoint[]): string {\n const escape = (value: string | number): string => {\n const text = String(value)\n return /[\",\\r\\n]/.test(text) ? `\"${text.replace(/\"/g, '\"\"')}\"` : text\n }\n const rows: Array<Array<string | number>> = [\n [\n '序号',\n 'X(东向,m)',\n 'Z(南向,m)',\n '高度(m)',\n '速度(m/s,空=巡航)',\n '悬停(s)',\n '云台俯仰(度)',\n '动作',\n ],\n ...waypoints.map((waypoint, index) => [\n index + 1,\n waypoint.x,\n waypoint.z,\n waypoint.altitude,\n waypoint.speed > 0 ? waypoint.speed : '',\n waypoint.hoverSeconds,\n waypoint.gimbalPitch ?? '',\n waypoint.action === 'photo' ? '拍照' : '无动作',\n ]),\n ]\n // CRLF:Excel / WPS 对它的兼容性最好\n return `${rows.map((row) => row.map(escape).join(',')).join('\\r\\n')}\\r\\n`\n}\n\n/**\n * 导出航线文本。`json` 是完整存档(含坐标约定、任务参数、概览统计,可再喂回 `setMission`),\n * `csv` 只出航点表 —— 表头中文、CRLF 换行,Excel / WPS 双击即可打开。\n */\nexport function exportMissionText(\n data: MissionExport,\n format: MissionExportFormat = 'json',\n): string {\n if (format === 'csv') return missionToCsv(data.waypoints)\n return `${JSON.stringify(data, null, 2)}\\n`\n}\n\n/**\n * 新航点插到哪个位置:取\"绕路最少\"的那一格(three / Cesium 两个场景内编辑器共用)。\n *\n * 判据是插入后航线增加的长度(prev→新点→next 减去原来的 prev→next),\n * 而不是\"离哪一段最近\" —— 后者在等距处会出现平手,可能把一个明明贴着前面航段的点\n * 插到后面去,航线就绕了。最小绕路法天然没有这个歧义。\n */\nexport function findMissionInsertIndex(\n list: MissionWaypoint[],\n x: number,\n z: number,\n homeX: number,\n homeZ: number,\n): number {\n const distance = (fromX: number, fromZ: number, toX: number, toZ: number): number =>\n Math.hypot(toX - fromX, toZ - fromZ)\n let bestIndex = 0\n let bestDetour = Number.POSITIVE_INFINITY\n for (let index = 0; index <= list.length; index += 1) {\n const previous = index === 0 ? null : list[index - 1]\n const next = list[index] ?? null\n const fromX = previous ? previous.x : homeX\n const fromZ = previous ? previous.z : homeZ\n const detour = next\n ? distance(fromX, fromZ, x, z) +\n distance(x, z, next.x, next.z) -\n distance(fromX, fromZ, next.x, next.z)\n : distance(fromX, fromZ, x, z) // 接到队尾:只看从上一个航点飞过去的距离\n // 减去 1e-6 容差:浮点上打成平手时保留更靠前的插入位,避免结果抖动\n if (detour < bestDetour - 1e-6) {\n bestDetour = detour\n bestIndex = index\n }\n }\n return bestIndex\n}\n\n/** 航线任务的实时进度(界面轮询用) */\nexport interface MissionSnapshot {\n status: MissionStatus\n statusLabel: string\n stage: MissionStage\n stageLabel: string\n /** 当前目标航点索引(0 基);-1 = 无 */\n index: number\n total: number\n /** 已完成的整圈数 */\n passes: number\n /** 任务已执行时长(秒) */\n elapsed: number\n /** 剩余航程(米,含高度差) */\n distanceLeft: number\n /** 按航程折算的完成度 0~1 */\n progress: number\n /** 预计剩余时间(秒,含悬停) */\n etaSeconds: number\n /** 暂停原因 */\n pauseReason: string\n /** 当前目标航点副本 */\n active: MissionWaypoint | null\n /** 航线航点列表副本(界面据此渲染) */\n waypoints: MissionWaypoint[]\n config: MissionConfig\n}\n\n/** 事件日志级别 */\nexport type EventLevel = 'info' | 'warn' | 'error' | 'success'\n\n/** 一条事件日志 */\nexport interface SimEvent {\n id: number\n time: number\n level: EventLevel\n text: string\n}\n\n/** 轴对齐包围盒(AABB)障碍物,坐标与飞行位置同一套约定 */\nexport interface ObstacleBox {\n name: string\n minX: number\n maxX: number\n minY: number\n maxY: number\n minZ: number\n maxZ: number\n /** 是否需要绕开(否则只是地面建筑,飞过去即可) */\n solid: boolean\n}\n\n/** 当前帧的避障探测结果(机身坐标系六向) */\nexport interface ObstacleReport {\n /** 六个方向的最近距离(米),null 表示该方向无障碍 */\n forward: number | null\n backward: number | null\n left: number | null\n right: number | null\n up: number | null\n down: number | null\n /** 本帧是否因避障刹停 */\n braking: boolean\n /** 触发方向 */\n brakingDirection: 'forward' | 'backward' | 'left' | 'right' | 'up' | 'down' | null\n}\n\n/** 起飞前检查项 */\nexport interface ChecklistItem {\n id: string\n label: string\n ok: boolean\n detail: string\n /** 不通过是否阻止起飞 */\n blocking: boolean\n}\n\n/** 单向快照:界面/回放读取的全部遥测字段(数组字段已复制,改它不影响仿真) */\nexport interface DroneSnapshot {\n phase: FlightPhase\n phaseLabel: string\n mode: FlightMode\n modeLabel: string\n /** 当前生效的飞行速度倍率(0 ~ 1000,1 = 真机原速;见 SimConfig.speedScale) */\n speedScale: number\n positionSource: PositionSource\n positionSourceLabel: string\n airborne: boolean\n motorsOn: boolean\n damaged: boolean\n batteryPercent: number\n batteryVoltage: number\n batteryCurrent: number\n batteryTemp: number\n batteryWhLeft: number\n batteryModeText: string\n altitude: number\n altitudeMax: number\n /** 相对起飞点的水平坐标:x 向东,z 向南(与模型坐标一致) */\n positionX: number\n positionZ: number\n horizontalSpeed: number\n verticalSpeed: number\n heading: number\n tiltPitch: number\n tiltRoll: number\n distanceToHome: number\n homeRecorded: boolean\n altitudeLimitReached: boolean\n distanceLimitReached: boolean\n satellites: number\n hdop: number\n gpsBars: number\n rcBars: number\n visionAvailable: boolean\n gimbalPitch: number\n gimbalRoll: number\n gimbalYaw: number\n motorLoad: number\n /** 四电机归一化转速(悬停 ≈ 1):混控差动的读数口,面板/调试可视化用 */\n motorSpeeds: Record<MotorPosition, number>\n armFold: number\n stick: StickState\n windSpeed: number\n windDirection: number\n windRelative: string\n flightTime: number\n totalTime: number\n remainingMinutes: number\n lowBattery: boolean\n criticalBattery: boolean\n lowBatteryCountdown: number\n rthStage: string\n /** 自动降落的细分阶段:approach=降向低位悬停位 / hold=低位悬停确认 / settle=缓慢触地 */\n landingStage: string\n rthReason: string\n warnings: string[]\n events: SimEvent[]\n checklist: ChecklistItem[]\n obstacle: ObstacleReport\n recording: boolean\n recordSeconds: number\n photoCount: number\n cameraZoom: number\n /** 航线任务进度 */\n mission: MissionSnapshot\n}\n\nconst GRAVITY = 9.81\nconst RC_FAILSAFE_DELAY = 3\nconst LOW_BATTERY_COUNTDOWN = 10\n/** 下视视觉可用的最高离地高度(米):再高就看不清地面纹理 */\nconst VISION_MAX_ALTITUDE = 12\n/** 智能返航:机头与返航航向的夹角小于这个值才允许前进(度) */\nconst RTH_ALIGN_TOLERANCE_DEG = 12\n/** 智能返航的转向角速度(度/秒):原地对准与巡航段共用 */\nconst RTH_YAW_RATE_DEG = 45\n/** 自动降落的低位悬停高度(米):先降到这个高度稳住,再缓慢触地 */\nconst LANDING_HOLD_ALTITUDE_M = 1\n/** 低位悬停的停顿时间(秒):停一下让飞手看清落点 */\nconst LANDING_HOLD_SECONDS = 1\n/** 悬停确认后的触地速度(米/秒):比常规下降慢得多 */\nconst LANDING_FINAL_DESCENT_RATE = 0.3\n/** 航线任务:判定\"已到达航点\"的水平半径(米) */\nconst MISSION_ARRIVE_RADIUS = 0.9\n/** 航线任务:到达航点时允许的高度误差(米) */\nconst MISSION_ARRIVE_ALTITUDE_TOLERANCE = 0.8\n/** 航线任务:曲线过点的切角半径(米),距航点这么远就切向下一个点 */\nconst MISSION_CURVED_TURN_RADIUS = 3\n/** 航线任务:机头转向下一航点的角速度(度/秒) */\nconst MISSION_YAW_RATE_DEG = 60\n/** 航线任务:单个航点的最长飞行时限(秒),超时视为不可达并跳过 */\nconst MISSION_WAYPOINT_TIMEOUT = 90\n/** 启航段:水平飞向首航点的到位半径(米)。比常规航点小得多 —— 启航要落到点上,不然对准航线时会有一次肉眼可见的位移 */\nconst MISSION_DEPART_ARRIVE_RADIUS = 0.25\n/** 启航段:垂直调整到首航点高度的到位容差(米) */\nconst MISSION_DEPART_ALTITUDE_TOLERANCE = 0.3\n/** 启航段:到达首航点后高度收尾的到位容差(米) */\nconst MISSION_DEPART_SETTLE_TOLERANCE = 0.15\n/** 启航段:机头对准航线方向的到位容差(度) */\nconst MISSION_DEPART_ALIGN_TOLERANCE = 2.5\n\nconst clamp = (value: number, min: number, max: number): number =>\n Math.min(max, Math.max(min, value))\nconst lerp = (from: number, to: number, t: number): number => from + (to - from) * t\nconst degToRad = (deg: number): number => (deg * Math.PI) / 180\nconst radToDeg = (rad: number): number => (rad * 180) / Math.PI\n\n/** 方位角归一到 [0, 360):罗盘角没有负数 */\nfunction normalizeHeading(deg: number): number {\n return ((deg % 360) + 360) % 360\n}\n\n/** 速度倍率的显示文案:整数直接写,小数最多两位(0.5 → \"0.5×\") */\nfunction formatSpeedScale(scale: number): string {\n const text = Number.isInteger(scale) ? String(scale) : scale.toFixed(2).replace(/\\.?0+$/, '')\n return `${text}×`\n}\n\n/** 航向角(罗盘 0=北 90=东)对应的机头方向单位向量 */\nfunction headingVector(headingDeg: number): { x: number; z: number } {\n const rad = degToRad(headingDeg)\n return { x: Math.sin(rad), z: -Math.cos(rad) }\n}\n\n/**\n * 飞控仿真内核(不依赖 three)。\n *\n * 持有全部飞行状态与可调参数:`step(dt)` 按时间片推进状态机、飞行、电池与失效保护;\n * 外部通过 getter / `snapshot()` 只读遥测,通过 powerOn / autoTakeOff / setStick / startMission 等下达指令。\n * 状态字段虽为 public,但正常路径应只经 step 与上述方法修改。\n */\nexport class DroneSim {\n config: SimConfig = { ...DEFAULT_CONFIG }\n faults: FaultFlags = { ...NO_FAULTS }\n obstacles: ObstacleBox[] = []\n\n phase: FlightPhase = 'powerOff'\n mode: FlightMode = 'normal'\n /** 位置:y 为相对起飞点的高度(AGL),x/z 为水平偏移 */\n position = { x: 0, y: 0, z: 0 }\n velocity = { x: 0, y: 0, z: 0 }\n heading = 0\n tiltPitch = 0\n tiltRoll = 0\n motorLoad = 0\n /**\n * 四电机归一化转速(悬停 ≈ 1.0,0 = 停转):由四旋翼混控得出 ——\n * 总距(整体升力)+ 俯仰差动(前后)+ 横滚差动(左右)+ 偏航反扭矩差动(对角)。\n * 真四旋翼正是靠这四路差动打破/恢复升力平衡来实现全姿态控制的,\n * 渲染层按它给每个桨叶单独转速,不再四桨同速。\n */\n motorSpeeds: Record<MotorPosition, number> = {\n FrontLeft: 0,\n FrontRight: 0,\n RearLeft: 0,\n RearRight: 0,\n }\n armFold = 0\n damaged = false\n\n stick: StickState = { ...NEUTRAL_STICK }\n /** 外部(模型侧)提供的检查项,例如机臂是否展开 */\n externalChecks: ChecklistItem[] = []\n /** 相机状态 */\n recording = false\n recordSeconds = 0\n photoCount = 0\n cameraZoom = 1\n gimbalPitch = -10\n gimbalRoll = 0\n gimbalYaw = 0\n\n /** 航线任务:航点列表(出厂带一条示例航线,方便一键演示) */\n mission: MissionWaypoint[] = DEFAULT_MISSION.map((waypoint) => ({ ...waypoint }))\n missionConfig: MissionConfig = { ...DEFAULT_MISSION_CONFIG }\n missionStatus: MissionStatus = 'idle'\n /** 当前目标航点索引(0 基),-1 表示没有在执行的航点 */\n missionIndex = -1\n /** 当前任务阶段(未执行任务时为 'idle') */\n missionStage: MissionStage = 'idle'\n\n events: SimEvent[] = []\n\n private time = 0\n private phaseTime = 0\n private flightTime = 0\n /** 本步机身系加速度(米/秒²,由速度差分得出):倾角模型与混控都要用 */\n private accelX = 0\n private accelZ = 0\n /** 倾角目标(度):mixer 用\"目标 − 当前\"衡量飞控正在多用力压杆 */\n private tiltPitchTarget = 0\n private tiltRollTarget = 0\n /** 偏航角速度实际值(度/秒,一阶惯性):机身有转动惯量,转速建立/消失都需要时间 */\n private yawRateCurrent = 0\n /**\n * 机身原地自转的目标方位角(度,绝对罗盘角);null = 没有在自转。\n * 优先级高于摇杆偏航 —— 真机拍全景时偏航通道由飞控接管,人打杆不参与。\n */\n private spinHeading: number | null = null\n private spinRateDeg = BODY_SPIN_RATE_DEG\n private batteryUsedWh = 0\n private batteryTemp = 25\n private satellites = 0\n private visionAvailable = false\n private rcLinkQuality = 1\n private rcLostTimer = 0\n private lowBatteryCountdown = 0\n private rthStage: 'ascend' | 'cruise' | 'descend' | 'landing' = 'ascend'\n private rthReason = ''\n private rthStickGrace = 0\n private home = { x: 0, z: 0 }\n private homeRecorded = false\n private altitudeMax = 0\n private altitudeLimitReached = false\n private distanceLimitReached = false\n private obstacleReport: ObstacleReport = {\n forward: null,\n backward: null,\n left: null,\n right: null,\n up: null,\n down: null,\n braking: false,\n brakingDirection: null,\n }\n private eventId = 0\n private readonly warnedAt = new Map<string, number>()\n private landingTimer = 0\n /** 降落细分阶段:先降向 1 米悬停位,停一下再缓速触地 */\n private landingStage: 'approach' | 'hold' | 'settle' = 'approach'\n private landingHoldTimer = 0\n /** 航线任务:已下发但等起飞完成后才开始执行 */\n private missionPending = false\n /** 航线任务:起始航向(headingMode='fixed' 时保持不变) */\n private missionStartHeading = 0\n /** 航线任务:起步保护期,避免启动瞬间被残留摇杆指令打断 */\n private missionStickGrace = 0\n private missionHoverTimer = 0\n /** 当前航点已耗时(超时保护) */\n private missionWaypointTimer = 0\n private missionElapsed = 0\n private missionPasses = 0\n private missionPauseReason = ''\n /** 暂停前所处的阶段:继续执行时按它决定是接着走启航段还是直接飞航点 */\n private missionPausedStage: MissionStage = 'idle'\n /** 任务起始时的总航程(米),仅用于进度条 */\n private missionTotalDistance = 0\n /** 按速度倍率缩放后的挡位参数缓存(见 specFor) */\n private scaledSpec: { mode: FlightMode; scale: number; spec: ModeSpec } | null = null\n\n // ————————————————————————————— 状态查询 —————————————————————————————\n\n /** 降落细分标签:低位悬停确认是飞手最关心的一步,单独标出来 */\n private get phaseLabel(): string {\n if (this.phase === 'landing') {\n if (this.landingStage === 'hold') return `降落 · ${LANDING_HOLD_ALTITUDE_M} 米悬停确认`\n if (this.landingStage === 'settle') return '降落 · 缓慢触地'\n if (this.landingStage === 'approach') return '降落 · 下降至悬停位'\n }\n return PHASE_LABELS[this.phase]\n }\n\n /** 是否已离地(起落架离地即算,含低空悬停) */\n get airborne(): boolean {\n // 离地 5 cm 以上且未断电才算空中(触地/未上电都不算)\n return this.position.y > 0.05 && this.phase !== 'powerOff'\n }\n\n /** 电机是否已启动(空中为 true;停桨后回 false) */\n get motorsOn(): boolean {\n // 电机负载 >2% 视为桨叶已转动(怠速也在转)\n return this.motorLoad > 0.02\n }\n\n /**\n * 当前生效的限高(米)。`unlimitedFlight` 打开时为 `Infinity`,即不夹。\n *\n * 面板、航线编辑器、场景拖拽都通过 `getLimits()` 拿这个值来夹航点,\n * 所以只要一个开关,UI 侧的输入上限也会同步失效 —— 不会出现\"内核放开了但拖不出去\"。\n */\n get altitudeCeiling(): number {\n return this.config.unlimitedFlight ? Number.POSITIVE_INFINITY : this.config.maxAltitude\n }\n\n /** 当前生效的限距(米)。`unlimitedFlight` 打开时为 `Infinity`,即不夹。 */\n get distanceCeiling(): number {\n return this.config.unlimitedFlight ? Number.POSITIVE_INFINITY : this.config.maxDistance\n }\n\n /**\n * 增量修改仿真配置(限高限距开关 / 风 / 电量阈值等),立即生效,只覆盖传入的字段。\n *\n * 只改配置本身:不清位置、不清电量、不动正在执行的任务。若把 `unlimitedFlight`\n * 从 `true` 改回 `false`,下一帧物理步进就会把机体夹回限高面 / 限距圆。\n */\n setConfig(patch: Partial<SimConfig>): void {\n const unlimitedBefore = this.config.unlimitedFlight\n this.config = { ...this.config, ...patch }\n if (patch.unlimitedFlight !== undefined && patch.unlimitedFlight !== unlimitedBefore) {\n this.pushEvent(\n 'info',\n patch.unlimitedFlight\n ? '已取消高度 / 距离限制(演示模式)'\n : `已恢复限高 ${this.config.maxAltitude} m、限距 ${this.config.maxDistance} m`,\n )\n }\n }\n\n /** 当前生效的飞行速度倍率(夹到 0 ~ 1000;非法值退回 1) */\n get speedScale(): number {\n const value = this.config.speedScale\n if (!Number.isFinite(value)) return DEFAULT_SPEED_SCALE\n return clamp(value, SPEED_SCALE_MIN, SPEED_SCALE_MAX)\n }\n\n /**\n * 设置飞行速度倍率(0 ~ 1000,数值即倍数)。\n * 非有限值一律按 1(真机原速)处理 —— 面板滑杆中途可能给出空串/NaN。\n */\n setSpeedScale(scale: number): void {\n const next = Number.isFinite(scale) ? clamp(scale, SPEED_SCALE_MIN, SPEED_SCALE_MAX) : 1\n const previous = this.speedScale\n this.config.speedScale = next\n if (Math.abs(next - previous) < 1e-9) return\n this.pushEvent('info', `飞行速度倍率调整为 ${formatSpeedScale(next)}`)\n }\n\n /**\n * 挡位参数按速度倍率缩放后的副本。\n *\n * 线性速度、加速度、刹车加速度、偏航角速度一起乘系数 —— 于是\"满舵 = 该挡位最大倾角\"\n * 的标定关系不变(倾角模型的阻力系数由 horizontalSpeed 反推,自动跟着缩放),\n * 观感就是同一台机器整体快/慢了几倍;`maxTiltDeg` 是物理上限,不缩放。\n * 结果按(挡位, 倍率)缓存,倍率不动时每帧零分配。\n */\n private specFor(mode: FlightMode): ModeSpec {\n const scale = this.speedScale\n const cached = this.scaledSpec\n if (cached && cached.mode === mode && cached.scale === scale) return cached.spec\n const base = FLIGHT_MODES[mode]\n const spec: ModeSpec =\n scale === 1\n ? base\n : {\n ...base,\n horizontalSpeed: base.horizontalSpeed * scale,\n climbSpeed: base.climbSpeed * scale,\n descendSpeed: base.descendSpeed * scale,\n accel: base.accel * scale,\n brakeAccel: base.brakeAccel * scale,\n yawRateDeg: base.yawRateDeg * scale,\n }\n this.scaledSpec = { mode, scale, spec }\n return spec\n }\n\n /** 当前挡位参数(已按速度倍率缩放):速度上限 / 加速度 / 偏航角速度都取自它 */\n get modeSpec(): ModeSpec {\n return this.specFor(this.mode)\n }\n\n /** 电量百分比 0~100(按真实容量积分,非线性掉电) */\n get batteryPercent(): number {\n return clamp(100 - (this.batteryUsedWh / DRONE_SPEC.batteryWh) * 100, 0, 100)\n }\n\n /** 开路电压(只与电量有关,不依赖电流,避免与电流互为依赖) */\n private get openCircuitVoltage(): number {\n const stateOfCharge = this.batteryPercent / 100\n return (\n DRONE_SPEC.batteryEmptyVoltage +\n (DRONE_SPEC.batteryFullVoltage - DRONE_SPEC.batteryEmptyVoltage) *\n // 0.55:锂电放电曲线的经验指数,让低电量段电压掉得更快,贴近真机表现\n Math.pow(stateOfCharge, 0.55)\n )\n }\n\n /** 放电电流:按当前功率 / 近似电压估算 */\n get batteryCurrent(): number {\n // 6.4 V:除法的电压下限,防止电量趋零时电流发散\n return this.currentPowerWatts / Math.max(6.4, this.openCircuitVoltage)\n }\n\n /** 端电压 = 开路电压 − 内阻压降 */\n get batteryVoltage(): number {\n return Math.max(\n 6,\n // 下限 6 V:避免低电量时内阻压降把端电压算成负数\n this.openCircuitVoltage - this.batteryCurrent * DRONE_SPEC.batteryInternalResistance,\n )\n }\n\n /** 当前定位来源:GNSS(≥6 星且指南针正常)→ 视觉 → 姿态模式 */\n get positionSource(): PositionSource {\n if (this.faults.gnssLost) {\n return this.visionAvailable ? 'vision' : 'atti'\n }\n if (this.satellites >= 6 && !this.faults.compassError) return 'gps'\n if (this.visionAvailable) return 'vision'\n return 'atti'\n }\n\n /** 当前总功耗(W):怠速 + 悬停(含平飞增益)+ 爬升功耗,再乘风速系数 */\n get currentPowerWatts(): number {\n if (this.phase === 'powerOff') return 0\n const airSpeed = Math.hypot(this.velocity.x, this.velocity.z)\n // 16 m/s:平飞满速参考(运动挡极速);>1 表示超速(仅姿态模式可能出现)\n const speedRatio = clamp(airSpeed / 16, 0, 1.2)\n const spinning = this.motorLoad > 0.02\n let watts =\n DRONE_SPEC.idlePowerWatts +\n DRONE_SPEC.hoverPowerWatts *\n (spinning ? 1 : 0.05) *\n (1 + DRONE_SPEC.levelSpeedPowerGain * speedRatio * speedRatio)\n watts += Math.max(0, this.velocity.y) * DRONE_SPEC.climbWattsPerMps\n // 电机转动时的最低功耗下限 12 W\n if (spinning) watts = Math.max(watts, 12)\n // 逆风要保持位置需要更大的空气速度,额外耗电\n // 30 m/s 为风速归一化上限、0.35 为最大额外功耗比例\n const windFactor = 1 + clamp(this.config.windSpeed / 30, 0, 1) * 0.35\n return watts * windFactor\n }\n\n /** 剩余可飞时间(分钟) */\n get remainingMinutes(): number {\n // 用 12 W 下限兜底,避免待机功耗极小导致剩余时间虚高\n const watts = Math.max(this.currentPowerWatts, 12)\n return ((this.batteryPercent / 100) * DRONE_SPEC.batteryWh * 60) / watts\n }\n\n // ————————————————————————————— 操作指令 —————————————————————————————\n\n /** 上电:重置飞行状态与电量并进入自检;受损时拒绝 */\n powerOn(): void {\n if (this.phase !== 'powerOff' && this.phase !== 'stopped') return\n if (this.damaged) {\n this.pushEvent('error', '飞行器受损,需重新上电复位后再次自检')\n return\n }\n this.phase = 'selfCheck'\n this.phaseTime = 0\n this.heading = 0\n this.position = { x: 0, y: 0, z: 0 }\n this.velocity = { x: 0, y: 0, z: 0 }\n this.batteryUsedWh = 0\n this.batteryTemp = 25\n // 上电即有 1 星,随后由 updateGnss 逐步搜星到目标数\n this.satellites = this.faults.gnssLost ? 0 : 1\n this.flightTime = 0\n this.altitudeMax = 0\n this.damaged = false\n this.pushEvent('info', '飞行器上电,开始系统自检')\n }\n\n /** 关机:仅地面可用;顺带作废未起飞的航线任务 */\n powerOff(): void {\n if (this.airborne || this.phase === 'powerOff') return\n this.phase = 'powerOff'\n this.motorLoad = 0\n // 已下发但尚未起飞的航线随关机一起作废\n if (this.missionPending) {\n this.missionPending = false\n this.missionStatus = 'idle'\n this.missionIndex = -1\n }\n if (this.recording) this.stopRecording()\n this.pushEvent('info', '飞行器已关机')\n }\n\n /** 切飞行挡位:'cine' 平稳 / 'normal' 普通 / 'sport' 运动(影响限速与倾角) */\n setMode(mode: FlightMode): void {\n if (this.mode === mode) return\n this.mode = mode\n this.pushEvent('info', `切换至${FLIGHT_MODES[mode].label}`)\n }\n\n /** 写摇杆连续量(Partial 合并;松杆由飞控刹停并定点悬停) */\n setStick(stick: Partial<StickState>): void {\n this.stick = { ...this.stick, ...stick }\n }\n\n /** 内八掰杆启动电机(地面待机时可用) */\n startMotors(): boolean {\n if (this.phase !== 'standby') return false\n const blocked = this.checklist.filter((item) => item.blocking && !item.ok)\n if (blocked.length > 0) {\n this.pushEvent('error', `起飞前检查未通过:${blocked.map((item) => item.label).join('、')}`)\n return false\n }\n this.phase = 'motorsOn'\n this.phaseTime = 0\n this.pushEvent('success', '电机已启动(内八掰杆),桨叶开始怠速旋转')\n return true\n }\n\n /** 停止电机(仅地面已启动电机的状态可用);返回是否真的停下来了 */\n stopMotors(): boolean {\n if (this.phase !== 'motorsOn') return false\n this.phase = 'standby'\n this.pushEvent('info', '电机已停止')\n return true\n }\n\n /** 起飞:记录返航点并自动爬升到 1.2 米(需已启动电机) */\n takeOff(): boolean {\n if (this.phase !== 'motorsOn') return false\n this.phase = 'takingOff'\n this.phaseTime = 0\n this.home = { x: this.position.x, z: this.position.z }\n this.homeRecorded = true\n this.damageCheck()\n this.pushEvent('success', '起飞:自动上升至 1.2 米')\n return true\n }\n\n /** 一键起飞(地面待机时自动完成电机启动 + 起飞) */\n autoTakeOff(): boolean {\n if (this.phase === 'standby' && !this.startMotors()) return false\n return this.takeOff()\n }\n\n /** 触发自动降落(会先中止正在执行的航线任务) */\n startLanding(): boolean {\n if (!this.airborne && this.phase !== 'takingOff') return false\n if (this.phase === 'landing' || this.phase === 'emergency') return false\n this.abortMission('触发自动降落')\n // 降落优先级高于任何自动拍摄:自转立刻停(全景扫描会因对准超时自行收尾)\n this.spinHeading = null\n this.phase = 'landing'\n this.phaseTime = 0\n this.landingTimer = 0\n this.beginLandingDescent()\n this.pushEvent('info', '开始自动降落')\n return true\n }\n\n /** 触发智能返航(reason 用于日志与快照);返航优先级高于航线,会中止任务 */\n startRth(reason: string): boolean {\n if (!this.airborne) return false\n if (this.phase === 'rth') return false\n // 返航优先级高于航线任务:真机触发返航会同时中止未完成的航线\n this.abortMission('触发返航')\n this.spinHeading = null\n this.phase = 'rth'\n this.phaseTime = 0\n // 已在返航高度附近(差 <0.5 m)就直接巡航,否则先爬升\n this.rthStage = this.position.y < this.config.rthAltitude - 0.5 ? 'ascend' : 'cruise'\n this.rthReason = reason\n // 1.2 s 内忽略摇杆,防止触发返航的那次打杆立刻把返航取消掉\n this.rthStickGrace = 1.2\n this.pushEvent('warn', `触发智能返航(${reason}),返航高度 ${this.config.rthAltitude} 米`)\n return true\n }\n\n /** 取消正在执行的智能返航,转入手动飞行 */\n cancelRth(reason = '用户取消'): void {\n if (this.phase !== 'rth') return\n this.phase = 'flying'\n this.pushEvent('info', `返航已中止(${reason})`)\n }\n\n /**\n * 紧急停桨(停桨保护)。\n * ⚠️ **只在飞行器落地后允许**:空中停桨会直接导致坠机,本沙盒按用户要求禁止,\n * 必须先把飞行器降落下来。空中调用会被拒绝(不改变任何飞行状态),只写一条警告日志。\n */\n emergencyStop(): boolean {\n if (this.airborne) {\n this.pushEvent(\n 'warn',\n `飞行器尚未落地(离地 ${this.position.y.toFixed(1)} 米),已拒绝紧急停桨,请先降落`,\n )\n return false\n }\n return this.stopMotors()\n }\n\n /** 重置到可再次起飞的状态(用于沙盒反复测试) */\n reset(): void {\n this.phase = 'powerOff'\n this.position = { x: 0, y: 0, z: 0 }\n this.velocity = { x: 0, y: 0, z: 0 }\n this.tiltPitch = 0\n this.tiltRoll = 0\n this.heading = 0\n this.spinHeading = null\n this.yawRateCurrent = 0\n this.motorLoad = 0\n this.damaged = false\n this.batteryUsedWh = 0\n this.flightTime = 0\n this.altitudeMax = 0\n this.stick = { ...NEUTRAL_STICK }\n this.recording = false\n this.recordSeconds = 0\n this.rthReason = ''\n // 航线任务一并复位(静默,不写日志:重置本身就是\"回到出厂\")\n this.missionStatus = 'idle'\n this.missionIndex = -1\n this.missionStage = 'idle'\n this.missionPending = false\n this.missionHoverTimer = 0\n this.missionWaypointTimer = 0\n this.missionElapsed = 0\n this.missionPasses = 0\n this.missionPauseReason = ''\n this.pushEvent('info', '沙盒已重置')\n }\n\n // ————————————————————————————— 每帧推进 —————————————————————————————\n\n /** 推进一个仿真时间片(dtSeconds 秒;负值不推进,过大的值会被夹到单步上限) */\n step(dtSeconds: number): void {\n // 单步上限 0.1 s:后台标签页卡顿后一帧可能攒下很久,夹住避免积分出巨大位移\n const delta = clamp(dtSeconds, 0, 0.1)\n if (delta <= 0) return\n this.time += delta\n this.phaseTime += delta\n // 录像计时与电源状态无关(相机录制本身由引擎层的 MediaRecorder 负责)\n if (this.recording) this.recordSeconds += delta\n\n if (this.phase === 'powerOff') {\n this.motorLoad = lerp(this.motorLoad, 0, Math.min(1, delta * 6))\n this.updateBattery(delta)\n return\n }\n\n this.updatePhaseMachine(delta)\n this.updateGnss(delta)\n this.updateFlight(delta)\n this.updateCamera(delta)\n this.updateBattery(delta)\n this.updateFailsafe(delta)\n }\n\n /** 分阶段推进:自检 → 预热 → 待机;起飞/降落/返航的自动段也在这里 */\n private updatePhaseMachine(delta: number): void {\n switch (this.phase) {\n case 'selfCheck':\n this.motorLoad = lerp(this.motorLoad, 0, Math.min(1, delta * 4))\n if (this.phaseTime >= this.config.selfCheckSeconds) {\n this.phase = 'warmingUp'\n this.phaseTime = 0\n this.pushEvent('info', '自检通过,开始 IMU / 传感器预热')\n }\n break\n case 'warmingUp':\n if (this.phaseTime >= this.config.warmupSeconds) {\n this.phase = 'standby'\n this.phaseTime = 0\n this.home = { x: this.position.x, z: this.position.z }\n this.homeRecorded = true\n this.pushEvent('success', '预热完成,已记录返航点,等待起飞指令')\n }\n break\n case 'motorsOn':\n this.motorLoad = lerp(this.motorLoad, 0.32, Math.min(1, delta * 3))\n break\n case 'takingOff': {\n this.motorLoad = lerp(this.motorLoad, 0.62, Math.min(1, delta * 3))\n // 起飞爬升率固定 1.5 m/s(按速度倍率缩放,0 倍率时起飞也会停住)\n const climbRate = 1.5 * this.speedScale\n this.position.y = Math.min(DRONE_SPEC.takeoffAltitude, this.position.y + climbRate * delta)\n this.velocity.y = climbRate\n this.settleHorizontal(delta)\n if (this.position.y >= DRONE_SPEC.takeoffAltitude - 0.01) {\n this.position.y = DRONE_SPEC.takeoffAltitude\n this.velocity.y = 0\n if (this.missionPending) {\n // 航线下发时飞机还在地面:起飞到位后自动接上航线\n this.beginMissionPath()\n } else {\n this.phase = 'flying'\n this.pushEvent('success', '已到达 1.2 米,进入悬停')\n }\n }\n break\n }\n case 'landing':\n this.updateLanding(delta)\n break\n case 'emergency':\n this.applyGravityFall(delta)\n break\n case 'stopped':\n case 'standby':\n this.motorLoad = lerp(this.motorLoad, 0, Math.min(1, delta * 3))\n this.settleHorizontal(delta)\n break\n default:\n break\n }\n }\n\n /** 按下降落:高于悬停位则先降向 1 米,已经贴地则直接缓降 */\n private beginLandingDescent(): void {\n this.landingHoldTimer = 0\n this.landingStage = this.position.y > LANDING_HOLD_ALTITUDE_M + 0.05 ? 'approach' : 'settle'\n }\n\n /**\n * 自动降落:降向 1 米悬停位 → 稳住一下 → 缓慢触地。\n * 真机不会从巡航高度一路匀速直插地面;低位那一下停顿既是确认落点,也是留给飞手接管的时间。\n */\n private updateLanding(delta: number): void {\n this.motorLoad = lerp(this.motorLoad, 0.55, Math.min(1, delta * 3))\n this.settleHorizontal(delta)\n\n if (this.landingStage === 'approach') {\n // 低于 2 m 改用 0.6 m/s 缓降,避免贴地时下冲过猛(倍率下同比例缩放)\n const rate =\n this.position.y <= DRONE_SPEC.landingSlowAltitude\n ? 0.6 * this.speedScale\n : this.modeSpec.descendSpeed\n this.velocity.y = -rate\n this.position.y -= rate * delta\n if (this.position.y <= LANDING_HOLD_ALTITUDE_M) {\n this.position.y = LANDING_HOLD_ALTITUDE_M\n this.velocity.y = 0\n this.enterLandingHold()\n }\n return\n }\n\n if (this.landingStage === 'hold') {\n // 低位悬停:高度锁死不动,等停顿走完再缓降\n this.position.y = LANDING_HOLD_ALTITUDE_M\n this.velocity.y = 0\n this.landingHoldTimer += delta\n if (this.landingHoldTimer >= LANDING_HOLD_SECONDS) {\n this.landingStage = 'settle'\n this.pushEvent('info', '悬停确认完成,开始缓慢降落')\n }\n return\n }\n\n // 缓速触地:接触地面后停桨(触地速率同样吃速度倍率)\n const finalRate = LANDING_FINAL_DESCENT_RATE * this.speedScale\n this.velocity.y = -finalRate\n this.position.y -= finalRate * delta\n if (this.position.y <= 0.02) {\n this.position.y = 0\n this.velocity.y = 0\n this.landingTimer += delta\n // 触地后停 1.2 s 再停桨,确认确实落稳\n if (this.landingTimer >= 1.2) {\n this.finishLanding()\n }\n } else {\n this.landingTimer = 0\n }\n }\n\n /** 进入 1 米悬停位(手动降落与返航下降段共用) */\n private enterLandingHold(): void {\n this.landingStage = 'hold'\n this.landingHoldTimer = 0\n this.pushEvent('info', `已到达 ${LANDING_HOLD_ALTITUDE_M} 米悬停位,确认后缓降`)\n }\n\n private finishLanding(): void {\n this.phase = 'standby'\n this.landingTimer = 0\n this.landingHoldTimer = 0\n this.landingStage = 'settle'\n this.motorLoad = 0\n this.velocity = { x: 0, y: 0, z: 0 }\n if (this.recording) this.stopRecording()\n this.pushEvent('success', `已安全降落,本次飞行 ${this.formatDuration(this.flightTime)}`)\n if (this.batteryPercent <= this.config.criticalBatteryPercent) {\n this.pushEvent('warn', '严重低电量,建议更换电池后再起飞')\n }\n }\n\n private applyGravityFall(delta: number): void {\n this.velocity.y -= GRAVITY * delta\n // 近似空气阻力:水平速度按指数衰减(0.4 为衰减系数)\n this.velocity.x *= 1 - Math.min(1, delta * 0.4)\n this.velocity.z *= 1 - Math.min(1, delta * 0.4)\n this.position.x += this.velocity.x * delta\n this.position.y += this.velocity.y * delta\n this.position.z += this.velocity.z * delta\n // 坠落时机头前倾 25°\n this.tiltPitch = lerp(this.tiltPitch, 25, Math.min(1, delta * 2))\n if (this.position.y <= 0) {\n this.position.y = 0\n this.velocity = { x: 0, y: 0, z: 0 }\n this.damaged = true\n this.phase = 'stopped'\n this.pushEvent('error', '飞行器坠地受损,请重新上电复位')\n }\n }\n\n /** 水平速度归零(GPS/视觉保持位置,姿态模式保留风漂移) */\n private settleHorizontal(delta: number): void {\n if (this.positionSource === 'atti') {\n const wind = this.windVector()\n this.velocity.x = lerp(this.velocity.x, wind.x, Math.min(1, delta * 0.8))\n this.velocity.z = lerp(this.velocity.z, wind.z, Math.min(1, delta * 0.8))\n } else {\n const brake = Math.min(1, delta * 4)\n this.velocity.x = lerp(this.velocity.x, 0, brake)\n this.velocity.z = lerp(this.velocity.z, 0, brake)\n }\n this.position.x += this.velocity.x * delta\n this.position.z += this.velocity.z * delta\n }\n\n // ————————————————————————————— 手动飞行 —————————————————————————————\n\n private updateFlight(delta: number): void {\n // 步前速度先存下:步后差分出本步加速度(打杆瞬间机体必须先加速,\n // 倾角里要叠加\"加速所需\"的分量,混控也按它分配前后/左右差动)\n const prevVX = this.velocity.x\n const prevVZ = this.velocity.z\n if (this.phase === 'flying' || this.phase === 'rth' || this.phase === 'waypoint') {\n // 航线任务自己算航迹(不受摇杆指令直接驱动),其余两态走手动/返航通道\n if (this.phase === 'waypoint') {\n this.updateMission(delta)\n } else {\n this.updateGnssPositionFlight(delta)\n }\n this.updateMotorLoad(delta)\n }\n this.accelX = (this.velocity.x - prevVX) / delta\n this.accelZ = (this.velocity.z - prevVZ) / delta\n this.updateTilt(delta)\n this.updateMotorSpeeds(delta)\n this.applyObstacleBrake()\n this.altitudeMax = Math.max(this.altitudeMax, this.position.y)\n }\n\n private updateGnssPositionFlight(delta: number): void {\n const spec = this.modeSpec\n const source = this.positionSource\n const horizontalStick = { pitch: this.stick.pitch, roll: this.stick.roll }\n const stickMagnitude = Math.hypot(horizontalStick.pitch, horizontalStick.roll)\n\n if (this.phase === 'rth') {\n this.updateRth(delta)\n return\n }\n\n if (source === 'atti' && !this.faults.gnssLost) {\n // 指南针异常:姿态模式左右漂移,真机表现为航向漂移\n this.heading += Math.sin(this.time * 0.7) * 8 * delta\n }\n\n // 摇杆幅值超过 1 时按圆行程归一,避免斜向超速\n const scale = stickMagnitude > 1 ? 1 / stickMagnitude : 1\n const forwardSpeed = horizontalStick.pitch * spec.horizontalSpeed * scale\n const rightSpeed = horizontalStick.roll * spec.horizontalSpeed * scale\n\n const nose = headingVector(this.heading)\n const right = { x: -nose.z, z: nose.x }\n // 机身坐标系下的空气速度指令\n let airX = nose.x * forwardSpeed + right.x * rightSpeed\n let airZ = nose.z * forwardSpeed + right.z * rightSpeed\n\n const wind = this.windVector()\n if (source !== 'atti') {\n // 定点保持:飞控用 -风 的空气速度顶住风,顶不住的部分变成漂移\n airX -= wind.x\n airZ -= wind.z\n }\n const airMagnitude = Math.hypot(airX, airZ)\n const airLimit = source === 'atti' ? spec.horizontalSpeed : spec.horizontalSpeed + 0.01\n if (airMagnitude > airLimit) {\n const ratio = airLimit / airMagnitude\n airX *= ratio\n airZ *= ratio\n }\n const targetX = airX + wind.x\n const targetZ = airZ + wind.z\n\n const stickActive = stickMagnitude > 0.06\n const accel = (stickActive ? spec.accel : spec.brakeAccel) * delta\n this.velocity.x += clamp(targetX - this.velocity.x, -accel, accel)\n this.velocity.z += clamp(targetZ - this.velocity.z, -accel, accel)\n\n // 垂直:GPS/视觉下松杆定高;姿态模式有轻微掉高。升降的加速度限幅同样按倍率缩放\n const verticalInput = this.stick.throttle\n if (Math.abs(verticalInput) > 0.06) {\n const rate =\n verticalInput > 0 ? verticalInput * spec.climbSpeed : verticalInput * spec.descendSpeed\n const climbAccel = 3 * this.speedScale * delta\n this.velocity.y += clamp(rate - this.velocity.y, -climbAccel, climbAccel)\n } else {\n // 姿态模式无定高:松杆仍以 -0.35 m/s 缓慢掉高\n const hold = source === 'atti' ? -0.35 * this.speedScale : 0\n const holdAccel = 2 * this.speedScale * delta\n this.velocity.y += clamp(hold - this.velocity.y, -holdAccel, holdAccel)\n }\n\n this.integratePosition(delta)\n this.applyYaw(delta)\n }\n\n private integratePosition(delta: number): void {\n this.position.x += this.velocity.x * delta\n this.position.z += this.velocity.z * delta\n this.position.y += this.velocity.y * delta\n\n // 限高 / 限距(unlimitedFlight 打开时两个上限都是 Infinity,两段都不进入)\n this.altitudeLimitReached = false\n const ceiling = this.altitudeCeiling\n if (Number.isFinite(ceiling) && this.position.y >= ceiling) {\n this.position.y = ceiling\n if (this.velocity.y > 0) this.velocity.y = 0\n this.altitudeLimitReached = true\n this.warnOnce('altitude', '已达最大飞行高度限制')\n }\n if (this.position.y < 0) {\n this.position.y = 0\n if (this.velocity.y < 0) this.velocity.y = 0\n }\n const distance = this.distanceToHome\n this.distanceLimitReached = false\n const radius = this.distanceCeiling\n if (Number.isFinite(radius) && distance > radius) {\n // 超距时沿返航点方向等比缩回,精确落到限距圆上\n const ratio = radius / distance\n this.position.x = this.home.x + (this.position.x - this.home.x) * ratio\n this.position.z = this.home.z + (this.position.z - this.home.z) * ratio\n this.velocity.x = 0\n this.velocity.z = 0\n this.distanceLimitReached = true\n this.warnOnce('distance', '已达最大飞行距离限制')\n }\n }\n\n private applyYaw(delta: number): void {\n // 机身原地自转(全景扫描):偏航由飞控接管,摇杆让位。\n // 位置不动、只改航向分量,所以是\"原地\"转 —— 这正是 180°/360° 全景补水平视野的方式。\n const spin = this.spinHeading\n if (spin !== null) {\n this.yawRateCurrent = lerp(this.yawRateCurrent, 0, Math.min(1, delta * 7))\n this.heading = this.turnToward(this.heading, spin, this.spinRateDeg * this.speedScale * delta)\n return\n }\n // 电机异常时偏航权限降到 70%,与下面的被动自旋叠加\n const command = this.stick.yaw * this.modeSpec.yawRateDeg * (this.faults.motorFailure ? 0.7 : 1)\n // 偏航指令一阶惯性:机身有转动惯量,角速度建立/消失都需要一小段时间,\n // 松杆后机头还会再转一小段才停(真机手感),不会瞬间钉死在目标角速度上\n this.yawRateCurrent = lerp(this.yawRateCurrent, command, Math.min(1, delta * 7))\n this.heading = (this.heading + this.yawRateCurrent * delta + 360) % 360\n if (this.faults.motorFailure) {\n // 单电机动力衰减:机身持续缓慢自旋,需要飞控不断修正\n this.heading = (this.heading + 22 * delta + 360) % 360\n }\n }\n\n /**\n * 机体倾斜:四旋翼靠\"总升力矢量倾斜\"产生水平分力,所以倾角由水平方向\n * 需要的力决定 —— 稳态巡航时是气动阻力(∝ v²),加减速时叠加惯性力(m·a):\n * tan θ = (k·v² + m·a) / (m·g)\n * 用挡位上限标定 k:满速平飞时倾角恰好 = maxTiltDeg。低速时倾角远小于\n * 线性模型(真实阻力随速度平方增长),打杆瞬间则因惯性力额外下压机头、\n * 刹车时反向抬头\"拉飘\" —— 与真机的加减速姿态一致。\n * 采用航空惯例:俯仰正 = 抬头,横滚正 = 右倾。姿态模式叠加轻微摆动。\n */\n private updateTilt(delta: number): void {\n const spec = this.modeSpec\n const wind = this.windVector()\n const airX = this.velocity.x - wind.x\n const airZ = this.velocity.z - wind.z\n const nose = headingVector(this.heading)\n const right = { x: -nose.z, z: nose.x }\n const forwardAir = airX * nose.x + airZ * nose.z\n const rightAir = airX * right.x + airZ * right.z\n\n const vMax = Math.max(spec.horizontalSpeed, 0.1)\n // 阻力配平系数:满速时 tan θmax = k·vMax²\n const dragK = Math.tan(degToRad(spec.maxTiltDeg)) / (vMax * vMax)\n // 惯性力分量(以 g 归一):加速需要额外前倾、刹车抬头\n const accelFwd = (this.accelX * nose.x + this.accelZ * nose.z) / GRAVITY\n const accelRight = (this.accelX * right.x + this.accelZ * right.z) / GRAVITY\n\n let pitch = -radToDeg(\n Math.atan(dragK * forwardAir * Math.abs(forwardAir) + clamp(accelFwd, -1.2, 1.2)),\n )\n let roll = radToDeg(\n Math.atan(dragK * rightAir * Math.abs(rightAir) + clamp(accelRight, -1.2, 1.2)),\n )\n // 倾角上限跟随挡位(惯性力只影响\"怎么到上限\",不突破上限)\n pitch = clamp(pitch, -spec.maxTiltDeg, spec.maxTiltDeg)\n roll = clamp(roll, -spec.maxTiltDeg, spec.maxTiltDeg)\n if (this.positionSource === 'atti' && this.airborne) {\n pitch += Math.sin(this.time * 2.1) * 1.6\n roll += Math.cos(this.time * 1.7) * 2.2\n }\n // 地面待机时姿态回中更快(8),飞行中柔和些(3.2)\n const smoothing = this.phase === 'standby' ? 8 : 3.2\n this.tiltPitchTarget = pitch\n this.tiltRollTarget = roll\n this.tiltPitch = lerp(this.tiltPitch, pitch, Math.min(1, delta * smoothing))\n this.tiltRoll = lerp(this.tiltRoll, roll, Math.min(1, delta * smoothing))\n }\n\n /**\n * 四旋翼混控:把\"总升力 + 三路姿态差动\"分配到四个电机。\n *\n * - 总距:motorLoad(含爬升/平飞的负载)已经承载整体转速;\n * 机体倾斜 θ 后总升力还要除以 cos θ 才能托住重力,转速按推力开方折算。\n * - 俯仰差动:前倾(前进)由后侧两桨加速、前侧两桨减速产生 —— 机尾抬升、\n * 机头下压,总升力的水平分力驱动前飞。差动量 ∝ 飞控正在施加的俯仰力矩,\n * 即\"目标倾角 − 当前倾角\":打杆瞬间四桨差动最大,稳态匀速时回到近似同速\n * (只剩对抗气动俯仰力矩的微小配平)。\n * - 横滚差动:同理,向右压坡由左侧两桨加速、右侧两桨减速产生(右侧加速\n * 会让右端抬起,是向左横滚)。\n * - 偏航差动:相邻桨反转、对角同向(本模型 前左/后右 = 顺时针)。桨旋转时\n * 气动阻力给机身反扭矩,加速一组对角桨、减速另一组,总升力不变而净反扭矩\n * 驱动原地转向 —— 差动量 ∝ 实际偏航角速度(转动全程都需要净扭矩)。\n */\n private updateMotorSpeeds(delta: number): void {\n const speeds = this.motorSpeeds\n if (this.motorLoad <= 0.02) {\n for (const key of MOTOR_POSITIONS) speeds[key] = 0\n return\n }\n const spec = this.modeSpec\n const maxTilt = Math.max(spec.maxTiltDeg, 1)\n const pitchCmd = clamp((this.tiltPitchTarget - this.tiltPitch) / maxTilt, -1, 1)\n const rollCmd = clamp((this.tiltRollTarget - this.tiltRoll) / maxTilt, -1, 1)\n const yawCmd = clamp(this.yawRateCurrent / Math.max(spec.yawRateDeg, 1), -1, 1)\n\n // 差动满量系数:满舵打杆瞬间的转速差约 ±26%(0.55 × sqrt 折算后)\n const PITCH_MIX = 0.55\n const ROLL_MIX = 0.55\n const YAW_MIX = 0.3\n const pitchDiff = pitchCmd * PITCH_MIX\n const rollDiff = rollCmd * ROLL_MIX\n const yawDiff = yawCmd * YAW_MIX\n // 稳态气动配平:机体持续倾斜时,气动俯仰/横滚力矩(阻力作用线不过重心)\n // 需要一个小的常值差动去对抗 —— 前飞时后桨始终略快于前桨,与真机一致\n const TRIM_MIX = 0.15\n const pitchTrim = (-this.tiltPitch / maxTilt) * TRIM_MIX\n const rollTrim = (this.tiltRoll / maxTilt) * TRIM_MIX\n\n // 倾斜后总升力增大:1/cos θ;转速按推力的平方根折算(升力 ∝ 转速²)\n const tiltMag = Math.min(degToRad(Math.hypot(this.tiltPitch, this.tiltRoll)), degToRad(80))\n const rpmFactor = Math.sqrt(1 / Math.max(Math.cos(tiltMag), 0.15))\n\n for (const position of MOTOR_POSITIONS) {\n const isFront = position === 'FrontLeft' || position === 'FrontRight'\n const isRight = position === 'FrontRight' || position === 'RearRight'\n // 俯仰:抬头(正 tiltPitch)需要前桨加速;低头(前进)需要后桨加速\n const pitchTerm = (isFront ? 1 : -1) * pitchDiff + (isFront ? -1 : 1) * pitchTrim\n // 横滚:右倾(正 tiltRoll)需要左桨加速、右桨减速\n const rollTerm = (isRight ? -1 : 1) * rollDiff + (isRight ? -1 : 1) * rollTrim\n // 偏航:右转(正 yaw)加速逆时针桨(前右/后左),其反扭矩推动机身顺时针转\n const isCcw = position === 'FrontRight' || position === 'RearLeft'\n const yawTerm = (isCcw ? 1 : -1) * yawDiff\n const differential = 1 + pitchTerm + rollTerm + yawTerm\n speeds[position] = clamp(this.motorLoad * rpmFactor * differential, 0, 1.15)\n }\n }\n\n private updateMotorLoad(delta: number): void {\n if (this.phase === 'emergency' || this.phase === 'powerOff') {\n this.motorLoad = lerp(this.motorLoad, 0, Math.min(1, delta * 8))\n return\n }\n // 5 m/s 为爬升归一化参考、16 m/s 为平飞参考;0.42 是悬停基准负载\n const climbFactor = clamp(this.velocity.y / 5, -0.3, 1)\n const speedFactor = clamp(Math.hypot(this.velocity.x, this.velocity.z) / 16, 0, 1)\n const target = clamp(0.42 + climbFactor * 0.45 + speedFactor * 0.28, 0.2, 1)\n const shouldSpin = this.phase !== 'standby' && this.phase !== 'stopped'\n this.motorLoad = lerp(this.motorLoad, shouldSpin ? target : 0, Math.min(1, delta * 4))\n }\n\n // ————————————————————————————— 智能返航 —————————————————————————————\n\n private updateRth(delta: number): void {\n // 返航按普通挡 N 的参数走(真机返航也是 N 挡),同样吃速度倍率\n const spec = this.specFor('normal')\n const yawRate = RTH_YAW_RATE_DEG * this.speedScale\n this.rthStickGrace = Math.max(0, this.rthStickGrace - delta)\n const stickMagnitude = Math.hypot(this.stick.pitch, this.stick.roll, this.stick.throttle)\n if (this.rthStickGrace <= 0 && stickMagnitude > 0.15) {\n this.cancelRth('检测到摇杆操作')\n return\n }\n\n // 返航高度下限 2 m,避免把返航高度设得过低时贴地飞\n const targetAltitude = Math.max(this.config.rthAltitude, 2)\n if (this.rthStage === 'ascend') {\n const delta2 = targetAltitude - this.position.y\n this.velocity.x = lerp(this.velocity.x, 0, Math.min(1, delta * 2))\n this.velocity.z = lerp(this.velocity.z, 0, Math.min(1, delta * 2))\n this.velocity.y = clamp(delta2, -spec.climbSpeed, spec.climbSpeed)\n if (Math.abs(delta2) < 0.3) {\n this.velocity.y = 0\n this.rthStage = 'cruise'\n this.pushEvent('info', `已升至返航高度 ${targetAltitude} 米,开始返航`)\n }\n } else if (this.rthStage === 'cruise') {\n const dx = this.home.x - this.position.x\n const dz = this.home.z - this.position.z\n const distance = Math.hypot(dx, dz)\n if (distance < 0.8) {\n this.velocity.x = 0\n this.velocity.z = 0\n this.rthStage = 'descend'\n this.pushEvent('info', '已到达返航点上方,开始下降')\n } else {\n const dirX = dx / distance\n const dirZ = dz / distance\n // 由方向向量反算罗盘角:atan2(x, -z),0°=北、90°=东\n const targetHeading = radToDeg(Math.atan2(dirX, -dirZ))\n const headingError = Math.abs(((targetHeading - this.heading + 540) % 360) - 180)\n this.velocity.y = lerp(this.velocity.y, 0, Math.min(1, delta * 2))\n if (headingError > RTH_ALIGN_TOLERANCE_DEG) {\n // 真机返航是\"先对准、再前进\":机头转到返航航向之前原地悬停转向,\n // 不带着位移一起转(否则会飞出一条弧线,航迹也对不上返航点)\n this.velocity.x = lerp(this.velocity.x, 0, Math.min(1, delta * 8))\n this.velocity.z = lerp(this.velocity.z, 0, Math.min(1, delta * 8))\n this.heading = this.turnToward(this.heading, targetHeading, yawRate * delta)\n } else {\n // 接近返航点时按距离比例减速,最低 2 m/s(倍率下\"最低\"也跟着走)\n const cruiseSpeed = Math.min(\n spec.horizontalSpeed,\n Math.max(2 * this.speedScale, distance * 0.5),\n )\n this.velocity.x = lerp(this.velocity.x, dirX * cruiseSpeed, Math.min(1, delta * 1.6))\n this.velocity.z = lerp(this.velocity.z, dirZ * cruiseSpeed, Math.min(1, delta * 1.6))\n // 对准之后机头即航向,前进方向与机头一致\n this.heading = this.turnToward(this.heading, targetHeading, yawRate * delta)\n }\n }\n } else if (this.rthStage === 'descend') {\n this.velocity.x = lerp(this.velocity.x, 0, Math.min(1, delta * 3))\n this.velocity.z = lerp(this.velocity.z, 0, Math.min(1, delta * 3))\n this.velocity.y = -spec.descendSpeed\n this.integratePosition(delta)\n if (this.position.y <= LANDING_HOLD_ALTITUDE_M) {\n // 降到 1 米悬停位就交给降落阶段:先稳住,再缓慢触地\n this.rthStage = 'landing'\n this.phase = 'landing'\n this.landingTimer = 0\n this.position.y = LANDING_HOLD_ALTITUDE_M\n this.velocity.y = 0\n this.enterLandingHold()\n }\n return\n } else {\n this.phase = 'landing'\n return\n }\n this.integratePosition(delta)\n }\n\n private turnToward(current: number, target: number, maxDelta: number): number {\n // 角度差归一到 (-180, 180];+540 是为了让 JS 的 % 对负数也给出正余数\n let difference = ((target - current + 540) % 360) - 180\n difference = clamp(difference, -maxDelta, maxDelta)\n return (current + difference + 360) % 360\n }\n\n // ————————————————————————————— 航线任务 —————————————————————————————\n\n /**\n * 航线内部的航点归一化:在字段护栏之外,把高度上限跟 `unlimitedFlight` 对齐。\n *\n * 默认仍是 {@link MISSION_ALTITUDE_HARD_LIMIT}(500 m);取消限高时传 `Infinity`,\n * 否则会出现\"限高放开了、航点却被 500 m 静默夹住\"的矛盾。\n */\n private normalizeWaypoint(source: Partial<MissionWaypoint>): MissionWaypoint {\n return normalizeMissionWaypoint(\n source,\n this.config.unlimitedFlight ? Number.POSITIVE_INFINITY : MISSION_ALTITUDE_HARD_LIMIT,\n )\n }\n\n /** 设置航点列表(执行中禁止修改,与真机\"上传后不可改\"一致) */\n setMission(waypoints: Array<Partial<MissionWaypoint>>): boolean {\n if (this.missionStatus !== 'idle' || this.missionPending) {\n this.pushEvent('warn', '航线执行中无法修改航点列表,请先停止任务')\n return false\n }\n this.mission = waypoints.map((waypoint) => this.normalizeWaypoint(waypoint))\n this.missionIndex = -1\n this.missionStage = 'idle'\n return true\n }\n\n /**\n * 只改一个航点(列表编辑与场景内拖拽共用)。\n *\n * 与 `setMission` 的区别:不整表替换、不重排,因此执行进度、其他航点的引用都保持原样 ——\n * 场景里拖动航点时每帧都要下发一次,用整表替换会连带重建整套三维标记。\n *\n * 返回值是**归一化并夹紧后**的航点(限高/限距),调用方拿它当权威值,别用自己算的原始值。\n */\n updateMissionWaypoint(index: number, patch: Partial<MissionWaypoint>): MissionWaypoint | null {\n if (this.missionStatus !== 'idle' || this.missionPending) return null\n const current = this.mission[index]\n if (!current) return null\n const next = this.normalizeWaypoint({ ...current, ...patch })\n this.constrainMissionWaypoint(next)\n this.mission[index] = next\n return next\n }\n\n /**\n * 插入一个航点(场景内双击地面新增)。index 越界则追加到末尾,返回实际插入位置。\n * 与真机一致:新航点的高度默认取相邻航点,避免插一个 0 米高、撞地的点。\n */\n addMissionWaypoint(waypoint: Partial<MissionWaypoint>, index = this.mission.length): number {\n if (this.missionStatus !== 'idle' || this.missionPending) {\n this.pushEvent('warn', '航线执行中无法新增航点,请先停止任务')\n return -1\n }\n const at = clamp(Math.round(index), 0, this.mission.length)\n const next = this.normalizeWaypoint(waypoint)\n this.constrainMissionWaypoint(next)\n const list = this.mission.slice()\n list.splice(at, 0, next)\n this.mission = list\n return at\n }\n\n /** 删除一个航点,返回是否删掉了 */\n removeMissionWaypoint(index: number): boolean {\n if (this.missionStatus !== 'idle' || this.missionPending) {\n this.pushEvent('warn', '航线执行中无法删除航点,请先停止任务')\n return false\n }\n if (index < 0 || index >= this.mission.length) return false\n const list = this.mission.slice()\n list.splice(index, 1)\n this.mission = list\n return true\n }\n\n /**\n * 把一个航点夹进可执行范围:高度不超限高,水平距离不超限距(超出则沿返航点方向缩回)。\n *\n * `unlimitedFlight` 打开时两步都跳过(上限为 `Infinity`),只有\"不低于 1 m\"仍保留。\n */\n private constrainMissionWaypoint(waypoint: MissionWaypoint): void {\n const ceiling = this.altitudeCeiling\n waypoint.altitude = Number.isFinite(ceiling) ? clamp(waypoint.altitude, 1, ceiling) : Math.max(1, waypoint.altitude)\n const offsetX = waypoint.x - this.home.x\n const offsetZ = waypoint.z - this.home.z\n const distance = Math.hypot(offsetX, offsetZ)\n const radius = this.distanceCeiling\n if (!Number.isFinite(radius) || distance <= radius) return\n const scale = radius / distance\n waypoint.x = this.home.x + offsetX * scale\n waypoint.z = this.home.z + offsetZ * scale\n }\n\n /** 增量更新航线任务的全局设置(只覆盖传入的字段) */\n setMissionConfig(patch: Partial<MissionConfig>): void {\n this.missionConfig = { ...this.missionConfig, ...patch }\n }\n\n /** 恢复出厂示例航线 */\n resetMissionToDefault(): void {\n this.setMission(DEFAULT_MISSION.map((waypoint) => ({ ...waypoint })))\n }\n\n /**\n * 启动航线任务。\n * - 地面待机时先自动起飞,到 1.2 米后自动接上航线(对应 Pilot 里的\"执行航线\");\n * - 已在空中则直接飞向第一个航点(正在返航会先中止返航);\n * - 姿态模式(无定位)拒绝执行 —— 航点任务依赖 GNSS,真机同样如此。\n */\n startMission(): boolean {\n if (this.missionStatus !== 'idle' || this.missionPending) return false\n if (this.mission.length === 0) {\n this.pushEvent('error', '航线任务启动失败:航点列表为空')\n return false\n }\n const problem = this.validateMission()\n if (problem) {\n this.pushEvent('error', `航线任务启动失败:${problem}`)\n return false\n }\n if (this.positionSource === 'atti') {\n this.pushEvent('error', '航线任务启动失败:当前无定位(姿态模式),航点任务需要 GNSS 定位')\n return false\n }\n if (this.phase === 'standby' || this.phase === 'motorsOn' || this.phase === 'takingOff') {\n const needsTakeOff = this.phase !== 'takingOff'\n this.missionPending = true\n if (needsTakeOff && !this.autoTakeOff()) {\n this.missionPending = false\n return false\n }\n this.pushEvent('info', `航线已下发(${this.mission.length} 个航点),起飞到位后自动开始执行`)\n return true\n }\n if (\n this.phase === 'landing' ||\n this.phase === 'emergency' ||\n this.phase === 'stopped' ||\n this.phase === 'powerOff'\n ) {\n this.pushEvent('error', `航线任务启动失败:当前阶段(${PHASE_LABELS[this.phase]})无法执行`)\n return false\n }\n if (this.phase === 'rth') this.cancelRth('执行航线任务')\n this.beginMissionPath()\n return true\n }\n\n /** 进入航线执行态(已在空中的入口) */\n /**\n * 进入航线执行态(已在空中 / 起飞到位后的入口)。\n * 无论是地面起飞接上航线还是空中直接下发,都先从「启航四步」走起 ——\n * 垂直调整到首航点高度 → 水平飞过去 → 高度收尾 → 对准航线方向,然后才开始推航点。\n */\n private beginMissionPath(): void {\n this.missionPending = false\n this.missionStatus = 'running'\n this.missionIndex = 0\n this.missionStage = 'depart-climb'\n this.missionHoverTimer = 0\n this.missionWaypointTimer = 0\n this.missionElapsed = 0\n this.missionPasses = 0\n this.missionStickGrace = 1\n this.missionPauseReason = ''\n this.missionStartHeading = this.heading\n this.missionTotalDistance = this.computeMissionDistanceLeft()\n this.phase = 'waypoint'\n this.phaseTime = 0\n const first = this.mission[0]\n this.pushEvent(\n 'success',\n `开始执行航线任务(${this.mission.length} 个航点 · 巡航 ${this.missionConfig.autoSpeed} m/s)${\n first ? ` · 先垂直调整到 ${first.altitude} 米` : ''\n }`,\n )\n }\n\n /** 暂停任务:交回手动控制并原地悬停,航点进度保留 */\n pauseMission(reason = '用户暂停'): boolean {\n if (this.missionStatus !== 'running') return false\n this.missionStatus = 'paused'\n this.missionPauseReason = reason\n this.missionPausedStage = this.missionStage\n this.velocity = { x: 0, y: 0, z: 0 }\n if (this.phase === 'waypoint') this.phase = 'flying'\n this.pushEvent('warn', `航线任务已暂停(${reason}),可手动接管;点「继续」回到当前航点`)\n return true\n }\n\n /** 继续任务:回到当前目标航点(首航点还没走完启航段的话接着走) */\n resumeMission(): boolean {\n if (this.missionStatus !== 'paused') return false\n this.missionStatus = 'running'\n this.missionPauseReason = ''\n this.missionStickGrace = 1\n this.missionWaypointTimer = 0\n // 暂停期间可能被手动飞走了:按当前位置重新判断启航段走到哪一步(幂等)\n this.missionStage = isMissionDepartStage(this.missionPausedStage)\n ? this.resolveMissionDepartStage()\n : 'goto'\n if (this.airborne) this.phase = 'waypoint'\n this.pushEvent('info', `航线任务继续执行(航点 ${this.missionIndex + 1}/${this.mission.length})`)\n return true\n }\n\n /** 停止任务:飞机原地悬停,航线归零(真机\"停止任务\"不返航) */\n stopMission(reason = '用户停止'): boolean {\n if (this.missionStatus === 'idle' && !this.missionPending) return false\n this.missionPending = false\n this.abortMission(reason)\n if (this.phase === 'waypoint') {\n this.phase = 'flying'\n this.velocity = { x: 0, y: 0, z: 0 }\n }\n return true\n }\n\n /** 中止任务(内部使用:返航/降落等更高优先级动作会走到这里) */\n private abortMission(reason: string): void {\n if (this.missionStatus === 'idle' && !this.missionPending) return\n this.missionStatus = 'idle'\n this.missionIndex = -1\n this.missionStage = 'idle'\n this.missionPending = false\n this.missionHoverTimer = 0\n this.missionWaypointTimer = 0\n this.missionPauseReason = ''\n this.pushEvent('warn', `航线任务已中止(${reason})`)\n }\n\n /** 航点合法性校验(限高限距):真机在上传航线时就会拦下这类航点;`unlimitedFlight` 打开时整段跳过 */\n private validateMission(): string | null {\n const ceiling = this.altitudeCeiling\n const radius = this.distanceCeiling\n if (!Number.isFinite(ceiling) && !Number.isFinite(radius)) return null\n for (let index = 0; index < this.mission.length; index += 1) {\n const waypoint = this.mission[index]\n if (!waypoint) continue\n if (Number.isFinite(ceiling) && waypoint.altitude > ceiling) {\n return `航点 ${index + 1} 高度 ${waypoint.altitude} m 超过限高 ${ceiling} m`\n }\n const distance = Math.hypot(waypoint.x - this.home.x, waypoint.z - this.home.z)\n if (Number.isFinite(radius) && distance > radius) {\n return `航点 ${index + 1} 距返航点 ${distance.toFixed(0)} m,超过限距 ${radius} m`\n }\n }\n return null\n }\n\n /**\n * 航线任务每帧推进。\n *\n * 真机飞航线的三个观感要点都在这里:\n * - **先对准再前进**:headingMode='auto' 时机头没转到航向之前原地悬停转向,否则会飞出一条弧线;\n * - **到点减速**:直线模式下按刹车距离提前收油,过点不甩过头;曲线模式则保持巡航速度、\n * 距航点 3 米就切下一个点,形成圆滑切角;\n * - **打杆即暂停**:执行中拨动摇杆 → 任务暂停并交回手动控制(与 Pilot 行为一致)。\n */\n private updateMission(delta: number): void {\n this.missionStickGrace = Math.max(0, this.missionStickGrace - delta)\n const stickMagnitude = Math.hypot(\n this.stick.pitch,\n this.stick.roll,\n this.stick.throttle,\n this.stick.yaw,\n )\n if (this.missionStickGrace <= 0 && stickMagnitude > 0.15) {\n this.pauseMission('检测到摇杆操作')\n return\n }\n // 航点任务依赖 GNSS:飞着飞着丢了定位,真机会把任务挂起(而不是蒙着眼继续飞)\n if (this.positionSource === 'atti') {\n this.pauseMission('GNSS 失锁,进入姿态模式')\n return\n }\n this.missionElapsed += delta\n\n const waypoint = this.mission[this.missionIndex]\n if (!waypoint) {\n this.finishMission()\n return\n }\n\n // 首航点走「启航四步」(垂直 → 水平 → 收高度 → 对准航线),和常规航点推进分开写\n if (isMissionDepartStage(this.missionStage)) {\n this.updateMissionDepart(delta)\n return\n }\n\n const spec = this.modeSpec\n const dx = waypoint.x - this.position.x\n const dz = waypoint.z - this.position.z\n const distance = Math.hypot(dx, dz)\n const altitudeError = waypoint.altitude - this.position.y\n const curved = this.missionConfig.pathMode === 'curved'\n\n if (this.missionStage === 'hover') {\n this.position.y = waypoint.altitude\n this.velocity.x = lerp(this.velocity.x, 0, Math.min(1, delta * 4))\n this.velocity.z = lerp(this.velocity.z, 0, Math.min(1, delta * 4))\n this.velocity.y = lerp(this.velocity.y, 0, Math.min(1, delta * 2))\n this.missionHoverTimer += delta\n if (this.missionHoverTimer >= waypoint.hoverSeconds) this.advanceMission()\n if (this.phase !== 'waypoint') return\n this.integratePosition(delta)\n return\n }\n\n // 机头:auto = 始终指向下一航点;fixed = 保持任务起始航向(相当于常说的\"锁定航向\")\n const scale = this.speedScale\n const targetHeading = distance > 0.4 ? radToDeg(Math.atan2(dx, -dz)) : this.heading\n const desiredHeading =\n this.missionConfig.headingMode === 'auto' ? targetHeading : this.missionStartHeading\n this.heading = this.turnToward(\n this.heading,\n desiredHeading,\n MISSION_YAW_RATE_DEG * scale * delta,\n )\n const headingError = Math.abs(((desiredHeading - this.heading + 540) % 360) - 180)\n\n // 高度与水平段同时收敛\n const desiredVertical = clamp(altitudeError, -spec.descendSpeed, spec.climbSpeed)\n this.velocity.y = lerp(this.velocity.y, desiredVertical, Math.min(1, delta * 2))\n\n // 航点速度(m/s)与巡航速度一起吃倍率:倍率下\"巡航 6 m/s\"就是 6×倍率\n const cruise =\n clamp(\n waypoint.speed > 0 ? waypoint.speed : this.missionConfig.autoSpeed,\n 0.5,\n Math.max(0.5, this.missionConfig.autoSpeed),\n ) * scale\n const aligned =\n this.missionConfig.headingMode !== 'auto' || headingError <= RTH_ALIGN_TOLERANCE_DEG\n // 刹车距离 = v²/(2a);+0.6 m 是留出的余量(收尾速度同样按倍率缩放)\n const brakeDistance = (cruise * cruise) / (2 * spec.brakeAccel) + 0.6 * scale\n let targetSpeed = 0\n if (aligned) {\n targetSpeed =\n !curved && distance < brakeDistance\n ? Math.max(0.6 * scale, cruise * (distance / brakeDistance))\n : cruise\n }\n const dirX = distance > 0.001 ? dx / distance : 0\n const dirZ = distance > 0.001 ? dz / distance : 0\n // 转向/变速平滑系数:曲线过点更跟手(2.6),直线更稳(1.8)\n const steer = Math.min(1, delta * (curved ? 2.6 : 1.8))\n this.velocity.x = lerp(this.velocity.x, dirX * targetSpeed, steer)\n this.velocity.z = lerp(this.velocity.z, dirZ * targetSpeed, steer)\n\n const arrived = curved\n ? distance <= MISSION_CURVED_TURN_RADIUS\n : distance <= MISSION_ARRIVE_RADIUS &&\n Math.abs(altitudeError) <= MISSION_ARRIVE_ALTITUDE_TOLERANCE\n\n if (arrived) {\n this.arriveWaypoint()\n if (this.phase !== 'waypoint') return\n this.integratePosition(delta)\n return\n }\n\n this.missionWaypointTimer += delta\n if (this.missionWaypointTimer > MISSION_WAYPOINT_TIMEOUT) {\n this.pushEvent('warn', `航点 ${this.missionIndex + 1} 长时间未到达,已跳过`)\n this.advanceMission()\n if (this.phase !== 'waypoint') return\n }\n this.integratePosition(delta)\n }\n\n /**\n * 启航段:飞往首航点的四步分段机动(与智能返航同款的分段逻辑)。\n *\n * 1. `depart-climb` 垂直调整到首航点高度 —— 这一段**只动高度**,水平速度锁死;\n * 2. `depart-cruise` 保持该高度水平飞向首航点,机头朝行进方向;\n * 3. `depart-settle` 到达后把高度收干净(水平位置锁死);\n * 4. `depart-align` 原地把机头转到与航线一致,然后才交回常规航点推进。\n */\n private updateMissionDepart(delta: number): void {\n const waypoint = this.mission[0]\n if (!waypoint) {\n this.finishMission()\n return\n }\n const spec = this.modeSpec\n const scale = this.speedScale\n const dx = waypoint.x - this.position.x\n const dz = waypoint.z - this.position.z\n const distance = Math.hypot(dx, dz)\n const altitudeError = waypoint.altitude - this.position.y\n\n this.missionWaypointTimer += delta\n if (this.missionWaypointTimer > MISSION_WAYPOINT_TIMEOUT) {\n this.pushEvent('warn', '首航点长时间未到达,跳过启航段直接执行航线')\n this.missionWaypointTimer = 0\n this.missionStage = 'goto'\n this.integratePosition(delta)\n return\n }\n\n if (this.missionStage === 'depart-climb') {\n // 垂直段:水平速度归零,只把高度送到首航点高度\n this.velocity.x = lerp(this.velocity.x, 0, Math.min(1, delta * 3))\n this.velocity.z = lerp(this.velocity.z, 0, Math.min(1, delta * 3))\n this.velocity.y = clamp(altitudeError, -spec.descendSpeed, spec.climbSpeed)\n if (Math.abs(altitudeError) <= MISSION_DEPART_ALTITUDE_TOLERANCE) {\n this.position.y = waypoint.altitude\n this.velocity.y = 0\n this.missionStage = 'depart-cruise'\n this.pushEvent('info', `已垂直调整到 ${waypoint.altitude} 米(首航点高度),水平飞向航点 1`)\n }\n this.integratePosition(delta)\n return\n }\n\n if (this.missionStage === 'depart-cruise') {\n // 水平段:高度锁在首航点高度,机头朝行进方向(auto 航向模式)\n this.velocity.y = clamp(altitudeError, -spec.descendSpeed, spec.climbSpeed)\n const cruise =\n clamp(\n waypoint.speed > 0 ? waypoint.speed : this.missionConfig.autoSpeed,\n 0.5,\n Math.max(0.5, this.missionConfig.autoSpeed),\n ) * scale\n // 刹车距离 = v²/(2a);+0.6 m 是留出的余量(收尾速度同样按倍率缩放)\n const brakeDistance = (cruise * cruise) / (2 * spec.brakeAccel) + 0.6 * scale\n const targetSpeed =\n distance < brakeDistance\n ? Math.max(0.4 * scale, cruise * (distance / brakeDistance))\n : cruise\n const dirX = distance > 0.001 ? dx / distance : 0\n const dirZ = distance > 0.001 ? dz / distance : 0\n const desiredHeading =\n this.missionConfig.headingMode === 'auto' && distance > 0.4\n ? radToDeg(Math.atan2(dx, -dz))\n : this.missionStartHeading\n this.heading = this.turnToward(\n this.heading,\n desiredHeading,\n MISSION_YAW_RATE_DEG * scale * delta,\n )\n const steer = Math.min(1, delta * 2.2)\n this.velocity.x = lerp(this.velocity.x, dirX * targetSpeed, steer)\n this.velocity.z = lerp(this.velocity.z, dirZ * targetSpeed, steer)\n if (distance <= MISSION_DEPART_ARRIVE_RADIUS) {\n this.velocity.x = 0\n this.velocity.z = 0\n this.missionStage = 'depart-settle'\n }\n this.integratePosition(delta)\n return\n }\n\n // 收尾与对准:水平位置已锁死,只动高度 / 只转身\n this.velocity.x = 0\n this.velocity.z = 0\n if (this.missionStage === 'depart-settle') {\n this.velocity.y = clamp(altitudeError, -spec.descendSpeed, spec.climbSpeed)\n if (Math.abs(altitudeError) <= MISSION_DEPART_SETTLE_TOLERANCE) {\n this.position.y = waypoint.altitude\n this.velocity.y = 0\n this.missionStage = 'depart-align'\n }\n this.integratePosition(delta)\n return\n }\n\n this.velocity.y = lerp(this.velocity.y, 0, Math.min(1, delta * 4))\n const routeHeading = this.missionRouteHeading(0)\n const desiredHeading =\n this.missionConfig.headingMode === 'auto' ? routeHeading : this.missionStartHeading\n this.heading = this.turnToward(\n this.heading,\n desiredHeading,\n MISSION_YAW_RATE_DEG * scale * delta,\n )\n const headingError = Math.abs(((desiredHeading - this.heading + 540) % 360) - 180)\n if (headingError <= MISSION_DEPART_ALIGN_TOLERANCE) {\n this.position.x = waypoint.x\n this.position.z = waypoint.z\n this.position.y = waypoint.altitude\n this.velocity = { x: 0, y: 0, z: 0 }\n this.missionStage = 'goto'\n this.missionWaypointTimer = 0\n this.pushEvent('success', `机头已对准航线方向(${desiredHeading.toFixed(0)}°),开始执行航线`)\n this.arriveWaypoint()\n }\n this.integratePosition(delta)\n }\n\n /**\n * 暂停后按当前位置反推启航段走到了哪一步(幂等,可反复调用)。\n * 判定顺序与启航四步一致:高度不对 → 先垂直;水平没到 → 再水平;高度没干净 → 收高度;否则对准。\n */\n private resolveMissionDepartStage(): MissionStage {\n const waypoint = this.mission[0]\n if (!waypoint) return 'goto'\n const distance = Math.hypot(waypoint.x - this.position.x, waypoint.z - this.position.z)\n const altitudeError = Math.abs(waypoint.altitude - this.position.y)\n if (altitudeError > MISSION_DEPART_ALTITUDE_TOLERANCE) return 'depart-climb'\n if (distance > MISSION_ARRIVE_RADIUS) return 'depart-cruise'\n if (altitudeError > MISSION_DEPART_SETTLE_TOLERANCE) return 'depart-settle'\n return 'depart-align'\n }\n\n /**\n * 航线在某个航点处的「出航方向」(罗盘角)。\n * 取该航点 → 下一个航点的方位;末航点没有出航段,退回上一段的方向(末点保持入航向)。\n */\n private missionRouteHeading(index: number): number {\n const current = this.mission[index]\n if (!current) return this.heading\n const next = this.mission[index + 1]\n if (next) return this.bearingBetween(current, next)\n const previous = this.mission[index - 1]\n if (previous) return this.bearingBetween(previous, current)\n return this.bearingBetween({ x: this.home.x, z: this.home.z }, current)\n }\n\n private bearingBetween(from: { x: number; z: number }, to: { x: number; z: number }): number {\n const dx = to.x - from.x\n const dz = to.z - from.z\n if (Math.hypot(dx, dz) < 0.01) return this.heading\n return radToDeg(Math.atan2(dx, -dz))\n }\n\n /** 到点:执行航点动作(云台 / 拍照 / 悬停),然后飞下一个点 */\n private arriveWaypoint(): void {\n const waypoint = this.mission[this.missionIndex]\n if (!waypoint) return\n const label = `航点 ${this.missionIndex + 1}/${this.mission.length}`\n this.missionWaypointTimer = 0\n if (waypoint.gimbalPitch !== null) this.setGimbalPitch(waypoint.gimbalPitch)\n if (waypoint.action === 'photo') this.takePhoto()\n\n // 曲线过点不减速,也就没有\"停下来悬停\"这回事\n const canHover = this.missionConfig.pathMode !== 'curved' && waypoint.hoverSeconds > 0\n const extras: string[] = []\n if (waypoint.gimbalPitch !== null) extras.push(`云台俯仰 ${waypoint.gimbalPitch}°`)\n if (canHover) extras.push(`悬停 ${waypoint.hoverSeconds} 秒`)\n this.pushEvent('success', `到达${label}${extras.length ? ` · 执行:${extras.join(' · ')}` : ''}`)\n\n if (!canHover) {\n this.advanceMission()\n return\n }\n this.missionStage = 'hover'\n this.missionHoverTimer = 0\n this.position.y = waypoint.altitude\n this.velocity = { x: 0, y: 0, z: 0 }\n }\n\n /** 推进到下一个航点;已到末尾则按结束动作收尾或循环 */\n private advanceMission(): void {\n this.missionStage = 'goto'\n this.missionHoverTimer = 0\n this.missionWaypointTimer = 0\n const next = this.missionIndex + 1\n if (next < this.mission.length) {\n this.missionIndex = next\n return\n }\n if (this.missionConfig.loop) {\n this.missionIndex = 0\n this.missionPasses += 1\n this.pushEvent('info', `航线完成第 ${this.missionPasses} 圈,循环执行`)\n return\n }\n this.finishMission()\n }\n\n /** 全部航点执行完毕:按配置悬停 / 返航 / 降落 */\n private finishMission(): void {\n const count = this.mission.length\n const passes = this.missionPasses + 1\n const elapsed = this.missionElapsed\n this.missionStatus = 'idle'\n this.missionIndex = -1\n this.missionStage = 'idle'\n this.missionHoverTimer = 0\n this.missionWaypointTimer = 0\n this.missionPauseReason = ''\n this.pushEvent(\n 'success',\n `航线任务完成(共 ${count} 个航点 · ${passes} 圈 · 用时 ${this.formatDuration(elapsed)})`,\n )\n switch (this.missionConfig.finishAction) {\n case 'rth':\n this.startRth('航线任务完成')\n break\n case 'land':\n this.startLanding()\n break\n default:\n this.phase = 'flying'\n this.velocity = { x: 0, y: 0, z: 0 }\n break\n }\n }\n\n /**\n * 航线是否处于\"已下发、等待起飞到位\"状态。\n * 地面点「执行航线」时 missionStatus 仍是 idle,真正的执行要等自动起飞到 1.2 米 ——\n * 这段时间里航点列表同样不许改(改了下发的还是旧航线),所以外部需要这个判据。\n */\n get missionPendingStart(): boolean {\n return this.missionPending\n }\n\n /** 剩余航程(米):沿剩余航点折线累计,含高度差 */\n get missionDistanceLeft(): number {\n if (this.missionStatus === 'idle' || this.missionIndex < 0) return 0\n return this.computeMissionDistanceLeft()\n }\n\n private computeMissionDistanceLeft(): number {\n let total = 0\n let previousX = this.position.x\n let previousY = this.position.y\n let previousZ = this.position.z\n for (let index = Math.max(0, this.missionIndex); index < this.mission.length; index += 1) {\n const waypoint = this.mission[index]\n if (!waypoint) continue\n total += Math.hypot(\n waypoint.x - previousX,\n waypoint.z - previousZ,\n waypoint.altitude - previousY,\n )\n previousX = waypoint.x\n previousY = waypoint.altitude\n previousZ = waypoint.z\n }\n return total\n }\n\n /** 航点完成度 0~1(按航程折算,暂停期间保持不变) */\n get missionProgress(): number {\n if (this.missionStatus === 'idle' || this.missionTotalDistance <= 0) return 0\n const done = 1 - this.computeMissionDistanceLeft() / this.missionTotalDistance\n return clamp(done, 0, 1)\n }\n\n /** 预计剩余时间(秒):剩余航程 / 巡航速度 + 剩余悬停时间 */\n get missionEtaSeconds(): number {\n if (this.missionStatus === 'idle' || this.missionIndex < 0) return 0\n // 巡航速度同样吃倍率:倍率越高预计耗时越短(0 倍率下速度取极小值,ETA 给出一个有限的大数)\n const speed = Math.max(0.5, this.missionConfig.autoSpeed) * Math.max(this.speedScale, 1e-3)\n let hover = 0\n for (let index = this.missionIndex; index < this.mission.length; index += 1) {\n const waypoint = this.mission[index]\n if (waypoint) hover += waypoint.hoverSeconds\n }\n return this.missionDistanceLeft / speed + hover\n }\n\n // ————————————————————————————— GNSS / 视觉定位 —————————————————————————————\n\n private updateGnss(delta: number): void {\n const electrified = this.phase !== 'powerOff'\n let target = 0\n if (electrified && !this.faults.gnssLost) {\n target = this.faults.compassError ? 12 : 18\n // 60 m 以上目标星数少 1 颗(真机高空的常见现象)\n if (this.position.y > 60) target -= 1\n }\n if (this.satellites < target) {\n this.satellites = Math.min(target, this.satellites + this.config.gnssAcquireRate * delta)\n } else if (this.satellites > target) {\n this.satellites = Math.max(\n target,\n this.satellites - this.config.gnssAcquireRate * 2.5 * delta,\n )\n }\n\n const visionCapable =\n !this.faults.visionLost && !this.faults.imuError && this.position.y < VISION_MAX_ALTITUDE\n this.visionAvailable = visionCapable && this.phase !== 'powerOff'\n\n if (this.faults.rcLost) {\n this.rcLinkQuality = Math.max(0, this.rcLinkQuality - delta * 1.6)\n } else {\n // 2000 m 为信号衰减参考距离,最多扣 60% 信号质量\n const distancePenalty = clamp(this.distanceToHome / 2000, 0, 0.6)\n this.rcLinkQuality = clamp(1 - distancePenalty, 0.35, 1)\n }\n }\n\n /** 水平精度因子(越小越好);无星时返回 99 表示不可用 */\n get hdop(): number {\n if (this.satellites <= 0) return 99\n // 1.6 是基准值,每多一颗星改善 0.06;失锁时 +4 直接变差\n return clamp(1.6 - this.satellites * 0.06 + (this.faults.gnssLost ? 4 : 0), 0.5, 5)\n }\n\n /** GPS 信号格数 0~5(按卫星数与 hdop 折算,面板显示用) */\n get gpsBars(): number {\n if (this.satellites <= 0 || this.faults.gnssLost) return 0\n if (this.satellites >= 14) return 5\n if (this.satellites >= 11) return 4\n if (this.satellites >= 8) return 3\n if (this.satellites >= 6) return 2\n return 1\n }\n\n /** 遥控信号格数 0~5(按链路质量折算,面板显示用) */\n get rcBars(): number {\n return Math.round(clamp(this.rcLinkQuality, 0, 1) * 5)\n }\n\n /** 距返航点水平距离(米) */\n get distanceToHome(): number {\n return Math.hypot(this.position.x - this.home.x, this.position.z - this.home.z)\n }\n\n /** 返航点(起飞点)水平坐标 */\n get homePosition(): { x: number; z: number } {\n return { ...this.home }\n }\n\n /** 是否已记录返航点(起飞那一刻记下)。界面据此决定画不画 H 标记与距离读数 */\n get homeReady(): boolean {\n return this.homeRecorded\n }\n\n /** 面板调试用:直接把电量设成某个百分比 */\n forceBatteryLevel(percent: number): void {\n const clamped = clamp(percent, 0, 100)\n this.batteryUsedWh = ((100 - clamped) / 100) * DRONE_SPEC.batteryWh\n }\n\n // ————————————————————————————— 电池 —————————————————————————————\n\n private updateBattery(delta: number): void {\n if (this.phase === 'powerOff') {\n // 断电后电池自然降到环境温度 24 ℃(0.02 为很慢的时间常数)\n this.batteryTemp = lerp(this.batteryTemp, 24, Math.min(1, delta * 0.02))\n return\n }\n const watts = this.currentPowerWatts\n // 能耗换算:W·s / 3600 = Wh\n this.batteryUsedWh += (watts * delta) / 3600\n // 60 W 为发热归一化参考;升空后散热差,温升上限更高(28→50 ℃ 区间)\n const loadHeat = clamp(watts / 60, 0, 1)\n const targetTemp = this.airborne ? 28 + loadHeat * 22 : 24 + loadHeat * 6\n this.batteryTemp = lerp(this.batteryTemp, targetTemp, Math.min(1, delta * 0.02))\n if (this.airborne) this.flightTime += delta\n }\n\n /** 是否触发低电量(将进入智能返航) */\n get lowBattery(): boolean {\n return this.batteryPercent <= this.config.lowBatteryPercent\n }\n\n /** 是否触发严重低电量(将强制降落) */\n get criticalBattery(): boolean {\n return this.batteryPercent <= this.config.criticalBatteryPercent\n }\n\n // ————————————————————————————— 失效保护 —————————————————————————————\n\n private updateFailsafe(delta: number): void {\n // 遥控失联 → 按配置动作\n if (this.faults.rcLost) {\n this.rcLostTimer += delta\n if (this.rcLostTimer > RC_FAILSAFE_DELAY && this.airborne && this.phase !== 'rth') {\n if (this.config.rcFailsafe === 'rth') {\n this.startRth('遥控信号丢失')\n this.pushEvent('warn', '遥控器失联超过 3 秒,已自动执行返航')\n } else if (this.config.rcFailsafe === 'land') {\n this.startLanding()\n this.pushEvent('warn', '遥控器失联,原地降落')\n } else {\n this.pushEvent('warn', '遥控器失联,保持悬停等待信号恢复')\n }\n }\n } else {\n this.rcLostTimer = 0\n }\n\n // 低电量:倒计时后自动返航\n if (this.airborne && this.lowBattery && !this.criticalBattery && this.phase === 'flying') {\n if (this.lowBatteryCountdown <= 0) {\n this.lowBatteryCountdown = LOW_BATTERY_COUNTDOWN\n this.pushEvent(\n 'warn',\n `电量低于 ${this.config.lowBatteryPercent}%,${LOW_BATTERY_COUNTDOWN} 秒后自动返航`,\n )\n this.warnOnce('lowBattery', '低电量:请尽快返航')\n } else {\n this.lowBatteryCountdown -= delta\n if (this.lowBatteryCountdown <= 0) {\n this.lowBatteryCountdown = 0\n if (!this.startRth('低电量返航')) {\n this.pushEvent('error', '无法触发返航')\n }\n }\n }\n } else if (!this.lowBattery) {\n this.lowBatteryCountdown = 0\n }\n\n // 严重低电量:强制原地降落\n if (\n this.airborne &&\n this.criticalBattery &&\n this.phase !== 'landing' &&\n this.phase !== 'emergency'\n ) {\n this.warnOnce('criticalBattery', '严重低电量:强制降落')\n this.startLanding()\n this.pushEvent('error', '严重低电量,已强制降落')\n }\n\n if (this.batteryPercent <= 0 && this.airborne) {\n this.phase = 'emergency'\n this.pushEvent('error', '电池耗尽,动力失效')\n }\n }\n\n // ————————————————————————————— 避障 —————————————————————————————\n\n private applyObstacleBrake(): void {\n this.obstacleReport = {\n forward: null,\n backward: null,\n left: null,\n right: null,\n up: null,\n down: null,\n braking: false,\n brakingDirection: null,\n }\n // 下视距离:默认就是离地高度(地面本身也算\"下方障碍\")\n this.obstacleReport.down = this.position.y\n const enabled = !this.faults.obstacleAvoidanceOff\n if (!enabled || this.obstacles.length === 0) return\n\n // 1.6 m:把飞行器当成半径 1.6 m 的球,盒子按此半径膨胀后再做射线检测\n const probeRadius = 1.6\n const origin = this.position\n const nose = headingVector(this.heading)\n const right = { x: -nose.z, z: nose.x }\n const directions: Array<{ key: keyof ObstacleReport; x: number; y: number; z: number }> = [\n { key: 'forward', x: nose.x, y: 0, z: nose.z },\n { key: 'backward', x: -nose.x, y: 0, z: -nose.z },\n { key: 'right', x: right.x, y: 0, z: right.z },\n { key: 'left', x: -right.x, y: 0, z: -right.z },\n { key: 'up', x: 0, y: 1, z: 0 },\n { key: 'down', x: 0, y: -1, z: 0 },\n ]\n\n for (const direction of directions) {\n let nearest: number | null = null\n for (const box of this.obstacles) {\n const distance = rayBoxDistance(origin, direction, box, probeRadius)\n if (distance === null) continue\n if (nearest === null || distance < nearest) nearest = distance\n }\n ;(this.obstacleReport[direction.key] as number | null) = nearest\n }\n\n // 迎面障碍在刹停距离内 → 直接刹停\n const horizontal: Array<'forward' | 'backward' | 'left' | 'right'> = [\n 'forward',\n 'backward',\n 'left',\n 'right',\n ]\n for (const key of horizontal) {\n const distance = this.obstacleReport[key]\n if (distance === null) continue\n const axis = directions.find((item) => item.key === key)\n if (!axis) continue\n const closing = this.velocity.x * axis.x + this.velocity.z * axis.z\n // 只有正在朝障碍物接近(closing > 0.15 m/s)才需要避障\n if (closing <= 0.15) continue\n // 同航线:刹停距离 = v²/(2a) + 余量\n const brakingDistance = (closing * closing) / (2 * this.modeSpec.brakeAccel) + 0.6\n if (distance <= brakingDistance) {\n this.velocity.x -= axis.x * closing\n this.velocity.z -= axis.z * closing\n this.obstacleReport.braking = true\n this.obstacleReport.brakingDirection = key\n this.warnOnce('obstacle', '避障刹停:前方障碍物过近')\n }\n }\n }\n\n // ———————————————————————— 机身原地自转(全景) ————————————————————————\n\n /**\n * 让机身**原地自转**到绝对方位角 `headingDeg`(度,罗盘角);传 `null` 停止自转。\n *\n * 为什么需要它:Mini 4 Pro 的云台 Pan 机械行程只有 ±30°,靠云台转不出 180° 水平视野。\n * 真机拍 180°/360° 全景是**机体原地自转**补水平、云台只做俯仰分层\n * (见 `drone-panorama`),所以水平网格必须施加在机身上,而不是取景相机上。\n *\n * 只改航向:`applyYaw` 里自转优先于摇杆偏航,位置照常由定点保持锁住 —— 转的过程中\n * 机体不位移(\"原地\"的含义)。角速度同样吃 `speedScale`(倍率 0 时自转也停住)。\n */\n spinTo(headingDeg: number | null, rateDeg = BODY_SPIN_RATE_DEG): void {\n this.spinHeading = headingDeg === null ? null : normalizeHeading(headingDeg)\n this.spinRateDeg = rateDeg\n }\n\n /** 自转目标方位角(度,罗盘角);没有在自转时返回 null */\n get spinTarget(): number | null {\n return this.spinHeading\n }\n\n /** 是否正在自转(目标未清除即为是,到位后目标仍在,直到调用方 `spinTo(null)`) */\n get spinning(): boolean {\n return this.spinHeading !== null\n }\n\n /** 自转是否已对准目标(容差 0.5°;没有自转时恒为 true) */\n get spinAligned(): boolean {\n const target = this.spinHeading\n if (target === null) return true\n return Math.abs(((target - this.heading + 540) % 360) - 180) <= BODY_SPIN_TOLERANCE_DEG\n }\n\n // ————————————————————————————— 云台 / 相机 —————————————————————————————\n\n /** 手动云台俯仰增量(度),正 = 抬头 */\n nudgeGimbal(deltaPitch: number, deltaYaw = 0): void {\n this.gimbalPitch = clamp(\n this.gimbalPitch + deltaPitch,\n DRONE_SPEC.gimbalPitchMin,\n DRONE_SPEC.gimbalPitchMax,\n )\n this.gimbalYaw = clamp(\n this.gimbalYaw + deltaYaw,\n -DRONE_SPEC.gimbalYawRange,\n DRONE_SPEC.gimbalYawRange,\n )\n }\n\n /** 设置云台俯仰角(度,正 = 抬头),自动夹到机型行程内 */\n setGimbalPitch(pitch: number): void {\n this.gimbalPitch = clamp(pitch, DRONE_SPEC.gimbalPitchMin, DRONE_SPEC.gimbalPitchMax)\n }\n\n private updateCamera(delta: number): void {\n // 云台横滚由 updateTilt 增稳,偏航缓慢回中\n this.gimbalYaw = lerp(this.gimbalYaw, 0, Math.min(1, delta * 1.2))\n }\n\n /** 切换录像开关;返回切换后是否在录 */\n toggleRecording(): void {\n this.setRecordingState(!this.recording)\n }\n\n /**\n * 录像状态的唯一写入口(UI/引擎都走这里):\n * 只负责状态、计时与事件日志;真正的录制(MediaRecorder)在 drone-fly 层,\n * 由它按本状态启动/收尾,避免两处状态打架。\n */\n setRecordingState(active: boolean): void {\n if (active === this.recording) return\n if (active) {\n this.recording = true\n this.recordSeconds = 0\n this.pushEvent('info', '开始录像')\n } else {\n this.recording = false\n this.pushEvent('info', `录像结束,时长 ${this.formatDuration(this.recordSeconds)}`)\n }\n }\n\n /** 停止录像并收尾成片(已在录才有效) */\n stopRecording(): void {\n this.setRecordingState(false)\n }\n\n /** 拍照:计数 +1 并记一条事件(实际成像由引擎层负责) */\n takePhoto(): void {\n this.photoCount += 1\n this.pushEvent('info', `拍照(第 ${this.photoCount} 张)`)\n }\n\n /** 设置变焦倍数,夹在 1~4 倍之间 */\n setZoom(zoom: number): void {\n this.cameraZoom = clamp(zoom, 1, 4)\n }\n\n // ————————————————————————————— 起飞前检查 —————————————————————————————\n\n /** 起飞前检查项列表(外部注入项 + 内置项),每次读取即时构造 */\n get checklist(): ChecklistItem[] {\n const items: ChecklistItem[] = []\n for (const external of this.externalChecks) items.push(external)\n items.push({\n id: 'imu',\n label: 'IMU 惯性测量单元',\n ok: !this.faults.imuError,\n detail: this.faults.imuError ? 'IMU 异常,禁止起飞' : '正常',\n blocking: true,\n })\n items.push({\n id: 'compass',\n label: '指南针',\n ok: !this.faults.compassError,\n detail: this.faults.compassError ? '受磁干扰,请重新校准' : '正常',\n blocking: true,\n })\n const positioning = this.satellites >= 6\n items.push({\n id: 'gnss',\n label: '定位系统',\n ok: positioning || this.visionAvailable,\n detail: positioning\n ? `GNSS ${Math.round(this.satellites)} 星 · HDOP ${this.hdop.toFixed(1)}`\n : this.visionAvailable\n ? '仅视觉定位(建议等待搜星完成)'\n : '无 GNSS 无视觉定位',\n blocking: !positioning && !this.visionAvailable,\n })\n items.push({\n id: 'home',\n label: '返航点',\n ok: this.homeRecorded,\n detail: this.homeRecorded ? '已记录' : '等待记录',\n blocking: true,\n })\n items.push({\n id: 'battery',\n label: '电池电量',\n ok: this.batteryPercent > this.config.criticalBatteryPercent + 3,\n detail: `${this.batteryPercent.toFixed(0)}% · ${this.batteryVoltage.toFixed(1)} V · ${this.batteryTemp.toFixed(0)} ℃`,\n blocking: this.batteryPercent <= this.config.criticalBatteryPercent,\n })\n items.push({\n id: 'wind',\n label: '风速',\n ok: this.config.windSpeed <= DRONE_SPEC.maxWindResistance,\n detail: `${this.config.windSpeed.toFixed(1)} m/s(抗风上限 ${DRONE_SPEC.maxWindResistance} m/s)`,\n blocking: false,\n })\n const airborneReady = this.phase === 'standby' || this.phase === 'motorsOn'\n items.push({\n id: 'phase',\n label: '飞行器状态',\n ok: airborneReady,\n detail: PHASE_LABELS[this.phase],\n blocking: !airborneReady,\n })\n return items\n }\n\n /** 是否满足起飞条件:阶段为待机/电机已启动,且所有阻塞项都通过 */\n get canTakeOff(): boolean {\n return (\n (this.phase === 'standby' || this.phase === 'motorsOn') &&\n this.checklist.every((item) => !item.blocking || item.ok)\n )\n }\n\n // ————————————————————————————— 警告 / 事件 —————————————————————————————\n\n /** 当前生效的警告文案列表(界面红条用) */\n get warnings(): string[] {\n const list: string[] = []\n if (this.faults.imuError) list.push('IMU 异常')\n if (this.faults.compassError) list.push('指南针受扰')\n if (this.faults.gnssLost) list.push('GNSS 失锁')\n if (this.faults.rcLost) list.push('遥控信号丢失')\n if (this.faults.visionLost) list.push('下视视觉失效')\n if (this.faults.motorFailure) list.push('电机异常')\n if (this.faults.obstacleAvoidanceOff) list.push('避障已关闭')\n if (this.positionSource === 'atti') list.push('姿态模式 · 无定位')\n else if (this.positionSource === 'vision') list.push('视觉定位')\n if (this.config.windSpeed > DRONE_SPEC.maxWindResistance) list.push('风速超过抗风上限')\n if (this.altitudeLimitReached) list.push('已达限高')\n if (this.distanceLimitReached) list.push('已达限距')\n if (this.obstacleReport.braking) list.push('避障刹停')\n if (this.lowBattery && !this.criticalBattery) list.push('低电量')\n if (this.criticalBattery) list.push('严重低电量')\n return list\n }\n\n private warnOnce(key: string, text: string): void {\n const last = this.warnedAt.get(key) ?? -Infinity\n // 同一 key 的警告 6 s 内只记一次,防止每帧刷屏\n if (this.time - last < 6) return\n this.warnedAt.set(key, this.time)\n this.pushEventInternal('warn', text)\n }\n\n private damageCheck(): void {\n if (this.config.windSpeed > DRONE_SPEC.maxWindResistance) {\n this.pushEvent('warn', '风速超过最大抗风能力,谨慎飞行')\n }\n }\n\n /** 追加一条事件(面板事件流;level 决定颜色) */\n pushEvent(level: EventLevel, text: string): void {\n this.pushEventInternal(level, text)\n }\n\n private pushEventInternal(level: EventLevel, text: string): void {\n this.eventId += 1\n // unshift:最新事件在数组头部 events[0];超出上限则从尾部丢最旧的\n this.events.unshift({ id: this.eventId, time: this.time, level, text })\n if (this.events.length > 200) this.events.pop()\n }\n\n private formatDuration(seconds: number): string {\n const total = Math.max(0, Math.round(seconds))\n const minutes = Math.floor(total / 60)\n const rest = total % 60\n return `${minutes} 分 ${String(rest).padStart(2, '0')} 秒`\n }\n\n private windVector(): { x: number; z: number } {\n const speed = this.config.windSpeed\n if (speed <= 0) return { x: 0, z: 0 }\n const dir = headingVector(this.config.windDirection)\n return { x: dir.x * speed, z: dir.z * speed }\n }\n\n /** 风向相对机头的描述,例如\"正逆风\" */\n get windRelative(): string {\n if (this.config.windSpeed < 0.3) return '静风'\n // windDirection 记录的是\"风吹向\"的方位;+180° 换成风的来向再与机头比\n const fromDrone = this.config.windDirection + 180\n const diff = ((fromDrone - this.heading + 540) % 360) - 180\n const abs = Math.abs(diff)\n const side = diff >= 0 ? '右' : '左'\n if (abs < 20) return '正逆风'\n if (abs > 160) return '正顺风'\n if (abs <= 60) return `逆风偏${side} ${Math.round(abs)}°`\n if (abs >= 120) return `顺风偏${side} ${Math.round(180 - abs)}°`\n return `侧风偏${side} ${Math.round(abs)}°`\n }\n\n // ————————————————————————————— 快照 —————————————————————————————\n\n /** 生成完整遥测快照(数组字段已复制,见 DroneSnapshot) */\n snapshot(): DroneSnapshot {\n return {\n phase: this.phase,\n phaseLabel: this.phaseLabel,\n mode: this.mode,\n modeLabel: this.modeSpec.label,\n speedScale: this.speedScale,\n positionSource: this.positionSource,\n positionSourceLabel: POSITION_SOURCE_LABELS[this.positionSource],\n airborne: this.airborne,\n motorsOn: this.motorLoad > 0.02,\n damaged: this.damaged,\n batteryPercent: this.batteryPercent,\n batteryVoltage: this.batteryVoltage,\n batteryCurrent: this.batteryCurrent,\n batteryTemp: this.batteryTemp,\n batteryWhLeft: (this.batteryPercent / 100) * DRONE_SPEC.batteryWh,\n batteryModeText: this.criticalBattery ? '严重低电量' : this.lowBattery ? '低电量' : '正常',\n altitude: this.position.y,\n altitudeMax: this.altitudeMax,\n positionX: this.position.x,\n positionZ: this.position.z,\n horizontalSpeed: Math.hypot(this.velocity.x, this.velocity.z),\n verticalSpeed: this.velocity.y,\n heading: this.heading,\n tiltPitch: this.tiltPitch,\n tiltRoll: this.tiltRoll,\n distanceToHome: this.distanceToHome,\n homeRecorded: this.homeRecorded,\n altitudeLimitReached: this.altitudeLimitReached,\n distanceLimitReached: this.distanceLimitReached,\n satellites: this.satellites,\n hdop: this.hdop,\n gpsBars: this.gpsBars,\n rcBars: this.rcBars,\n visionAvailable: this.visionAvailable,\n gimbalPitch: this.gimbalPitch,\n gimbalRoll: this.gimbalRoll,\n gimbalYaw: this.gimbalYaw,\n motorLoad: this.motorLoad,\n motorSpeeds: { ...this.motorSpeeds },\n armFold: this.armFold,\n stick: { ...this.stick },\n windSpeed: this.config.windSpeed,\n windDirection: this.config.windDirection,\n windRelative: this.windRelative,\n flightTime: this.flightTime,\n totalTime: this.time,\n remainingMinutes: this.remainingMinutes,\n lowBattery: this.lowBattery,\n criticalBattery: this.criticalBattery,\n lowBatteryCountdown: this.lowBatteryCountdown,\n rthStage: this.phase === 'rth' ? this.rthStage : '',\n landingStage: this.phase === 'landing' ? this.landingStage : '',\n rthReason: this.rthReason,\n warnings: this.warnings,\n // 快照只带最近 12 条,避免每帧复制整段日志\n events: this.events.slice(0, 12),\n checklist: this.checklist,\n obstacle: { ...this.obstacleReport },\n recording: this.recording,\n recordSeconds: this.recordSeconds,\n photoCount: this.photoCount,\n cameraZoom: this.cameraZoom,\n mission: {\n status: this.missionStatus,\n statusLabel: MISSION_STATUS_LABELS[this.missionStatus],\n stage: this.missionStage,\n stageLabel: MISSION_STAGE_LABELS[this.missionStage],\n index: this.missionIndex,\n total: this.mission.length,\n passes: this.missionPasses,\n elapsed: this.missionElapsed,\n distanceLeft: this.missionDistanceLeft,\n progress: this.missionProgress,\n etaSeconds: this.missionEtaSeconds,\n pauseReason: this.missionPauseReason,\n active: this.mission[this.missionIndex]\n ? { ...(this.mission[this.missionIndex] as MissionWaypoint) }\n : null,\n waypoints: this.mission.map((waypoint) => ({ ...waypoint })),\n config: { ...this.missionConfig },\n },\n }\n }\n\n /** 仿真累计时长(秒):含地面上电时间,与飞行时长 flightTime 不同 */\n get elapsed(): number {\n return this.time\n }\n}\n\n/** 从 origin 沿 direction 到 AABB 表面的距离(null = 射线不与盒体相交) */\nfunction rayBoxDistance(\n origin: { x: number; y: number; z: number },\n direction: { x: number; y: number; z: number },\n box: ObstacleBox,\n radius: number,\n): number | null {\n const minX = box.minX - radius\n const maxX = box.maxX + radius\n const minY = box.minY - radius\n const maxY = box.maxY + radius\n const minZ = box.minZ - radius\n const maxZ = box.maxZ + radius\n\n // 标准 slab 法:逐轴求射线进入/离开盒体的参数 t,取各轴交集\n let tMin = 0\n let tMax = Number.POSITIVE_INFINITY\n const axes: Array<[number, number, number, number]> = [\n [origin.x, direction.x, minX, maxX],\n [origin.y, direction.y, minY, maxY],\n [origin.z, direction.z, minZ, maxZ],\n ]\n for (const [start, dir, low, high] of axes) {\n if (Math.abs(dir) < 1e-6) {\n if (start < low || start > high) return null\n continue\n }\n let t1 = (low - start) / dir\n let t2 = (high - start) / dir\n if (t1 > t2) [t1, t2] = [t2, t1]\n tMin = Math.max(tMin, t1)\n tMax = Math.min(tMax, t2)\n if (tMin > tMax) return null\n }\n // 起点在盒内时视为 0\n return tMin >= 0 ? tMin : null\n}\n","import * as THREE from 'three'\nimport type { ObstacleBox } from './drone-sim'\n\n/**\n * 飞行沙盒的场景辅助:地面网格、返航点标记、障碍物、航迹尾迹。\n *\n * 障碍物同时喂给仿真内核做避障判定(纯 AABB),所以可视化与物理用同一份数据,\n * 不会出现\"看着撞上了但没刹停\"这类不一致。\n */\n\n/** 一个障碍物(AABB 盒)规格:x/z 为中心平面坐标,width/depth/height 单位为米,color 为 0xRRGGBB */\nexport interface ObstacleSpec {\n name: string\n label: string\n x: number\n z: number\n width: number\n depth: number\n height: number\n color: number\n /** 底面离地高度(米)。默认 0 = 落地障碍物;>0 = 悬空障碍物(天桥等),机体上方测距用 */\n baseY?: number\n}\n\n/** 默认障碍物:两组建筑 + 一根正前方灯杆 + 三棵树 + 一道悬空天桥,都在起飞点 30 米内 */\nexport const DEFAULT_OBSTACLES: ObstacleSpec[] = [\n {\n name: 'BUILDING_A',\n label: '建筑 A',\n x: 16,\n z: -12,\n width: 9,\n depth: 9,\n height: 13,\n color: 0x2c3a44,\n },\n {\n name: 'BUILDING_B',\n label: '建筑 B',\n x: -20,\n z: 10,\n width: 11,\n depth: 15,\n height: 7,\n color: 0x2a3740,\n },\n // 正前方细柱(灯杆,直径 0.3 m):验证\"正前方中线\"盲区是否真的被消除(两条镜头视场在中线重叠)。\n // 放 16m:在 18m 量程内,又不至于挡住起飞点正前方的平飞测试路径\n {\n name: 'LAMP_POST',\n label: '灯杆',\n x: 0,\n z: -16,\n width: 0.3,\n depth: 0.3,\n height: 8,\n color: 0x44535f,\n },\n // 悬空天桥:桥底离地 7.5 m,横跨起飞点后方(x 向 18 m 宽),用来演示机体上方的视觉测距\n {\n name: 'SKY_BRIDGE',\n label: '天桥',\n x: 0,\n z: 10.5,\n width: 18,\n depth: 6,\n height: 2,\n color: 0x3a4a55,\n baseY: 7.5,\n },\n {\n name: 'TREE_1',\n label: '树 1',\n x: 9,\n z: 14,\n width: 3.2,\n depth: 3.2,\n height: 6,\n color: 0x24443a,\n },\n {\n name: 'TREE_2',\n label: '树 2',\n x: -10,\n z: -17,\n width: 2.8,\n depth: 2.8,\n height: 4.6,\n color: 0x21403a,\n },\n { name: 'TREE_3', label: '树 3', x: 24, z: 7, width: 3, depth: 3, height: 5.4, color: 0x24443a },\n]\n\n/** 航线航点(可视化用):x/z 为水平坐标,altitude 为相对起飞点高度 */\nexport interface MissionMarker {\n x: number\n z: number\n altitude: number\n}\n\n/** 一个航点对应的三维标记节点:原位更新(场景里拖动)时逐项改这几个,不整层重建 */\ninterface MissionMarkerNode {\n group: THREE.Group\n /** 竖直光柱:几何体是\"单位高度、底部对齐\",用 scale.y 表示高度 */\n pillar: THREE.Mesh\n altitudeRing: THREE.Mesh\n label: THREE.Sprite\n /** 拾取体:沿光柱的一根不可见圆柱,点击光柱任意高度都能抓住航点 */\n pick: THREE.Mesh\n}\n\n/** 航迹缓冲最大点数:按每 0.25 m 取一点,2400 点约覆盖 600 m 航程,防止长航线把缓冲撑爆 */\nconst TRAIL_MAX_POINTS = 2400\n/** 航点配色:普通航点青色,当前目标琥珀色(与雷达/返航点区分开) */\nconst MISSION_COLOR = 0x39d0b0\nconst MISSION_ACTIVE_COLOR = 0xffc53d\n/** 场景编辑:选中航点亮青色,悬停航点淡白(与\"当前目标\"的琥珀色区分开) */\nconst MISSION_SELECT_COLOR = 0x9ef4ff\nconst MISSION_HOVER_COLOR = 0xdffbff\n/** 航点拾取半径(米):光柱本体只有 7 cm,得给一个手好抓的粗管子 */\nconst MISSION_PICK_RADIUS = 1.7\n\n/**\n * 场景辅助的创建与更新(见文件头):地面网格 / 返航点标记 / 障碍物 / 航迹,以及航线任务可视化。\n * 障碍物同时以 AABB 形式写入 collisionBoxes 供仿真避障使用,可视化与物理共用同一份数据。\n */\nexport class DroneWorld {\n private readonly scene: THREE.Scene\n private readonly group = new THREE.Group()\n private readonly homeMarker = new THREE.Object3D()\n private readonly gridHelpers: THREE.GridHelper[] = []\n private readonly obstacles: THREE.Object3D[] = []\n private readonly disposables: Array<THREE.BufferGeometry | THREE.Material | THREE.Texture> = []\n private readonly trailPositions: Float32Array\n private trailLine: THREE.Line | null = null\n private trailCount = 0\n private readonly lastTrailPoint = new THREE.Vector3(Number.NaN, 0, 0)\n\n // —— 航线任务可视化(航点标记 + 航线折线 + 当前目标 / 场景编辑高亮) ——\n private readonly missionGroup = new THREE.Group()\n /** 航点标记(地面环 / 光柱 / 高度环 / 序号牌),与航线折线分开控制显隐 */\n private readonly missionMarkerGroup = new THREE.Group()\n /** 航线折线 */\n private readonly missionPathGroup = new THREE.Group()\n private readonly missionActiveGroup = new THREE.Group()\n /** 场景编辑的选中高亮与悬停高亮 */\n private readonly missionSelectGroup = new THREE.Group()\n private readonly missionHoverGroup = new THREE.Group()\n /** 每次重建航点时产生的可释放资源(几何 / 贴图 / 材质) */\n private readonly missionDisposables: Array<\n THREE.BufferGeometry | THREE.Texture | THREE.Material\n > = []\n private readonly missionMarkerNodes: MissionMarkerNode[] = []\n /** 当前航点数据(原位更新时改它,并据此重算航线折线) */\n private readonly missionMarkers: MissionMarker[] = []\n private missionOrigin = { x: 0, z: 0 }\n private missionPathLine: THREE.Line | null = null\n private missionPathPositions: Float32Array | null = null\n private missionActiveAltitudeRing: THREE.Mesh | null = null\n private missionSelectAltitudeRing: THREE.Mesh | null = null\n private missionHoverAltitudeRing: THREE.Mesh | null = null\n /** 图层总开关 */\n private missionVisible = true\n /** 航点标记开关 */\n private missionMarkersVisible = true\n /** 航线折线开关 */\n private missionPathVisible = true\n private missionActiveIndex = -1\n private missionSelectIndex = -1\n private missionHoverIndex = -1\n /** 地面拾取用的水平面(y=0)与命中点缓存 */\n private readonly groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)\n private readonly groundHit = new THREE.Vector3()\n private readonly missionPillarMaterial = new THREE.MeshBasicMaterial({\n color: MISSION_COLOR,\n transparent: true,\n opacity: 0.26,\n side: THREE.DoubleSide,\n depthWrite: false,\n toneMapped: false,\n })\n private readonly missionRingMaterial = new THREE.MeshBasicMaterial({\n color: MISSION_COLOR,\n transparent: true,\n opacity: 0.72,\n side: THREE.DoubleSide,\n depthWrite: false,\n toneMapped: false,\n })\n private readonly missionActiveMaterial = new THREE.MeshBasicMaterial({\n color: MISSION_ACTIVE_COLOR,\n transparent: true,\n opacity: 0.9,\n side: THREE.DoubleSide,\n depthWrite: false,\n toneMapped: false,\n })\n private readonly missionSelectMaterial = new THREE.MeshBasicMaterial({\n color: MISSION_SELECT_COLOR,\n transparent: true,\n opacity: 0.95,\n side: THREE.DoubleSide,\n depthWrite: false,\n toneMapped: false,\n })\n private readonly missionHoverMaterial = new THREE.MeshBasicMaterial({\n color: MISSION_HOVER_COLOR,\n transparent: true,\n opacity: 0.42,\n side: THREE.DoubleSide,\n depthWrite: false,\n toneMapped: false,\n })\n /** 拾取用的不可见材质:光柱太细,给每根航点套一个粗管子专门用来抓 */\n private readonly missionPickMaterial = new THREE.MeshBasicMaterial({\n transparent: true,\n opacity: 0,\n side: THREE.DoubleSide,\n depthWrite: false,\n depthTest: false,\n })\n\n readonly collisionBoxes: ObstacleBox[] = []\n readonly specs: ObstacleSpec[]\n\n /** 构造地面世界:底板/网格/返航点标记内部建,障碍物与任务组可见性外部可切 */\n constructor(scene: THREE.Scene, specs: ObstacleSpec[] = DEFAULT_OBSTACLES) {\n this.scene = scene\n this.specs = specs\n\n // —— 地面:超大底板 + 1 米细网格 + 100 米粗网格,给出明确尺度参照 ——\n const groundGeometry = new THREE.PlaneGeometry(4000, 4000)\n const groundMaterial = new THREE.MeshStandardMaterial({\n color: 0x0a1216,\n roughness: 0.95,\n metalness: 0.05,\n })\n this.disposables.push(groundGeometry, groundMaterial)\n const ground = new THREE.Mesh(groundGeometry, groundMaterial)\n ground.rotation.x = -Math.PI / 2\n // 略微下沉,避免与 y≈0 的细网格、返航点标记同面闪烁(z-fighting)\n ground.position.y = -0.012\n ground.receiveShadow = true\n this.group.add(ground)\n\n const fineGrid = new THREE.GridHelper(200, 200, 0x1d3d42, 0x143033)\n fineGrid.position.y = 0\n this.tintGrid(fineGrid, 0.55)\n this.group.add(fineGrid)\n\n const coarseGrid = new THREE.GridHelper(2000, 20, 0x2f6f72, 0x1f4a4d)\n // 抬高 4 mm 盖在细网格之上,避免两张网格同面闪烁\n coarseGrid.position.y = 0.004\n this.tintGrid(coarseGrid, 0.75)\n this.group.add(coarseGrid)\n this.gridHelpers.push(fineGrid, coarseGrid)\n\n // —— 返航点标记:地面上一个 H 环 + 竖直光柱 ——\n const ringGeometry = new THREE.RingGeometry(1.1, 1.5, 40)\n ringGeometry.rotateX(-Math.PI / 2)\n const ringMaterial = new THREE.MeshBasicMaterial({\n color: 0x6ff0d0,\n transparent: true,\n opacity: 0.7,\n side: THREE.DoubleSide,\n toneMapped: false,\n })\n this.disposables.push(ringGeometry, ringMaterial)\n const ring = new THREE.Mesh(ringGeometry, ringMaterial)\n ring.position.y = 0.02\n this.homeMarker.add(ring)\n\n const beaconGeometry = new THREE.CylinderGeometry(0.045, 0.045, 3, 8, 1, true)\n beaconGeometry.translate(0, 1.5, 0)\n const beaconMaterial = new THREE.MeshBasicMaterial({\n color: 0x6ff0d0,\n transparent: true,\n opacity: 0.28,\n blending: THREE.AdditiveBlending,\n depthWrite: false,\n toneMapped: false,\n })\n this.disposables.push(beaconGeometry, beaconMaterial)\n const beacon = new THREE.Mesh(beaconGeometry, beaconMaterial)\n beacon.position.y = 0.02\n this.homeMarker.add(beacon)\n this.group.add(this.homeMarker)\n\n // —— 障碍物(含悬空件:baseY > 0,如天桥) ——\n for (const spec of specs) {\n const baseY = spec.baseY ?? 0\n const geometry = new THREE.BoxGeometry(spec.width, spec.height, spec.depth)\n const material = new THREE.MeshStandardMaterial({\n color: spec.color,\n roughness: 0.8,\n metalness: 0.12,\n })\n this.disposables.push(geometry, material)\n const box = new THREE.Mesh(geometry, material)\n box.position.set(spec.x, baseY + spec.height / 2, spec.z)\n box.castShadow = true\n box.receiveShadow = true\n box.name = spec.name\n this.group.add(box)\n this.obstacles.push(box)\n\n this.collisionBoxes.push({\n name: spec.label,\n minX: spec.x - spec.width / 2,\n maxX: spec.x + spec.width / 2,\n minY: baseY,\n maxY: baseY + spec.height,\n minZ: spec.z - spec.depth / 2,\n maxZ: spec.z + spec.depth / 2,\n solid: true,\n })\n }\n\n // —— 航迹尾迹 ——\n this.trailPositions = new Float32Array(TRAIL_MAX_POINTS * 3)\n const trailGeometry = new THREE.BufferGeometry()\n trailGeometry.setAttribute('position', new THREE.BufferAttribute(this.trailPositions, 3))\n trailGeometry.setDrawRange(0, 0)\n const trailMaterial = new THREE.LineBasicMaterial({\n color: 0x7fd8ff,\n transparent: true,\n opacity: 0.75,\n toneMapped: false,\n })\n this.disposables.push(trailGeometry, trailMaterial)\n this.trailLine = new THREE.Line(trailGeometry, trailMaterial)\n this.trailLine.frustumCulled = false\n this.group.add(this.trailLine)\n\n // —— 航线任务图层(空航线时不可见,内容由 setMissionWaypoints 填充) ——\n const activeGroundGeometry = new THREE.RingGeometry(2.5, 3.1, 40)\n activeGroundGeometry.rotateX(-Math.PI / 2)\n const activeGroundRing = new THREE.Mesh(activeGroundGeometry, this.missionActiveMaterial)\n activeGroundRing.position.y = 0.04\n this.missionActiveGroup.add(activeGroundRing)\n\n const activeAltitudeGeometry = new THREE.RingGeometry(1.25, 1.65, 28)\n activeAltitudeGeometry.rotateX(-Math.PI / 2)\n this.missionActiveAltitudeRing = new THREE.Mesh(\n activeAltitudeGeometry,\n this.missionActiveMaterial,\n )\n this.missionActiveGroup.add(this.missionActiveAltitudeRing)\n\n // 场景编辑高亮:选中(亮青,双层环)与悬停(淡白,细环)\n const selectGroundGeometry = new THREE.RingGeometry(2.3, 2.55, 40)\n selectGroundGeometry.rotateX(-Math.PI / 2)\n const selectGroundRing = new THREE.Mesh(selectGroundGeometry, this.missionSelectMaterial)\n selectGroundRing.position.y = 0.05\n this.missionSelectGroup.add(selectGroundRing)\n const selectAltitudeGeometry = new THREE.RingGeometry(1.15, 1.35, 28)\n selectAltitudeGeometry.rotateX(-Math.PI / 2)\n this.missionSelectAltitudeRing = new THREE.Mesh(\n selectAltitudeGeometry,\n this.missionSelectMaterial,\n )\n this.missionSelectGroup.add(this.missionSelectAltitudeRing)\n\n const hoverGroundGeometry = new THREE.RingGeometry(2.05, 2.2, 36)\n hoverGroundGeometry.rotateX(-Math.PI / 2)\n const hoverGroundRing = new THREE.Mesh(hoverGroundGeometry, this.missionHoverMaterial)\n hoverGroundRing.position.y = 0.05\n this.missionHoverGroup.add(hoverGroundRing)\n const hoverAltitudeGeometry = new THREE.RingGeometry(1.0, 1.12, 28)\n hoverAltitudeGeometry.rotateX(-Math.PI / 2)\n this.missionHoverAltitudeRing = new THREE.Mesh(hoverAltitudeGeometry, this.missionHoverMaterial)\n this.missionHoverGroup.add(this.missionHoverAltitudeRing)\n\n this.disposables.push(\n activeGroundGeometry,\n activeAltitudeGeometry,\n selectGroundGeometry,\n selectAltitudeGeometry,\n hoverGroundGeometry,\n hoverAltitudeGeometry,\n this.missionPillarMaterial,\n this.missionRingMaterial,\n this.missionActiveMaterial,\n this.missionSelectMaterial,\n this.missionHoverMaterial,\n this.missionPickMaterial,\n )\n this.missionActiveGroup.visible = false\n this.missionSelectGroup.visible = false\n this.missionHoverGroup.visible = false\n\n this.missionGroup.add(this.missionMarkerGroup)\n this.missionGroup.add(this.missionPathGroup)\n this.group.add(this.missionGroup)\n this.group.add(this.missionActiveGroup)\n this.group.add(this.missionSelectGroup)\n this.group.add(this.missionHoverGroup)\n\n scene.add(this.group)\n }\n\n private tintGrid(grid: THREE.GridHelper, opacity: number): void {\n const material = grid.material as THREE.Material | THREE.Material[]\n const list = Array.isArray(material) ? material : [material]\n list.forEach((item) => {\n item.transparent = true\n item.opacity = opacity\n item.depthWrite = false\n this.disposables.push(item)\n })\n }\n\n /** 记录航迹:每移动 0.25 米落一个点,避免长航线把缓冲撑爆 */\n pushTrail(x: number, y: number, z: number): void {\n if (Number.isNaN(this.lastTrailPoint.x)) {\n this.lastTrailPoint.set(x, y, z)\n }\n const moved = Math.hypot(\n x - this.lastTrailPoint.x,\n y - this.lastTrailPoint.y,\n z - this.lastTrailPoint.z,\n )\n if (moved < 0.25) return\n if (this.trailCount >= TRAIL_MAX_POINTS) return\n this.trailPositions[this.trailCount * 3] = x\n this.trailPositions[this.trailCount * 3 + 1] = y\n this.trailPositions[this.trailCount * 3 + 2] = z\n this.trailCount += 1\n this.lastTrailPoint.set(x, y, z)\n if (this.trailLine) {\n const attribute = this.trailLine.geometry.getAttribute('position') as THREE.BufferAttribute\n attribute.needsUpdate = true\n this.trailLine.geometry.setDrawRange(0, this.trailCount)\n this.trailLine.geometry.computeBoundingSphere()\n }\n }\n\n /** 清空航迹线(场景里的飞行尾迹与鹰眼图一起清) */\n clearTrail(): void {\n this.trailCount = 0\n this.lastTrailPoint.set(Number.NaN, 0, 0)\n if (this.trailLine) {\n this.trailLine.geometry.setDrawRange(0, 0)\n }\n }\n\n /**\n * 航迹尾迹快照(只读):`positions` 是 x/y/z 连续排布的缓冲,前 `count * 3` 个分量有效。\n * 鹰眼图(DroneMiniMap)靠它把已飞路径投到俯视图上;直接返回内部缓冲,调用方只读不改。\n */\n getTrail(): { positions: Float32Array; count: number } {\n return { positions: this.trailPositions, count: this.trailCount }\n }\n\n /** 显示/隐藏全部障碍物(碰撞盒保留,只影响渲染) */\n setObstaclesVisible(visible: boolean): void {\n this.obstacles.forEach((obstacle) => {\n obstacle.visible = visible\n })\n }\n\n /**\n * 采集辅助显隐 —— 拍照 / 录像取景时应临时隐藏的**场景标记**。\n *\n * 这些是给**用户**看的辅助图层,不是场景内容,却都长得像\"竖线\":\n * 返航点光柱(3m 高,就在机腹正下方,离镜头只有几厘米)、航迹尾迹、\n * 航点光柱(按航点高度拉到 30~40m,远看是一根从地面戳进天里的细线 + 序号牌)、\n * 地面网格(正对镜头的那条网格线会从机脚下一直汇聚到地平线,泛光一衬就是一根\n * 贯穿画面的亮线,转一个方位拍一张,全景里就复制出一排)。不藏起来,\n * 每张照片都带几根竖线,全景拼接后更是逐列复制。\n * 地面底板与障碍物是真实场景,不在其列。\n * 调用方负责配对(隐藏后必须在同一帧内恢复),成对的调用点在 DroneFly 的取景路径里。\n */\n setCaptureHelpersVisible(visible: boolean): void {\n this.homeMarker.visible = visible\n if (this.trailLine) this.trailLine.visible = visible\n this.missionGroup.visible = visible\n for (const grid of this.gridHelpers) grid.visible = visible\n }\n\n /**\n * 锥形视场检测(雷达/视觉传感器用):传感器是一台有视场的相机,不是一条无粗细的射线。\n *\n * 以 origin 为锥顶、axis 为瞄准轴、两个正交轴 u1/u2 上的半张角为 halfU1/halfU2,\n * 判断量程内哪些障碍物落在视场里,返回最近的距离与标签。\n * 判定方式:障碍 AABB 在锥坐标系里用\"支撑半径\"(Σ|d·axisᵢ|·halfᵢ)近似成盒,\n * 再做椭圆锥内部测试 (e1/(a·tanU1))² + (e2/(a·tanU2))² ≤ 1 —— 连续判定,\n * 于是不存在\"两条采样射线之间漏掉细柱\"的盲区。\n * 说明:只对障碍物 AABB 判定,不含地面(真机前视也不会把地面当成前方障碍)。\n * 与仿真内核的 AABB 避障相互独立:这里是\"传感器测量\",那里是\"飞控保护\"。\n */\n coneCast(\n origin: THREE.Vector3,\n axis: THREE.Vector3,\n u1: THREE.Vector3,\n u2: THREE.Vector3,\n tanU1: number,\n tanU2: number,\n maxDistance: number,\n ): { distance: number; label: string } | null {\n let best: { distance: number; label: string } | null = null\n for (const box of this.collisionBoxes) {\n const cx = (box.minX + box.maxX) / 2\n const cy = (box.minY + box.maxY) / 2\n const cz = (box.minZ + box.maxZ) / 2\n const hx = (box.maxX - box.minX) / 2\n const hy = (box.maxY - box.minY) / 2\n const hz = (box.maxZ - box.minZ) / 2\n // 盒沿任意方向的支撑半宽\n const projHalf = (dir: THREE.Vector3): number =>\n Math.abs(dir.x) * hx + Math.abs(dir.y) * hy + Math.abs(dir.z) * hz\n\n const vx = cx - origin.x\n const vy = cy - origin.y\n const vz = cz - origin.z\n const along = vx * axis.x + vy * axis.y + vz * axis.z\n const radiusAxis = projHalf(axis)\n const near = along - radiusAxis\n if (near > maxDistance) continue // 太远\n if (along + radiusAxis < 0) continue // 在锥顶后方\n\n const t1 = Math.abs(vx * u1.x + vy * u1.y + vz * u1.z) - projHalf(u1)\n const t2 = Math.abs(vx * u2.x + vy * u2.y + vz * u2.z) - projHalf(u2)\n const e1 = Math.max(0, t1)\n const e2 = Math.max(0, t2)\n const reference = Math.max(near, 0.05) // 下限防止靠近锥顶时除以极小值、把阈值放大成误判\n if ((e1 / (reference * tanU1)) ** 2 + (e2 / (reference * tanU2)) ** 2 > 1) continue\n\n // 距离:取盒上距锥顶最近的一点,再沿瞄准轴投影 ——\n // ⚠️ 不能用支撑半径算距离:对\"宽扁\"的盒(天桥 18×2 m)会把沿轴厚度夸大好几米\n const qx = Math.min(Math.max(origin.x, box.minX), box.maxX)\n const qy = Math.min(Math.max(origin.y, box.minY), box.maxY)\n const qz = Math.min(Math.max(origin.z, box.minZ), box.maxZ)\n const distance = Math.max(\n 0,\n (qx - origin.x) * axis.x + (qy - origin.y) * axis.y + (qz - origin.z) * axis.z,\n )\n if (!best || distance < best.distance) best = { distance, label: box.name }\n }\n return best\n }\n\n // ————————————————————————————— 航线任务可视化 —————————————————————————————\n\n /**\n * 重建航点标记与航线折线(只在航线内容变化时调用,内部先清掉旧对象)。\n *\n * 每个航点画三样东西,缺一不可:\n * - **地面环**:告诉你在平面上的什么位置;\n * - **竖直光柱**:把平面位置\"抬\"到天上,一眼看出航点高度;\n * - **高度环 + 序号牌**:真正的三维航点位置(飞机要飞到那儿),以及第几个点。\n */\n setMissionWaypoints(waypoints: MissionMarker[], origin: { x: number; z: number }): void {\n this.clearMissionWaypoints()\n this.missionOrigin = { x: origin.x, z: origin.z }\n for (const waypoint of waypoints) {\n this.missionMarkers.push({ x: waypoint.x, z: waypoint.z, altitude: waypoint.altitude })\n }\n if (waypoints.length === 0) {\n this.applyMissionVisibility()\n return\n }\n\n for (let index = 0; index < waypoints.length; index += 1) {\n const waypoint = waypoints[index]\n if (!waypoint) continue\n const altitude = Math.max(1, waypoint.altitude)\n const group = new THREE.Group()\n group.position.set(waypoint.x, 0, waypoint.z)\n\n const groundGeometry = new THREE.RingGeometry(1.7, 2.05, 36)\n groundGeometry.rotateX(-Math.PI / 2)\n const groundRing = new THREE.Mesh(groundGeometry, this.missionRingMaterial)\n groundRing.position.y = 0.03\n group.add(groundRing)\n\n // ⚠️ 光柱几何按\"单位高度、底部对齐\"建,高度改用 scale.y 表达 ——\n // 场景里拖动改高度时只需改 scale,不必重建几何(每帧重建会掉帧 + 显存抖动)\n const pillarGeometry = new THREE.CylinderGeometry(0.07, 0.07, 1, 6, 1, true)\n pillarGeometry.translate(0, 0.5, 0)\n const pillar = new THREE.Mesh(pillarGeometry, this.missionPillarMaterial)\n pillar.scale.y = altitude\n group.add(pillar)\n\n const altitudeGeometry = new THREE.RingGeometry(0.8, 1.1, 28)\n altitudeGeometry.rotateX(-Math.PI / 2)\n const altitudeRing = new THREE.Mesh(altitudeGeometry, this.missionRingMaterial)\n altitudeRing.position.y = altitude\n group.add(altitudeRing)\n\n const label = this.createMissionLabel(`${index + 1}`, altitude + 2)\n group.add(label)\n\n // 拾取体:套在光柱外面的不可见粗管(0 高度也行,但飞行中不许编辑,所以无所谓)\n const pickGeometry = new THREE.CylinderGeometry(\n MISSION_PICK_RADIUS,\n MISSION_PICK_RADIUS,\n 1,\n 10,\n 1,\n true,\n )\n const pick = new THREE.Mesh(pickGeometry, this.missionPickMaterial)\n pick.scale.y = altitude\n pick.position.y = altitude / 2\n group.add(pick)\n\n this.missionDisposables.push(groundGeometry, pillarGeometry, altitudeGeometry, pickGeometry)\n this.missionMarkerGroup.add(group)\n this.missionMarkerNodes.push({ group, pillar, altitudeRing, label, pick })\n }\n\n this.buildMissionPath()\n this.applyMissionVisibility()\n }\n\n /**\n * 原位更新一个航点(场景里拖拽时每帧调用)。\n *\n * 只改这一根标记的位姿与折线顶点,不动其他航点 —— 因此拖动过程不会有整层重建的闪烁,\n * 也不会让\"当前目标/选中/悬停\"三套高亮错位。\n */\n updateMissionWaypoint(index: number, waypoint: MissionMarker): void {\n const node = this.missionMarkerNodes[index]\n const stored = this.missionMarkers[index]\n if (!node || !stored) return\n stored.x = waypoint.x\n stored.z = waypoint.z\n stored.altitude = waypoint.altitude\n\n const altitude = Math.max(1, waypoint.altitude)\n node.group.position.set(waypoint.x, 0, waypoint.z)\n node.pillar.scale.y = altitude\n node.altitudeRing.position.y = altitude\n node.label.position.y = altitude + 2\n node.pick.scale.y = altitude\n node.pick.position.y = altitude / 2\n\n this.refreshMissionPath()\n if (this.missionActiveIndex === index) this.setMissionActive(index, null)\n if (this.missionSelectIndex === index) this.setMissionSelection(index, null)\n if (this.missionHoverIndex === index) this.setMissionHover(index, null)\n }\n\n /** 高亮当前执行目标航点(传 null 取消高亮;waypoint 省略时按索引取当前数据) */\n setMissionActive(index: number, waypoint: MissionMarker | null): void {\n this.missionActiveIndex = index >= 0 ? index : -1\n this.placeHighlightGroup(\n this.missionActiveGroup,\n this.missionActiveAltitudeRing,\n index,\n waypoint,\n )\n }\n\n /** 场景编辑:选中高亮 */\n setMissionSelection(index: number, waypoint: MissionMarker | null = null): void {\n this.missionSelectIndex = index >= 0 ? index : -1\n this.placeHighlightGroup(\n this.missionSelectGroup,\n this.missionSelectAltitudeRing,\n index,\n waypoint,\n )\n }\n\n /** 场景编辑:悬停高亮 */\n setMissionHover(index: number, waypoint: MissionMarker | null = null): void {\n this.missionHoverIndex = index >= 0 ? index : -1\n this.placeHighlightGroup(this.missionHoverGroup, this.missionHoverAltitudeRing, index, waypoint)\n }\n\n /** 把一组\"地面环 + 高度环\"高亮挪到某个航点上(索引/数据非法就藏起来) */\n private placeHighlightGroup(\n group: THREE.Group,\n altitudeRing: THREE.Mesh | null,\n index: number,\n waypoint: MissionMarker | null,\n ): void {\n const target = index >= 0 ? (waypoint ?? this.missionMarkers[index] ?? null) : null\n if (!target) {\n group.visible = false\n return\n }\n group.position.set(target.x, 0, target.z)\n if (altitudeRing) altitudeRing.position.y = Math.max(1, target.altitude)\n group.visible = this.missionVisible\n }\n\n /** 拾取航点:返回命中的航点索引,没命中返回 -1(看不见的图层不参与拾取) */\n pickMissionWaypoint(raycaster: THREE.Raycaster): number {\n if (!this.missionVisible || !this.missionMarkersVisible) return -1\n if (this.missionMarkerNodes.length === 0) return -1\n const targets = this.missionMarkerNodes.map((node) => node.pick)\n const hit = raycaster.intersectObjects(targets, false)[0]\n if (!hit) return -1\n const index = this.missionMarkerNodes.findIndex((node) => node.pick === hit.object)\n return index\n }\n\n /**\n * 拾取地面(水平面 y=0):场景里双击地面新增航点用。\n * 射线几乎平行于地面时会打到很远的地方,由调用方按限距夹紧。\n */\n pickMissionGround(raycaster: THREE.Raycaster): { x: number; z: number } | null {\n const hit = raycaster.ray.intersectPlane(this.groundPlane, this.groundHit)\n if (!hit) return null\n return { x: hit.x, z: hit.z }\n }\n\n /** 图层总开关(航点 + 航线 + 三套高亮) */\n setMissionVisible(visible: boolean): void {\n this.missionVisible = visible\n this.applyMissionVisibility()\n }\n\n /** 航线是否可见(只读真值,面板 200ms 轮询同步) */\n isMissionVisible(): boolean {\n return this.missionVisible\n }\n\n /** 只开关航点标记(不影响航线折线) */\n setMissionWaypointsVisible(visible: boolean): void {\n this.missionMarkersVisible = visible\n this.applyMissionVisibility()\n }\n\n /** 航线航点标记是否可见 */\n isMissionWaypointsVisible(): boolean {\n return this.missionMarkersVisible\n }\n\n /** 只开关航线折线(不影响航点标记) */\n setMissionPathVisible(visible: boolean): void {\n this.missionPathVisible = visible\n this.applyMissionVisibility()\n }\n\n /** 航线路径线是否可见 */\n isMissionPathVisible(): boolean {\n return this.missionPathVisible\n }\n\n private applyMissionVisibility(): void {\n this.missionGroup.visible = this.missionVisible\n this.missionMarkerGroup.visible = this.missionVisible && this.missionMarkersVisible\n this.missionPathGroup.visible = this.missionVisible && this.missionPathVisible\n this.missionActiveGroup.visible = this.missionVisible && this.missionActiveIndex >= 0\n this.missionSelectGroup.visible = this.missionVisible && this.missionSelectIndex >= 0\n this.missionHoverGroup.visible = this.missionVisible && this.missionHoverIndex >= 0\n }\n\n /** 建航线折线:起点从返航点按首点高度拉起,再依次连过各航点 */\n private buildMissionPath(): void {\n if (this.missionMarkers.length === 0) return\n const count = this.missionMarkers.length + 1\n this.missionPathPositions = new Float32Array(count * 3)\n const geometry = new THREE.BufferGeometry()\n geometry.setAttribute('position', new THREE.BufferAttribute(this.missionPathPositions, 3))\n const material = new THREE.LineDashedMaterial({\n color: MISSION_COLOR,\n transparent: true,\n opacity: 0.55,\n dashSize: 1.6,\n gapSize: 1.1,\n toneMapped: false,\n })\n const line = new THREE.Line(geometry, material)\n line.frustumCulled = false\n this.writeMissionPathPositions()\n line.computeLineDistances()\n this.missionPathLine = line\n this.missionPathGroup.add(line)\n this.missionDisposables.push(geometry, material)\n }\n\n /** 顶点改完重算一次(拖动时每帧走这里:只写 Float32Array,不重建几何) */\n private refreshMissionPath(): void {\n const line = this.missionPathLine\n if (!line) return\n this.writeMissionPathPositions()\n const attribute = line.geometry.getAttribute('position')\n attribute.needsUpdate = true\n line.geometry.computeBoundingSphere()\n line.computeLineDistances()\n }\n\n private writeMissionPathPositions(): void {\n const positions = this.missionPathPositions\n if (!positions) return\n const first = this.missionMarkers[0]\n if (!first) return\n positions[0] = this.missionOrigin.x\n positions[1] = Math.max(1, first.altitude)\n positions[2] = this.missionOrigin.z\n for (let index = 0; index < this.missionMarkers.length; index += 1) {\n const waypoint = this.missionMarkers[index]\n if (!waypoint) continue\n const offset = (index + 1) * 3\n positions[offset] = waypoint.x\n positions[offset + 1] = Math.max(1, waypoint.altitude)\n positions[offset + 2] = waypoint.z\n }\n }\n\n private clearMissionWaypoints(): void {\n this.missionMarkerGroup.clear()\n this.missionPathGroup.clear()\n this.missionMarkerNodes.length = 0\n this.missionMarkers.length = 0\n this.missionPathLine = null\n this.missionPathPositions = null\n this.missionDisposables.forEach((item) => item.dispose())\n this.missionDisposables.length = 0\n }\n\n /** 序号牌:用面向屏幕的 Sprite,飞机在哪个角度看牌子都是正的 */\n private createMissionLabel(text: string, y: number): THREE.Sprite {\n const size = 96\n const canvas = document.createElement('canvas')\n canvas.width = size\n canvas.height = size\n const texture = new THREE.CanvasTexture(canvas)\n const context = canvas.getContext('2d')\n if (context) {\n context.fillStyle = 'rgba(7, 26, 30, 0.84)'\n context.beginPath()\n context.arc(size / 2, size / 2, size / 2 - 5, 0, Math.PI * 2)\n context.fill()\n context.lineWidth = 4\n context.strokeStyle = '#39d0b0'\n context.stroke()\n context.fillStyle = '#d8fff5'\n context.font = 'bold 46px ui-sans-serif, system-ui, sans-serif'\n context.textAlign = 'center'\n context.textBaseline = 'middle'\n context.fillText(text, size / 2, size / 2 + 2)\n texture.needsUpdate = true\n }\n const material = new THREE.SpriteMaterial({\n map: texture,\n transparent: true,\n depthWrite: false,\n toneMapped: false,\n })\n const sprite = new THREE.Sprite(material)\n sprite.position.y = y\n sprite.scale.setScalar(2.4)\n this.missionDisposables.push(texture, material)\n return sprite\n }\n\n /** 把\"起飞点\"标记挪到指定位置(重新起飞时会更新返航点) */\n setHome(x: number, z: number): void {\n this.homeMarker.position.set(x, 0, z)\n }\n\n /** 解散并释放场景图与几何/材质 */\n destroy(): void {\n this.scene.remove(this.group)\n this.clearMissionWaypoints()\n this.disposables.forEach((item) => item.dispose())\n this.disposables.length = 0\n this.obstacles.length = 0\n this.collisionBoxes.length = 0\n this.trailLine = null\n this.missionActiveAltitudeRing = null\n this.missionSelectAltitudeRing = null\n this.missionHoverAltitudeRing = null\n this.missionGroup.clear()\n this.missionSelectGroup.clear()\n this.missionHoverGroup.clear()\n }\n}\n","import * as THREE from 'three'\n\n/**\n * 无人机模型的机械驱动。\n *\n * 全部通过语义节点名操作(见《DJI Mini 4 Pro 模型 ID 与名称对应表》),不做索引假设:\n * - 桨叶:CTRL_Prop_<位置>_Spin 绕自身局部 Y 轴自转(导出到 Three.js 后 Blender 的局部 Z 即局部 Y),\n * 对角两个电机同向。桨叶桨距已由模型出厂烘进几何(实测 ~15°,与自转方向配对),\n * 这里不做任何几何补烘,尊重出厂状态。\n * - 机臂:骨骼化导出驱动 ARM_<位置>_Fold_Bone(旧模型退回 CTRL_Arm_<位置>_Fold)。\n * 折叠 = 绕机体竖轴(父系 +Y)的旋转,前乘在出厂装配四元数上——骨节自带\n * 烘焙旋转,直接改 Euler 会把折叠轴带偏。折叠角度不是猜的,而是载入时用\n * \"铰链 → 桨心\"向量与该机臂应有的收纳方向(前臂朝机尾、后臂朝机头)算出来的。\n * - 云台:CTRL_Gimbal_Yaw / Pitch / Roll 三轴。真机云台是增稳平台,\n * 所以这里做反向补偿:机体倾斜多少,云台就往回摆多少(超出机械行程才让画面跟着歪)。\n * - 起落架腿:LEG_<位置>_<关节>(Hip/Knee/Ankle)三关节链,收腿 = 各关节绕局部 +X\n * 前乘固定折叠角(哈士奇机体几何实测,见 LEG_FOLD_ANGLES)。没有这些节点的\n * 机体(真机 GLB)整套腿功能自动停用。\n * - 机体姿态:整体倾斜施加在 CTRL_DJI_Root(模型内部根节点,原点≈机身重心)上。\n */\n\n/** 机臂位置标签(前/后 × 左/右),同时用作 PROP_SPIN_SIGN / ARM_LABELS 等表的键。 */\nexport type ArmPosition = 'FrontLeft' | 'FrontRight' | 'RearLeft' | 'RearRight'\n\n/** 四个机臂的固定遍历顺序(前左→前右→后左→后右):各处初始化、自检、建场都按它展开。 */\nexport const ARM_POSITIONS: ArmPosition[] = ['FrontLeft', 'FrontRight', 'RearLeft', 'RearRight']\n\nconst degToRad = (deg: number): number => (deg * Math.PI) / 180\n\n/**\n * 对角反转:前左/后右 一组,前右/后左 一组,旋转方向相反。\n * 符号不是猜的:实测出厂桨叶法线与自转轴的桨距角 φ(前左/后右 ≈ +15°,\n * 前右/后左 ≈ −15°)。要产生向上拉力,φ>0 的桨必须俯视顺时针转(自转角递减),\n * φ<0 的桨必须俯视逆时针转(自转角递增)——据此取反。\n *\n * 导出给机体模型使用:程序化机体(见 drone-husky-model)要按同一张表把两组\n * 对角桨的桨叶反装(顺桨/逆桨),才与这里的自转方向配对。\n */\nexport const PROP_SPIN_SIGN: Record<ArmPosition, number> = {\n FrontLeft: -1,\n RearRight: -1,\n FrontRight: 1,\n RearLeft: 1,\n}\n\nconst ARM_LABELS: Record<ArmPosition, string> = {\n FrontLeft: '前左机臂',\n FrontRight: '前右机臂',\n RearLeft: '后左机臂',\n RearRight: '后右机臂',\n}\n\n/**\n * 收腿姿态角(度,绕各关节局部 +X;哈士奇机体几何实测,四视角截图确认不穿模):\n * 髋关节向后上方摆 65°,膝关节反向折回 145°(小腿贴大腿),踝关节再顺 45° 让脚掌\n * 收平贴腹 —— 四条腿整体收进机身轮廓内,全机最低点从机足抬到机腹。\n * 符号约定:模型内部坐标 +Z 为机头方向(哈士奇),θ>0 让下垂的腿摆向机尾。\n */\nconst LEG_FOLD_ANGLES = { Hip: 65, Knee: -145, Ankle: 45 } as const\n/** 腿关节旋转轴:各关节局部 +X(机体横轴) */\nconst LEG_FOLD_AXIS = new THREE.Vector3(1, 0, 0)\nconst LEG_FOLD_QUAT = new THREE.Quaternion()\n\n/**\n * 机体外形前伸测量口径(米):以云台挂点为轴,只统计该半径内(光轴附近)的机体网格。\n * 0.3 m 足够覆盖机身与头部,又把四角机臂、电机、桨叶护圈这些\"侧向远处\"的部件排除掉。\n */\nconst BODY_CLEARANCE_RADIUS_M = 0.3\n/**\n * 机体外形之前的安全余量(米):相机要沿光轴领先机体外观这么多。\n * 上仰时最危险的点未必是\"世界坐标最前\"的那个(光轴是斜的),留 8 cm 覆盖这点误差。\n */\nconst GIMBAL_CAMERA_BODY_MARGIN = 0.08\n\n/** 一条起落架腿:按 LEG_<位置>_<关节> 语义名收集到的三关节(缺哪个补哪个) */\ninterface LegJointUnit {\n node: THREE.Object3D\n bindQuaternion: THREE.Quaternion\n foldAngle: number\n}\ninterface LegUnit {\n position: ArmPosition\n joints: LegJointUnit[]\n}\n\n/**\n * 云台机械行程。\n * rollLimit 取 45°:挡位倾角上限最高 35°(运动挡),留 10° 余量 —— 否则满舵侧倾\n * 就把横滚轴顶到机械限位,补偿被截断,回正瞬间画面会\"抖一下\"。\n */\nexport const GIMBAL_LIMITS = {\n pitchMin: -90,\n pitchMax: 60,\n rollLimit: 45,\n yawLimit: 5,\n} as const\n\n/** 模型自检结果(测试沙盒里逐项显示) */\nexport interface RigPartReport {\n id: string\n label: string\n found: boolean\n /** 该部件是否可被驱动 */\n drivable: boolean\n}\n\n/** 单片桨叶:折叠 = 绕电机轴(自转局部 Y)偏航叠拢到机臂方向 */\ninterface BladeUnit {\n mesh: THREE.Mesh\n bindPosition: THREE.Vector3\n bindQuaternion: THREE.Quaternion\n /** 叠拢偏航(弧度):0 = 自然张开位置,施加 stackYaw 后桨叶指向机臂方向 */\n stackYaw: number\n}\n\ninterface PropUnit {\n position: ArmPosition\n node: THREE.Object3D\n bindPosition: THREE.Vector3\n spinSign: number\n angle: number\n /** 出厂装配姿态(自转控制器的静态装配倾角,电机轴方向) */\n bindQuaternion: THREE.Quaternion\n blades: BladeUnit[]\n /** 桨叶折叠旋转轴:电机轴在桨叶父系中的方向(骨骼化=折叠骨节;旧模型=ROTOR 局部 Y) */\n bladeFoldAxis: THREE.Vector3\n spinners: SpinUnit[]\n lifts: LiftUnit[]\n /**\n * 收纳抬升量(米,几何实测):叠拢后桨叶与机身的最大侵入 + 余量。\n * 折叠时转子沿电机轴抬起这么多,模拟真机\"桨叶架在机身上\"的收纳效果。\n */\n foldLift: number\n /** 桨叶/自转节点局部系 1 单位 = 多少米(GLB 内部坐标可能带缩放) */\n localUnits: number\n}\n\ninterface ArmUnit {\n position: ArmPosition\n node: THREE.Object3D\n /** 出厂装配姿态四元数:骨骼化导出时骨节/控制节点自带烘焙旋转,必须整体保留 */\n bindQuaternion: THREE.Quaternion\n /** 收纳方向对应的旋转角(弧度),折叠度 1 时施加 */\n foldAngle: number\n}\n\n/** 自转件:绕电机轴(父系表示)驱动的节点(自转节点本体 + 骨骼化的桨叶折叠骨节) */\ninterface SpinUnit {\n node: THREE.Object3D\n bindQuaternion: THREE.Quaternion\n /** 电机轴在该节点父系中的方向(单位向量) */\n axisParent: THREE.Vector3\n}\n\n/** 收纳抬升件:沿电机轴平移的节点(自转节点 + 桨叶骨节;钟罩留在原地) */\ninterface LiftUnit {\n node: THREE.Object3D\n bindPosition: THREE.Vector3\n axisParent: THREE.Vector3\n /** 该节点局部系 1 单位 = 多少米(GLB 内部坐标可能带缩放) */\n localUnits: number\n}\n\n/** 自转四元数复用对象(避免每帧分配) */\nconst spinQuat = new THREE.Quaternion()\nconst foldQuat = new THREE.Quaternion()\nconst gimbalVec = new THREE.Vector3()\nconst gimbalEuler = new THREE.Euler()\nconst UP_AXIS = new THREE.Vector3(0, 1, 0)\n\n/**\n * 云台关节一阶跟随时间常数(秒)默认值:无刷直驱电机 + 控制环的实际响应,\n * 真机量级 5~30 ms。0 = 立即到位(数学理想云台)。\n */\nconst DEFAULT_GIMBAL_RESPONSE = 0.02\n\n/** 桨叶张开/叠拢的一阶缓动速率(1/秒):9 ≈ 0.45 s 走完 */\nconst BLADE_SWING_RATE = 9\n\n/** 高度场网格间距(米)与键值 */\nconst HEIGHTFIELD_CELL = 0.01\n/** 顶点低于实体下沿多少米以内仍视为接触(桨叶有厚度,留一点擦碰余量) */\nconst SOLID_GRAZE = 0.004\nconst heightfieldKey = (x: number, z: number): string =>\n `${Math.round(x / HEIGHTFIELD_CELL)},${Math.round(z / HEIGHTFIELD_CELL)}`\n\n/** 实体区间:同一竖直格内某个实体的下沿/上沿(世界系,米) */\ninterface SolidInterval {\n lo: number\n hi: number\n}\n\n/** 实体场:格子 → 不相交的竖直区间列表。桨叶从实体下方穿过不算侵入。 */\ntype SolidField = Map<string, SolidInterval[]>\n\nconst addSolid = (field: SolidField, key: string, lo: number, hi: number): void => {\n const list = field.get(key)\n if (!list) {\n field.set(key, [{ lo, hi }])\n return\n }\n // 合并相交/相邻区间,保持列表很短\n for (const interval of list) {\n if (lo <= interval.hi + 1e-4 && hi >= interval.lo - 1e-4) {\n interval.lo = Math.min(interval.lo, lo)\n interval.hi = Math.max(interval.hi, hi)\n return\n }\n }\n list.push({ lo, hi })\n}\n\n/** 顶点 (x,y,z) 深入实体内部的深度:在某个区间内返回 hi−y,否则 0 */\nconst solidPenetration = (field: SolidField, x: number, y: number, z: number): number => {\n const list = field.get(heightfieldKey(x, z))\n if (!list) return 0\n let deepest = 0\n for (const interval of list) {\n if (y < interval.hi && y > interval.lo - SOLID_GRAZE) {\n const depth = interval.hi - y\n if (depth > deepest) deepest = depth\n }\n }\n return deepest\n}\n\n/** 平滑 0-1 曲线(收纳抬升用,起止无突跳) */\nconst smoothstep01 = (t: number): number => {\n const c = Math.max(0, Math.min(1, t))\n return c * c * (3 - 2 * c)\n}\n\n/**\n * ————————————————— 统一坐标系约定(全项目唯一声明) —————————————————\n *\n * 世界系(Three.js 标准):\n * +X 东, +Y 上, -Z 北。航向 heading 从正北顺时针,0°=北 90°=东。\n *\n * 机体系(模型局部,glTF 导出后):\n * -Z 机头(云台), +Z 机尾(电池), -X 左 / +X 右, +Y 上。\n *\n * 机体姿态角(相对世界水平面):\n * pitch 正 = 抬头(绕机体系 X 轴), roll 正 = 右压坡(绕机体系 Z 轴取负)。\n *\n * 整机按「刚体」驱动,变换层级自外向内:\n * model(世界位置+航向) → CTRL_DJI_Root(俯仰/横滚) → 机臂折叠 → 转子/桨叶自转\n * 云台是挂载载荷,在 CTRL_DJI_Root 内部做增稳补偿,不属于刚体位姿。\n * 整机位姿只有一个写入入口:applyBodyPose()。\n */\n\n/** 整机刚体位姿:位置为世界系(米),角度为度 */\nexport interface DroneBodyPose {\n /** 世界系 X(东为正) */\n x: number\n /** 世界系 Y(离地高度,米) */\n y: number\n /** 世界系 Z(南为正) */\n z: number\n /** 航向角:正北 0°,顺时针增加 */\n headingDeg: number\n /** 俯仰:正 = 抬头 */\n pitchDeg: number\n /** 横滚:正 = 右压坡 */\n rollDeg: number\n}\n\n/**\n * 无人机模型的机械驱动层:载入后按语义节点名解析出机臂/转子/桨叶/云台,\n * 并对外提供折叠、自转、云台增稳、整机位姿四类写入接口及自检报告。\n *\n * 唯一的世界系/机体系约定见文件顶部「统一坐标系约定」;所有长度比较都在世界系(米)做,\n * 给节点平移赋值前须乘 localUnits 换算回节点局部单位(见后文注释)。\n * 整机位姿只有 applyBodyPose() 一个写入入口;骨骼化重导出的模型必须同时驱动\n * CTRL_DJI_Root 与 RIG_DJI_Arms_Props 两个节点,否则机体会与机臂\"撕开\"。\n */\nexport class DroneRig {\n readonly model: THREE.Group\n readonly parts: RigPartReport[] = []\n private readonly root: THREE.Object3D | null\n /**\n * 骨骼架根节点(RIG_DJI_Arms_Props)。重导出的模型把机臂/电机/桨叶/脚撑\n * 挂到了这个与 CTRL_DJI_Root 平级的顶级骨架下(为骨骼动画导出),\n * 刚体俯仰/横滚必须同时施加在两个节点上,否则飞行时机体会\"散架\"。\n * 旧模型(无骨骼)此值为 null,自动退回单节点模式。\n */\n private readonly rigBonesRoot: THREE.Object3D | null\n private readonly props: PropUnit[] = []\n private readonly arms: ArmUnit[] = []\n private readonly legs: LegUnit[] = []\n private gimbalYaw: THREE.Object3D | null = null\n private gimbalPitch: THREE.Object3D | null = null\n private gimbalRoll: THREE.Object3D | null = null\n /** 云台载荷(相机外壳)节点;取景时相机必须越过它的前端,否则拍进自己的壳 */\n private gimbalPod: THREE.Object3D | null = null\n /**\n * 载荷前端面到云台枢轴的沿光轴(-Z)距离(**roll 局部单位,与模型缩放无关**)。\n * 取景相机要摆到这段距离之外(换算成米后),外壳才不会挡住视线。\n */\n private gimbalClearanceLocal = 0\n /**\n * 挂点前方\"机体外形\"的前伸量(模型局部单位):哈士奇鼻尖在挂点前 0.84 单位。\n * 相机前移量必须同时越过它,否则云台上仰时会把机体外观拍进画面(见 measureBodyClearance)。\n */\n private gimbalBodyClearanceLocal = 0\n /** 云台偏航节点出厂位置(枢轴 P,父系局部单位) */\n private readonly gimbalBindPosition = new THREE.Vector3()\n /** 相机中心 C − 枢轴 P(父系局部单位);C≈P 时为零向量,补偿自动无效 */\n private readonly gimbalCenterOffset = new THREE.Vector3()\n /**\n * 云台枢轴在**机体局部系**的挂点(出厂水平态实测):位置解耦用。\n * 取景相机按\"机体平移 + 航向旋转 + 该挂点\"定位,不随机体倾斜绕横滚轴画弧\n * (实测侧倾 27° 时旧做法镜头横移 ±6.7 cm,就是\"回正时画面明显晃\"的主因)。\n */\n private readonly gimbalMountLocal = new THREE.Vector3()\n private gimbalMountReady = false\n /**\n * 相机在\"航向已剥离\"参考系下的安装朝向(出厂水平态实测):姿态解耦用。\n * 取景相机的世界朝向直接由 `R_y(航向+云台偏航)·R_x(云台俯仰)·R_z(0) ⊗ 本偏置`\n * 解出,不再读骨骼链世界变换 —— 于是关节一阶跟随的滞后、机械行程夹紧、\n * 层级数值误差全部不会传到画面上(模型那边照旧,只是相机不再\"采样\"它)。\n */\n private readonly gimbalMountQuat = new THREE.Quaternion()\n private gimbalOrientationReady = false\n private readonly gimbalScratchQuat = new THREE.Quaternion()\n /** 云台关节当前角(弧度):一阶跟随用,代表电机实际到位的位置 */\n private gimbalJointYaw = 0\n private gimbalJointPitch = 0\n private gimbalJointRoll = 0\n private readonly gimbalBodyQuat = new THREE.Quaternion()\n private readonly gimbalTargetQuat = new THREE.Quaternion()\n private readonly gimbalJointQuat = new THREE.Quaternion()\n /**\n * 云台关节一阶跟随时间常数(秒)。>0 时关节不瞬时到位(模拟无刷电机响应),\n * 0 = 立即到位。默认 20 ms。\n */\n gimbalResponse = DEFAULT_GIMBAL_RESPONSE\n private readonly disposables: Array<THREE.BufferGeometry | THREE.Material> = []\n private fold = 0\n private spinAngle = 0\n private lastLoad = 0\n /** 起落架腿收放度(1 = 放下,0 = 完全收起);出厂即放下 */\n private legDeployState = 1\n /**\n * 桨叶张开度(0 = 叠拢,1 = 自然张开):有自己的缓动节奏——\n * 机臂完全展开后才开始缓慢张开(与真机一致),收纳时快速叠拢。\n */\n private bladeOpenState = 1\n /** 展开态停放配平抬头角(度,几何实测):前脚撑与机尾底部同时落地 */\n private stancePitchOpen = 0\n /** 展开态配平整体下沉量(米),保证静止时脚撑恰好贴地 */\n private stanceDropOpen = 0\n /** 收纳态下沉量(米):机身平贴地面(俯仰 0),脚撑随机臂收起不再接地 */\n private stanceDropFolded = 0\n\n /** 绑定机体模型根节点,建立机臂/桨叶/云台/起落架的机械控制 */\n constructor(model: THREE.Group) {\n this.model = model\n model.updateMatrixWorld(true)\n this.root = model.getObjectByName('CTRL_DJI_Root') ?? null\n this.rigBonesRoot = model.getObjectByName('RIG_DJI_Arms_Props') ?? null\n this.collectParts()\n this.buildProps()\n this.buildArms()\n this.collectLegs()\n this.computeBladeFold()\n this.computeStanceTrim()\n this.bindGimbal()\n }\n\n /** 机臂当前折叠度(0 = 展开,1 = 完全收纳) */\n get armFold(): number {\n return this.fold\n }\n\n /** 桨叶张开度(0 = 叠拢,1 = 张开):起飞自检要求完全张开 */\n get bladeOpen(): number {\n return this.bladeOpenState\n }\n\n /** 桨叶是否已开始旋转 */\n get motorsSpinning(): boolean {\n return this.spinAngle > 0\n }\n\n /** 设置机臂折叠度(0 = 展开,1 = 完全收纳);变化量 <1e-4 直接返回,避免每帧重复写节点。 */\n setArmFold(fold: number): void {\n const next = Math.max(0, Math.min(1, fold))\n if (Math.abs(next - this.fold) < 1e-4) return\n this.fold = next\n this.arms.forEach((arm) => {\n this.applyArmFold(arm, next)\n })\n }\n\n /**\n * 机臂折叠的唯一写入口:绕机体竖轴(父系 +Y)旋转,前乘在出厂装配四元数上。\n * 旧模型折叠节点出厂四元数为单位阵,等价于直接写 rotation.y;骨骼化重导出后\n * 折叠骨节自带烘焙旋转,直接改 Euler 的 y 分量会把折叠轴带偏(实测偏 32°,\n * 机臂甩出水平面),必须四元数整体前乘。\n */\n private applyArmFold(arm: ArmUnit, factor: number): void {\n foldQuat.setFromAxisAngle(UP_AXIS, arm.foldAngle * factor)\n arm.node.quaternion.copy(foldQuat).multiply(arm.bindQuaternion)\n }\n\n /** 起落架腿当前收放度(1 = 放下,0 = 完全收起);无腿节点的机体恒为 1 */\n get legDeploy(): number {\n return this.legs.length === 0 ? 1 : this.legDeployState\n }\n\n /**\n * 设置起落架腿收放度(1 = 放下,0 = 完全收起)。\n * 与机臂折叠同一条纪律:旋转四元数整体前乘在出厂姿态上 —— 哈士奇腿关节出厂\n * 四元数是单位阵(等价于直接写 rotation.x),但保持同构写法,未来带烘焙姿态的\n * 骨骼导出也不跑偏。没有 LEG_* 节点的机体(真机 GLB)是 no-op。\n */\n setLegDeploy(deploy: number): void {\n if (this.legs.length === 0) return\n const next = clamp(deploy, 0, 1)\n if (Math.abs(next - this.legDeployState) < 1e-4) return\n this.legDeployState = next\n const foldFactor = 1 - next\n for (const leg of this.legs) {\n for (const joint of leg.joints) {\n LEG_FOLD_QUAT.setFromAxisAngle(LEG_FOLD_AXIS, joint.foldAngle * foldFactor)\n joint.node.quaternion.copy(LEG_FOLD_QUAT).multiply(joint.bindQuaternion)\n }\n }\n }\n\n /** 收集起落架腿:按 LEG_<位置>_(Hip|Knee|Ankle) 语义名扫描(见 drone-husky-model 适配层) */\n private collectLegs(): void {\n const pattern = /^LEG_(FrontLeft|FrontRight|RearLeft|RearRight)_(Hip|Knee|Ankle)$/\n this.model.traverse((object) => {\n const match = pattern.exec(object.name)\n if (!match) return\n const position = match[1] as ArmPosition\n let leg = this.legs.find((item) => item.position === position)\n if (!leg) {\n leg = { position, joints: [] }\n this.legs.push(leg)\n }\n leg.joints.push({\n node: object,\n bindQuaternion: object.quaternion.clone(),\n foldAngle: degToRad(LEG_FOLD_ANGLES[match[2] as keyof typeof LEG_FOLD_ANGLES]),\n })\n })\n }\n\n /**\n * 桨叶姿态的唯一写入口(每帧调用):\n * - 叠拢/张开:bladeOpen 以自己的节奏缓动。机臂完全展开(fold < 0.02)\n * 且电机未转时目标为张开,以慢速率\"缓缓转回原位\";收纳时以快速率先叠拢。\n * - 收纳抬升:仅当叠拢姿态求不出零侵入时的兜底(当前几何下四桨均为 0,\n * 桨毂保持坐在电机钟罩内);折叠时转子沿电机轴抬起 foldLift。\n */\n updateBlades(deltaSeconds: number): void {\n // 目标:电机在转或机臂展开 ⇒ 张开;仅\"电机停转且机臂收纳\"才叠拢\n const spinning = this.lastLoad > 0.01\n const target = spinning || this.fold < 0.02 ? 1 : 0\n // 张开/叠拢都是一阶缓动:9/秒 ≈ 0.45 s 走完,配合 9/秒 的展臂速率,\n // 让\"上电 → 展臂 → 桨叶张开 → 可起飞\"整条流程在 1 秒内完成\n const rate = BLADE_SWING_RATE\n this.bladeOpenState += (target - this.bladeOpenState) * Math.min(1, deltaSeconds * rate)\n if (Math.abs(target - this.bladeOpenState) < 0.002) this.bladeOpenState = target\n const stackFactor = 1 - this.bladeOpenState\n const liftFactor = smoothstep01(this.fold)\n for (const prop of this.props) {\n this.applyBladeFold(prop, stackFactor)\n // 收纳抬升:转子(自转节点+桨叶骨节)沿电机轴抬起,钟罩(已挂在\n // 自转节点父级)留在原地。foldLift 是米,换算成节点局部单位。\n for (const lift of prop.lifts) {\n lift.node.position\n .copy(lift.bindPosition)\n .addScaledVector(lift.axisParent, prop.foldLift * lift.localUnits * liftFactor)\n }\n }\n }\n\n /** 叠拢/张开姿态:绕电机轴(桨叶父系表示的 bladeFoldAxis)偏航 stackYaw×factor */\n private applyBladeFold(prop: PropUnit, factor: number): void {\n for (const [index, blade] of prop.blades.entries()) {\n foldQuat.setFromAxisAngle(prop.bladeFoldAxis, blade.stackYaw * factor)\n blade.mesh.position.copy(blade.bindPosition).applyQuaternion(foldQuat)\n blade.mesh.quaternion.copy(foldQuat).multiply(blade.bindQuaternion)\n // 两片桨叶叠拢时沿电机轴错开一点层差,像真机桨夹上下叠放\n if (index === 1) {\n // 0.012 单位是米(12mm 叠层间距);节点平移用局部单位,故 ×localUnits 换算回来\n blade.mesh.position.addScaledVector(prop.bladeFoldAxis, 0.012 * prop.localUnits * factor)\n }\n }\n }\n\n /**\n * 自转姿态的唯一写入口:对每个自转件前乘\"绕父系表示的电机轴\"的旋转:\n * q = Q(axisParent, angle) ⊗ 出厂姿态\n * 与旧版\"后乘局部 Y\"在世界系完全等价(同一根电机轴、同向、同角)。\n * 自转节点本体带动桨毂;骨骼化导出还把两片桨叶挂进独立折叠骨节\n * (PROP_*_Blade_*_Fold_Bone,与自转节点同级),必须一并驱动,\n * 否则桨毂转而桨叶不转。\n */\n private applyPropRotation(prop: PropUnit): void {\n for (const spinner of prop.spinners) {\n spinQuat.setFromAxisAngle(spinner.axisParent, prop.angle)\n spinner.node.quaternion.copy(spinQuat).multiply(spinner.bindQuaternion)\n }\n }\n\n /**\n * 驱动桨叶:load 为电机负荷 0~1;也可传入按机臂位置索引的四电机转速表\n * (悬停 ≈ 1,来自 DroneSim 的四旋翼混控)—— 前飞时后桨快、前桨慢,\n * 转向时一组对角桨快、另一组慢,与真机的升力差/反扭矩机制一致。\n */\n updateProps(load: number | Partial<Record<ArmPosition, number>>, deltaSeconds: number): void {\n const scalar = typeof load === 'number' ? load : null\n this.lastLoad = Math.max(0, Math.min(1.15, scalar ?? Math.max(...Object.values(load ?? {}), 0)))\n let fastest = 0\n for (const prop of this.props) {\n const individual = typeof load === 'number' ? load : (load?.[prop.position] ?? 0)\n const clamped = Math.max(0, Math.min(1.15, individual))\n fastest = Math.max(fastest, clamped)\n // 目视转速:开机怠速即有明显快转,满载接近视觉走样上限(60fps 下 2 叶桨 ~15 转/秒开始频闪)\n if (clamped <= 0.01) continue\n const revolutionsPerSecond = 3.8 + clamped * 7\n prop.angle =\n (prop.angle + revolutionsPerSecond * deltaSeconds * prop.spinSign * Math.PI * 2) %\n (Math.PI * 2)\n this.applyPropRotation(prop)\n }\n }\n\n /**\n * 整机刚体位姿的唯一写入入口:世界位置 + 航向施加在模型根节点,\n * 俯仰/横滚施加在 CTRL_DJI_Root(模型内部重心)。机臂、转子、云台\n * 全部挂在这条变换链下游,自动跟随整机运动,无需单独处理。\n *\n * 停放配平随折叠度过渡:展开态机身上仰 stancePitchOpen(电机轴前倾的\n * 补偿,前脚撑与机尾同时着地);收纳态机身平贴(俯仰 0,像真机收纳\n * 后平放在桌面上)。pose.y 语义是\"触地点高度\",下沉量在这里内部消化。\n */\n applyBodyPose(pose: DroneBodyPose): void {\n const openFactor = 1 - this.fold\n this.model.position.set(\n pose.x,\n pose.y - this.stanceDropOpen * openFactor - this.stanceDropFolded * this.fold,\n pose.z,\n )\n this.model.rotation.y = -degToRad(pose.headingDeg)\n const pitchRad = degToRad(pose.pitchDeg + this.stancePitchOpen * openFactor)\n const rollRad = -degToRad(pose.rollDeg)\n if (this.root) {\n this.root.rotation.x = pitchRad\n this.root.rotation.z = rollRad\n }\n // 骨骼架根与机身保持同一刚体姿态(重导出后机臂/桨叶/脚撑挂在 RIG 下,与机身平级)\n if (this.rigBonesRoot) {\n this.rigBonesRoot.rotation.x = pitchRad\n this.rigBonesRoot.rotation.z = rollRad\n }\n }\n\n /**\n * 云台增稳(三轴解耦):传入相对水平面的期望角度,机体倾斜由云端解算一次性反掉。\n *\n * 做法:先组装\"相机应有的世界姿态\"——航向 = 模型航向 + 云台偏航命令,俯仰 =\n * 云台俯仰命令,横滚恒为 0(锁水平);再除以云台挂载父节点的世界姿态,得到\n * 云台链应处的**局部位姿**,按 YXZ 顺序(偏航→俯仰→横滚,与模型关节链一致)\n * 分解成三个关节角。\n *\n * 为什么不用\"期望角 − 机体角\"直接相减:欧拉角不可交换,机体同时俯仰+横滚时\n * 旧做法会漏出交叉耦合(实测侧倾机动中镜头 roll 残差 0.6°、pitch 波动 2.5°,\n * 回正时残差反向 → 画面转一下 + 点一下头)。解算后残差在数学上归零。\n * 关节仍受机械行程夹紧,超程部分保留 —— 大角度机动时画面会跟着歪,与真机一致。\n *\n * @param pitchDeg 云台俯仰期望角(度,相对水平面,正 = 抬头)\n * @param rollDeg 云台横滚期望角(度,相对水平面,通常恒 0 = 锁水平)\n * @param yawDeg 云台偏航命令(度,相对机身,受 ±yawLimit 限制)\n * @param bodyPitch 旧版角度相减参数,现已由机体世界姿态取代,保留仅为兼容调用\n * @param bodyRoll 同上\n * @param deltaSeconds 帧间隔(秒):>0 时关节按一阶惯性跟随电机响应,0 = 立即到位\n */\n setGimbalAttitude(\n pitchDeg: number,\n rollDeg: number,\n yawDeg: number,\n bodyPitch = 0,\n bodyRoll = 0,\n deltaSeconds = 0,\n ): void {\n void bodyPitch\n void bodyRoll\n if (!this.gimbalYaw && !this.gimbalPitch && !this.gimbalRoll) return\n\n // 1) 期望的相机世界姿态:航向 → 俯仰 → 横滚锁 0\n const yawWorld = this.model.rotation.y + degToRad(yawDeg)\n this.gimbalTargetQuat.setFromEuler(\n gimbalEuler.set(degToRad(pitchDeg), yawWorld, -degToRad(rollDeg), 'YXZ'),\n )\n\n // 2) 除以挂载父节点的世界姿态(含机头航向、机体俯仰/横滚、停放配平)= 局部姿态\n const parent =\n this.gimbalYaw?.parent ?? this.gimbalRoll?.parent ?? this.gimbalPitch?.parent ?? null\n if (parent) {\n parent.updateWorldMatrix(true, false)\n parent.getWorldQuaternion(this.gimbalBodyQuat)\n this.gimbalTargetQuat.premultiply(this.gimbalBodyQuat.invert())\n }\n\n // 3) 按 YXZ 分解成三个关节角,再夹到机械行程\n gimbalEuler.setFromQuaternion(this.gimbalTargetQuat, 'YXZ')\n const yawLimit = degToRad(GIMBAL_LIMITS.yawLimit)\n const targetYaw = clamp(gimbalEuler.y, -yawLimit, yawLimit)\n const targetPitch = clamp(\n gimbalEuler.x,\n degToRad(GIMBAL_LIMITS.pitchMin),\n degToRad(GIMBAL_LIMITS.pitchMax),\n )\n const rollLimit = degToRad(GIMBAL_LIMITS.rollLimit)\n const targetRoll = clamp(gimbalEuler.z, -rollLimit, rollLimit)\n\n // 4) 一阶跟随:真实云台的无刷电机不是瞬时的,给一个 20ms 量级的时间常数\n const follow =\n deltaSeconds > 0 && this.gimbalResponse > 0\n ? 1 - Math.exp(-deltaSeconds / this.gimbalResponse)\n : 1\n this.gimbalJointYaw += (targetYaw - this.gimbalJointYaw) * follow\n this.gimbalJointPitch += (targetPitch - this.gimbalJointPitch) * follow\n this.gimbalJointRoll += (targetRoll - this.gimbalJointRoll) * follow\n\n if (this.gimbalPitch) this.gimbalPitch.rotation.x = this.gimbalJointPitch\n if (this.gimbalRoll) this.gimbalRoll.rotation.z = this.gimbalJointRoll\n if (this.gimbalYaw) this.gimbalYaw.rotation.y = this.gimbalJointYaw\n\n // 5) 旋转中心补偿(仅影响模型的观感):骨骼化导出把云台枢轴链放在鼻尖安装位、\n // 网格用反向平移补偿回原位 —— 静止正确,但转动时云台壳体会绕枢轴甩弧线。\n // 在偏航节点(链的最外层,其平移在旋转之外)加 Δ = (I−R)·(C−P),等价于把\n // 旋转中心从枢轴 P 搬到载荷中心 C。C≈P 时(枢轴放得准)Δ≈0 自动无效。\n if (this.gimbalYaw && this.gimbalCenterOffset.lengthSq() > 1e-10) {\n this.gimbalJointQuat.setFromEuler(\n gimbalEuler.set(this.gimbalJointPitch, this.gimbalJointYaw, this.gimbalJointRoll, 'YXZ'),\n )\n gimbalVec.copy(this.gimbalCenterOffset).applyQuaternion(this.gimbalJointQuat)\n this.gimbalYaw.position\n .copy(this.gimbalBindPosition)\n .add(this.gimbalCenterOffset)\n .sub(gimbalVec)\n }\n }\n\n /** 云台相机的世界位置与朝向(机载视角用) */\n getGimbalCameraTransform(target: THREE.Object3D): boolean {\n if (!this.gimbalRoll) return false\n this.gimbalRoll.updateWorldMatrix(true, false)\n this.gimbalRoll.getWorldPosition(target.position)\n this.gimbalRoll.getWorldQuaternion(target.quaternion)\n return true\n }\n\n /**\n * 机载相机的**位置解耦**挂点(世界坐标):机体只贡献\"平移 + 航向\",不含倾斜。\n *\n * 真机上镜头挂在重心下方约 0.14 m,机体侧倾会带着它绕横滚轴画弧(侧倾 27° 即\n * 横移 ±6.7 cm)。姿态已经被三轴解算锁平,位置再跟着画弧,画面就\"整体左右平移\"\n * —— 人眼对平移比对角度的容差低得多,这就是回正时\"明显晃\"的来源。\n * 真机云台的橡胶减震球 + 电子增稳在观感上也等价于\"镜头被稳稳托住\",所以这里\n * 把位置一起解耦:挂点随机体平移、随航向旋转,不随机体俯仰/横滚摆动。\n *\n * @param target 写入世界坐标\n * @param forwardOffset 沿**云台光轴**的前移量(米):相机要越过载荷外壳与机体外形前端\n * @param pitchDeg 云台俯仰真值(度,正 = 抬头):决定光轴方向\n * @returns 云台挂点未就绪(模型无云台)时返回 false\n *\n * ⚠️ 前移必须沿**光轴**而不能只沿航向(纯水平):云台上仰时光轴朝前上方,机体的\n * 前上方外形(哈士奇狗鼻尖在挂点上方 0.16 m、前方 0.26 m)在**相机空间**里就落在\n * 镜头前面 —— 世界 z 上\"相机在鼻子之前\"是错觉,判据是沿光轴的距离。实测只做水平\n * 前移时仰到 60° 仍有 0.72% 的画面被鼻壳占据(差异像素全在画面顶部一带)。\n * 沿光轴前移 = 真机云台俯仰时镜头绕俯仰轴画弧,位置随仰角自然抬起。\n */\n getGimbalCameraMount(target: THREE.Vector3, forwardOffset: number, pitchDeg = 0): boolean {\n if (!this.gimbalMountReady) return false\n const yaw = this.model.rotation.y\n target\n .copy(this.gimbalMountLocal)\n .multiply(this.model.scale)\n .applyAxisAngle(UP_AXIS, yaw)\n .add(this.model.position)\n const pitch = degToRad(pitchDeg)\n // 水平分量按 cos(俯仰) 收缩,同时抬起 offset·sin(俯仰):俯仰为 0 时与纯水平前进等价\n gimbalVec.set(0, 0, -1).applyAxisAngle(UP_AXIS, yaw)\n target.addScaledVector(gimbalVec, forwardOffset * Math.cos(pitch))\n target.y += forwardOffset * Math.sin(pitch)\n return true\n }\n\n /**\n * 机载相机的**姿态解耦**朝向(世界四元数):直接由飞控真值解算,不读骨骼链。\n *\n * 与位置解耦配套:`引擎给出期望姿态 → 相机照做`,而不是`模型转成什么样 → 相机照抄`。\n * 骨骼链上那些\"拟真副作用\"(关节一阶跟随的 20 ms 滞后、机械行程夹紧、层级数值残差)\n * 只留在模型外观上,不会再传进画面 —— 这正是真机三轴云台\"硬件解耦\"在做的事:\n * 机身怎么动都只是被隔离掉,镜头自己稳稳对着目标。\n *\n * @param target 写入世界四元数\n * @param pitchDeg 云台俯仰期望角(度,相对水平面,正 = 抬头)\n * @param rollDeg 云台横滚期望角(度,通常恒 0 = 锁水平)\n * @param yawDeg 云台偏航命令(度,相对机身)\n * @returns 云台未就绪(模型无云台)时返回 false\n */\n getGimbalCameraOrientation(\n target: THREE.Quaternion,\n pitchDeg: number,\n rollDeg: number,\n yawDeg: number,\n ): boolean {\n if (!this.gimbalOrientationReady) return false\n const yawWorld = this.model.rotation.y + degToRad(yawDeg)\n target.setFromEuler(gimbalEuler.set(degToRad(pitchDeg), yawWorld, -degToRad(rollDeg), 'YXZ'))\n target.multiply(this.gimbalMountQuat)\n return true\n }\n\n /** 释放本类记录的可释放资源(几何/材质);模型本体归调用方所有,不在此销毁。 */\n destroy(): void {\n this.disposables.forEach((item) => item.dispose())\n this.disposables.length = 0\n this.props.length = 0\n this.arms.length = 0\n }\n\n // ————————————————————————————— 初始化 —————————————————————————————\n\n /** 逐项核对 ID 表里的部件是否存在(缺失的会列进自检报告) */\n private collectParts(): void {\n const entries: Array<{ id: string; label: string; drivable?: boolean }> = [\n { id: 'BODY_Main_Fuselage', label: '机身主体' },\n { id: 'BODY_Black_Underside_SensorHousing', label: '黑色下壳' },\n { id: 'GIMBAL_CameraHousing', label: '云台相机壳体' },\n { id: 'GIMBAL_CameraLensFrame', label: '云台镜头框' },\n { id: 'GIMBAL_Camera_SideRing_Right', label: '云台侧环' },\n { id: 'SENSOR_Glass_GimbalLens', label: '云台主镜片' },\n { id: 'SENSOR_Glass_Front_Left', label: '前视玻璃(左)' },\n { id: 'SENSOR_Glass_Front_Right', label: '前视玻璃(右)' },\n { id: 'SENSOR_Glass_Side_Left', label: '侧视玻璃(左)' },\n { id: 'SENSOR_Glass_Side_Right', label: '侧视玻璃(右)' },\n { id: 'SENSOR_Glass_Downward', label: '下视玻璃' },\n { id: 'SENSOR_FrontHousing_Left', label: '前视壳体(左)' },\n { id: 'SENSOR_FrontHousing_Right', label: '前视壳体(右)' },\n { id: 'LED_Status_Tail_Left', label: '尾灯(左)' },\n { id: 'LED_Status_Tail_Right', label: '尾灯(右)' },\n { id: 'LED_BatteryCharge_01', label: '电池指示灯珠 1' },\n { id: 'LED_BatteryCharge_02', label: '电池指示灯珠 2' },\n { id: 'LED_BatteryCharge_03', label: '电池指示灯珠 3' },\n { id: 'LED_BatteryCharge_04', label: '电池指示灯珠 4' },\n { id: 'BUTTON_Power', label: '电源按键' },\n { id: 'DECAL_Text_Labels', label: '文字贴花' },\n { id: 'SRC_GeometryRoot', label: '几何根节点' },\n { id: 'SRC_TailStatusLED_Group', label: '尾灯几何组' },\n { id: 'SRC_Decals_Group', label: '贴花几何组' },\n { id: 'SRC_BlackHousing_Group', label: '黑色壳体组' },\n { id: 'SRC_SensorGlass_Group', label: '传感器玻璃组' },\n { id: 'CTRL_DJI_Root', label: '整机总控', drivable: true },\n { id: 'CTRL_Gimbal_Yaw', label: '云台偏航', drivable: true },\n { id: 'CTRL_Gimbal_Pitch', label: '云台俯仰', drivable: true },\n { id: 'CTRL_Gimbal_Roll', label: '云台横滚', drivable: true },\n ]\n for (const entry of entries) {\n this.parts.push({\n id: entry.id,\n label: entry.label,\n found: this.model.getObjectByName(entry.id) !== undefined,\n drivable: entry.drivable ?? false,\n })\n }\n for (const position of ARM_POSITIONS) {\n this.parts.push({\n id: `ARM_${position}`,\n label: `${ARM_LABELS[position]}网格`,\n found: this.model.getObjectByName(`ARM_${position}`) !== undefined,\n drivable: false,\n })\n this.parts.push({\n id: `CTRL_Arm_${position}_Fold`,\n label: `${ARM_LABELS[position]}折叠`,\n found: this.model.getObjectByName(`CTRL_Arm_${position}_Fold`) !== undefined,\n drivable: true,\n })\n this.parts.push({\n id: `CTRL_Prop_${position}_Spin`,\n label: `${ARM_LABELS[position]}桨叶自转`,\n found: this.model.getObjectByName(`CTRL_Prop_${position}_Spin`) !== undefined,\n drivable: true,\n })\n this.parts.push({\n id: `ROTOR_${position}`,\n label: `${ARM_LABELS[position]}转子桨毂`,\n // 桨毂是 CTRL_Prop_*_Spin 的子节点,钟罩与桨叶都挂在它下面\n found: this.model.getObjectByName(`ROTOR_${position}`) !== undefined,\n drivable: true,\n })\n this.parts.push({\n id: `MOTOR_${position}_RotorBell`,\n label: `${ARM_LABELS[position]}电机钟罩(固定外壳)`,\n found: this.model.getObjectByName(`MOTOR_${position}_RotorBell`) !== undefined,\n drivable: false,\n })\n }\n }\n\n private buildProps(): void {\n this.model.updateMatrixWorld(true)\n for (const position of ARM_POSITIONS) {\n const node = this.model.getObjectByName(`CTRL_Prop_${position}_Spin`)\n if (!node) continue\n // 磁钢钟罩 = 电机的固定外壳,不随转子自转:\n // 从自转子树摘出、挂回自转节点的父级(attach 保持世界变换不变),\n // 之后依旧随折叠机臂运动。若模型日后在 Blender 里自行移出\n // Spin 子树,getObjectByName 找不到会自动跳过,无副作用。\n const bell = node.getObjectByName(`MOTOR_${position}_RotorBell`)\n if (bell && node.parent) {\n node.parent.attach(bell)\n this.model.updateMatrixWorld(true)\n }\n // 出厂几何已自洽(实测):ROTOR 桨毂与钟罩的几何轴和自转节点局部 Y\n // 同轴(偏差 <1.5°),桨叶自带 ~15° 桨距且符号与对角反转约定配对。\n // 因此这里不做任何几何补烘,尊重模型出厂状态。\n const motorAxisWorld = new THREE.Vector3(0, 1, 0)\n .applyQuaternion(node.getWorldQuaternion(new THREE.Quaternion()))\n .normalize()\n const blades: BladeUnit[] = []\n for (const index of [1, 2]) {\n const mesh = this.model.getObjectByName(\n `PROP_${position}_Blade_${index}`,\n ) as THREE.Mesh | null\n if (mesh?.isMesh) {\n blades.push({\n mesh,\n bindPosition: mesh.position.clone(),\n bindQuaternion: mesh.quaternion.clone(),\n stackYaw: 0,\n })\n }\n }\n // 电机轴 → 各父系表示:骨骼化重导出后自转节点/桨叶折叠骨节的父系各不相同\n // (旧模型里桨叶父系=ROTOR,局部 Y 即电机轴),一律按世界几何反推。\n const axisIn = (frame: THREE.Object3D | null): THREE.Vector3 => {\n const inverse = (frame ?? this.model).getWorldQuaternion(new THREE.Quaternion()).invert()\n return motorAxisWorld.clone().applyQuaternion(inverse).normalize()\n }\n // 自转件:自转节点(带动桨毂;钟罩已摘出)。骨骼化导出还把两片桨叶\n // 挂进独立的折叠骨节(PROP_*_Blade_*_Fold_Bone,与自转节点同级),\n // 必须一并驱动,否则桨毂转而桨叶不转。\n const spinners: SpinUnit[] = []\n const addSpinner = (object: THREE.Object3D): void => {\n spinners.push({\n node: object,\n bindQuaternion: object.quaternion.clone(),\n axisParent: axisIn(object.parent),\n })\n }\n addSpinner(node)\n for (const index of [1, 2]) {\n const bone = this.model.getObjectByName(`PROP_${position}_Blade_${index}_Fold_Bone`)\n if (bone) addSpinner(bone)\n }\n // 桨叶折叠轴:电机轴在桨叶父系(骨骼化=折叠骨节;旧模型=ROTOR)的表示\n const bladeParent = blades[0]?.mesh.parent ?? node\n const bladeFoldAxis = axisIn(bladeParent)\n // 收纳抬升件:自转节点 + 桨叶折叠骨节(钟罩留在原地不抬)\n const lifts: LiftUnit[] = []\n const addLift = (object: THREE.Object3D): void => {\n lifts.push({\n node: object,\n bindPosition: object.position.clone(),\n axisParent: axisIn(object.parent),\n localUnits: 1 / object.matrixWorld.getMaxScaleOnAxis(),\n })\n }\n addLift(node)\n spinners.forEach((spinner) => {\n if (spinner.node !== node) addLift(spinner.node)\n })\n this.props.push({\n position,\n node,\n bindPosition: node.position.clone(),\n spinSign: PROP_SPIN_SIGN[position],\n angle: 0,\n bindQuaternion: node.quaternion.clone(),\n blades,\n bladeFoldAxis,\n spinners,\n lifts,\n foldLift: 0,\n // 局部系 1 单位 = 多少米(沿节点世界矩阵的缩放求逆)\n localUnits: 1 / node.matrixWorld.getMaxScaleOnAxis(),\n })\n }\n }\n\n /**\n * 实测每个桨的\"叠拢偏航角\"(与真机一致:收纳时两片桨叶绕桨夹转拢、\n * 叠在一起指向机臂方向)。临时折叠到 1,量出桨叶径向与机臂方向的夹角;\n * 再整圈扫描整体偏航,取对机身/机臂/其他桨叶侵入最小的角度。求不出\n * 零侵入时才用残余侵入量 + 余量作收纳抬升兜底(当前四桨均为 0)。\n */\n private computeBladeFold(): void {\n this.model.updateMatrixWorld(true)\n // 避障场:先定姿的桨叶顶点写入,后续桨扫描时避开(防桨叶互穿)\n const obstacles: SolidField = new Map()\n // 临时折叠机臂 —— ⚠️ 必须先折叠再建高度场:收纳态下机臂/电机扫过机身\n // 两侧上方,展开态建场会漏掉这些表面,桨叶会从折叠臂下方\"幽灵穿越\"。\n this.arms.forEach((arm) => {\n this.applyArmFold(arm, 1)\n })\n this.model.updateMatrixWorld(true)\n // 高度场与侵入量一律在世界系(米)量测,避开 GLB 内部坐标缩放\n const heightfield = this.buildBodyHeightfield()\n for (const prop of this.props) {\n const armUnit = this.arms.find((unit) => unit.position === prop.position)\n if (!armUnit || prop.blades.length === 0) continue\n // 机臂方向(世界水平投影)\n const hinge = armUnit.node.getWorldPosition(new THREE.Vector3())\n const motor = prop.node.getWorldPosition(new THREE.Vector3())\n const armDirWorld = motor.clone().sub(hinge)\n armDirWorld.y = 0\n if (armDirWorld.lengthSq() < 1e-6) continue\n armDirWorld.normalize()\n // 转到桨叶父系局部系:折叠是绕电机轴的旋转,对齐角在父系里求,\n // 且只比较垂直于电机轴的分量(骨骼化导出后父系局部 Y 不再是电机轴)。\n // 注意:本模型 ROTOR 原点即桨毂中心。\n const firstBlade = prop.blades[0]\n if (!firstBlade) continue\n const axis = prop.bladeFoldAxis\n const parentQuat = (firstBlade.mesh.parent ?? prop.node).getWorldQuaternion(\n new THREE.Quaternion(),\n )\n const armDirLocal = armDirWorld.clone().applyQuaternion(parentQuat.clone().invert())\n armDirLocal.addScaledVector(axis, -axis.dot(armDirLocal))\n if (armDirLocal.lengthSq() < 1e-6) continue\n armDirLocal.normalize()\n for (const blade of prop.blades) {\n // 桨叶径向(父系局部):几何质心 − 桨毂(父系原点)\n const center = new THREE.Box3().setFromObject(blade.mesh).getCenter(new THREE.Vector3())\n const rotorInv = new THREE.Matrix4()\n .copy((blade.mesh.parent ?? prop.node).matrixWorld)\n .invert()\n const radial = center.applyMatrix4(rotorInv)\n radial.addScaledVector(axis, -axis.dot(radial))\n if (radial.lengthSq() < 1e-6) continue\n radial.normalize()\n // 绕电机轴旋转 θ 使径向对齐机臂方向:θ = atan2(cross(径向,臂向)·轴, 径向·臂向)\n const cross = new THREE.Vector3().crossVectors(radial, armDirLocal)\n blade.stackYaw = Math.atan2(cross.dot(axis), radial.dot(armDirLocal))\n }\n // 偏航扫描(整圈):找对机身(含机臂/电机/其他桨叶)侵入最小的整体偏角。\n // 前桨沿机臂方向会被机尾上表面挡住,但向外偏 ~50° 即完全无侵入 ——\n // 转子因此不必抬升,桨毂稳稳坐在电机钟罩里。零侵入的解往往不止一个,\n // 平手时取\"桨叶方向最贴近机身纵轴\"的 —— 与真机顺机身叠桨一致。\n let bestOffsetDeg = 0\n let bestPen = Infinity\n let bestAxis = -1\n for (let offsetDeg = -180; offsetDeg < 180; offsetDeg += 5) {\n const penetration = this.measureStackPenetration(\n prop,\n degToRad(offsetDeg),\n heightfield,\n obstacles,\n )\n const axis = this.stackAxisAlignment(prop)\n if (\n penetration < bestPen - 1e-6 ||\n (Math.abs(penetration - bestPen) <= 1e-6 && axis > bestAxis + 1e-6)\n ) {\n bestPen = penetration\n bestAxis = axis\n bestOffsetDeg = offsetDeg\n }\n }\n // 粗扫最优附近再做 1° 细扫,尽量压低残余侵入\n const fineBase = bestOffsetDeg\n for (let fine = fineBase - 4; fine <= fineBase + 4; fine += 1) {\n const penetration = this.measureStackPenetration(\n prop,\n degToRad(fine),\n heightfield,\n obstacles,\n )\n const axis = this.stackAxisAlignment(prop)\n if (\n penetration < bestPen - 1e-6 ||\n (Math.abs(penetration - bestPen) <= 1e-6 && axis > bestAxis + 1e-6)\n ) {\n bestPen = penetration\n bestAxis = axis\n bestOffsetDeg = fine\n }\n }\n prop.blades.forEach((blade) => {\n blade.stackYaw += degToRad(bestOffsetDeg)\n })\n // 残余侵入 > 4mm 才抬升转子兜底,否则桨毂保持在电机钟罩内\n prop.foldLift = bestPen > 0.004 ? clamp(bestPen + 0.008, 0, 0.2) : 0\n // 把本桨的叠拢姿态 stamp 进避障场,后续桨的扫描会避开它\n this.applyBladeFold(prop, 1)\n this.model.updateMatrixWorld(true)\n const vertex = new THREE.Vector3()\n for (const blade of prop.blades) {\n const attribute = blade.mesh.geometry.getAttribute('position')\n for (let i = 0; i < attribute.count; i += 1) {\n vertex.fromBufferAttribute(attribute, i).applyMatrix4(blade.mesh.matrixWorld)\n addSolid(\n obstacles,\n heightfieldKey(vertex.x, vertex.z),\n vertex.y - 0.004,\n vertex.y + 0.004,\n )\n }\n }\n }\n // 还原展开\n this.arms.forEach((arm) => {\n this.applyArmFold(arm, 0)\n })\n this.model.updateMatrixWorld(true)\n }\n\n /**\n * 当前(已由 measureStackPenetration 摆好的)叠拢姿态下,桨叶 1 相对电机\n * 的水平方向与机身纵轴(世界 Z,载入时机身无旋转)的对齐度:0~1,越大越顺轴。\n */\n private stackAxisAlignment(prop: PropUnit): number {\n const firstBlade = prop.blades[0]\n if (!firstBlade) return 0\n const hub = prop.node.getWorldPosition(new THREE.Vector3())\n const center = new THREE.Box3().setFromObject(firstBlade.mesh).getCenter(new THREE.Vector3())\n const dir = center.sub(hub)\n dir.y = 0\n const length = dir.length()\n if (length < 1e-6) return 0\n return Math.abs(dir.z) / length\n }\n\n /** 临时施加\"叠拢 + 偏移\"姿态,量桨叶顶点对实体场(机身 + 已定姿的其他桨叶)的最大侵入(世界系/米) */\n private measureStackPenetration(\n prop: PropUnit,\n offset: number,\n heightfield: SolidField,\n obstacles: SolidField,\n ): number {\n const saved = prop.blades.map((blade) => blade.stackYaw)\n prop.blades.forEach((blade) => {\n blade.stackYaw += offset\n })\n this.applyBladeFold(prop, 1)\n this.model.updateMatrixWorld(true)\n const vertex = new THREE.Vector3()\n let deepest = 0\n for (const blade of prop.blades) {\n const attribute = blade.mesh.geometry.getAttribute('position')\n for (let i = 0; i < attribute.count; i += 1) {\n vertex.fromBufferAttribute(attribute, i).applyMatrix4(blade.mesh.matrixWorld)\n const depth = solidPenetration(heightfield, vertex.x, vertex.y, vertex.z)\n if (depth > deepest) deepest = depth\n const blocked = solidPenetration(obstacles, vertex.x, vertex.y, vertex.z)\n if (blocked > deepest) deepest = blocked\n }\n }\n prop.blades.forEach((blade, index) => {\n const yaw = saved[index]\n if (yaw !== undefined) blade.stackYaw = yaw\n })\n return deepest\n }\n\n /**\n * 机身实体场:BODY_/ARM_/MOTOR_ 顶点按 (x,z) 0.01m 网格归入竖直区间\n * (下沿~上沿)。只有顶点真正落入区间内部才算侵入 —— 桨叶从折叠臂\n * 下方/上方掠过不算,避免\"最高点高度场\"把擦边当成穿透。\n */\n private buildBodyHeightfield(): SolidField {\n const grid: SolidField = new Map()\n const vertex = new THREE.Vector3()\n this.model.traverse((object) => {\n const mesh = object as THREE.Mesh\n if (!mesh.isMesh || !mesh.geometry) return\n if (\n !mesh.name.startsWith('BODY_') &&\n !mesh.name.startsWith('ARM_') &&\n !mesh.name.startsWith('MOTOR_')\n )\n return\n const attribute = mesh.geometry.getAttribute('position')\n for (let i = 0; i < attribute.count; i += 1) {\n vertex.fromBufferAttribute(attribute, i).applyMatrix4(mesh.matrixWorld)\n addSolid(grid, heightfieldKey(vertex.x, vertex.z), vertex.y, vertex.y)\n }\n })\n return grid\n }\n\n /**\n * 折叠角由几何推算:取\"铰链 → 桨心\"在水平面的方向,与收纳方向作差。\n * 前臂收向机尾(+Z),后臂收向机头(-Z) —— 与 Mini 系列的实际收纳姿态一致。\n * 驱动点优先取骨骼化导出的折叠骨节 ARM_*_Fold_Bone(其子树 = 折叠子树:\n * 臂网格 + 电机 + 桨),旧模型退回 CTRL_Arm_*_Fold。\n */\n private buildArms(): void {\n const hingeWorld = new THREE.Vector3()\n const propWorld = new THREE.Vector3()\n for (const position of ARM_POSITIONS) {\n const node =\n this.model.getObjectByName(`ARM_${position}_Fold_Bone`) ??\n this.model.getObjectByName(`CTRL_Arm_${position}_Fold`)\n const prop = this.model.getObjectByName(`CTRL_Prop_${position}_Spin`)\n if (!node || !prop) continue\n node.getWorldPosition(hingeWorld)\n prop.getWorldPosition(propWorld)\n const dx = propWorld.x - hingeWorld.x\n const dz = propWorld.z - hingeWorld.z\n const current = Math.atan2(dz, dx)\n const target = position.startsWith('Front') ? Math.atan2(1, 0) : Math.atan2(-1, 0)\n let foldAngle = current - target\n // 归一到 (-π, π]:取最短路径,避免绕大半圈\n foldAngle =\n ((((foldAngle + Math.PI) % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2)) - Math.PI\n this.arms.push({ position, node, bindQuaternion: node.quaternion.clone(), foldAngle })\n }\n }\n\n /**\n * 实测\"停放配平\"(分展开/收纳两态,随折叠度过渡):\n * - 展开态:前脚撑(GEAR_* 最低点)与机尾底部(BODY_* 机尾区域最低点)\n * 是两个触地带,机身平放时只有前脚撑着地、机尾悬空。解出让两带同高\n * 的抬头角并整体下沉,静止时\"机尾轻触地、机头微抬\"地停放。\n * - 收纳态:脚撑随机臂翻起不再接地,真机收纳后是机身平贴桌面 —— 俯仰 0,\n * 下沉量 = 机身系(不含脚撑/桨叶)最低点。\n * 角度按几何解算,不写死数值,模型日后重导出也能自适应。\n */\n private computeStanceTrim(): void {\n this.model.updateMatrixWorld(true)\n const toModelLocal = new THREE.Matrix4().copy(this.model.matrixWorld).invert()\n // 触地带必须在展开姿下量测(出厂默认收纳,脚撑随机臂翻起贴不到地面):\n // 直接把臂折叠节点转到展开位 → 量测 → 恢复出厂烘焙值。\n // (不能走 setArmFold:构造期 this.fold=0,会被 early-return 跳过;\n // 也不能只记 this.fold:出厂姿态烘在节点局部变换里,不走 setArmFold 的状态量。)\n // computeBladeFold 已在此之前完成(它依赖收纳态)。\n const savedFoldQ = this.arms.map((arm) => arm.node.quaternion.clone())\n this.arms.forEach((arm) => {\n this.applyArmFold(arm, 0)\n })\n this.model.updateMatrixWorld(true)\n // —— 展开态 ——\n const contacts = this.scanStanceContacts(toModelLocal)\n // 配平角首选\"电机轴前倾角均值\":设计基准是桨盘水平(停放/悬停时四桨盘接近水平,\n // 与飞行状态一致)。骨骼化重导出后机臂挂进 RIG 骨架,触地双带解算与该基准\n // 不再一致(机尾下壳斜面会让解算撞到 ±25° 上限),故只在量不到电机轴时兜底。\n const motorTilt = this.measureMotorForwardTilt()\n if (motorTilt !== null) {\n this.stancePitchOpen = THREE.MathUtils.radToDeg(clamp(motorTilt, -degToRad(25), degToRad(25)))\n } else if (contacts.front && contacts.rear) {\n const dz = contacts.front.z - contacts.rear.z\n if (Math.abs(dz) > 1e-3) {\n // 旋转任意平行轴不改变\"两点同高\"的解:tanφ = (y前-y后)/(z前-z后)\n const pitch = clamp(\n Math.atan((contacts.front.y - contacts.rear.y) / dz),\n -degToRad(25),\n degToRad(25),\n )\n this.stancePitchOpen = THREE.MathUtils.radToDeg(pitch)\n }\n }\n if (this.stancePitchOpen !== 0) {\n // 施加配平后量触地带的世界最低点 → 静止时的整体下沉量。\n // 注意不可钳到 ≥0:载入基准(normalizeModel)是\"收纳态整体最低点贴地\",\n // 新模型展开配平后的脚撑可能比它还低(负值 = 需要整体抬升,否则穿地)。\n this.setBodyPitchRad(degToRad(this.stancePitchOpen))\n this.model.updateMatrixWorld(true)\n this.stanceDropOpen = this.measureLowest(['GEAR_', 'BODY_'])\n this.setBodyPitchRad(0)\n this.model.updateMatrixWorld(true)\n }\n // 恢复出厂烘焙姿\n this.arms.forEach((arm, index) => {\n const q = savedFoldQ[index]\n if (q) arm.node.quaternion.copy(q)\n })\n this.model.updateMatrixWorld(true)\n // —— 收纳态:机身平贴,下沉量 = 机身系最低点相对模型原点的深度\n // (世界系量测;不含脚撑 GEAR_ 与桨叶 PROP_,它们收纳后不接地) ——\n this.stanceDropFolded = Math.max(\n 0,\n this.model.position.y - this.measureLowest(['BODY_', 'ARM_', 'MOTOR_']),\n )\n }\n\n /** 刚体俯仰(弧度)统一写入:机身根 + 骨骼架根(若存在)同步施加 */\n private setBodyPitchRad(pitchRad: number): void {\n if (this.root) this.root.rotation.x = pitchRad\n if (this.rigBonesRoot) this.rigBonesRoot.rotation.x = pitchRad\n }\n\n /**\n * 电机轴前倾角(弧度,展开姿、机身水平时量测):四个转子节点局部 Y(=电机轴)\n * 的世界方向,取前向分量的均值。调用前须已把机臂展开并 updateMatrixWorld。\n */\n private measureMotorForwardTilt(): number | null {\n if (this.props.length === 0) return null\n const axis = new THREE.Vector3()\n let sum = 0\n let count = 0\n for (const prop of this.props) {\n axis.set(0, 1, 0).transformDirection(prop.node.matrixWorld)\n if (!Number.isFinite(axis.z)) continue\n // 模型前向为 -z:轴前倾时 axis.z < 0,-axis.z 为正\n sum += Math.asin(THREE.MathUtils.clamp(-axis.z, -1, 1))\n count += 1\n }\n return count > 0 ? sum / count : null\n }\n\n /** 所有匹配前缀网格的世界最低点;toModelLocal 提供时返回模型系坐标 */\n private measureLowest(prefixes: string[], toModelLocal?: THREE.Matrix4): number {\n const vertex = new THREE.Vector3()\n let lowest = Infinity\n this.model.traverse((object) => {\n const mesh = object as THREE.Mesh\n if (!mesh.isMesh || !mesh.geometry) return\n if (!prefixes.some((prefix) => mesh.name.startsWith(prefix))) return\n const attribute = mesh.geometry.getAttribute('position')\n for (let i = 0; i < attribute.count; i += 1) {\n vertex.fromBufferAttribute(attribute, i).applyMatrix4(mesh.matrixWorld)\n if (toModelLocal) vertex.applyMatrix4(toModelLocal)\n if (vertex.y < lowest) lowest = vertex.y\n }\n })\n return Number.isFinite(lowest) ? lowest : 0\n }\n\n /** 触地带扫描:前脚撑最低点 + 机尾底部最低点(模型局部系) */\n private scanStanceContacts(toModelLocal: THREE.Matrix4): {\n front: THREE.Vector3 | null\n rear: THREE.Vector3 | null\n } {\n const vertex = new THREE.Vector3()\n let front: THREE.Vector3 | null = null\n let rear: THREE.Vector3 | null = null\n this.model.traverse((object) => {\n const mesh = object as THREE.Mesh\n if (!mesh.isMesh || !mesh.geometry) return\n const isGear = mesh.name.startsWith('GEAR_')\n const isBody = mesh.name.startsWith('BODY_')\n if (!isGear && !isBody) return\n const attribute = mesh.geometry.getAttribute('position')\n for (let i = 0; i < attribute.count; i += 1) {\n vertex\n .fromBufferAttribute(attribute, i)\n .applyMatrix4(mesh.matrixWorld)\n .applyMatrix4(toModelLocal)\n if (isGear && (front === null || vertex.y < front.y)) front = vertex.clone()\n if (isBody && vertex.z > 0.15 && (rear === null || vertex.y < rear.y)) rear = vertex.clone()\n }\n })\n return { front, rear }\n }\n\n private bindGimbal(): void {\n this.gimbalYaw = this.model.getObjectByName('CTRL_Gimbal_Yaw') ?? null\n this.gimbalPitch = this.model.getObjectByName('CTRL_Gimbal_Pitch') ?? null\n this.gimbalRoll = this.model.getObjectByName('CTRL_Gimbal_Roll') ?? null\n // 量\"相机中心 C − 枢轴 P\":C = 云台载荷网格(GIMBAL_BlackCameraPod 子树)\n // 包围盒中心,P = 偏航节点出厂位置。骨骼化导出常把枢轴放在安装位而把网格\n // 平移补偿回原位,导致旋转绕安装位而非相机本身(见 setGimbalAttitude)。\n if (this.gimbalYaw?.parent) {\n this.gimbalBindPosition.copy(this.gimbalYaw.position)\n // 位置解耦挂点:趁模型还是出厂水平态(未施加任何机体姿态)量一次枢轴\n // 在机体局部系的坐标 —— 之后取景相机按\"平移 + 航向 + 该挂点\"定位。\n const pivot = this.gimbalRoll ?? this.gimbalYaw\n this.model.updateWorldMatrix(true, true)\n const world = pivot.getWorldPosition(new THREE.Vector3())\n this.gimbalMountLocal.copy(this.model.worldToLocal(world))\n this.gimbalMountReady = true\n // 姿态解耦偏置:出厂态相机的世界朝向剥掉航向,剩下\"相机在机体安装系里的朝向\"\n // (老模型通常≈单位四元数,即相机光轴与机头一致;写通用以便适配任意模型)\n const camNode = this.gimbalRoll ?? this.gimbalPitch ?? this.gimbalYaw\n if (camNode) {\n camNode.updateWorldMatrix(true, false)\n camNode.getWorldQuaternion(this.gimbalMountQuat)\n this.gimbalMountQuat.premultiply(\n this.gimbalScratchQuat.setFromAxisAngle(UP_AXIS, -this.model.rotation.y),\n )\n this.gimbalOrientationReady = true\n }\n const pod = this.model.getObjectByName('GIMBAL_BlackCameraPod') ?? this.gimbalRoll\n this.gimbalPod = pod\n this.measureGimbalClearance(pod)\n this.measureBodyClearance()\n if (pod) {\n const box = new THREE.Box3()\n let hasMesh = false\n pod.traverse((object) => {\n const mesh = object as THREE.Mesh\n if (!mesh.isMesh || !mesh.geometry) return\n hasMesh = true\n box.union(new THREE.Box3().setFromObject(mesh))\n })\n if (hasMesh && !box.isEmpty()) {\n const center = box.getCenter(new THREE.Vector3())\n this.gimbalYaw.parent.updateWorldMatrix(true, false)\n const inverse = this.gimbalYaw.parent.matrixWorld.clone().invert()\n center.applyMatrix4(inverse)\n this.gimbalCenterOffset.copy(center).sub(this.gimbalBindPosition)\n }\n }\n }\n }\n\n /**\n * 量\"云台载荷沿光轴(-Z)的前端面到枢轴的距离\"(roll 局部单位)。\n *\n * 取景相机默认摆在枢轴沿光轴前移一小段处;若这段距离没越过载荷前端,\n * 相机就泡在相机外壳里,画面正中是一颗对着的镜头(fpv 视角\"看相机\"的根因)。\n *\n * 做法:载荷包围盒 8 个角变换到 roll 局部系取最小 Z(前方为 -Z)。\n * pod 是 roll 的子节点,几何相对 roll 固定,量一次即可;局部值与模型\n * 缩放无关,使用方(getGimbalCameraClearance)按当前世界缩放换算成米。\n */\n private measureGimbalClearance(pod: THREE.Object3D | null): void {\n this.gimbalClearanceLocal = 0\n const target = pod ?? this.gimbalRoll\n if (!target || !this.gimbalRoll) return\n target.updateWorldMatrix(true, true)\n const box = new THREE.Box3().setFromObject(target)\n if (box.isEmpty()) return\n this.gimbalRoll.updateWorldMatrix(true, false)\n const toLocal = this.gimbalRoll.matrixWorld.clone().invert()\n const corner = new THREE.Vector3()\n let minZ = Number.POSITIVE_INFINITY\n for (let i = 0; i < 8; i += 1) {\n corner.set(\n i & 1 ? box.max.x : box.min.x,\n i & 2 ? box.max.y : box.min.y,\n i & 4 ? box.max.z : box.min.z,\n )\n corner.applyMatrix4(toLocal)\n if (corner.z < minZ) minZ = corner.z\n }\n if (Number.isFinite(minZ)) this.gimbalClearanceLocal = Math.max(0, -minZ)\n }\n\n /**\n * 量\"挂点前方、光轴口径内\"的机体外形前伸量(模型局部单位)。\n *\n * 与 measureGimbalClearance(载荷壳体自身)是两件事:那条是\"别泡进自己的相机壳\",\n * 这条是\"别把机体外观拍进画面\"。哈士奇的云台枢轴在机头下方,而狗鼻尖伸到枢轴\n * 前 0.84 单位(0.27 m)—— 相机若只按壳体前移,就还落在鼻子后面:平视看不到\n * (鼻子在视野上边界之外),但云台上仰超过 ~35° 后画面下缘扫过鼻头(实测\n * pitch 60° 时鼻壳最近 0.139 m,画面里挂着半个狗鼻子)。\n *\n * 做法:遍历所有网格的包围盒 8 角,换算到模型局部系,只统计\"在挂点前方(Δz<0)\n * 且落在光轴口径内(|Δx|、|Δy| ≤ BODY_CLEARANCE_RADIUS_M)\"的点,取最大前伸量。\n * 相机前移量取本值与载荷前端的较大者后,机体外观整体落在相机背后,\n * 任何俯仰角都不再入镜。\n */\n private measureBodyClearance(): void {\n this.gimbalBodyClearanceLocal = 0\n if (!this.gimbalMountReady) return\n const scale = this.model.getWorldScale(new THREE.Vector3())\n const worldScale = (Math.abs(scale.x) + Math.abs(scale.y) + Math.abs(scale.z)) / 3\n if (worldScale <= 0) return\n const radiusLocal = BODY_CLEARANCE_RADIUS_M / worldScale\n const mount = this.gimbalMountLocal\n this.model.updateWorldMatrix(true, false)\n const toLocal = this.model.matrixWorld.clone().invert()\n const box = new THREE.Box3()\n const point = new THREE.Vector3()\n let maxAdvance = 0\n this.model.traverse((object) => {\n const mesh = object as THREE.Mesh\n if (!mesh.isMesh || !mesh.geometry) return\n box.setFromObject(mesh)\n if (box.isEmpty()) return\n for (let i = 0; i < 8; i += 1) {\n point.set(\n i & 1 ? box.max.x : box.min.x,\n i & 2 ? box.max.y : box.min.y,\n i & 4 ? box.max.z : box.min.z,\n )\n point.applyMatrix4(toLocal)\n const dz = point.z - mount.z\n if (dz >= 0) continue\n if (Math.abs(point.x - mount.x) > radiusLocal || Math.abs(point.y - mount.y) > radiusLocal)\n continue\n if (-dz > maxAdvance) maxAdvance = -dz\n }\n })\n this.gimbalBodyClearanceLocal = maxAdvance\n }\n\n /**\n * 云台载荷沿光轴(-Z)的前端距离(**米**)。\n * 取景相机沿光轴前移量必须大于该值(外加小余量),外壳才不会挡住视线。\n * 同时取\"挂点前方的机体外形前伸量\"的较大者:否则机体自己的外观\n * (哈士奇的狗鼻子)会在云台上仰时入镜,见 measureBodyClearance。\n * 云台未就绪返回 null,调用方退回自己的默认偏移。\n */\n getGimbalCameraClearance(): number | null {\n if (!this.gimbalRoll) return null\n this.gimbalRoll.updateWorldMatrix(true, false)\n const scale = this.gimbalRoll.getWorldScale(new THREE.Vector3())\n // 均匀缩放模型;用平均分量抵御非均匀缩放的极端情况\n const worldScale = (Math.abs(scale.x) + Math.abs(scale.y) + Math.abs(scale.z)) / 3\n const podNeed = this.gimbalClearanceLocal * worldScale\n const bodyNeed = this.gimbalBodyClearanceLocal * worldScale + GIMBAL_CAMERA_BODY_MARGIN\n return Math.max(podNeed, bodyNeed)\n }\n\n /** 机臂折叠枢轴节点(状态灯等机臂附件挂到它下面即可跟随折叠/展开) */\n getArmFoldNode(position: ArmPosition): THREE.Object3D | null {\n return this.model.getObjectByName(`CTRL_Arm_${position}_Fold`) ?? null\n }\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n","import * as THREE from 'three'\n\nimport type { DroneSim } from './drone-sim'\n\n/**\n * DJI Mini 4 Pro 灯光模拟。\n *\n * 依据《DJI Mini 4 Pro 灯光/灯语说明手册 v1.0》实现三类灯光:\n * 1. 尾部状态指示灯(左右各一颗,常规灯语左右同色;电机启动后按国内固件左红右绿)\n * 2. 智能飞行电池电量指示灯(4 颗 LED)\n * 3. 底部辅助照明灯(Mini 4 Pro 独有:地面上电无法点亮,必须起飞后才能开启)\n *\n * 安装位置(依据手册与实机):\n * 1. 飞行器状态指示灯(Remote ID 识别灯):机尾左右两个机臂末端,左右各 1 颗\n * 2. 电池电量指示灯:机身尾部电池模块的尾面,电源按键旁,横向 4 颗\n * 3. 底部辅助照明灯:机身正下方,紧贴下视视觉传感器,朝下照射\n *\n * 坐标约定:模型局部 -Z 为机头(云台),+Z 为机尾(电池),-X 为左侧、+X 为右侧。\n * 锚点全部取自模型自带语义节点(LED_Status_Tail_* / SENSOR_Glass_Downward / LIGHT_*),\n * 灯组 rig 挂在模型节点下,随模型一起移动旋转;模型自带的 LIGHT_* 点光源也由本模块驱动。\n */\n\n/** 尾部状态灯语的键;每键对应 STATUS_PATTERNS 里的一套闪灯时序,由 statusPatternFor 按飞行状态映射 */\nexport type StatusLightKey =\n | 'selfCheck'\n | 'sensorWarmup'\n | 'gnssNormal'\n | 'gnssWeak'\n | 'attiMode'\n | 'rcLost'\n | 'lowBattery'\n | 'criticalBattery'\n | 'tilted'\n | 'fcError'\n | 'compassError'\n | 'motorRunning'\n | 'off'\n\n/** 电池电量灯模式:level=按电量分级点亮,其余为充电/满电/故障/关闭 */\nexport type BatteryLightMode = 'level' | 'charging' | 'full' | 'fault' | 'off'\n/** 底部辅助灯模式:auto=夜间低空自动,on=强制开,off=关 */\nexport type AuxLightMode = 'auto' | 'on' | 'off'\n\n/** 灯语的一个时间片段:colors 依次对应 [左灯, 右灯](只给一个颜色时左右同色) */\nexport interface LightStep {\n colors: number[]\n onMs: number\n offMs: number\n}\n\n/** 一种灯语的元数据 + 时序(供面板展示,并由 applyStatusPattern 按时间轴驱动) */\nexport interface StatusPatternMeta {\n key: StatusLightKey\n label: string\n meaning: string\n uiColor: string\n steps: LightStep[]\n}\n\nconst RED = 0xff3b30\nconst GREEN = 0x30ff70\nconst YELLOW = 0xffc53d\nconst BATTERY_COLOR = 0x9fffcb\n\nconst step = (colors: number[], onMs: number, offMs = 0): LightStep => ({ colors, onMs, offMs })\nconst repeat = (s: LightStep, n: number): LightStep[] => Array.from({ length: n }, () => ({ ...s }))\n\n/** 尾部状态灯语库(与手册第 1 节一一对应) */\nexport const STATUS_PATTERNS: Record<StatusLightKey, Omit<StatusPatternMeta, 'key'>> = {\n selfCheck: {\n label: '开机系统自检',\n meaning: '红绿黄交替闪烁',\n uiColor: '#ff3b30',\n steps: [step([RED], 150, 110), step([GREEN], 150, 110), step([YELLOW], 150, 110)],\n },\n sensorWarmup: {\n label: '传感器预热中',\n meaning: '黄灯连续闪 4 次',\n uiColor: '#ffc53d',\n steps: [...repeat(step([YELLOW], 140, 130), 4), step([YELLOW], 0, 1000)],\n },\n gnssNormal: {\n label: 'GNSS 定位正常',\n meaning: '绿灯慢闪 · 可以起飞',\n uiColor: '#30ff70',\n steps: [step([GREEN], 450, 1250)],\n },\n gnssWeak: {\n label: 'GNSS 信号弱',\n meaning: '绿灯双闪 · 依靠视觉定位',\n uiColor: '#30ff70',\n steps: [step([GREEN], 130, 160), step([GREEN], 130, 1150)],\n },\n attiMode: {\n label: '姿态模式',\n meaning: '黄灯慢闪 · 无 GPS 无视觉定位,谨慎飞行',\n uiColor: '#ffc53d',\n steps: [step([YELLOW], 450, 1250)],\n },\n rcLost: {\n label: '遥控器信号丢失',\n meaning: '黄灯快闪',\n uiColor: '#ffc53d',\n steps: [step([YELLOW], 110, 110)],\n },\n lowBattery: {\n label: '低电量预警',\n meaning: '红灯慢闪 · 准备自动返航',\n uiColor: '#ff3b30',\n steps: [step([RED], 450, 1250)],\n },\n criticalBattery: {\n label: '严重低电量',\n meaning: '红灯快闪 · 强制返航',\n uiColor: '#ff3b30',\n steps: [step([RED], 110, 110)],\n },\n tilted: {\n label: '机身放置不平',\n meaning: '红灯间隔闪烁 · 传感器误差大',\n uiColor: '#ff3b30',\n steps: [step([RED], 220, 1700)],\n },\n fcError: {\n label: '飞控严重故障',\n meaning: '红灯常亮 · 禁止起飞',\n uiColor: '#ff3b30',\n steps: [step([RED], 1200, 0)],\n },\n compassError: {\n label: '指南针异常',\n meaning: '红黄灯交替闪烁 · 需要校准',\n uiColor: '#ff8c1a',\n steps: [step([RED], 330, 140), step([YELLOW], 330, 140)],\n },\n motorRunning: {\n label: '电机启动 · Remote ID',\n meaning: '国内固件规则:左侧红灯闪烁、右侧绿灯闪烁',\n uiColor: '#ff3b30',\n steps: [step([RED, GREEN], 500, 500)],\n },\n off: {\n label: '未上电 / 关闭',\n meaning: '状态灯熄灭',\n uiColor: '#5a6b70',\n steps: [step([0x000000], 1200, 0)],\n },\n}\n\n/** 供面板渲染的模式列表(保持手册顺序) */\nexport const STATUS_PATTERN_LIST: StatusPatternMeta[] = (\n Object.keys(STATUS_PATTERNS) as StatusLightKey[]\n).map((key) => ({ key, ...STATUS_PATTERNS[key] }))\n\n/** 手册第 2 节:电量 -> [常亮数, 闪烁的那一颗索引(从 0 计, null 无)] */\nexport function batteryLedPlan(levelPercent: number): { lit: number; blinkIndex: number | null } {\n const level = Math.max(0, Math.min(100, levelPercent))\n if (level >= 88) return { lit: 4, blinkIndex: null }\n if (level >= 76) return { lit: 3, blinkIndex: 3 }\n if (level >= 63) return { lit: 3, blinkIndex: null }\n if (level >= 51) return { lit: 2, blinkIndex: 2 }\n if (level >= 38) return { lit: 2, blinkIndex: null }\n if (level >= 26) return { lit: 1, blinkIndex: 1 }\n return { lit: 1, blinkIndex: null }\n}\n\n/** 生成电量对应的文字说明,例如 \"3 常亮 + 1 闪烁\" */\nexport function describeBatteryLevel(levelPercent: number): string {\n const { lit, blinkIndex } = batteryLedPlan(levelPercent)\n return blinkIndex === null ? `${lit} 灯常亮` : `${lit} 常亮 + 1 闪烁`\n}\n\n/**\n * 灯语与飞行状态的对应关系(与《Mini 4 Pro 灯光说明》一致)。\n *\n * 纯逻辑:只读 DroneSim 真值,不碰 three,也不碰任何渲染器 —— 所以 three 的 DroneLights\n * 与 Cesium 的机体表现层共用同一份映射,两个后端在同样飞行状态下闪一样的灯。\n * (原先定义在 drone-fly.ts,搬到这里是为了让 Cesium 侧不必 import 整个 three 装配层。)\n */\nexport function statusPatternFor(sim: DroneSim): StatusLightKey {\n if (sim.damaged) return 'fcError'\n if (sim.phase === 'powerOff') return 'off'\n switch (sim.phase) {\n case 'selfCheck':\n return 'selfCheck'\n case 'warmingUp':\n return 'sensorWarmup'\n case 'emergency':\n return 'fcError'\n default:\n break\n }\n if (sim.faults.imuError) return 'fcError'\n if (sim.faults.compassError) return 'compassError'\n if (sim.faults.rcLost) return 'rcLost'\n if (sim.criticalBattery) return 'criticalBattery'\n if (sim.lowBattery) return 'lowBattery'\n if (sim.motorsOn) return 'motorRunning'\n const source = sim.positionSource\n if (source === 'gps') return sim.gpsBars >= 3 ? 'gnssNormal' : 'gnssWeak'\n if (source === 'vision') return 'gnssWeak'\n return 'attiMode'\n}\n\n/** 单颗电量灯的对外快照(给面板做 CSS 动画镜像) */\nexport interface BatteryLedSnapshot {\n /** 当前帧是否点亮 */\n on: boolean\n /** 该灯是否属于\"闪烁\"角色(供面板用 CSS 动画镜像) */\n blinking: boolean\n}\n\n/** 灯光系统的整体快照(面板 100ms 轮询取用) */\nexport interface DroneLightsSnapshot {\n statusKey: StatusLightKey\n statusLabel: string\n statusMeaning: string\n batteryMode: BatteryLightMode\n batteryLevel: number\n batteryLeds: BatteryLedSnapshot[]\n auxMode: AuxLightMode\n /** 辅助灯当前是否真的点亮(受起飞状态门控) */\n auxOn: boolean\n /** 是否处于\"地面锁定\"(未起飞,辅助灯不可点亮) */\n auxLocked: boolean\n}\n\ninterface Led {\n /** 发光网格:优先直接复用模型自带的灯珠网格本体 */\n mesh: THREE.Object3D\n material: THREE.MeshStandardMaterial\n /** 模型自带灯珠网格(= mesh 本身;换上的自发光材质只赋给该网格,不动全机共享材质) */\n sourceMesh: THREE.Object3D | null\n /** 灯珠网格出厂时的原材质(全机共享,销毁时必须还原引用) */\n originalMaterial: THREE.Material | null\n /** 模型自带的点光源对象(LIGHT_*),位置由我们摆放、颜色强度由灯语驱动 */\n light: THREE.PointLight | null\n /** 点亮时点光源强度;不填用 LED_LIGHT_INTENSITY(近距离成排的灯珠应调低,溢光会互相叠加) */\n lightIntensity?: number\n /**\n * 挂进机臂折叠节点时的\"骨骼绑定位姿\"(机臂局部系)。\n * 每帧按它强制同步,灯珠像焊在骨骼上一样跟随机臂折叠/展开 ——\n * 即使中途被热更新重建等外力挪动过,下一帧也会自动归位。\n */\n bindLocal?: { position: THREE.Vector3; quaternion: THREE.Quaternion; scale: THREE.Vector3 }\n /**\n * 灯珠网格出厂时的父节点。attachStatusToArms 会把灯珠网格移挂进机臂折叠节点,\n * 销毁时必须放回原父节点,否则模型树里永久缺了这盏灯(热更新重建灯组时再也找不到网格)。\n * 为 null 表示灯珠是模型缺网格时我们手工补的兜底球体,销毁直接摘掉即可。\n */\n originalParent: THREE.Object3D | null\n /** 出厂点光源的父节点(点光源同样会被移挂进机臂折叠节点,销毁时一并放回) */\n originalLightParent: THREE.Object3D | null\n}\n\nconst AUTO_LIGHT_MAX_AGL_METERS = 8 // 自动模式:模拟\"夜间低空降落辅助\",高于此高度自动熄灭\nconst AUX_SPOT_ANGLE = 0.42\nconst AUX_SPOT_INTENSITY = 26\n// 灯珠点光源强度(轻微溢光)。\n// ⚠️ 不能调大:白天场景里白色壳体被阳光照亮(线性 ~0.55),红色溢光是\"加色\"叠加 ——\n// 壳体亮底 + 红光 = 粉色晕,溢光越强机身越粉,红灯的观感反而越差(实测 0.35 时主体像素\n// (238,175,162) 三文鱼粉)。溢光只留一点\"灯在发光\"的暗示,红色信号靠灯珠本体自发光。\nconst LED_LIGHT_INTENSITY = 0.12\nconst LED_LIGHT_DISTANCE = 1.6\nconst AUX_MODEL_LIGHT_INTENSITY = 1.6 // 模型自带 LIGHT_BottomAssist 的峰值强度\n\n/**\n * 灯光系统(见文件头):尾部状态灯 / 电池电量灯 / 底部辅助灯,以及模型自带的 LIGHT_* 点光源。\n *\n * ⚠️ 唯一驱动入口是每帧的 update(delta)(由 drone-fly 的 syncPose 调用);\n * setStatusPattern / setBatteryMode 等只登记\"意图\",真正的灯效在 update 内按时间轴应用。\n */\nexport class DroneLights {\n private rig = new THREE.Group()\n private model: THREE.Group | null = null\n private scene: THREE.Scene | null = null\n\n private statusLeds: Led[] = []\n private batteryLeds: Led[] = []\n private auxDisc: THREE.Mesh | null = null\n private auxDiscMaterial: THREE.MeshStandardMaterial | null = null\n private auxSpot: THREE.SpotLight | null = null\n private auxGlow: THREE.PointLight | null = null\n private auxBeam: THREE.Mesh | null = null\n /** 模型自带的 LIGHT_BottomAssist,强度随辅助灯淡入淡出 */\n private auxModelLight: THREE.PointLight | null = null\n\n private statusKey: StatusLightKey = 'gnssNormal'\n private batteryMode: BatteryLightMode = 'level'\n /**\n * 底部辅助照明灯的下向光束锥是否绘制(页面\"照明光束\"勾选框)。\n * 默认 false:辅助灯本身照常工作(聚光灯/灯面/模型点光源),只是不画那根锥形光柱。\n * ⚠️ 这是\"每帧被 applyAux() 刷新的显隐\",必须存状态字段,不能只改 auxBeam.visible。\n */\n private beamEnabled = false\n private batteryLevel = 80\n private auxMode: AuxLightMode = 'auto'\n private flying = false\n private auxOn = false\n private auxIntensity = 0\n private timeMs = 0\n /** 状态灯珠是否已挂到后机臂折叠节点下(一次性,须在展开态完成) */\n private statusArmsAttached = false\n /** 机臂折叠节点引用(按 statusLeds 索引对齐:[0]=右臂,[1]=左臂),供每帧骨骼同步 */\n private statusArmFolds: Array<THREE.Object3D | null> = [null, null]\n private batteryLedsUi: BatteryLedSnapshot[] = []\n private readonly tmpVector = new THREE.Vector3()\n private readonly disposables: Array<THREE.BufferGeometry | THREE.Material> = []\n\n /** 在模型局部坐标系中构建灯组并挂载到模型节点下 */\n attach(model: THREE.Group, scene: THREE.Scene): void {\n this.model = model\n this.scene = scene\n\n // 计算模型\"自身坐标系\"下的包围盒:临时归一模型变换后测量\n const savedPosition = model.position.clone()\n const savedQuaternion = model.quaternion.clone()\n const savedScale = model.scale.clone()\n model.position.set(0, 0, 0)\n model.quaternion.identity()\n model.scale.set(1, 1, 1)\n model.updateMatrixWorld(true)\n const box = new THREE.Box3().setFromObject(model)\n model.position.copy(savedPosition)\n model.quaternion.copy(savedQuaternion)\n model.scale.copy(savedScale)\n model.updateMatrixWorld(true)\n\n const size = box.getSize(new THREE.Vector3())\n\n // —— 模型自带灯位对象解析(优先使用,避免手写坐标导致悬空) ——\n model.updateMatrixWorld(true)\n const raycaster = new THREE.Raycaster()\n const raycastLocal = (\n localX: number,\n localY: number,\n fromLocalZ: number,\n dir: THREE.Vector3,\n ): THREE.Vector3 => {\n const origin = model.localToWorld(new THREE.Vector3(localX, localY, fromLocalZ))\n raycaster.set(origin, dir.clone().normalize())\n const hits = raycaster.intersectObject(model, true)\n const hit = hits[0]\n return hit\n ? model.worldToLocal(hit.point.clone())\n : new THREE.Vector3(localX, localY, fromLocalZ)\n }\n /** 取命名节点在模型自身坐标系中的中心(网格按几何包围盒中心,灯光按世界位置) */\n const nodeCenterLocal = (name: string): THREE.Vector3 | null => {\n const object = model.getObjectByName(name)\n if (!object) return null\n if (object instanceof THREE.Mesh && object.geometry) {\n object.geometry.computeBoundingBox()\n const geometryBox = object.geometry.boundingBox\n if (geometryBox) {\n const center = geometryBox.getCenter(new THREE.Vector3())\n return model.worldToLocal(object.localToWorld(center.clone()))\n }\n }\n return model.worldToLocal(object.getWorldPosition(new THREE.Vector3()))\n }\n /** 把模型自带的 LIGHT_* 点光源摆到锚点,交给灯语驱动 */\n const takeModelLight = (name: string, anchor: THREE.Vector3): THREE.PointLight | null => {\n const light = model.getObjectByName(name)\n if (!(light instanceof THREE.PointLight)) return null\n const parent = light.parent ?? model\n light.position.copy(parent.worldToLocal(model.localToWorld(anchor.clone())))\n light.intensity = 0\n light.distance = LED_LIGHT_DISTANCE\n light.decay = 2\n return light\n }\n\n // —— 1. 飞行器状态指示灯(Remote ID 识别灯):机尾左右两个机臂末端(±X, +Z 侧) ——\n // 直接复用模型自带灯珠网格 LED_Status_Tail_Left / Right:保留原几何与朝向,\n // 仅给该网格单独赋自发光材质(全机材质共享,不能改共享材质本身)\n for (const spec of [\n {\n meshName: 'LED_Status_Tail_Right',\n lightName: 'LIGHT_StatusLED_Right',\n fallback: new THREE.Vector3(8.6, 0.95, 9.71),\n },\n {\n meshName: 'LED_Status_Tail_Left',\n lightName: 'LIGHT_StatusLED_Left',\n fallback: new THREE.Vector3(-8.6, 0.95, 9.71),\n },\n ]) {\n const sourceMesh = model.getObjectByName(spec.meshName) ?? null\n const anchor = nodeCenterLocal(spec.meshName) ?? spec.fallback\n // 灯珠几何包围球 = 径向亮度梯度的中心/半径(模型灯珠网格几何烘在顶点里,包围球即灯珠本体)\n let lightCenter: THREE.Vector3 | null = null\n let lightRadius = 0\n if (sourceMesh instanceof THREE.Mesh) {\n if (!sourceMesh.geometry.boundingSphere) sourceMesh.geometry.computeBoundingSphere()\n lightCenter = sourceMesh.geometry.boundingSphere.center.clone()\n lightRadius = sourceMesh.geometry.boundingSphere.radius\n }\n // ⚠️ 强度必须配着径向梯度用:梯度边缘增益 1.0 = 目标色原样输出,\n // 中心由梯度过曝成亮核。若在此再叠 intensity 2.4,边缘就变成 (255,91,81) 的粉红,\n // 永远到不了目标色(双重过曝,实测踩坑)。\n const material = this.createLedMaterial(1.0, ledShellMap(sourceMesh), lightCenter, lightRadius)\n let mesh: THREE.Object3D\n let originalMaterial: THREE.Material | null = null\n // 此刻 attachStatusToArms 尚未调用,sourceMesh.parent 就是出厂父节点\n let originalParent: THREE.Object3D | null = null\n if (sourceMesh instanceof THREE.Mesh) {\n // 网格本体即发光体:原地换材质,可见性不变,位置先不动(展开后由 attachStatusToArms 挂进机臂)\n originalMaterial = sourceMesh.material\n originalParent = sourceMesh.parent\n sourceMesh.material = material\n mesh = sourceMesh\n } else {\n const geometry = new THREE.SphereGeometry(0.62, 14, 12)\n this.disposables.push(geometry)\n mesh = new THREE.Mesh(geometry, material)\n mesh.position.copy(anchor)\n this.rig.add(mesh)\n }\n const light = takeModelLight(spec.lightName, anchor)\n this.statusLeds.push({\n mesh,\n material,\n sourceMesh,\n originalMaterial,\n originalParent,\n originalLightParent: light?.parent ?? null,\n light,\n })\n }\n\n // —— 2. 电池电量指示灯:直接复用模型自带灯珠网格 LED_BatteryCharge_01..04(01 左 → 04 右) ——\n // 网格挂在 CTRL_DJI_Root 之下(带停放配平俯仰),原位就是正确位置,无需移挂;\n // 与状态灯一样只给网格单独赋自发光材质(全机材质共享,不能改共享材质本身)\n for (let index = 0; index < 4; index += 1) {\n const meshName = `LED_BatteryCharge_0${index + 1}`\n const sourceMesh = model.getObjectByName(meshName) ?? null\n // 4 颗灯珠连排且离相机近,自发光调低避免 Bloom 过曝糊成一片\n let lightCenter: THREE.Vector3 | null = null\n let lightRadius = 0\n if (sourceMesh instanceof THREE.Mesh) {\n if (!sourceMesh.geometry.boundingSphere) sourceMesh.geometry.computeBoundingSphere()\n lightCenter = sourceMesh.geometry.boundingSphere.center.clone()\n lightRadius = sourceMesh.geometry.boundingSphere.radius\n }\n const material = this.createLedMaterial(0.9, ledShellMap(sourceMesh), lightCenter, lightRadius)\n let mesh: THREE.Object3D\n let originalMaterial: THREE.Material | null = null\n let anchor: THREE.Vector3\n let originalParent: THREE.Object3D | null = null\n if (sourceMesh instanceof THREE.Mesh) {\n originalMaterial = sourceMesh.material\n originalParent = sourceMesh.parent\n sourceMesh.material = material\n mesh = sourceMesh\n anchor = nodeCenterLocal(meshName) ?? new THREE.Vector3()\n } else {\n // 兜底:模型缺网格时在电池尾面手放小灯珠\n anchor = new THREE.Vector3(-1.5 + index, 1.4, 6.2 + 0.28)\n const geometry = new THREE.SphereGeometry(0.32, 14, 12)\n this.disposables.push(geometry)\n mesh = new THREE.Mesh(geometry, material)\n mesh.position.copy(anchor)\n this.rig.add(mesh)\n }\n const batteryLight = takeModelLight(`LIGHT_BatteryLED_0${index + 1}`, anchor)\n const led: Led = {\n mesh,\n material,\n sourceMesh,\n originalMaterial,\n originalParent,\n originalLightParent: batteryLight?.parent ?? null,\n light: batteryLight,\n // 真机灯珠嵌在壳体开孔内,光不会打亮外壳;点光源溢光归零,只留灯珠自发光\n lightIntensity: 0,\n }\n this.batteryLeds.push(led)\n }\n this.batteryLedsUi = this.batteryLeds.map(() => ({ on: false, blinking: false }))\n\n // —— 3. 底部辅助照明灯:机身正下方,紧贴下视视觉传感器(SENSOR_Glass_Downward) ——\n const downSensor =\n nodeCenterLocal('SENSOR_Glass_Downward') ?? new THREE.Vector3(0, -2.45, -0.93)\n const auxZ = downSensor.z + 1.15 // 下视传感器旁(向机尾侧偏移,避免与玻璃重叠)\n const auxAnchor = raycastLocal(0, box.min.y - size.y, auxZ, new THREE.Vector3(0, 1, 0)) // 从机腹下方往上吸附\n const auxY = auxAnchor.y - 0.06\n this.auxModelLight = takeModelLight(\n 'LIGHT_BottomAssist',\n new THREE.Vector3(0, auxY - 0.25, auxZ),\n )\n const discGeometry = new THREE.CircleGeometry(1.15, 20)\n discGeometry.rotateX(Math.PI / 2) // 面朝下\n this.disposables.push(discGeometry)\n this.auxDiscMaterial = new THREE.MeshStandardMaterial({\n color: 0x1c2126,\n emissive: 0xffe9c0,\n emissiveIntensity: 0,\n roughness: 0.5,\n })\n // 辅助灯照明面同样用径向梯度(中心过曝亮核 + 边缘暖白),与 Cesium 版同款\n this.installLedGradient(this.auxDiscMaterial, new THREE.Vector3(0, 0, 0), 1.15)\n this.auxDisc = new THREE.Mesh(discGeometry, this.auxDiscMaterial)\n this.auxDisc.position.set(0, auxY, auxZ)\n this.rig.add(this.auxDisc)\n\n this.auxSpot = new THREE.SpotLight('#ffe9c4', 0, 0, AUX_SPOT_ANGLE, 0.45, 1.4)\n this.auxSpot.position.copy(this.auxDisc.position)\n const spotTarget = new THREE.Object3D()\n spotTarget.position.set(0, auxY - 1.2, auxZ)\n this.rig.add(spotTarget)\n this.auxSpot.target = spotTarget\n this.rig.add(this.auxSpot)\n\n this.auxGlow = new THREE.PointLight('#ffe9c4', 0, 2.4, 1.8)\n this.auxGlow.position.set(0, auxY - 0.3, auxZ)\n this.rig.add(this.auxGlow)\n\n // 光束锥体放在场景坐标系中,每帧按\"灯到地面\"的真实距离拉伸\n const beamGeometry = new THREE.ConeGeometry(1, 1, 24, 1, true)\n beamGeometry.translate(0, -0.5, 0) // 锥顶移到原点,便于从灯位向下缩放\n this.disposables.push(beamGeometry)\n const beamMaterial = new THREE.MeshBasicMaterial({\n color: '#ffe2a8',\n transparent: true,\n opacity: 0,\n blending: THREE.AdditiveBlending,\n depthWrite: false,\n side: THREE.DoubleSide,\n toneMapped: false,\n })\n this.disposables.push(beamMaterial)\n this.auxBeam = new THREE.Mesh(beamGeometry, beamMaterial)\n this.auxBeam.visible = false\n scene.add(this.auxBeam)\n\n model.add(this.rig)\n }\n\n /** 每帧驱动:deltaSeconds */\n update(deltaSeconds: number): void {\n // 单帧时长截断到 0.1s:标签页挂起后恢复时 delta 会很大,不截断会让灯语时间轴大跳、淡入错乱\n const delta = Math.max(0, Math.min(deltaSeconds, 0.1))\n this.timeMs += delta * 1000\n this.applyStatusPattern()\n this.applyBattery()\n this.applyAux(delta)\n }\n\n /** 设置状态灯语(仅登记意图,下一帧 update 生效);未知键忽略 */\n setStatusPattern(key: StatusLightKey): void {\n if (key in STATUS_PATTERNS) this.statusKey = key\n }\n\n /** 设置电池灯模式(仅登记意图,下一帧 update 生效) */\n setBatteryMode(mode: BatteryLightMode): void {\n this.batteryMode = mode\n }\n\n /** 设置电量百分比(0~100,越界自动夹紧);到灯珠的映射见 batteryLedPlan */\n setBatteryLevel(levelPercent: number): void {\n this.batteryLevel = Math.max(0, Math.min(100, levelPercent))\n }\n\n /** 设置底部辅助灯模式(仅登记意图;实际点亮还受\"已起飞\"门控,见 applyAux) */\n setAuxLightMode(mode: AuxLightMode): void {\n this.auxMode = mode\n }\n\n /**\n * 底部辅助灯的锥形光束显隐(不影响辅助灯本身的照明)。\n * 立即重算一次,不必等下一帧。\n */\n setBeamVisible(visible: boolean): void {\n this.beamEnabled = visible\n this.applyAux(0)\n }\n\n /** 当前是否绘制锥形光束(探针用) */\n isBeamVisible(): boolean {\n return this.beamEnabled\n }\n\n /** 由飞行控制器每帧同步:是否已离地起飞 */\n setFlying(flying: boolean): void {\n this.flying = flying\n }\n\n /**\n * 把尾部状态灯(模型自带灯珠网格 + 点光源)挂到后机臂折叠节点下,跟随折叠/展开运动。\n *\n * 真机(DJI Mini 4 Pro)状态灯位于\"后机臂外端末梢\",但模型把灯珠网格静态\n * 烘焙在机身坐标系,折叠时灯珠悬空在机外、展开动画中也不跟随。\n * 必须在机臂【展开态】调用一次:此刻灯珠世界位姿正好落在机臂末端\n * (电机钟罩下方的端板),用 Object3D.attach 保持世界变换移挂进折叠节点,\n * 之后随臂折叠、展开、飞行姿态一起运动。\n *\n * ⚠️ 锚点/移挂对象必须是模型自带灯珠网格本体:它挂在 CTRL_DJI_Root 之下,\n * 带着停放配平俯仰,是真实渲染位置。早期版本用自发光克隆体(rig 挂在模型根,\n * 不吃配平俯仰)导致灯珠恒定浮高 sin13.6°×0.73≈0.175m。\n */\n attachStatusToArms(leftFold: THREE.Object3D | null, rightFold: THREE.Object3D | null): void {\n if (!this.model) return\n if (this.statusArmsAttached) {\n // 已绑定过:只检查父子关系是否仍然成立(热更新重建等外力可能破坏它)\n this.statusArmFolds = [rightFold, leftFold] // 与 statusLeds 对齐:[0]=右,[1]=左\n for (const [index, led] of this.statusLeds.entries()) {\n const fold = this.statusArmFolds[index]\n if (led && fold && led.mesh.parent !== fold) {\n fold.attach(led.mesh) // 父子关系被破坏 → 按当前世界位姿重新收养\n if (led.light) fold.attach(led.light)\n }\n }\n return\n }\n this.model.updateMatrixWorld(true)\n const pairs: Array<{ led: Led | undefined; fold: THREE.Object3D | null }> = [\n { led: this.statusLeds[1], fold: leftFold }, // statusLeds[1] 在 -X(左)\n { led: this.statusLeds[0], fold: rightFold }, // statusLeds[0] 在 +X(右)\n ]\n for (const { led, fold } of pairs) {\n if (!led || !fold) continue\n fold.attach(led.mesh) // 保持世界变换移挂(实测精确;灯珠本体在正确变换链上)\n if (led.light) fold.attach(led.light)\n // 记录骨骼绑定位姿:此后每帧按它强制同步(见 syncStatusToArms)\n led.bindLocal = {\n position: led.mesh.position.clone(),\n quaternion: led.mesh.quaternion.clone(),\n scale: led.mesh.scale.clone(),\n }\n }\n this.statusArmFolds = [rightFold, leftFold]\n this.statusArmsAttached = true\n }\n\n /**\n * 每帧把状态灯珠按\"骨骼绑定位姿\"强制同步(骨骼动画式跟随)。\n * 灯珠像焊死在机臂末端一样随折叠/展开运动;任何外力(热更新、调试脚本\n * 挪动、意外重挂)造成的偏移都会在下一帧自动归位。\n */\n syncStatusToArms(): void {\n if (!this.statusArmsAttached) return\n for (const [index, led] of this.statusLeds.entries()) {\n const bind = led?.bindLocal\n const fold = this.statusArmFolds[index]\n if (!bind || !fold || led.mesh.parent !== fold) continue\n led.mesh.position.copy(bind.position)\n led.mesh.quaternion.copy(bind.quaternion)\n led.mesh.scale.copy(bind.scale)\n }\n }\n\n /** 灯光系统快照(面板轮询用) */\n getSnapshot(): DroneLightsSnapshot {\n const pattern = STATUS_PATTERNS[this.statusKey]\n return {\n statusKey: this.statusKey,\n statusLabel: pattern.label,\n statusMeaning: pattern.meaning,\n batteryMode: this.batteryMode,\n batteryLevel: this.batteryLevel,\n batteryLeds: this.batteryLedsUi,\n auxMode: this.auxMode,\n auxOn: this.auxOn,\n auxLocked: !this.flying,\n }\n }\n\n /** 拆除灯组:灯珠/点光源放回出厂父节点、还原共享材质、释放自建资源(漏掉会让热更新重建时找不到灯珠) */\n destroy(): void {\n this.rig.parent?.remove(this.rig)\n if (this.auxBeam) this.scene?.remove(this.auxBeam)\n for (const led of [...this.statusLeds, ...this.batteryLeds]) {\n // 原生灯珠网格可能已被移挂进机臂折叠节点:先放回出厂父节点。\n // 不能直接 remove —— 那会让模型树里永久缺了这盏灯,热更新重建灯组时再也找不到网格。\n if (led.originalParent) {\n if (led.mesh.parent !== led.originalParent) led.originalParent.attach(led.mesh)\n } else {\n // 兜底自建灯珠(模型缺网格时手工补的球体)不属于模型,直接摘掉\n led.mesh.parent?.remove(led.mesh)\n }\n // 点光源同理:可能被移挂进了机臂折叠节点\n if (led.light && led.originalLightParent && led.light.parent !== led.originalLightParent) {\n led.originalLightParent.attach(led.light)\n }\n // 原材质是全机共享的,必须还原引用\n if (led.originalMaterial && led.sourceMesh instanceof THREE.Mesh) {\n led.sourceMesh.material = led.originalMaterial\n }\n }\n for (const disposable of this.disposables) disposable.dispose()\n this.disposables.length = 0\n this.statusLeds = []\n this.batteryLeds = []\n this.auxDisc = null\n this.auxDiscMaterial = null\n this.auxSpot = null\n this.auxGlow = null\n this.auxBeam = null\n this.auxModelLight = null\n this.statusArmsAttached = false\n this.model = null\n this.scene = null\n }\n\n /**\n * 灯珠自发光材质。\n *\n * ⚠️ 底色必须跟着模型自带贴图走:灯珠网格用的是全机共享的贴图材质,灯位在贴图上就是\n * \"灯罩本色\"(机臂末端为绿色)。早期版本把 color 写成深色(0x181d21)且不带 map,\n * 结果灯没亮(emissive=0)时整颗灯珠只剩近黑底色,把原模型的绿色灯罩盖成了黑疙瘩。\n *\n * emissiveIntensity 默认按\"远距离可辨\"的臂尖状态灯取值;近距离成排的灯珠(电池条)应调低,否则过曝糊成一片。\n *\n * ⚠️ 径向亮度梯度(lightCenter/lightRadius 给出时启用),与 Cesium 版 CustomShader 同款:\n * 发光感来自\"过曝亮核 + 纯色边缘\"的亮度梯度,不是整体提亮 —— 整体 ×2.4 会让红通道削顶、\n * 绿蓝分量抬升,整颗灯变粉(观感\"像粉红色\")。边缘强度 1.0 保持灯语目标色纯度,\n * 中心过曝 ~3.2 倍成亮核。中心/半径取灯珠网格几何包围球(几何烘在顶点里,包围球即灯珠本体)。\n */\n private createLedMaterial(\n emissiveIntensity = 2.4,\n shellMap: THREE.Texture | null = null,\n lightCenter: THREE.Vector3 | null = null,\n lightRadius = 0,\n ): THREE.MeshStandardMaterial {\n const material = new THREE.MeshStandardMaterial({\n // 有原贴图 → 白底乘贴图(还原灯罩本色);模型缺网格走手放球兜底时才用暗底\n color: shellMap ? 0xffffff : 0x181d21,\n map: shellMap,\n emissive: 0x000000,\n emissiveIntensity,\n roughness: 0.4,\n metalness: 0.1,\n // 原共享材质是 doubleSided,灯珠嵌在壳体开孔里,保持双面避免背面缺失\n side: THREE.DoubleSide,\n toneMapped: false,\n })\n // 灭灯态的漫反射底色(setLed 点亮时会临时清零,熄灭时按它还原)\n material.userData.baseColorHex = shellMap ? 0xffffff : 0x181d21\n // 灭灯态的表面参数(setLed 点亮时会临时压平,熄灭时还原) —— 见 setLed 注释\n material.userData.baseRoughness = material.roughness\n material.userData.baseMetalness = material.metalness\n if (lightCenter && lightRadius > 0) {\n this.installLedGradient(material, lightCenter, lightRadius)\n }\n this.disposables.push(material)\n return material\n }\n\n /**\n * 给自发光材质装上径向亮度梯度(过曝亮核 + 纯色边缘),状态灯/电量灯/辅助灯共用。\n * 几何局部坐标经 varying 传入片元,与网格挂到哪个节点无关(移挂进机臂折叠节点也不受影响)。\n */\n private installLedGradient(\n material: THREE.MeshStandardMaterial,\n center: THREE.Vector3,\n radius: number,\n ): void {\n const c = center.clone()\n material.onBeforeCompile = (shader) => {\n shader.uniforms.uLedCenter = { value: c }\n shader.uniforms.uLedRadius = { value: radius }\n shader.vertexShader = shader.vertexShader\n .replace('#include <common>', '#include <common>\\nvarying vec3 vLedLocal;')\n .replace('#include <begin_vertex>', '#include <begin_vertex>\\nvLedLocal = position;')\n shader.fragmentShader = shader.fragmentShader\n .replace(\n '#include <common>',\n '#include <common>\\nvarying vec3 vLedLocal;\\nuniform vec3 uLedCenter;\\nuniform float uLedRadius;',\n )\n .replace(\n '#include <emissivemap_fragment>',\n [\n '#include <emissivemap_fragment>',\n 'float ledT = clamp(length(vLedLocal - uLedCenter) / uLedRadius, 0.0, 1.0);',\n // 中心增益 5.0(核心 ~6 倍):核心必须冲进 HDR(线性亮度 >1.0)才能被 demo 的\n // bloom 后期管线捕获溢出光晕 —— 饱和红的亮度天然低(#ff3b30 核心线性亮度仅 ~0.79,\n // 绿核却有 ~1.9),增益不足时红灯永远不泛光。边缘增益 1.0 仍是灯语目标色原样输出。\n // 输出经 ACES 后核心呈白热色 + 周圈饱和红,正是真实 LED「白热芯 + 色晕」的观感。\n 'totalEmissiveRadiance *= (1.0 + 5.0 * (1.0 - ledT) * (1.0 - ledT));',\n ].join('\\n'),\n )\n }\n }\n\n /** 点亮/熄灭一颗灯珠:同时驱动自发光材质与模型自带的点光源 */\n private setLed(led: Led, color: number, on: boolean): void {\n const m = led.material\n m.emissive.setHex(on ? color : 0x000000)\n // 点亮时把 PBR 的\"受光项\"全部压平,只留自发光 —— 否则四路白光叠加把纯红冲成粉色:\n // ① 漫反射:灯罩贴图(白壳)被太阳照亮 → color=0(与 Cesium 版 material.diffuse=0 同源);\n // ② 高光:白色平行光在 roughness 0.4 的镜面上打出白色 specular 高光;\n // ③ 环境反射:demo 场景挂了 RoomEnvironment(白色房间) 环境贴图 → envMapIntensity=0;\n // ④ 点光源溢光保留(LED_LIGHT_INTENSITY)——那是真实 LED 打亮周围壳体的辉光,属正常观感。\n // 熄灭时按 userData 里的出厂值还原灯罩本色。\n m.color.setHex(on ? 0x000000 : (m.userData.baseColorHex as number))\n m.roughness = on ? 1.0 : (m.userData.baseRoughness as number)\n m.metalness = on ? 0.0 : (m.userData.baseMetalness as number)\n m.envMapIntensity = on ? 0 : 1\n if (led.light) {\n led.light.intensity = on ? (led.lightIntensity ?? LED_LIGHT_INTENSITY) : 0\n if (on) led.light.color.setHex(color)\n }\n }\n\n /** 手册第 1 节:尾部状态灯语(左右两颗;常规灯语左右同色,电机启动后左红右绿) */\n private applyStatusPattern(): void {\n const pattern = STATUS_PATTERNS[this.statusKey]\n const loopMs = pattern.steps.reduce((sum, s) => sum + s.onMs + s.offMs, 0)\n let t = loopMs > 0 ? this.timeMs % loopMs : 0\n let colors = pattern.steps[0]?.colors ?? [0x000000]\n let on = false\n for (const s of pattern.steps) {\n if (t < s.onMs) {\n colors = s.colors\n on = true\n break\n }\n if (t < s.onMs + s.offMs) {\n colors = s.colors\n on = false\n break\n }\n t -= s.onMs + s.offMs\n }\n // statusLeds[0] 在 +X(右侧),statusLeds[1] 在 -X(左侧);colors 约定 [左, 右]\n const left = this.statusLeds[1]\n const right = this.statusLeds[0]\n if (!left || !right) return\n this.setLed(left, colors[0] ?? 0x000000, on)\n this.setLed(right, colors[1] ?? colors[0] ?? 0x000000, on)\n }\n\n /** 手册第 2 节:电池 4 颗 LED */\n private applyBattery(): void {\n if (this.batteryLeds.length === 0) return\n let states: boolean[]\n let blinkFlags: boolean[]\n\n if (this.batteryMode === 'off') {\n // 关闭:4 颗电量灯全部熄灭\n states = [false, false, false, false]\n blinkFlags = [false, false, false, false]\n } else if (this.batteryMode === 'full') {\n states = [true, true, true, true]\n blinkFlags = [false, false, false, false]\n } else if (this.batteryMode === 'fault') {\n // 异常闪烁:电池保护触发\n states = this.batteryLeds.map(() => blinkPhase(this.timeMs, 110, 110))\n blinkFlags = [true, true, true, true]\n } else if (this.batteryMode === 'charging') {\n // 充电中:LED 依次跑马点亮\n const active = Math.floor(this.timeMs / 240) % 4\n states = this.batteryLeds.map((_, i) => i === active)\n blinkFlags = [false, false, false, false]\n } else {\n const plan = batteryLedPlan(this.batteryLevel)\n states = this.batteryLeds.map((_, i) => {\n if (i < plan.lit) return true\n if (plan.blinkIndex !== null && i === plan.blinkIndex)\n return blinkPhase(this.timeMs, 240, 240)\n return false\n })\n blinkFlags = this.batteryLeds.map((_, i) => plan.blinkIndex !== null && i === plan.blinkIndex)\n }\n\n this.batteryLeds.forEach((led, i) => {\n const ui = this.batteryLedsUi[i]\n if (!ui) return\n this.setLed(led, BATTERY_COLOR, states[i] ?? false)\n ui.on = states[i] ?? false\n ui.blinking = blinkFlags[i] ?? false\n })\n }\n\n /** 手册第 3 节:底部辅助照明灯(地面锁定 + 起飞后才能点亮) */\n private applyAux(delta: number): void {\n if (!this.auxSpot || !this.auxDisc || !this.auxBeam || !this.auxDiscMaterial || !this.auxGlow)\n return\n\n let want = false\n if (this.flying) {\n if (this.auxMode === 'on') want = true\n else if (this.auxMode === 'auto') {\n // 自动模式:模拟夜间低空降落辅助,离地过高时熄灭\n this.auxDisc.getWorldPosition(this.tmpVector)\n want = this.tmpVector.y <= AUTO_LIGHT_MAX_AGL_METERS\n }\n }\n this.auxOn = want\n\n const target = want ? AUX_SPOT_INTENSITY : 0\n this.auxIntensity += (target - this.auxIntensity) * Math.min(1, delta * 9)\n const ratio = this.auxIntensity / AUX_SPOT_INTENSITY\n\n this.auxSpot.intensity = this.auxIntensity\n this.auxGlow.intensity = this.auxIntensity * 0.06\n this.auxDiscMaterial.emissiveIntensity = 2.6 * ratio\n if (this.auxModelLight) this.auxModelLight.intensity = AUX_MODEL_LIGHT_INTENSITY * ratio\n\n // 光束锥:从灯位拉伸到地面(场景坐标系);未勾选\"照明光束\"时不画\n this.auxDisc.getWorldPosition(this.tmpVector)\n const distance = Math.max(0.05, this.tmpVector.y)\n const visible = this.beamEnabled && ratio > 0.03\n this.auxBeam.visible = visible\n if (visible) {\n const slope = Math.tan(AUX_SPOT_ANGLE)\n this.auxBeam.position.copy(this.tmpVector)\n this.auxBeam.scale.set(distance * slope, distance, distance * slope)\n ;(this.auxBeam.material as THREE.MeshBasicMaterial).opacity = 0.12 * ratio\n }\n }\n}\n\nfunction blinkPhase(timeMs: number, onMs: number, offMs: number): boolean {\n return timeMs % (onMs + offMs) < onMs\n}\n\n/**\n * 取灯珠网格原材质上的贴图(灯罩本色)。\n * 全机共享同一个贴图材质,这里只是只读引用,不会改动共享材质与贴图本身。\n */\nfunction ledShellMap(mesh: THREE.Object3D | null): THREE.Texture | null {\n if (!(mesh instanceof THREE.Mesh)) return null\n const material = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material\n const map = material ? (material as THREE.MeshStandardMaterial).map : null\n return map instanceof THREE.Texture ? map : null\n}\n","import * as THREE from 'three'\nimport type { DroneWorld } from './drone-world'\n\n/**\n * 测距雷达(简化版 DJI 全向视觉避障感知):\n *\n * 真机 Mini 4 Pro 的视觉系统是\"机身四角 + 下视 + 上视\"的多目布局,这里做两组:\n * - 前视:机头前脸、云台左右两侧的一对镜头(官方精密测距 0.5~18 m,FOV 90°×72°);\n * - 上视:机背顶部、电池前方的一对视觉孔(官方精密测距 0.5~15 m,FOV 左右 90°/前后 72°),\n * 用来判断机体上方有没有障碍物。\n *\n * ⚠️ 关键设计:视觉传感器是\"有视场的相机\",不是一条无粗细的射线,而且镜头可以转向 ——\n *\n * 1. **可转瞄准(aim)**:默认 `forward` —— 前视镜头转向机体【正前方】、上视镜头转向\n * 【正上方】(带平滑转动过渡,启动时能看到镜头\"转过去\"的过程);\n * 另有 `sensor` 模式 = 沿玻璃面法线(与画面里的镜头/孔面完全一致)。\n * ⚠️ 玻璃面法线 ≠ 摄像头光轴:机背孔贴在斜坡上,若直接沿法线看,上视会变成\n * \"朝后上方仰 35°\",正上方反而是盲区 —— 所以默认用 forward。\n *\n * 2. **锥形视场检测(coneCast)**:判定在椭圆锥内做(不是打几条采样射线),\n * 同侧两条镜头的视场在中线上重叠(各外张 20°、半张角 22.5°,合起来正好官方\n * 水平/左右 90°),因此正前方、正上方中线上的细柱不会被\"两条射线之间的缝\"漏掉。\n *\n * 起点仍逐帧取自模型玻璃网格的几何中心(与画面里的镜头/孔重合)。\n * 可视化:主射线(亮)+ 视场锥边界(淡),命中点标记球;近红 → 中黄 → 远距组色\n * (前视绿 / 上视蓝)。未上电时整组隐藏。\n *\n * 注意:这里只做\"传感器测量 + 可视化\",刹停保护仍由仿真内核的\n * AABB 避障(drone-sim.applyObstacleBrake)负责,两层互不干扰。\n */\n\nexport interface RadarHit {\n /** 传感器到命中物的距离(米,沿瞄准轴量到障碍物近端) */\n distance: number\n /** 命中物标签(建筑 A / 灯杆 / 天桥…) */\n label: string\n}\n\n/** 瞄准模式:forward = 镜头转去看正前方/正上方;sensor = 沿玻璃面法线(镜头本体朝向) */\nexport type RadarAim = 'forward' | 'sensor'\n\nexport interface RadarSnapshot {\n /** 雷达是否在工作(上电后可用) */\n detecting: boolean\n /** 前视·左传感器命中,null = 视场内无障碍物 */\n left: RadarHit | null\n /** 前视·右传感器命中 */\n right: RadarHit | null\n /** 前视量程(米),按官方 0.5~18 m */\n range: number\n /** 上视·左传感器命中(机背左孔) */\n upLeft: RadarHit | null\n /** 上视·右传感器命中(机背右孔) */\n upRight: RadarHit | null\n /** 上视量程(米),按官方 0.5~15 m */\n upRange: number\n /** 当前瞄准模式 */\n aim: RadarAim\n}\n\n/** 视觉玻璃节点:顺序与 UI 的左/右一致(左 = -X) */\nconst FRONT_NODES = ['SENSOR_Glass_Front_Left', 'SENSOR_Glass_Front_Right'] as const\n/**\n * 上视节点:模型把这对上视孔命名为 Side(实际位于机背顶部、全机最高件),\n * 对应真机 Mini 4 Pro 机体上方的视觉传感器。\n */\nconst UP_NODES = ['SENSOR_Glass_Side_Left', 'SENSOR_Glass_Side_Right'] as const\n\nconst FRONT_RANGE = 18\nconst UP_RANGE = 15\n\nconst V = (x: number, y: number, z: number): THREE.Vector3 => new THREE.Vector3(x, y, z)\n\n/** 距离分档阈值(米):近距红 / 中距黄 / 远距组色 */\nconst NEAR_M = 3\nconst MID_M = 9\n\nconst COLOR_NEAR = 0xff4d4d\nconst COLOR_MID = 0xffc14d\n/** 远距组色:前视绿 / 上视蓝,一眼区分是哪一组在测 */\nconst COLOR_FRONT_FAR = 0x35e08a\nconst COLOR_UP_FAR = 0x4dc3ff\n\n/** 镜头转向机构:瞄准方向每帧朝目标方向转动的速率(1/秒) */\nconst AIM_TURN_RATE = 3.2\n\nconst DEG = Math.PI / 180\n\n/**\n * 视场半张角与镜头外张角(照官方 FOV 拆分):\n * 前视 FOV 水平 90° / 垂直 72° → 各镜头外张 20°、半张角 22.5°(水平)/36°(垂直),\n * 两条镜头视场在中线 ±2.5° 处重叠,合起来正好 90°。\n * 上视 FOV 左右 90° / 前后 72° → 同理(左右 22.5°,前后 36°)。\n */\nconst LENS_SPLAY_DEG = 20\nconst HALF_SIDE_DEG = 22.5\nconst HALF_UP_DEG = 36\n\n/** 未上电 / 未检测时的空读数(update 复位时会保留用户选择的 aim) */\nconst EMPTY_SNAPSHOT: RadarSnapshot = {\n detecting: false,\n left: null,\n right: null,\n range: FRONT_RANGE,\n upLeft: null,\n upRight: null,\n upRange: UP_RANGE,\n aim: 'forward',\n}\n\n/** 面积加权平均面法线(几何自带顶点法线不可靠时用;叉积长度=2×面积) */\nfunction geometryNormal(mesh: THREE.Mesh): THREE.Vector3 {\n const geometry = mesh.geometry\n const position = geometry.getAttribute('position')\n const index = geometry.getIndex()\n const a = new THREE.Vector3()\n const b = new THREE.Vector3()\n const c = new THREE.Vector3()\n const cross = new THREE.Vector3()\n const edgeB = new THREE.Vector3()\n const edgeA = new THREE.Vector3()\n const sum = new THREE.Vector3()\n const count = index ? index.count : position.count\n for (let i = 0; i < count; i += 3) {\n const i0 = index ? index.getX(i) : i\n const i1 = index ? index.getX(i + 1) : i + 1\n const i2 = index ? index.getX(i + 2) : i + 2\n a.fromBufferAttribute(position, i0)\n b.fromBufferAttribute(position, i1)\n c.fromBufferAttribute(position, i2)\n edgeB.subVectors(c, b)\n edgeA.subVectors(a, b)\n cross.crossVectors(edgeB, edgeA)\n sum.add(cross)\n }\n // 面积加权和接近零(退化或对消几何)时退回 +Y,避免得到零向量\n return sum.lengthSq() > 1e-12 ? sum.normalize() : V(0, 1, 0)\n}\n\n/** 单个视觉传感器(一台有视场的相机) */\ninterface SensorUnit {\n group: 'front' | 'up'\n position: 'left' | 'right'\n nodeName: string\n range: number\n farColor: number\n /** 兜底起点(机体系偏移,米) */\n fallbackOffset: THREE.Vector3\n /** sensor 模式方向:玻璃/孔面法线(机体系,兜底用) */\n fallbackDir: THREE.Vector3\n /** forward 模式方向:镜头转向后的瞄准方向(机体系) */\n aimDir: THREE.Vector3\n /** 视场参考轴 1(机体系):halfU1 对应的方向(前视=机体左右,上视=机体左右) */\n refU1: THREE.Vector3\n /** 视场参考轴 2(机体系):halfU2 对应的方向(前视=机体上下,上视=机体前后) */\n refU2: THREE.Vector3\n halfU1: number\n halfU2: number\n /** 绑定的视觉玻璃网格;null = 用兜底常数 */\n mesh: THREE.Mesh | null\n localCenter: THREE.Vector3\n normalGeo: THREE.Vector3\n sign: number\n /** 当前瞄准方向(世界系,平滑转动) */\n aimWorld: THREE.Vector3\n /** 是否已用首帧目标方向初始化(避免开局从 (0,0,0) 转动) */\n aimReady: boolean\n}\n\n/** 光束可视化单元(每条传感器:1 条主射线 + 4 条锥边界) */\ninterface BeamUnit {\n line: THREE.Line\n positions: Float32Array\n material: THREE.LineBasicMaterial\n}\n\nconst splay = Math.sin(LENS_SPLAY_DEG * DEG)\nconst splayCos = Math.cos(LENS_SPLAY_DEG * DEG)\n\nconst SENSOR_SPECS: Array<\n Omit<SensorUnit, 'mesh' | 'localCenter' | 'normalGeo' | 'sign' | 'aimWorld' | 'aimReady'>\n> = [\n {\n group: 'front',\n position: 'left',\n nodeName: FRONT_NODES[0],\n range: FRONT_RANGE,\n farColor: COLOR_FRONT_FAR,\n fallbackOffset: V(-0.215, 0.161, -0.489),\n fallbackDir: V(-0.683, 0.086, -0.725),\n aimDir: V(-splay, 0, -splayCos),\n refU1: V(1, 0, 0),\n refU2: V(0, 1, 0),\n halfU1: HALF_SIDE_DEG * DEG,\n halfU2: HALF_UP_DEG * DEG,\n },\n {\n group: 'front',\n position: 'right',\n nodeName: FRONT_NODES[1],\n range: FRONT_RANGE,\n farColor: COLOR_FRONT_FAR,\n fallbackOffset: V(0.215, 0.161, -0.489),\n fallbackDir: V(0.683, 0.086, -0.725),\n aimDir: V(splay, 0, -splayCos),\n refU1: V(1, 0, 0),\n refU2: V(0, 1, 0),\n halfU1: HALF_SIDE_DEG * DEG,\n halfU2: HALF_UP_DEG * DEG,\n },\n {\n group: 'up',\n position: 'left',\n nodeName: UP_NODES[0],\n range: UP_RANGE,\n farColor: COLOR_UP_FAR,\n fallbackOffset: V(-0.18, 0.194, -0.207),\n fallbackDir: V(-0.579, 0.697, 0.423),\n aimDir: V(-splay, splayCos, 0),\n refU1: V(1, 0, 0),\n refU2: V(0, 0, -1),\n halfU1: HALF_SIDE_DEG * DEG,\n halfU2: HALF_UP_DEG * DEG,\n },\n {\n group: 'up',\n position: 'right',\n nodeName: UP_NODES[1],\n range: UP_RANGE,\n farColor: COLOR_UP_FAR,\n fallbackOffset: V(0.18, 0.194, -0.207),\n fallbackDir: V(0.579, 0.697, 0.423),\n aimDir: V(splay, splayCos, 0),\n refU1: V(1, 0, 0),\n refU2: V(0, 0, -1),\n halfU1: HALF_SIDE_DEG * DEG,\n halfU2: HALF_UP_DEG * DEG,\n },\n]\n\n/**\n * 测距雷达(见文件头):四台视觉传感器(前视双摄 + 上视双孔),每帧做锥形视场检测并更新可视化。\n * 只做\"传感器测量 + 可视化\";刹停保护由 drone-sim 的 AABB 避障负责,两层互不干扰。\n */\nexport class DroneRadar {\n private readonly group = new THREE.Group()\n private readonly disposables: Array<THREE.BufferGeometry | THREE.Material> = []\n /** 每条传感器 5 条线(主射线 + 4 条锥边界),索引 = sensorIndex * 5 + slot */\n private readonly beams: BeamUnit[] = []\n private readonly sensors: SensorUnit[] = SENSOR_SPECS.map((spec) => ({\n ...spec,\n mesh: null,\n localCenter: new THREE.Vector3(),\n normalGeo: spec.fallbackDir.clone(),\n sign: 1,\n aimWorld: spec.aimDir.clone(),\n aimReady: false,\n }))\n\n private aim: RadarAim = 'forward'\n /**\n * 射线可视化的显示开关(页面\"雷达射线\"勾选框)。\n * 默认 false = 不显示(读数照常工作),用户勾选后才画。\n * ⚠️ 必须存成状态字段:update() 每帧都会刷新 group.visible,\n * 若直接写死 true,会把 setVisible(false) 覆盖掉,开关就\"点了没反应\"。\n */\n private beamsVisible = false\n\n private readonly base = new THREE.Vector3()\n private readonly origin = new THREE.Vector3()\n private readonly targetAxis = new THREE.Vector3()\n private readonly axis = new THREE.Vector3()\n private readonly u1 = new THREE.Vector3()\n private readonly u2 = new THREE.Vector3()\n private readonly refU1World = new THREE.Vector3()\n private readonly refU2World = new THREE.Vector3()\n private readonly tmp = new THREE.Vector3()\n private readonly tmp2 = new THREE.Vector3()\n private readonly rotation = new THREE.Matrix4()\n\n private snapshot: RadarSnapshot = EMPTY_SNAPSHOT\n\n /** 绑定雷达扫描可视化(光束挂机体下方,随障碍物真值偏转) */\n constructor(scene: THREE.Scene) {\n for (let index = 0; index < this.sensors.length; index += 1) {\n const sensor = this.sensors[index]\n if (!sensor) continue\n // slot 0 = 主射线(亮),1~4 = 锥边界(淡)\n for (let slot = 0; slot < 5; slot += 1) {\n const positions = new Float32Array(6)\n const geometry = new THREE.BufferGeometry()\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))\n const material = new THREE.LineBasicMaterial({\n color: sensor.farColor,\n transparent: true,\n opacity: slot === 0 ? 0.75 : 0.16,\n toneMapped: false,\n })\n const line = new THREE.Line(geometry, material)\n line.frustumCulled = false\n this.group.add(line)\n this.disposables.push(geometry, material)\n this.beams.push({ line, positions, material })\n }\n }\n this.group.visible = false\n scene.add(this.group)\n }\n\n /** 前视量程(米),按官方 0.5~18 m */\n get range(): number {\n return FRONT_RANGE\n }\n\n /** 上视量程(米),按官方 0.5~15 m */\n get upRange(): number {\n return UP_RANGE\n }\n\n /** 当前读数(每帧刷新;Vue 100ms 轮询取用) */\n getSnapshot(): RadarSnapshot {\n return this.snapshot\n }\n\n /**\n * 瞄准模式:forward = 镜头转去看正前方(上视 = 正上方);sensor = 沿玻璃面法线。\n * 切换后镜头会平滑转过去(不是瞬移)。\n */\n setAim(aim: RadarAim): void {\n this.aim = aim\n this.snapshot = { ...this.snapshot, aim }\n }\n\n /** 当前瞄准模式(forward / sensor) */\n getAim(): RadarAim {\n return this.aim\n }\n\n /**\n * 从模型里读出两组视觉玻璃网格(前视双摄 + 机背上视双孔),\n * 记录其几何中心与镜面法线。开机时调用一次即可。\n */\n bindSensors(model: THREE.Object3D): void {\n model.updateMatrixWorld(true)\n const rotation = new THREE.Matrix4().extractRotation(model.matrixWorld)\n for (const sensor of this.sensors) {\n const mesh = model.getObjectByName(sensor.nodeName)\n if (!(mesh instanceof THREE.Mesh)) continue\n const geometry = mesh.geometry\n geometry.computeBoundingBox()\n const localCenter = new THREE.Vector3()\n geometry.boundingBox?.getCenter(localCenter) ?? localCenter.set(0, 0, 0)\n const normalGeo = geometryNormal(mesh)\n // 镜面法线可能按玻璃背面朝外的写法:统一取朝\"镜头朝外一侧\"\n const worldNormal = normalGeo.clone().transformDirection(mesh.matrixWorld).normalize()\n const worldForward = V(0, 0, -1).applyMatrix4(rotation).normalize()\n const worldUp = V(0, 1, 0).applyMatrix4(rotation).normalize()\n const reference = sensor.group === 'front' ? worldForward : worldUp\n sensor.mesh = mesh\n sensor.localCenter.copy(localCenter)\n sensor.normalGeo.copy(normalGeo)\n sensor.sign = worldNormal.dot(reference) < 0 ? -1 : 1\n }\n }\n\n /** 是否两组都成功绑定到模型(探针用) */\n isBoundToSensors(): boolean {\n return this.sensors.every((sensor) => sensor.mesh !== null)\n }\n\n /** 每个传感器的绑定情况(探针用) */\n debugInfo(): Array<{\n group: string\n position: string\n nodeName: string\n bound: boolean\n aimDeg: number[]\n }> {\n return this.sensors.map((sensor) => ({\n group: sensor.group,\n position: sensor.position,\n nodeName: sensor.nodeName,\n bound: sensor.mesh !== null,\n aimDeg: sensor.aimWorld.toArray().map((value) => Math.round(value * 1000) / 1000),\n }))\n }\n\n /**\n * 每帧更新:先让镜头朝目标方向平滑转动,再做锥形视场检测。\n * 起点逐帧取自视觉玻璃网格的世界矩阵(与画面里的镜头/孔完全重合)。\n */\n update(model: THREE.Object3D, world: DroneWorld, delta: number, detecting: boolean): void {\n if (!detecting) {\n if (this.snapshot !== EMPTY_SNAPSHOT) {\n this.snapshot = { ...EMPTY_SNAPSHOT, aim: this.aim }\n }\n this.group.visible = false\n return\n }\n // 上电后按用户开关决定显隐(不要写死 true,否则开关失效)\n this.group.visible = this.beamsVisible\n\n model.updateMatrixWorld(true)\n this.rotation.extractRotation(model.matrixWorld)\n model.getWorldPosition(this.base)\n\n const hits: Array<RadarHit | null> = []\n for (let index = 0; index < this.sensors.length; index += 1) {\n const sensor = this.sensors[index]\n if (!sensor) continue\n // —— 起点:玻璃中心(世界系) ——\n if (sensor.mesh) {\n this.origin.copy(sensor.localCenter).applyMatrix4(sensor.mesh.matrixWorld)\n } else {\n this.origin.copy(sensor.fallbackOffset).applyMatrix4(this.rotation).add(this.base)\n }\n\n // —— 镜头转向:目标方向 ——\n if (this.aim === 'sensor' && sensor.mesh) {\n this.targetAxis\n .copy(sensor.normalGeo)\n .transformDirection(sensor.mesh.matrixWorld)\n .normalize()\n .multiplyScalar(sensor.sign)\n } else {\n this.targetAxis.copy(sensor.aimDir).applyMatrix4(this.rotation).normalize()\n }\n if (!sensor.aimReady) {\n sensor.aimWorld.copy(this.targetAxis)\n sensor.aimReady = true\n } else {\n sensor.aimWorld\n .lerp(this.targetAxis, Math.min(1, Math.max(0, delta) * AIM_TURN_RATE))\n .normalize()\n }\n this.axis.copy(sensor.aimWorld)\n\n // —— 视场正交基:参考轴在垂直瞄准轴的平面内正交化 ——\n this.refU1World.copy(sensor.refU1).applyMatrix4(this.rotation)\n this.refU2World.copy(sensor.refU2).applyMatrix4(this.rotation)\n this.u1.copy(this.refU1World).addScaledVector(this.axis, -this.refU1World.dot(this.axis))\n if (this.u1.lengthSq() < 1e-8) this.u1.set(0, 1, 0).addScaledVector(this.axis, -this.axis.y)\n this.u1.normalize()\n this.u2.crossVectors(this.axis, this.u1).normalize()\n\n // —— 锥形视场检测(不是打几条采样射线,不存在线缝隙) ——\n const hit = world.coneCast(\n this.origin,\n this.axis,\n this.u1,\n this.u2,\n Math.tan(sensor.halfU1),\n Math.tan(sensor.halfU2),\n sensor.range,\n )\n hits.push(hit)\n this.updateSensorVisual(index, sensor, this.origin, this.axis, hit)\n }\n\n this.snapshot = {\n detecting: true,\n left: hits[0] ?? null,\n right: hits[1] ?? null,\n range: FRONT_RANGE,\n upLeft: hits[2] ?? null,\n upRight: hits[3] ?? null,\n upRange: UP_RANGE,\n aim: this.aim,\n }\n }\n\n /** 更新一条传感器的可视化:主射线 + 4 条视场锥边界 + 命中标记 */\n private updateSensorVisual(\n index: number,\n sensor: SensorUnit,\n origin: THREE.Vector3,\n axis: THREE.Vector3,\n hit: RadarHit | null,\n ): void {\n const color = this.levelColor(hit?.distance ?? Number.POSITIVE_INFINITY, sensor.farColor)\n const length = hit ? hit.distance : sensor.range\n\n // 主射线\n const main = this.beams[index * 5]\n if (!main) return\n this.tmp.copy(origin).addScaledVector(axis, length)\n this.writeBeam(main, origin, this.tmp, color)\n\n // 锥边界:绕 u1 / u2 各偏 ±半张角,示意视场覆盖范围\n const edges: Array<[THREE.Vector3, number, number]> = [\n [this.u2, sensor.halfU1, 0],\n [this.u2, -sensor.halfU1, 0],\n [this.u1, sensor.halfU2, 0],\n [this.u1, -sensor.halfU2, 0],\n ]\n for (let slot = 0; slot < edges.length; slot += 1) {\n const edge = edges[slot]\n const beam = this.beams[index * 5 + 1 + slot]\n if (!edge || !beam) continue\n const [rotAxis, angle] = edge\n this.tmp.copy(axis).applyAxisAngle(rotAxis, angle).normalize()\n this.tmp2.copy(origin).addScaledVector(this.tmp, sensor.range)\n this.writeBeam(beam, origin, this.tmp2, color)\n }\n }\n\n private writeBeam(beam: BeamUnit, start: THREE.Vector3, end: THREE.Vector3, color: number): void {\n beam.positions[0] = start.x\n beam.positions[1] = start.y\n beam.positions[2] = start.z\n beam.positions[3] = end.x\n beam.positions[4] = end.y\n beam.positions[5] = end.z\n const attribute = beam.line.geometry.getAttribute('position') as THREE.BufferAttribute\n attribute.needsUpdate = true\n beam.material.color.setHex(color)\n }\n\n /** 距离分档配色:近红 / 中黄 / 远距用组色(未命中按远距) */\n private levelColor(distance: number, farColor: number): number {\n if (distance < NEAR_M) return COLOR_NEAR\n if (distance < MID_M) return COLOR_MID\n return farColor\n }\n\n /**\n * 页面调试开关:隐藏/显示射线可视化(读数照常)。\n * 只记录状态并立即生效;随后每帧的 update() 会沿用这个状态,不会被覆盖。\n */\n setVisible(visible: boolean): void {\n this.beamsVisible = visible\n this.group.visible = visible && this.snapshot.detecting\n }\n\n /** 当前射线可视化是否显示(探针用) */\n isBeamsVisible(): boolean {\n return this.beamsVisible\n }\n\n /** 释放几何/材质并复位读数 */\n destroy(): void {\n this.group.parent?.remove(this.group)\n for (const disposable of this.disposables) disposable.dispose()\n this.disposables.length = 0\n this.beams.length = 0\n for (const sensor of this.sensors) {\n sensor.mesh = null\n sensor.aimReady = false\n }\n this.snapshot = EMPTY_SNAPSHOT\n }\n}\n","import * as THREE from 'three'\nimport { findMissionInsertIndex } from './drone-sim'\nimport type { MissionWaypoint } from './drone-sim'\nimport type { DroneWorld } from './drone-world'\n\n/**\n * 场景内航点编辑器:在三维场景里直接抓航点。\n *\n * 操作约定(与真机的\"地图上编辑航线\"对应,但这里是在三维场景里):\n * - **拖拽航点** = 在它当前高度的水平面内移动;\n * - **Shift + 拖拽** = 改高度(按屏幕纵向映射到竖直平面);\n * - **Alt** = 暂时关掉 1 米吸附,做微调;\n * - **双击地面** = 在该处插入一个航点(高度取相邻航点);\n * - **选中后 Delete** = 删除该航点;**Esc** = 取消选中。\n *\n * 编辑只改\"航点数据\",飞行逻辑一行都不碰 —— 允许编辑的前提是任务未执行(内核也会再拦一次)。\n */\n/** 拖拽模式:move=在当前高度的水平面内平移,altitude=改高度(Shift 拖拽) */\nexport type MissionDragMode = 'move' | 'altitude'\n\n/** 编辑界限:home 位置用于限距判定,高度/距离上限由仿真内核下发(单位:米) */\nexport interface MissionEditLimits {\n maxAltitude: number\n maxDistance: number\n homeX: number\n homeZ: number\n}\n\n/** 编辑器对外状态快照:enabled 是面板开关,active 还需「当前确实可编辑」(观察者视角 + 任务未执行) */\nexport interface MissionEditState {\n /** 面板开关 */\n enabled: boolean\n /** 开关开着且当前确实可编辑(观察者视角 + 任务未执行) */\n active: boolean\n selectedIndex: number\n hoverIndex: number\n dragging: boolean\n mode: MissionDragMode | null\n}\n\n/**\n * 编辑器依赖的宿主接口:航点数据以仿真内核为权威源,编辑器只通过它读写,\n * 因此这里不含任何三维标记的实现细节(标记由 world 负责重建)。\n */\nexport interface MissionEditorHost {\n canvas: HTMLCanvasElement\n camera: THREE.PerspectiveCamera\n world: DroneWorld\n /** 当前是否可编辑:观察者视角 + 航线未在执行 */\n canEdit(): boolean\n /** 航点列表(权威源 = 仿真内核) */\n getWaypoints(): MissionWaypoint[]\n /** 宿主查询当前选中航点索引(无选中返回 -1) */\n getSelected(): number\n /** 宿主同步选中航点(编辑器点击/面板列表双向联动) */\n setSelected(index: number): void\n /** 整表提交(新增 / 删除):会重建三维标记 */\n commitWaypoints(waypoints: Array<Partial<MissionWaypoint>>, selected: number): void\n /** 拖动中的单点实时更新,返回夹紧后的航点(限高限距已生效) */\n moveWaypoint(index: number, patch: Partial<MissionWaypoint>): MissionWaypoint | null\n /** 拖动期间暂停/恢复轨道控制 */\n setOrbitEnabled(enabled: boolean): void\n /** 取编辑限制(最多航点数/最低高度等,供面板校验) */\n getLimits(): MissionEditLimits\n /** 新航点的默认参数(继承相邻航点的高度与速度) */\n newWaypointTemplate(index: number): Partial<MissionWaypoint>\n /** 编辑器日志出口(宿主可接到面板或控制台) */\n log(level: 'info' | 'warn', text: string): void\n}\n\n/** 拖动判定阈值(像素):小于它算\"点击选中\"而不是拖动 */\nconst DRAG_THRESHOLD_PX = 3\n/** 默认吸附步长(米);按住 Alt 时用微调步长 */\nconst SNAP_STEP = 1\nconst SNAP_STEP_FINE = 0.2\n\ninterface DragState {\n index: number\n mode: MissionDragMode\n pointerId: number\n startClientX: number\n startClientY: number\n /** 航点按下瞬间的坐标:拖动全程以它为基准做相对位移 */\n startX: number\n startZ: number\n startAltitude: number\n moved: boolean\n /**\n * 拖动参考面(水平面或竖直面)与\"按下那一像素\"的交点。\n *\n * ⚠️ 必须做相对位移:用户抓住的是光柱中段或地面环,那个像素的射线与参考面的交点\n * 并不在航点本体上 —— 直接拿交点当航点坐标,一按下去航点就会瞬移到指针位置。\n */\n plane: THREE.Plane\n refX: number\n refY: number\n refZ: number\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\nfunction snap(value: number, step: number): number {\n return Math.round(value / step) * step\n}\n\n/**\n * 指针捕获的安全封装。\n *\n * ⚠️ Chromium 起 setPointerCapture / releasePointerCapture 返回 **Promise**:\n * 指针已失效时(合成事件、指针中途松开)是异步 reject —— 调用处的 try/catch 捕不到,\n * 会冒成未处理 rejection(Playwright 里表现为 pageerror)。同步抛错仍走 try/catch。\n */\nfunction capturePointer(canvas: HTMLCanvasElement, pointerId: number): void {\n try {\n const result: unknown = canvas.setPointerCapture(pointerId)\n if (result && typeof (result as Promise<void>).catch === 'function') {\n ;(result as Promise<void>).catch(() => {})\n }\n } catch {\n /* 指针已释放,同步抛错 */\n }\n}\n\nfunction releasePointer(canvas: HTMLCanvasElement, pointerId: number): void {\n try {\n const result: unknown = canvas.releasePointerCapture(pointerId)\n if (result && typeof (result as Promise<void>).catch === 'function') {\n ;(result as Promise<void>).catch(() => {})\n }\n } catch {\n /* 没捕获过,同步抛错 */\n }\n}\n\n/**\n * 场景内航点编辑器:接管画布的指针/键盘事件,把拖拽、双击新增、删除等操作翻译成\n * 对宿主航点数据的修改。使用:构造传入宿主 → 每帧 update() → 面板开关 setEnabled()\n * → 卸载时 destroy()。具体操作约定见本文件顶部模块说明。\n */\nexport class MissionEditor {\n private readonly host: MissionEditorHost\n private enabled = false\n private hoverIndex = -1\n private drag: DragState | null = null\n /** 在空白处按下:抬手时若没拖动过就取消选中 */\n private emptyClick: { x: number; y: number } | null = null\n private readonly raycaster = new THREE.Raycaster()\n private readonly pointer = new THREE.Vector2()\n /** 水平移动用的地面法线 */\n private readonly groundNormal = new THREE.Vector3(0, 1, 0)\n private readonly cameraForward = new THREE.Vector3()\n private readonly planeNormal = new THREE.Vector3()\n private readonly hitPoint = new THREE.Vector3()\n\n /** 构造航线编辑器:维护航点列表与 Home 点,产出可执行任务 */\n constructor(host: MissionEditorHost) {\n this.host = host\n const canvas = host.canvas\n canvas.addEventListener('pointerdown', this.onPointerDown)\n canvas.addEventListener('pointermove', this.onPointerMove)\n canvas.addEventListener('pointerup', this.onPointerUp)\n canvas.addEventListener('pointercancel', this.onPointerUp)\n canvas.addEventListener('pointerleave', this.onPointerLeave)\n canvas.addEventListener('dblclick', this.onDoubleClick)\n window.addEventListener('keydown', this.onKeyDown)\n }\n\n /** 编辑模式是否开启(开启时点场景可加/拖航点) */\n get isEnabled(): boolean {\n return this.enabled\n }\n\n /** 开关编辑模式 */\n setEnabled(enabled: boolean): void {\n if (this.enabled === enabled) return\n this.enabled = enabled\n if (!enabled) {\n this.cancelDrag()\n this.setHover(-1)\n this.host.canvas.style.cursor = ''\n }\n }\n\n /** 每帧校验:不可编辑时自动收摊(切成机载视角 / 任务开始执行都会走到这里) */\n update(): void {\n if (this.host.canEdit()) return\n if (this.drag) this.cancelDrag()\n if (this.hoverIndex >= 0) this.setHover(-1)\n // 不能编辑了就不该有\"选中的航点\"挂着高亮 —— 指针也点不动它了\n if (this.host.getSelected() >= 0) this.host.setSelected(-1)\n }\n\n /** 取航线只读快照(航点/Home/可视性,供面板与鹰眼图渲染) */\n getState(): MissionEditState {\n return {\n enabled: this.enabled,\n active: this.enabled && this.host.canEdit(),\n selectedIndex: this.host.getSelected(),\n hoverIndex: this.hoverIndex,\n dragging: this.drag !== null,\n mode: this.drag?.mode ?? null,\n }\n }\n\n /** 解散编辑器与场景里的航线辅助物 */\n destroy(): void {\n const canvas = this.host.canvas\n canvas.removeEventListener('pointerdown', this.onPointerDown)\n canvas.removeEventListener('pointermove', this.onPointerMove)\n canvas.removeEventListener('pointerup', this.onPointerUp)\n canvas.removeEventListener('pointercancel', this.onPointerUp)\n canvas.removeEventListener('pointerleave', this.onPointerLeave)\n canvas.removeEventListener('dblclick', this.onDoubleClick)\n window.removeEventListener('keydown', this.onKeyDown)\n this.cancelDrag()\n this.host.canvas.style.cursor = ''\n }\n\n // ————————————————————————————— 指针事件 —————————————————————————————\n\n private readonly onPointerDown = (event: PointerEvent): void => {\n if (!this.active() || event.button !== 0) return\n const index = this.pickWaypoint(event)\n if (index < 0) {\n // 空白处:可能是轨道旋转的开头,先不动选中,等抬手再决定\n this.emptyClick = { x: event.clientX, y: event.clientY }\n return\n }\n const waypoint = this.host.getWaypoints()[index]\n if (!waypoint) return\n this.emptyClick = null\n this.host.setSelected(index)\n this.drag = this.createDrag(event, index, waypoint)\n this.host.setOrbitEnabled(false)\n this.host.canvas.style.cursor = event.shiftKey ? 'ns-resize' : 'grabbing'\n capturePointer(this.host.canvas, event.pointerId)\n }\n\n /** 建立拖动状态:按 Shift 决定拖水平面还是竖直面,并记下\"按下那一像素\"的参考点 */\n private createDrag(\n event: { clientX: number; clientY: number; shiftKey: boolean; pointerId: number },\n index: number,\n waypoint: MissionWaypoint,\n ): DragState {\n this.updateRaycaster(event.clientX, event.clientY)\n // 参考面高度至少取 1 米:航点贴地时,水平参考面若落在 y=0 或以下,求交会退化\n const planeY = Math.max(1, waypoint.altitude)\n const plane = new THREE.Plane()\n if (event.shiftKey) {\n const normal = this.cameraForward.set(0, 0, 0)\n this.host.camera.getWorldDirection(normal)\n this.planeNormal.set(normal.x, 0, normal.z)\n // 相机几乎垂直向下时水平分量退化(lengthSq≈0),回退到 +Z 法线,避免得到非法平面\n if (this.planeNormal.lengthSq() < 1e-4) this.planeNormal.set(0, 0, 1)\n this.planeNormal.normalize()\n this.hitPoint.set(waypoint.x, planeY, waypoint.z)\n plane.setFromNormalAndCoplanarPoint(this.planeNormal, this.hitPoint)\n } else {\n plane.setFromNormalAndCoplanarPoint(\n this.groundNormal,\n this.hitPoint.set(waypoint.x, planeY, waypoint.z),\n )\n }\n const ref = this.raycaster.ray.intersectPlane(plane, this.hitPoint)\n return {\n index,\n mode: event.shiftKey ? 'altitude' : 'move',\n pointerId: event.pointerId,\n startClientX: event.clientX,\n startClientY: event.clientY,\n startX: waypoint.x,\n startZ: waypoint.z,\n startAltitude: waypoint.altitude,\n moved: false,\n plane,\n refX: ref?.x ?? waypoint.x,\n refY: ref?.y ?? planeY,\n refZ: ref?.z ?? waypoint.z,\n }\n }\n\n private readonly onPointerMove = (event: PointerEvent): void => {\n if (!this.enabled) return\n const drag = this.drag\n if (!drag) {\n this.setHover(this.active() ? this.pickWaypoint(event) : -1)\n this.host.canvas.style.cursor = this.hoverIndex >= 0 ? 'grab' : ''\n return\n }\n if (!drag.moved) {\n const moved = Math.hypot(event.clientX - drag.startClientX, event.clientY - drag.startClientY)\n if (moved < DRAG_THRESHOLD_PX) return\n drag.moved = true\n }\n this.applyDrag(event, drag)\n }\n\n private readonly onPointerUp = (event: PointerEvent): void => {\n releasePointer(this.host.canvas, event.pointerId)\n const emptyClick = this.emptyClick\n this.emptyClick = null\n const drag = this.drag\n if (!drag) {\n if (\n emptyClick &&\n Math.hypot(event.clientX - emptyClick.x, event.clientY - emptyClick.y) < DRAG_THRESHOLD_PX\n ) {\n this.host.setSelected(-1)\n }\n return\n }\n this.drag = null\n this.host.setOrbitEnabled(true)\n this.host.canvas.style.cursor = this.hoverIndex >= 0 ? 'grab' : ''\n if (!drag.moved) return\n const waypoint = this.host.getWaypoints()[drag.index]\n if (!waypoint) return\n this.host.log(\n 'info',\n `场景编辑:航点 ${drag.index + 1} 已更新为 E ${waypoint.x.toFixed(0)} / S ${waypoint.z.toFixed(0)} / 高度 ${waypoint.altitude.toFixed(1)} m`,\n )\n }\n\n private readonly onPointerLeave = (): void => {\n if (!this.drag) this.setHover(-1)\n }\n\n /** 双击地面新增航点(双击已有航点不做处理,避免误插一个重合点) */\n private readonly onDoubleClick = (event: MouseEvent): void => {\n if (!this.active() || event.button !== 0) return\n if (this.pickWaypoint(event) >= 0) return\n this.updateRaycaster(event.clientX, event.clientY)\n const ground = this.host.world.pickMissionGround(this.raycaster)\n if (!ground) return\n const limits = this.host.getLimits()\n // 瞄到地平线附近时交点会跑到几公里外,先按限距拉回来\n const offsetX = ground.x - limits.homeX\n const offsetZ = ground.z - limits.homeZ\n const distance = Math.hypot(offsetX, offsetZ)\n let x = ground.x\n let z = ground.z\n if (distance > limits.maxDistance) {\n const scale = limits.maxDistance / distance\n x = limits.homeX + offsetX * scale\n z = limits.homeZ + offsetZ * scale\n }\n x = snap(x, SNAP_STEP)\n z = snap(z, SNAP_STEP)\n\n const list = this.host.getWaypoints()\n const at = findMissionInsertIndex(list, x, z, limits.homeX, limits.homeZ)\n const template = this.host.newWaypointTemplate(at)\n const next: Array<Partial<MissionWaypoint>> = list.map((waypoint) => ({ ...waypoint }))\n next.splice(at, 0, { ...template, x, z })\n this.host.commitWaypoints(next, at)\n this.host.log(\n 'info',\n `场景编辑:在 E ${x.toFixed(0)} / S ${z.toFixed(0)} 新增航点 ${at + 1}(双击地面新增)`,\n )\n }\n\n private readonly onKeyDown = (event: KeyboardEvent): void => {\n if (!this.enabled) return\n const target = event.target as HTMLElement | null\n if (target && ['INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName)) return\n if (event.key === 'Escape') {\n this.host.setSelected(-1)\n return\n }\n if (event.key !== 'Delete' && event.key !== 'Backspace') return\n const index = this.host.getSelected()\n if (index < 0) return\n // 浏览器默认会拿 Backspace 当\"后退\",先拦掉\n event.preventDefault()\n this.deleteWaypoint(index)\n }\n\n /** 删除一个航点(面板按钮与 Delete 键共用) */\n deleteWaypoint(index: number): boolean {\n if (!this.host.canEdit()) return false\n const list = this.host.getWaypoints()\n if (index < 0 || index >= list.length) return false\n const next = list.filter((_, i) => i !== index).map((waypoint) => ({ ...waypoint }))\n this.host.commitWaypoints(next, Math.min(index, next.length - 1))\n this.host.log('info', `场景编辑:已删除航点 ${index + 1}`)\n return true\n }\n\n // ————————————————————————————— 拖拽求解 —————————————————————————————\n\n private applyDrag(event: PointerEvent, drag: DragState): void {\n this.updateRaycaster(event.clientX, event.clientY)\n const step = event.altKey ? SNAP_STEP_FINE : SNAP_STEP\n const hit = this.raycaster.ray.intersectPlane(drag.plane, this.hitPoint)\n if (drag.mode === 'move') {\n if (!hit) return\n this.host.moveWaypoint(drag.index, {\n x: snap(drag.startX + (hit.x - drag.refX), step),\n z: snap(drag.startZ + (hit.z - drag.refZ), step),\n })\n return\n }\n // 改高度:按屏幕纵向映射到\"过航点的竖直平面\"上;平面打不中时退回米/像素映射\n const altitude = hit\n ? drag.startAltitude + (hit.y - drag.refY)\n : drag.startAltitude - (event.clientY - drag.startClientY) * this.metersPerPixel(drag)\n this.host.moveWaypoint(drag.index, {\n altitude: clamp(snap(altitude, step), 1, this.host.getLimits().maxAltitude),\n })\n }\n\n /** 该航点所在距离上\"一个屏幕像素\"对应多少米(竖直面打不中时的兜底映射) */\n private metersPerPixel(drag: DragState): number {\n const camera = this.host.camera\n const distance = camera.position.distanceTo(\n this.hitPoint.set(drag.startX, drag.startAltitude, drag.startZ),\n )\n // 视锥在 distance 处的可见高度 = 2·tan(fov/2)·distance;再按画布像素高度折算成米/像素\n const height =\n (2 * Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)) * distance) /\n Math.max(0.01, camera.zoom)\n const rect = this.host.canvas.getBoundingClientRect()\n // 画布尚未布局(高度为 0)时给 0.2 米/像素的保守兜底,避免除零\n return rect.height > 0 ? height / rect.height : 0.2\n }\n\n // ————————————————————————————— 拾取 —————————————————————————————\n\n private active(): boolean {\n return this.enabled && this.host.canEdit()\n }\n\n private pickWaypoint(event: { clientX: number; clientY: number }): number {\n this.updateRaycaster(event.clientX, event.clientY)\n return this.host.world.pickMissionWaypoint(this.raycaster)\n }\n\n private updateRaycaster(clientX: number, clientY: number): void {\n const rect = this.host.canvas.getBoundingClientRect()\n if (rect.width <= 0 || rect.height <= 0) return\n // 屏幕像素 → 归一化设备坐标(NDC):y 轴要翻转(屏幕向下为正,NDC 向上为正)\n this.pointer.x = ((clientX - rect.left) / rect.width) * 2 - 1\n this.pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1\n this.raycaster.setFromCamera(this.pointer, this.host.camera)\n }\n\n private setHover(index: number): void {\n if (this.hoverIndex === index) return\n this.hoverIndex = index\n this.host.world.setMissionHover(index)\n }\n\n private cancelDrag(): void {\n if (!this.drag) return\n this.drag = null\n this.host.setOrbitEnabled(true)\n }\n}\n","import * as THREE from 'three'\nimport { RoundedBoxGeometry } from 'three/addons/geometries/RoundedBoxGeometry.js'\nimport { mergeGeometries, mergeVertices } from 'three/addons/utils/BufferGeometryUtils.js'\n\nexport type BuildPass =\n 'blockout' | 'structure' | 'form' | 'material' | 'lighting' | 'interaction' | 'optimization'\ntype V3 = [number, number, number]\n\n/**\n * 「机械哈士奇」机体建模 —— 程序化 Mesh 重建(零外部资产),迁自 img2threejs 沙盒的\n * AERIS 展示页(作者参考图驱动)。单位为比例单位,Y-up,**+Z 前向**(哈士奇头朝 +Z;\n * 无人机链路约定 -Z 前向,由文件尾的 `createHuskyDroneModel()` 适配层负责翻转)。\n *\n * 本段只负责\"造形\":按 blockout→structure→form→material→lighting→interaction→\n * optimization 的建造档位输出一个带部件/关节元数据的 THREE.Group;\n * 语义 ID(CTRL_DJI_Root / CTRL_Arm_*_Fold / CTRL_Prop_*_Spin / 云台三轴等)由\n * 适配层补挂 —— 本段不感知无人机仿真链路。\n */\nexport function createDroneModel(options: { pass?: BuildPass } = {}): THREE.Group {\n const pass = options.pass ?? 'optimization'\n const structured = pass !== 'blockout'\n const formed = !['blockout', 'structure'].includes(pass)\n const colored = !['blockout', 'structure', 'form'].includes(pass)\n const bodyLengthScale = 1.35\n const headShiftZ = 0.95 * (bodyLengthScale - 1)\n const armShiftZ = 0.78 * (bodyLengthScale - 1)\n const root = new THREE.Group()\n root.name = 'drone-root'\n const nodes: Record<string, THREE.Object3D> = { 'drone-root': root }\n const sockets: Record<string, THREE.Object3D> = {}\n const parts: THREE.Group[] = []\n const rotors: THREE.Group[] = []\n const clay = new THREE.MeshStandardMaterial({\n name: 'blockout-clay',\n color: 0x7c8994,\n roughness: 0.58,\n metalness: 0.12,\n })\n const pbr = (\n name: string,\n color: number,\n metalness: number,\n roughness: number,\n extra: THREE.MeshPhysicalMaterialParameters = {},\n ) => new THREE.MeshPhysicalMaterial({ name, color, metalness, roughness, ...extra })\n // Solid coatings from the reference. Relief, seams and machined recesses are modeled, not painted lighting.\n const materials = {\n graphite: pbr('graphite-anodized-shell', 0x303540, 0.76, 0.34),\n deck: pbr('satin-machined-deck', 0x777d89, 0.82, 0.3),\n rail: pbr('machined-arm-alloy', 0x434e60, 0.82, 0.28),\n silver: pbr('brushed-motor-aluminium', 0x8b97a9, 0.94, 0.26),\n edge: pbr('polished-machined-edge', 0xa4b1c0, 0.94, 0.19),\n black: pbr('recess-and-rubber', 0x070c13, 0.08, 0.7),\n dark: pbr('black-anodized-motor-stator', 0x121924, 0.74, 0.36),\n yellow: pbr('safety-yellow-propeller', 0xffc51b, 0.18, 0.28, {\n clearcoat: 0.44,\n clearcoatRoughness: 0.19,\n }),\n gold: pbr('gold-motor-collar', 0xd9a821, 0.72, 0.25),\n powerLed: pbr('power-green-led', 0x5bd98c, 0.05, 0.28, {\n emissive: 0x35ef72,\n emissiveIntensity: 1.1,\n }),\n blue: pbr('cyan-light-guide', 0x0c85e9, 0.1, 0.22, {\n emissive: 0x039eff,\n emissiveIntensity: 3.4,\n }),\n eye: pbr('cyan-eye-emitter', 0x057dc2, 0.12, 0.2, {\n emissive: 0x007cce,\n emissiveIntensity: 1.1,\n }),\n amber: pbr('amber-navigation-led', 0xffbd45, 0.12, 0.27, {\n emissive: 0xffa922,\n emissiveIntensity: 2.8,\n }),\n redLed: pbr('red-navigation-led', 0x980919, 0.1, 0.25, {\n emissive: 0xff0823,\n emissiveIntensity: 3.0,\n }),\n lens: pbr('blue-coated-optical-glass', 0x05102c, 0.55, 0.09, {\n clearcoat: 1,\n clearcoatRoughness: 0.0525,\n emissive: 0x022762,\n emissiveIntensity: 0.18,\n }),\n iris: pbr('deep-optical-aperture', 0x01030a, 0.24, 0.14, { clearcoat: 0.8 }),\n opticRing: pbr('blue-optic-ring', 0x12397b, 0.68, 0.15, {\n emissive: 0x034fbc,\n emissiveIntensity: 0.75,\n }),\n guard: pbr('dark-blue-guard-frame', 0x203855, 0.78, 0.25),\n huskyWhite: pbr('husky-ceramic-white', 0xcbd3dd, 0.32, 0.36, {\n clearcoat: 0.24,\n clearcoatRoughness: 0.26,\n }),\n nose: pbr('nose-graphite', 0x10151d, 0.18, 0.3, { clearcoat: 0.4, clearcoatRoughness: 0.22 }),\n }\n function materialFor(parent: THREE.Object3D, name: string): THREE.Material {\n if (!colored) return clay\n const partId = parent.name\n if (name.startsWith('screw-')) return materials.silver\n if (partId === 'beast-head' || partId === 'jaw') {\n if (name === 'muzzle-chin' || name === 'armor-mandible') return materials.graphite\n if (name.startsWith('mask-') || name === 'husky-muzzle') return materials.huskyWhite\n if (name === 'nose-shell') return materials.nose\n if (\n name.startsWith('nostril') ||\n name.startsWith('ear-inset') ||\n name.startsWith('mouth-seam')\n )\n return materials.black\n if (name.startsWith('ear-shell')) return materials.deck\n if (name.startsWith('ear-inner-panel')) return materials.graphite\n if (name.startsWith('eye-iris')) return materials.lens\n if (name === 'armor-face-mask' || name.startsWith('armor-crown')) return materials.graphite\n }\n if (name.startsWith('eye-light')) return materials.eye\n if (name.startsWith('tooth-')) return materials.edge\n if (name.includes('scanner-light')) return materials.blue\n if (name.includes('sole') || name.includes('socket')) return materials.black\n if (\n name.startsWith('recess-') ||\n name.includes('gasket') ||\n name.includes('cable') ||\n name.includes('vent-') ||\n name.includes('isolator') ||\n name.includes('damping')\n )\n return materials.black\n if (name.includes('pupil')) return materials.iris\n if (partId.startsWith('guard-'))\n return name === 'ring' || name === 'outer-rail'\n ? materials.blue\n : name.includes('clip')\n ? materials.silver\n : name.includes('support')\n ? materials.dark\n : materials.guard\n if (partId.startsWith('rotor-'))\n return name.startsWith('blade')\n ? materials.yellow\n : name === 'hub-cap'\n ? materials.dark\n : materials.silver\n if (partId.startsWith('motor-')) {\n if (name === 'status-led' || name === 'bottom-glow') return materials.amber\n if (name === 'blue-can-band') return materials.blue\n if (name === 'can-upper-band' || name === 'top-collar') return materials.gold\n if (name === 'silver-can') return materials.silver\n if (\n name.includes('band') ||\n name.includes('bell-rib') ||\n name === 'shaft' ||\n name === 'lower-neck'\n )\n return materials.edge\n if (name.includes('cooling-fin')) return materials.rail\n return materials.dark\n }\n if (partId.startsWith('arm-'))\n return name.includes('blue-trim')\n ? materials.blue\n : name.includes('spar') || name.includes('rail')\n ? materials.silver\n : name.includes('slotted')\n ? materials.rail\n : materials.graphite\n if (name.includes('light-strip') || name.includes('blue-trim')) return materials.blue\n if (name.startsWith('power-led-')) return materials.powerLed\n if (name.includes('power-seat')) return materials.black\n if (name.includes('rim') || name.includes('fastener') || name.includes('hydraulic'))\n return materials.silver\n if (name.startsWith('armor-'))\n return name.includes('inset') || name.includes('brow') ? materials.graphite : materials.deck\n if (name.includes('drum') || name.includes('joint')) return materials.dark\n if (name === 'skull-shell') return materials.graphite\n if (partId === 'landing-gear')\n return name.includes('rubber')\n ? materials.black\n : name.includes('strut')\n ? materials.rail\n : materials.graphite\n if (partId === 'gimbal') {\n if (name.includes('glass')) return materials.lens\n if (name === 'camera-status') return materials.blue\n if (name.startsWith('focus-ring'))\n return name.endsWith('-4') ? materials.opticRing : materials.dark\n if (name.includes('pitch-') || name.includes('fork-')) return materials.silver\n if (name === 'camera-bezel') return materials.dark\n return materials.graphite\n }\n if (name.startsWith('power-led-')) return materials.powerLed\n if (name.startsWith('power-icon-')) return materials.silver\n if (name.startsWith('power-')) return materials.black\n if (name.startsWith('front-status')) return materials.redLed\n if (name.includes('light-strip') || name.includes('led-cell') || name.includes('side-status'))\n return materials.blue\n if (name.includes('recess')) return materials.black\n if (name.startsWith('split-top') || name.startsWith('top-shoulder')) return materials.deck\n if (name === 'lower-skirt') return materials.dark\n return materials.graphite\n }\n function part(id: string, label: string, position: V3): THREE.Group {\n const group = new THREE.Group()\n group.name = id\n group.position.set(...position)\n group.userData = {\n componentId: id,\n partId: id,\n label,\n destructionGroup: id,\n collider: { shape: 'box' },\n }\n root.add(group)\n nodes[id] = group\n parts.push(group)\n const socket = new THREE.Object3D()\n socket.name = `socket-${id}`\n socket.position.set(...position)\n root.add(socket)\n sockets[id] = socket\n return group\n }\n function mesh(\n parent: THREE.Object3D,\n name: string,\n geometry: THREE.BufferGeometry,\n p: V3 = [0, 0, 0],\n ) {\n const object = new THREE.Mesh(geometry, materialFor(parent, name))\n object.name = `${parent.name}-${name}`\n object.position.set(...p)\n object.castShadow = true\n object.receiveShadow = true\n object.userData = { explodeWithParent: true, componentId: parent.name }\n parent.add(object)\n return object\n }\n function box(parent: THREE.Object3D, name: string, size: V3, p: V3 = [0, 0, 0]) {\n const geometry = formed\n ? new RoundedBoxGeometry(...size, 2, Math.min(0.055, Math.min(...size) * 0.2))\n : new THREE.BoxGeometry(...size)\n return mesh(parent, name, geometry, p)\n }\n function cylinder(\n parent: THREE.Object3D,\n name: string,\n r: number,\n h: number,\n p: V3,\n axis: 'x' | 'y' | 'z' = 'y',\n ) {\n const m = mesh(parent, name, new THREE.CylinderGeometry(r, r, h, 40), p)\n if (axis === 'x') m.rotation.z = Math.PI / 2\n if (axis === 'z') m.rotation.x = Math.PI / 2\n return m\n }\n function ring(\n parent: THREE.Object3D,\n name: string,\n r: number,\n t: number,\n p: V3,\n axis: 'x' | 'y' | 'z' = 'y',\n ) {\n const m = mesh(parent, name, new THREE.TorusGeometry(r, t, 8, 64), p)\n if (axis === 'y') m.rotation.x = Math.PI / 2\n if (axis === 'x') m.rotation.y = Math.PI / 2\n return m\n }\n function rod(parent: THREE.Object3D, name: string, a: V3, b: V3, r: number) {\n const start = new THREE.Vector3(...a),\n end = new THREE.Vector3(...b),\n delta = end.clone().sub(start)\n const m = cylinder(\n parent,\n name,\n r,\n delta.length(),\n start.add(end).multiplyScalar(0.5).toArray() as V3,\n )\n m.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), delta.normalize())\n return m\n }\n function slotPanel(\n parent: THREE.Object3D,\n name: string,\n length: number,\n height: number,\n depth: number,\n p: V3,\n ) {\n const roundedPath = (\n path: THREE.Path,\n x: number,\n y: number,\n w: number,\n h: number,\n r: number,\n ) => {\n if (r <= 0) {\n path.moveTo(x, y)\n path.lineTo(x + w, y)\n path.lineTo(x + w, y + h)\n path.lineTo(x, y + h)\n path.closePath()\n return\n }\n path.moveTo(x + r, y)\n path.lineTo(x + w - r, y)\n path.quadraticCurveTo(x + w, y, x + w, y + r)\n path.lineTo(x + w, y + h - r)\n path.quadraticCurveTo(x + w, y + h, x + w - r, y + h)\n path.lineTo(x + r, y + h)\n path.quadraticCurveTo(x, y + h, x, y + h - r)\n path.lineTo(x, y + r)\n path.quadraticCurveTo(x, y, x + r, y)\n path.closePath()\n }\n const s = new THREE.Shape()\n roundedPath(s, 0, -height / 2, length, height, formed ? 0.035 : 0.025)\n const hole = new THREE.Path()\n const x0 = 0.2,\n x1 = length - 0.2,\n h = height * 0.19\n roundedPath(hole, x0, -h, x1 - x0, h * 2, h * 0.95)\n s.holes.push(hole)\n const geometry = new THREE.ExtrudeGeometry(s, {\n depth: depth - (formed ? 0.008 : 0),\n steps: 1,\n bevelEnabled: formed,\n bevelThickness: 0.004,\n bevelSize: 0.004,\n bevelSegments: 1,\n curveSegments: 6,\n })\n geometry.translate(0, 0, -depth / 2 + (formed ? 0.004 : 0))\n return mesh(parent, name, geometry, p)\n }\n function screw(\n parent: THREE.Object3D,\n name: string,\n p: V3,\n axis: 'x' | 'y' | 'z' = 'y',\n r = 0.032,\n ) {\n if (axis === 'x') {\n const inner = r * 0.43\n const profile = [\n [inner, -0.006],\n [r, -0.006],\n [r, 0.003],\n [r - 0.002, 0.006],\n [inner + 0.001, 0.006],\n [inner, 0.004],\n [inner, -0.006],\n ].map(([x, y]) => new THREE.Vector2(x, y))\n const geometry = new THREE.LatheGeometry(profile, 40)\n geometry.normalizeNormals()\n const object = mesh(parent, 'screw-' + name, geometry, p)\n object.rotation.z = Math.PI / 2\n cylinder(parent, 'recess-' + name, inner, 0.004, p, axis)\n return\n }\n const shape = new THREE.Shape()\n shape.absarc(0, 0, r, 0, Math.PI * 2, false)\n const hole = new THREE.Path()\n for (let i = 0; i < 6; i++) {\n const a = (-i * Math.PI) / 3\n const x = Math.cos(a) * r * 0.43,\n y = Math.sin(a) * r * 0.43\n i ? hole.lineTo(x, y) : hole.moveTo(x, y)\n }\n hole.closePath()\n shape.holes.push(hole)\n // Preserve a crisp, closed hex recess: tiny inward bevels can fold through its corners.\n const geometry = new THREE.ExtrudeGeometry(shape, {\n depth: 0.012,\n bevelEnabled: false,\n curveSegments: 8,\n })\n geometry.translate(0, 0, -0.006)\n const m = mesh(parent, 'screw-' + name, geometry, p)\n if (axis === 'y') m.rotation.x = -Math.PI / 2\n cylinder(parent, 'recess-' + name, r * 0.85, 0.007, p, axis)\n }\n function cable(parent: THREE.Object3D, name: string, points: V3[], radius = 0.011) {\n const curve = new THREE.CatmullRomCurve3(points.map((p) => new THREE.Vector3(...p)))\n return mesh(parent, name, new THREE.TubeGeometry(curve, 24, radius, 6, false))\n }\n // Closed octagonal lofts give the armor a changing cross-section, with real planar facets.\n function armor(\n parent: THREE.Object3D,\n name: string,\n sections: { z: number; w: number; b: number; t: number }[],\n p: V3 = [0, 0, 0],\n ) {\n const positions: number[] = [],\n indices: number[] = []\n for (const s of sections) {\n const bevel = (s.t - s.b) * 0.24\n for (const [x, y] of [\n [-s.w * 0.7, s.b],\n [s.w * 0.7, s.b],\n [s.w, s.b + bevel],\n [s.w, s.t - bevel],\n [s.w * 0.68, s.t],\n [-s.w * 0.68, s.t],\n [-s.w, s.t - bevel],\n [-s.w, s.b + bevel],\n ] as const)\n positions.push(x, y, s.z)\n }\n for (let i = 0; i < sections.length - 1; i++)\n for (let j = 0; j < 8; j++) {\n const a = i * 8 + j,\n b = i * 8 + ((j + 1) % 8),\n c = a + 8,\n d = b + 8\n indices.push(a, b, c, b, d, c)\n }\n for (const [k, front] of [\n [0, false],\n [sections.length - 1, true],\n ] as [number, boolean][]) {\n const s = sections[k]!,\n c = positions.length / 3\n positions.push(0, (s.b + s.t) / 2, s.z)\n for (let j = 0; j < 8; j++) {\n const a = k * 8 + j,\n b = k * 8 + ((j + 1) % 8)\n indices.push(c, front ? a : b, front ? b : a)\n }\n }\n const indexed = new THREE.BufferGeometry()\n indexed.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))\n indexed.setIndex(indices)\n const geometry = indexed.toNonIndexed()\n indexed.dispose()\n geometry.computeVertexNormals()\n return mesh(parent, name, geometry, p)\n }\n function plate(\n parent: THREE.Object3D,\n name: string,\n outline: [number, number][],\n depth: number,\n p: V3,\n rotation: V3 = [0, 0, 0],\n ) {\n const shape = new THREE.Shape()\n outline.forEach(([x, y], i) => (i ? shape.lineTo(x, y) : shape.moveTo(x, y)))\n shape.closePath()\n const bevel =\n formed &&\n !name.startsWith('armor-jowl') &&\n !name.startsWith('ear-') &&\n !name.startsWith('mask-cheek')\n ? Math.min(depth * 0.16, 0.018)\n : 0\n const geometry = new THREE.ExtrudeGeometry(shape, {\n depth: depth - 2 * bevel,\n bevelEnabled: bevel > 0,\n bevelSize: bevel,\n bevelThickness: bevel,\n bevelSegments: 1,\n steps: 1,\n curveSegments: 1,\n })\n geometry.translate(0, 0, -depth / 2 + bevel)\n const object = mesh(parent, name, geometry, p)\n object.rotation.set(...rotation)\n return object\n }\n function armoredLink(\n parent: THREE.Object3D,\n name: string,\n a: V3,\n b: V3,\n width: number,\n thickness: number,\n ) {\n const start = new THREE.Vector3(...a),\n end = new THREE.Vector3(...b),\n delta = end.clone().sub(start),\n length = delta.length()\n const object = armor(\n parent,\n name,\n [\n { z: 0, w: width * 0.32, b: -thickness * 0.34, t: thickness * 0.34 },\n { z: length * 0.18, w: width * 0.5, b: -thickness * 0.5, t: thickness * 0.5 },\n { z: length * 0.68, w: width * 0.45, b: -thickness * 0.45, t: thickness * 0.45 },\n { z: length, w: width * 0.26, b: -thickness * 0.27, t: thickness * 0.27 },\n ],\n a,\n )\n object.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), delta.normalize())\n return object\n }\n function bladeGeometry() {\n // Closed variable-section airfoil. Rounded cross-sections taper and twist along the radius.\n const outline = new THREE.CatmullRomCurve3(\n [\n [0.09, 0.065, 0],\n [0.18, 0.105, 0],\n [0.3, 0.245, 0.025],\n [0.46, 0.31, 0.045],\n [0.64, 0.29, 0.055],\n [0.82, 0.24, 0.058],\n [0.98, 0.15, 0.03],\n [1.08, 0.065, 0.005],\n [1.105, 0.008, 0],\n ].map((v) => new THREE.Vector3(...v)),\n )\n const positions: number[] = [],\n indices: number[] = []\n const sections = 32,\n sides = 12\n for (let i = 0; i <= sections; i++) {\n const t = i / sections,\n p = outline.getPoint(t),\n thickness = 0.021 * (1 - 0.65 * t)\n for (let j = 0; j < sides; j++) {\n const a = (j * Math.PI * 2) / sides,\n z = (Math.cos(a) * p.y) / 2\n positions.push(p.x, Math.sin(a) * thickness + z * (0.3 - 0.18 * t), p.z + z)\n }\n }\n for (let i = 0; i < sections; i++)\n for (let j = 0; j < sides; j++) {\n const a = i * sides + j,\n b = i * sides + ((j + 1) % sides),\n c = a + sides,\n d = b + sides\n indices.push(a, c, b, b, c, d)\n }\n const first = positions.length / 3\n positions.push(0.09, 0, 0)\n const last = positions.length / 3\n positions.push(1.105, 0, 0)\n for (let j = 0; j < sides; j++) {\n const n = (j + 1) % sides\n indices.push(first, j, n, last, sections * sides + n, sections * sides + j)\n }\n const geometry = new THREE.BufferGeometry()\n geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))\n geometry.setIndex(indices)\n geometry.computeVertexNormals()\n return geometry\n }\n const body = part('chassis', '长脊装甲机身', [0, 0, 0])\n armor(body, 'armor-backbone', [\n { z: -1.55, w: 0.78, b: -0.25, t: 0.44 },\n { z: -1.35, w: 0.96, b: -0.43, t: 0.74 },\n { z: -0.3, w: 0.97, b: -0.46, t: 0.8 },\n { z: 0.75, w: 0.86, b: -0.42, t: 0.56 },\n { z: 0.95, w: 0.63, b: -0.24, t: 0.32 },\n ])\n box(body, 'belly-core', [1.45, 0.37, 2.0], [0, -0.54, -0.25])\n if (structured) {\n armor(body, 'armor-dorsal-crown', [\n { z: -1.34, w: 0.57, b: 0.65, t: 0.73 },\n { z: -0.38, w: 0.65, b: 0.7, t: 0.84 },\n { z: 0.3, w: 0.49, b: 0.67, t: 0.78 },\n ])\n box(body, 'belly-vent-recess', [1.5, 0.17, 1.84], [0, -0.66, -0.21])\n box(body, 'rear-access-panel', [1.21, 0.35, 0.038], [0, -0.01, -1.557])\n box(body, 'rear-light-strip', [0.68, 0.04, 0.025], [0, 0.21, -1.586])\n for (const side of [-1, 1]) {\n const id = side < 0 ? 'l' : 'r'\n box(body, 'power-seat-' + id, [0.025, 0.2, 1.12], [side * 0.967, 0.24, -0.64])\n for (let i = 0; i < 4; i++) {\n const z = -1.045 + i * 0.255\n cylinder(body, 'power-seat-' + id + '-' + i, 0.049, 0.027, [side * 0.986, 0.26, z], 'x')\n cylinder(body, 'power-led-' + id + '-' + i, 0.034, 0.015, [side * 1.003, 0.26, z], 'x')\n }\n cylinder(body, 'side-power-ring-' + id, 0.071, 0.025, [side * 0.98, -0.045, -0.7], 'x')\n ring(body, 'blue-trim-power-' + id, 0.073, 0.009, [side * 0.998, -0.045, -0.7], 'x')\n for (let i = 0; i < 16; i++)\n box(\n body,\n 'belly-cooling-fin-' + id + '-' + i,\n [0.048, 0.17, 0.032],\n [side * 0.776, -0.64, -0.98 + i * 0.104],\n )\n plate(\n body,\n 'armor-lower-flank-' + id,\n [\n [-0.82, 0.07],\n [0.51, 0.08],\n [0.7, -0.1],\n [0.53, -0.23],\n [-0.75, -0.23],\n ],\n 0.09,\n [side * 0.83, -0.31, -0.21],\n [0, Math.PI / 2, 0],\n )\n }\n }\n if (formed) {\n for (const side of [-1, 1]) {\n for (const z of [-1.1, -0.65, -0.2, 0.25])\n screw(body, 'flank-' + side + '-' + z, [side * 0.98, 0.09, z], 'x', 0.023)\n box(body, 'side-gasket-' + side, [0.014, 0.022, 1.12], [side * 0.967, -0.17, -0.64])\n box(body, 'rear-panel-seam-' + side, [0.015, 0.008, 0.68], [side * 0.44, 0.776, -0.81])\n }\n armor(body, 'armor-service-hatch', [\n { z: -1.15, w: 0.26, b: 0.763, t: 0.78 },\n { z: -0.72, w: 0.26, b: 0.794, t: 0.81 },\n ])\n for (const x of [-0.2, 0.2]) screw(body, 'hatch-' + x, [x, 0.802, -0.79], 'y', 0.018)\n for (const x of [-0.44, 0.44]) screw(body, 'rear-access-' + x, [x, 0.03, -1.584], 'z', 0.029)\n }\n // Extend the torso in its own frame, keeping lamps, buttons and fasteners round.\n // The articulated assemblies are translated separately, so no parent scale distorts them.\n const bodyStretch = new THREE.Matrix4().makeScale(1, 1, bodyLengthScale)\n for (const child of body.children) {\n if (!(child instanceof THREE.Mesh)) continue\n child.position.z *= bodyLengthScale\n const roundHardware =\n ['CylinderGeometry', 'TorusGeometry'].includes(child.geometry.type) ||\n child.name.startsWith('chassis-screw-') ||\n child.name.startsWith('chassis-recess-')\n if (roundHardware) continue\n const rotation = new THREE.Matrix4().makeRotationFromQuaternion(child.quaternion)\n const localStretch = rotation.clone().invert().multiply(bodyStretch).multiply(rotation)\n child.geometry.applyMatrix4(localStretch)\n }\n const head = part('beast-head', '机械哈士奇头部', [0, 0.18, 0.62 + headShiftZ])\n const mirrored = (points: [number, number][], side: number) =>\n points.map(([x, y]) => [x * side, y] as [number, number])\n function roundedOutline(points: [number, number][], radius = 0.02): THREE.Shape {\n const shape = new THREE.Shape()\n points.forEach(([x, y], i) => {\n const a = new THREE.Vector2(...points[(i + points.length - 1) % points.length]!),\n b = new THREE.Vector2(x, y),\n c = new THREE.Vector2(...points[(i + 1) % points.length]!)\n const cut = Math.min(radius, b.distanceTo(a) * 0.24, b.distanceTo(c) * 0.24)\n const enter = b.clone().add(a.sub(b).normalize().multiplyScalar(cut)),\n leave = b.clone().add(c.sub(b).normalize().multiplyScalar(cut))\n if (i === 0) shape.moveTo(enter.x, enter.y)\n else shape.lineTo(enter.x, enter.y)\n shape.quadraticCurveTo(x, y, leave.x, leave.y)\n })\n shape.closePath()\n return shape\n }\n // A thick, closed curved panel. Eye/nostril holes are real extruded tunnels.\n // Subdivision follows the surface equation; analytic cap normals keep the coatings smooth.\n function curvedPanel(\n parent: THREE.Object3D,\n name: string,\n shape: THREE.Shape,\n depth: number,\n surface: (x: number, y: number) => number,\n subdivisions = 1,\n ) {\n // Normalize both loops explicitly: mirrored outlines must not reverse tunnel walls.\n const contours = shape.extractPoints(6)\n const outline = THREE.ShapeUtils.isClockWise(contours.shape)\n ? contours.shape\n : contours.shape.reverse()\n const profile = new THREE.Shape(outline)\n profile.holes = contours.holes.map(\n (points) => new THREE.Path(THREE.ShapeUtils.isClockWise(points) ? points.reverse() : points),\n )\n const base = new THREE.ExtrudeGeometry(profile, {\n depth,\n steps: 1,\n bevelEnabled: false,\n curveSegments: 6,\n })\n const source = base.getAttribute('position'),\n positions: number[] = [],\n normals: number[] = []\n const emit = (a: THREE.Vector3, b: THREE.Vector3, c: THREE.Vector3, level: number) => {\n if (level > 0) {\n const ab = a.clone().add(b).multiplyScalar(0.5),\n bc = b.clone().add(c).multiplyScalar(0.5),\n ca = c.clone().add(a).multiplyScalar(0.5)\n emit(a, ab, ca, level - 1)\n emit(ab, b, bc, level - 1)\n emit(ca, bc, c, level - 1)\n emit(ab, bc, ca, level - 1)\n return\n }\n const cap = Math.abs(a.z - b.z) < 1e-6 && Math.abs(a.z - c.z) < 1e-6\n const mapped = [a, b, c].map(\n (p) => new THREE.Vector3(p.x, p.y, surface(p.x, p.y) + p.z - depth),\n )\n const faceNormal = mapped[1]!\n .clone()\n .sub(mapped[0]!)\n .cross(mapped[2]!.clone().sub(mapped[0]!))\n .normalize()\n mapped.forEach((p, i) => {\n positions.push(p.x, p.y, p.z)\n const n = faceNormal.clone()\n if (cap) {\n const e = 0.0001,\n corner = [a, b, c][i]!,\n x = corner.x,\n y = corner.y\n n.set(\n -(surface(x + e, y) - surface(x - e, y)) / (2 * e),\n -(surface(x, y + e) - surface(x, y - e)) / (2 * e),\n 1,\n )\n .normalize()\n .multiplyScalar(faceNormal.z < 0 ? -1 : 1)\n }\n normals.push(n.x, n.y, n.z)\n })\n }\n for (let i = 0; i < source.count; i += 3)\n emit(\n new THREE.Vector3().fromBufferAttribute(source, i),\n new THREE.Vector3().fromBufferAttribute(source, i + 1),\n new THREE.Vector3().fromBufferAttribute(source, i + 2),\n subdivisions,\n )\n base.dispose()\n const geometry = new THREE.BufferGeometry()\n geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))\n geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3))\n return mesh(parent, name, geometry)\n }\n function roundedLoft(\n parent: THREE.Object3D,\n name: string,\n sections: { z: number; w: number; y: number; h: number; hUpper?: number }[],\n sides = 24,\n upperTaper = 0,\n ) {\n const positions: number[] = [],\n indices: number[] = []\n for (const s of sections)\n for (let j = 0; j < sides; j++) {\n const a = (j * Math.PI * 2) / sides,\n sin = Math.sin(a)\n positions.push(\n Math.cos(a) * s.w * (1 - upperTaper * Math.max(0, sin)),\n s.y + sin * (sin > 0 ? (s.hUpper ?? s.h) : s.h),\n s.z,\n )\n }\n for (let i = 0; i < sections.length - 1; i++)\n for (let j = 0; j < sides; j++) {\n const a = i * sides + j,\n b = i * sides + ((j + 1) % sides),\n c = a + sides,\n d = b + sides\n indices.push(a, b, c, b, d, c)\n }\n for (const [i, front] of [\n [0, false],\n [sections.length - 1, true],\n ] as [number, boolean][]) {\n const s = sections[i]!,\n center = positions.length / 3\n positions.push(0, s.y, s.z)\n for (let j = 0; j < sides; j++) {\n const a = i * sides + j,\n b = i * sides + ((j + 1) % sides)\n indices.push(center, front ? a : b, front ? b : a)\n }\n }\n const geometry = new THREE.BufferGeometry()\n geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))\n geometry.setIndex(indices)\n geometry.computeVertexNormals()\n return mesh(parent, name, geometry)\n }\n roundedLoft(\n head,\n 'skull-shell',\n [\n { z: -0.24, w: 0.48, y: 0.26, h: 0.55 },\n { z: -0.06, w: 0.68, y: 0.27, h: 0.72 },\n { z: 0.16, w: 0.77, y: 0.28, h: 0.8 },\n { z: 0.38, w: 0.78, y: 0.27, h: 0.8 },\n { z: 0.57, w: 0.72, y: 0.25, h: 0.75 },\n { z: 0.7, w: 0.64, y: 0.23, h: 0.66 },\n ],\n 20,\n )\n const faceZ = (x: number, y: number) =>\n 1.02 - 0.28 * (x / 0.8) ** 2 - (y > 0.3 ? 0.4 : 0.06) * ((y - 0.3) / 0.8) ** 2\n const faceOutline: [number, number][] = [\n [-0.48, -0.41],\n [-0.69, -0.16],\n [-0.765, 0.27],\n [-0.69, 0.7],\n [-0.43, 0.99],\n [0, 1.07],\n [0.43, 0.99],\n [0.69, 0.7],\n [0.765, 0.27],\n [0.69, -0.16],\n [0.48, -0.41],\n ]\n const eyeOutline: [number, number][] = [\n [-0.2, 0.018],\n [-0.15, -0.092],\n [0.07, -0.103],\n [0.19, 0.025],\n [0.18, 0.12],\n [0.055, 0.085],\n ]\n const faceShape = roundedOutline(faceOutline, 0.065)\n for (const side of [-1, 1])\n faceShape.holes.push(\n roundedOutline(\n eyeOutline.map(([x, y]) => [(0.39 + x) * side, 0.3 + y]),\n 0.024,\n ),\n )\n curvedPanel(head, 'armor-face-mask', faceShape, 0.095, faceZ, 3)\n for (const side of [-1, 1]) {\n const id = side < 0 ? 'l' : 'r',\n eyeX = side * 0.39,\n eyeY = 0.3,\n eyeFront = faceZ(eyeX, eyeY)\n const aperture = roundedOutline(\n eyeOutline.map(([x, y]) => [(0.39 + x) * side, 0.3 + y]),\n 0.025,\n )\n curvedPanel(\n head,\n 'eye-socket-' + id,\n aperture,\n 0.025,\n (x, y) => faceZ(x, y) - 0.092,\n 1,\n ).material = colored ? materials.black : clay\n const optic = cylinder(\n head,\n 'eye-optic-' + id,\n 0.102,\n 0.018,\n [eyeX, eyeY, eyeFront - 0.048],\n 'z',\n )\n optic.rotation.y = side * 0.33\n optic.material = colored ? materials.lens : clay\n const iris = ring(head, 'eye-light-' + id, 0.074, 0.012, [eyeX, eyeY, eyeFront - 0.032], 'z')\n iris.rotation.y = side * 0.33\n const irisInner = ring(\n head,\n 'eye-light-inner-' + id,\n 0.058,\n 0.004,\n [eyeX, eyeY, eyeFront - 0.031],\n 'z',\n )\n irisInner.rotation.y = side * 0.33\n irisInner.material = colored ? materials.eye : clay\n const pupil = cylinder(\n head,\n 'eye-iris-' + id,\n 0.052,\n 0.012,\n [eyeX, eyeY, eyeFront - 0.033],\n 'z',\n )\n pupil.rotation.y = side * 0.33\n pupil.material = colored ? materials.iris : clay\n const glint = cylinder(\n head,\n 'eye-catchlight-' + id,\n 0.009,\n 0.007,\n [eyeX - side * 0.022, eyeY + 0.035, eyeFront - 0.025],\n 'z',\n )\n glint.rotation.y = side * 0.33\n glint.material = colored ? materials.eye : clay\n const brow: [number, number][] = [\n [0.12, 0.344],\n [0.215, 0.423],\n [0.278, 0.574],\n [0.382, 0.595],\n [0.468, 0.535],\n [0.644, 0.594],\n [0.574, 0.414],\n [0.314, 0.311],\n ]\n curvedPanel(\n head,\n 'mask-eyebrow-' + id,\n roundedOutline(mirrored(brow, side), 0.025),\n 0.026,\n (x, y) => faceZ(x, y) + 0.012,\n 2,\n )\n const cheek: [number, number][] = [\n [0.712, 0.385],\n [0.617, 0.296],\n [0.549, 0.13],\n [0.373, 0.153],\n [0.304, 0.01],\n [0.244, -0.271],\n [0.438, -0.392],\n [0.664, -0.247],\n [0.754, 0.002],\n ]\n curvedPanel(\n head,\n 'mask-cheek-' + id,\n roundedOutline(mirrored(cheek, side), 0.036),\n 0.04,\n (x, y) => faceZ(x, y) + 0.022,\n 2,\n )\n // A curved triangular rim surrounds a recessed ear bowl, with real taper in depth.\n const earPoints: [number, number][] = [\n [0.225, 0.785],\n [0.735, 0.768],\n [0.765, 0.875],\n [0.692, 1.494],\n [0.65, 1.6],\n [0.606, 1.544],\n ]\n const earInner: [number, number][] = [\n [0.337, 0.875],\n [0.66, 0.852],\n [0.684, 0.944],\n [0.639, 1.423],\n [0.606, 1.377],\n ]\n const earZ = (x: number, y: number) =>\n 0.305 + 0.065 * Math.max(0, 1 - ((Math.abs(x) - 0.515) / 0.29) ** 2) - 0.045 * (y - 0.8)\n const earShape = roundedOutline(mirrored(earPoints, side), 0.018)\n earShape.holes.push(roundedOutline(mirrored(earInner, side), 0.019))\n curvedPanel(head, 'ear-shell-' + id, earShape, 0.14, earZ, 1)\n curvedPanel(\n head,\n 'ear-inset-' + id,\n roundedOutline(mirrored(earInner, side), 0.022),\n 0.03,\n (x, y) => earZ(x, y) - 0.092,\n 1,\n )\n curvedPanel(\n head,\n 'ear-inner-panel-' + id,\n roundedOutline(\n mirrored(\n [\n [0.407, 0.881],\n [0.515, 0.912],\n [0.621, 1.325],\n [0.605, 1.368],\n [0.474, 1.089],\n ],\n side,\n ),\n 0.013,\n ),\n 0.025,\n (x, y) => earZ(x, y) - 0.071,\n 1,\n )\n if (structured) {\n plate(\n head,\n 'armor-temple-' + id,\n [\n [-0.54, 0.06],\n [-0.4, 0.49],\n [-0.08, 0.6],\n [0.19, 0.35],\n [0.2, -0.12],\n [-0.04, -0.38],\n [-0.38, -0.31],\n ],\n 0.1,\n [side * 0.714, 0.045, 0.12],\n [0, Math.PI / 2, 0],\n )\n plate(\n head,\n 'armor-temple-inset-' + id,\n [\n [-0.445, 0.065],\n [-0.333, 0.389],\n [-0.1, 0.48],\n [0.1, 0.29],\n [0.11, -0.055],\n [-0.067, -0.26],\n [-0.292, -0.23],\n ],\n 0.022,\n [side * 0.774, 0.045, 0.12],\n [0, Math.PI / 2, 0],\n )\n curvedPanel(\n head,\n 'mask-ear-mount-' + id,\n roundedOutline(\n mirrored(\n [\n [0.282, 0.803],\n [0.41, 0.804],\n [0.416, 0.924],\n [0.35, 0.965],\n ],\n side,\n ),\n 0.013,\n ),\n 0.045,\n (x, y) => earZ(x, y) + 0.02,\n 1,\n )\n if (formed) {\n for (const [y, z] of [\n [0.42, 0.21],\n [-0.16, 0.3],\n ] as const)\n screw(head, 'temple-' + id + y, [side * 0.784, y, z], 'x', 0.019)\n for (const [x, y] of [\n [0.696, 0.901],\n [0.675, 1.07],\n ] as const)\n screw(head, 'ear-' + id + y, [side * x, y, earZ(side * x, y) + 0.008], 'z', 0.012)\n screw(\n head,\n 'brow-' + id,\n [side * 0.514, 0.506, faceZ(side * 0.514, 0.506) + 0.035],\n 'z',\n 0.012,\n )\n }\n }\n }\n const muzzleSections = [\n { z: 0.64, w: 0.45, y: -0.115, h: 0.315, hUpper: 0.335 },\n { z: 0.79, w: 0.465, y: -0.105, h: 0.317, hUpper: 0.37 },\n { z: 0.94, w: 0.445, y: -0.115, h: 0.294, hUpper: 0.4 },\n { z: 1.1, w: 0.404, y: -0.127, h: 0.264, hUpper: 0.34 },\n { z: 1.24, w: 0.349, y: -0.139, h: 0.235, hUpper: 0.27 },\n { z: 1.37, w: 0.286, y: -0.145, h: 0.202, hUpper: 0.205 },\n { z: 1.405, w: 0.238, y: -0.155, h: 0.185, hUpper: 0.185 },\n ]\n roundedLoft(head, 'husky-muzzle', muzzleSections, 24, 0.22)\n const noseShape = roundedOutline(\n [\n [-0.186, 0.069],\n [0.186, 0.069],\n [0.222, 0.021],\n [0.184, -0.076],\n [0.072, -0.141],\n [0, -0.153],\n [-0.072, -0.141],\n [-0.184, -0.076],\n [-0.222, 0.021],\n ],\n 0.025,\n )\n for (const side of [-1, 1]) {\n const hole = new THREE.Path()\n hole.absellipse(side * 0.105, -0.03, 0.053, 0.036, 0, Math.PI * 2, true, -side * 0.22)\n noseShape.holes.push(hole)\n }\n const noseZ = (x: number, y: number) =>\n 1.58 - 0.063 * (x / 0.225) ** 2 - 0.025 * ((y + 0.025) / 0.15) ** 2\n curvedPanel(head, 'nose-shell', noseShape, 0.14, noseZ, 3)\n for (const side of [-1, 1]) {\n const cavity = mesh(\n head,\n 'nostril-cavity-' + (side < 0 ? 'l' : 'r'),\n new THREE.SphereGeometry(1, 20, 12),\n [side * 0.105, -0.03, noseZ(side * 0.105, -0.03) - 0.151],\n )\n cavity.scale.set(0.083, 0.06, 0.043)\n cavity.material = colored ? materials.black : clay\n }\n if (structured) {\n const cap = roundedOutline(\n [\n [-0.2, 0.63],\n [-0.27, 0.86],\n [-0.21, 1.013],\n [0, 1.07],\n [0.21, 1.013],\n [0.27, 0.86],\n [0.2, 0.63],\n ],\n 0.027,\n )\n curvedPanel(head, 'armor-crown-panel', cap, 0.03, (x, y) => faceZ(x, y) + 0.01, 2)\n for (const side of [-1, 1]) {\n const forehead = roundedOutline(\n mirrored(\n [\n [0.268, 0.659],\n [0.308, 0.903],\n [0.417, 0.951],\n [0.589, 0.779],\n [0.654, 0.655],\n [0.522, 0.637],\n [0.431, 0.704],\n ],\n side,\n ),\n 0.017,\n )\n curvedPanel(\n head,\n 'armor-forehead-panel-' + side,\n forehead,\n 0.025,\n (x, y) => faceZ(x, y) + 0.008,\n 2,\n ).material = colored ? materials.rail : clay\n }\n curvedPanel(\n head,\n 'mask-forehead-blaze',\n roundedOutline(\n [\n [-0.018, 0.466],\n [-0.025, 0.631],\n [0, 0.748],\n [0.025, 0.631],\n [0.018, 0.466],\n ],\n 0.005,\n ),\n 0.012,\n (x, y) => faceZ(x, y) + 0.023,\n 1,\n )\n for (const side of [-1, 1]) {\n const bridge = roundedOutline(\n mirrored(\n [\n [0.035, 0.014],\n [0.151, 0.055],\n [0.123, 0.193],\n [0.043, 0.282],\n ],\n side,\n ),\n 0.02,\n )\n curvedPanel(\n head,\n 'mask-nose-bridge-' + side,\n bridge,\n 0.024,\n (x, y) => 1.026 - 0.27 * (y / 0.31) ** 2 - 0.17 * (x / 0.46) ** 2,\n 3,\n )\n }\n }\n const jaw = part('jaw', '闭合机械下颚', [0, -0.1, 0.97 + headShiftZ])\n roundedLoft(\n jaw,\n 'armor-mandible',\n [\n { z: -0.12, w: 0.405, y: -0.025, h: 0.12 },\n { z: 0.12, w: 0.457, y: -0.065, h: 0.14 },\n { z: 0.41, w: 0.418, y: -0.13, h: 0.105 },\n { z: 0.7, w: 0.347, y: -0.132, h: 0.084 },\n { z: 0.94, w: 0.242, y: -0.105, h: 0.06 },\n { z: 1.035, w: 0.205, y: -0.098, h: 0.042 },\n ],\n 20,\n )\n roundedLoft(jaw, 'muzzle-chin', [\n { z: 0.38, w: 0.412, y: -0.094, h: 0.078 },\n { z: 0.63, w: 0.371, y: -0.1, h: 0.072 },\n { z: 0.86, w: 0.278, y: -0.09, h: 0.057 },\n { z: 0.99, w: 0.226, y: -0.073, h: 0.046 },\n { z: 1.035, w: 0.198, y: -0.063, h: 0.032 },\n ])\n // Follow actual lower muzzle stations so the mouth stays a single fine closed seam.\n const lipPoints: V3[] = []\n for (const s of muzzleSections)\n lipPoints.push([Math.cos(Math.PI / 6) * s.w + 0.006, s.y - 0.5 * s.h + 0.278, s.z - 0.35])\n for (let i = 1; i < 8; i++) {\n const x = 0.206 * (1 - (2 * i) / 8)\n lipPoints.push([x, -0.02 + 0.0525 * (x / 0.206) ** 2, 1.064])\n }\n for (const s of [...muzzleSections].reverse())\n lipPoints.push([-Math.cos(Math.PI / 6) * s.w - 0.006, s.y - 0.5 * s.h + 0.278, s.z - 0.35])\n lipPoints.push([0, -0.035, 0.285])\n const lipCurve = new THREE.CatmullRomCurve3(\n lipPoints.map((p) => new THREE.Vector3(...p)),\n true,\n 'centripetal',\n )\n const mouth = mesh(jaw, 'mouth-seam', new THREE.TubeGeometry(lipCurve, 192, 0.008, 8, true))\n mouth.material = colored ? materials.black : clay\n mouth.userData.tubeRadius = 0.008\n rod(head, 'mouth-philtrum', [0, -0.15, 1.417], [0, -0.3, 1.417], 0.007).material = colored\n ? materials.black\n : clay\n for (const side of [-1, 1]) {\n cylinder(jaw, 'jaw-hinge-' + side, 0.104, 0.12, [side * 0.47, 0.035, 0], 'x')\n if (structured) {\n ring(jaw, 'jaw-hinge-rim-' + side, 0.078, 0.008, [side * 0.542, 0.035, 0], 'x')\n if (formed) screw(jaw, 'jaw-pivot-' + side, [side * 0.554, 0.035, 0], 'x', 0.02)\n }\n }\n\n const legs: { upper: THREE.Group; lower: THREE.Group; foot: THREE.Group }[] = []\n for (const [id, side, front] of [\n ['fl', -1, true],\n ['fr', 1, true],\n ['rl', -1, false],\n ['rr', 1, false],\n ] as [string, number, boolean][]) {\n const legShiftZ = (front ? 0.78 : -1.0) * (bodyLengthScale - 1)\n const hip: V3 = [side * 0.95, -0.42, (front ? 0.78 : -1.0) + legShiftZ]\n const knee: V3 = [side * 1.16, -1.42, (front ? 0.32 : -1.45) + legShiftZ]\n const ankle: V3 = [side * 1.36, -2.37, (front ? 1.15 : -1.06) + legShiftZ]\n const upper = part('leg-upper-' + id, '髋关节与上腿 ' + id.toUpperCase(), hip)\n const kneeLocal = knee.map((v, i) => v - (hip[i] ?? 0)) as V3\n cylinder(upper, 'hip-drum', 0.31, 0.34, [side * 0.035, 0, 0], 'x')\n armoredLink(upper, 'armor-thigh', [0, -0.05, 0], kneeLocal, 0.48, 0.48)\n const lower = part('leg-lower-' + id, '膝关节与下腿 ' + id.toUpperCase(), knee)\n const ankleLocal = ankle.map((v, i) => v - (knee[i] ?? 0)) as V3\n cylinder(lower, 'knee-drum', 0.235, 0.38, [0, 0, 0], 'x')\n armoredLink(lower, 'armor-shin', [0, -0.02, 0], ankleLocal, 0.29, 0.3)\n const foot = part('foot-' + id, '机械支撑足 ' + id.toUpperCase(), ankle)\n cylinder(foot, 'ankle-drum', 0.125, 0.37, [0, 0.005, 0], 'x')\n armor(foot, 'armor-toe', [\n { z: -0.27, w: 0.17, b: -0.15, t: 0.025 },\n { z: -0.1, w: 0.225, b: -0.15, t: 0.095 },\n { z: 0.25, w: 0.23, b: -0.15, t: 0.015 },\n { z: 0.49, w: 0.16, b: -0.15, t: -0.09 },\n ])\n box(foot, 'sole', [0.38, 0.055, 0.63], [0, -0.1525, 0.075])\n if (structured) {\n const shoulderShape: [number, number][] = [\n [-0.23, 0.22],\n [0.04, 0.35],\n [0.31, 0.23],\n [0.39, -0.1],\n [0.19, -0.36],\n [-0.15, -0.31],\n [-0.31, -0.06],\n ]\n plate(\n upper,\n 'armor-hip-cover',\n shoulderShape,\n 0.16,\n [side * 0.225, 0, -0.02],\n [0, Math.PI / 2, 0],\n )\n plate(\n upper,\n 'armor-hip-inset',\n shoulderShape.map(([x, y]) => [x * 0.8, y * 0.8]),\n 0.025,\n [side * 0.315, 0, -0.02],\n [0, Math.PI / 2, 0],\n )\n cylinder(lower, 'knee-rim', 0.24, 0.04, [side * 0.214, 0, 0], 'x')\n cylinder(lower, 'knee-cap', 0.188, 0.035, [side * 0.248, 0, 0], 'x')\n ring(lower, 'knee-inner-rim', 0.151, 0.011, [side * 0.27, 0, 0], 'x')\n cylinder(lower, 'knee-center', 0.1, 0.026, [side * 0.273, 0, 0], 'x')\n armoredLink(\n upper,\n 'armor-thigh-inset',\n [side * 0.13, -0.2, -0.08],\n [kneeLocal[0] + side * 0.13, kneeLocal[1] + 0.16, kneeLocal[2]],\n 0.26,\n 0.12,\n )\n armoredLink(\n lower,\n 'armor-shin-inset',\n [side * 0.075, -0.2, 0.09],\n [ankleLocal[0] + side * 0.075, ankleLocal[1] + 0.18, ankleLocal[2] - 0.07],\n 0.18,\n 0.08,\n )\n rod(\n upper,\n 'hydraulic-piston',\n [0, -0.18, 0.12],\n [kneeLocal[0], kneeLocal[1] + 0.08, kneeLocal[2] + 0.13],\n 0.045,\n )\n for (const s of [-1, 1])\n ring(foot, 'ankle-rim-' + s, 0.102, 0.012, [s * 0.203, 0.005, 0], 'x')\n for (let i = 0; i < 2; i++)\n box(foot, 'toe-panel-seam-' + i, [0.008, 0.008, 0.23], [-0.065 + i * 0.13, -0.021, 0.22])\n }\n if (formed) {\n for (const [z, y] of [\n [-0.17, 0.12],\n [0.11, 0.22],\n [0.22, -0.09],\n ] as const)\n screw(upper, 'hip-' + z, [side * 0.34, y, z], 'x', 0.022)\n screw(lower, 'knee-fastener', [side * 0.297, 0, 0], 'x', 0.066)\n for (const t of [0.32, 0.65]) {\n const p = ankleLocal.map((v) => v * t) as V3\n screw(lower, 'shin-' + t, [(p[0] ?? 0) + side * 0.15, p[1] ?? 0, p[2] ?? 0], 'x', 0.023)\n }\n for (const s of [-1, 1]) screw(foot, 'ankle-' + s, [s * 0.227, 0.005, 0], 'x', 0.032)\n box(upper, 'hip-gold-mark', [0.01, 0.015, 0.16], [side * 0.335, 0.12, -0.04]).material =\n colored ? materials.gold : clay\n }\n legs.push({ upper, lower, foot })\n }\n for (const [id, x, baseZ] of [\n ['fl', -2.6, 1.4],\n ['fr', 2.6, 1.4],\n ['rl', -2.6, -1.4],\n ['rr', 2.6, -1.4],\n ] as [string, number, number][]) {\n const z = baseZ + Math.sign(baseZ) * armShiftZ\n const start = new THREE.Vector3(\n Math.sign(x) * 0.79,\n 0.05,\n Math.sign(z) * 0.78 * bodyLengthScale,\n )\n const end = new THREE.Vector3(x, 0.05, z)\n const delta = end.clone().sub(start)\n const arm = part('arm-' + id, '承力机臂 ' + id.toUpperCase(), start.toArray() as V3)\n arm.rotation.y = -Math.atan2(delta.z, delta.x)\n const length = delta.length()\n if (!structured) box(arm, 'beam', [length, 0.22, 0.25], [length / 2, 0, 0])\n else {\n for (const side of [-1, 1])\n slotPanel(arm, 'slotted-web-' + side, length, 0.22, 0.045, [0, 0, side * 0.108])\n for (const y of [-0.115, 0.115])\n box(arm, 'spar-' + y, [length, 0.035, 0.23], [length / 2, y, 0])\n for (const xh of [0.07, length - 0.2]) {\n box(arm, 'hinge-block-' + xh, [0.24, 0.28, 0.3], [xh, 0, 0])\n cylinder(arm, 'hinge-pin-' + xh, 0.055, 0.31, [xh, 0.03, 0])\n }\n const braceElbow: V3 = [length * 0.61, -0.64, 0]\n armoredLink(arm, 'brace-root', [0.19, -0.13, 0], braceElbow, 0.17, 0.14)\n armoredLink(arm, 'brace-tip', braceElbow, [length - 0.13, -0.15, 0], 0.14, 0.12)\n cylinder(arm, 'brace-joint', 0.16, 0.21, braceElbow, 'z')\n ring(arm, 'brace-joint-rim', 0.121, 0.012, [braceElbow[0], braceElbow[1], 0.126], 'z')\n rod(\n arm,\n 'blue-trim-brace',\n [0.36, -0.25, 0.082],\n [braceElbow[0] - 0.07, braceElbow[1] + 0.06, 0.082],\n 0.008,\n )\n if (formed) {\n for (const side of [-1, 1]) {\n for (const px of [0.28, length - 0.35])\n screw(arm, 'side-' + side + px, [px, 0, side * 0.141], 'z', 0.026)\n box(\n arm,\n 'top-rail-' + side,\n [length - 0.44, 0.012, 0.017],\n [length / 2, 0.142, side * 0.065],\n )\n }\n for (const px of [0.06, length - 0.19])\n screw(arm, 'pivot-' + px, [px, 0.195, 0], 'y', 0.044)\n cable(\n arm,\n 'under-cable',\n [\n [0.15, -0.072, -0.075],\n [length * 0.45, -0.1, -0.06],\n [length * 0.78, -0.08, -0.065],\n [length - 0.1, -0.025, -0.065],\n ],\n 0.012,\n )\n }\n }\n const motor = part('motor-' + id, '无刷电机 ' + id.toUpperCase(), [x, 0.32, z])\n if (!structured) mesh(motor, 'can', new THREE.CylinderGeometry(0.24, 0.24, 0.7, 32))\n else {\n cylinder(motor, 'stator', 0.26, 0.28, [0, -0.24, 0])\n cylinder(motor, 'lower-cap', 0.205, 0.075, [0, -0.412, 0])\n cylinder(motor, 'silver-can', 0.228, formed ? 0.39 : 0.25, [0, formed ? 0.145 : 0.055, 0])\n cylinder(motor, 'top-collar', 0.217, formed ? 0.06 : 0.065, [0, formed ? 0.355 : 0.215, 0])\n cylinder(motor, 'shaft', 0.091, formed ? 0.17 : 0.295, [0, formed ? 0.475 : 0.38, 0])\n ring(motor, 'can-lower-band', 0.228, 0.016, [0, -0.066, 0])\n ring(motor, 'can-upper-band', 0.23, 0.016, [0, formed ? 0.337 : 0.176, 0])\n cylinder(motor, 'vented-bell', 0.165, formed ? 0.05 : 0.06, [0, formed ? 0.425 : 0.28, 0])\n ring(motor, 'blue-can-band', 0.232, 0.008, [0, 0.11, 0])\n ring(motor, 'bottom-glow', 0.135, 0.016, [0, -0.46, 0])\n if (formed) {\n cylinder(motor, 'lower-neck', 0.225, 0.09, [0, -0.07, 0])\n ring(motor, 'stator-seam', 0.26, 0.008, [0, -0.115, 0])\n for (let j = 0; j < 20; j++) {\n const a = (j * Math.PI) / 10\n const fin = box(\n motor,\n 'cooling-fin-' + j,\n [0.017, 0.19, 0.024],\n [Math.sin(a) * 0.258, -0.25, Math.cos(a) * 0.258],\n )\n fin.rotation.y = a\n }\n for (let j = 0; j < 12; j++) {\n const a = (j * Math.PI) / 6\n const rib = box(\n motor,\n 'bell-rib-' + j,\n [0.031, 0.043, 0.048],\n [Math.sin(a) * 0.174, 0.41, Math.cos(a) * 0.174],\n )\n rib.rotation.y = a\n }\n for (let j = 0; j < 6; j++) {\n const a = (j * Math.PI) / 3\n screw(\n motor,\n 'stator-' + j,\n [Math.sin(a) * 0.226, -0.096, Math.cos(a) * 0.226],\n 'y',\n 0.016,\n )\n }\n box(motor, 'status-led', [0.046, 0.065, 0.014], [0, -0.23, 0.274])\n cylinder(motor, 'bottom-axle', 0.09, 0.05, [0, -0.47, 0])\n }\n }\n const rotor = part('rotor-' + id, '三叶旋翼 ' + id.toUpperCase(), [x, 0.94, z])\n rotor.rotation.y = id === 'fl' || id === 'rr' ? 0.22 : -0.35\n mesh(rotor, 'hub', new THREE.CylinderGeometry(0.14, 0.14, 0.15, 24))\n if (!formed) {\n for (let j = 0; j < 3; j++) {\n const blade = box(rotor, 'blade-' + j, [0.89, 0.035, 0.16], [0, 0, 0])\n blade.geometry.translate(0.52, 0, 0)\n blade.rotation.y = (j * Math.PI * 2) / 3\n }\n } else {\n const geometry = bladeGeometry()\n for (let j = 0; j < 3; j++) {\n const blade = mesh(rotor, 'blade-' + j, geometry)\n blade.rotation.y = (j * Math.PI * 2) / 3\n blade.scale.setScalar(0.9)\n }\n cylinder(rotor, 'hub-cap', 0.089, 0.035, [0, 0.091, 0])\n screw(rotor, 'hub-lock', [0, 0.118, 0], 'y', 0.045)\n }\n rotors.push(rotor)\n const guard = part('guard-' + id, '蓝色保护圈 ' + id.toUpperCase(), [x, 0.77, z])\n mesh(guard, 'ring', new THREE.TorusGeometry(1.06, 0.009, 6, 80)).rotation.x = Math.PI / 2\n if (structured) {\n ring(guard, 'outer-rail', 1.079, 0.007, [0, -0.024, 0])\n for (let j = 0; j < 3; j++) {\n const angle = (j * Math.PI * 2) / 3 + Math.PI / 6\n rod(\n guard,\n 'radial-support-' + j,\n [Math.cos(angle) * 0.205, -0.26, Math.sin(angle) * 0.205],\n [Math.cos(angle) * 1.06, -0.03, Math.sin(angle) * 1.06],\n 0.014,\n )\n }\n if (formed) {\n for (let j = 0; j < 12; j++) {\n const a = (j * Math.PI) / 6\n const clip = box(\n guard,\n 'perimeter-clip-' + j,\n [0.045, 0.041, 0.032],\n [Math.cos(a) * 1.07, -0.015, Math.sin(a) * 1.07],\n )\n clip.rotation.y = -a\n }\n }\n }\n }\n const parentIds: Record<string, string> = {\n chassis: 'drone-root',\n 'beast-head': 'chassis',\n jaw: 'beast-head',\n }\n for (const corner of ['fl', 'fr', 'rl', 'rr']) {\n parentIds['arm-' + corner] = 'chassis'\n parentIds['motor-' + corner] = 'arm-' + corner\n parentIds['rotor-' + corner] = 'motor-' + corner\n parentIds['guard-' + corner] = 'motor-' + corner\n parentIds['leg-upper-' + corner] = 'chassis'\n parentIds['leg-lower-' + corner] = 'leg-upper-' + corner\n parentIds['foot-' + corner] = 'leg-lower-' + corner\n }\n // Colliders describe each part in its own frame, before installing the articulated hierarchy.\n for (const group of parts) {\n const bounds = new THREE.Box3()\n for (const child of group.children) {\n if (!(child instanceof THREE.Mesh)) continue\n child.updateMatrix()\n child.geometry.computeBoundingBox()\n if (child.geometry.boundingBox)\n bounds.union(child.geometry.boundingBox.clone().applyMatrix4(child.matrix))\n }\n group.userData.collider = {\n shape: 'box',\n center: bounds.getCenter(new THREE.Vector3()).toArray(),\n halfExtents: bounds.getSize(new THREE.Vector3()).multiplyScalar(0.5).toArray(),\n }\n group.userData.attachment = {\n parentId: parentIds[group.name],\n parentSocket: `socket-${group.name}`,\n contactType: group.name.startsWith('rotor') ? 'shaft' : 'mechanical-mount',\n }\n }\n if (['interaction', 'optimization'].includes(pass)) {\n root.updateMatrixWorld(true)\n for (const group of parts) {\n const parent = nodes[parentIds[group.name]!]!\n parent.attach(group)\n parent.attach(sockets[group.name]!)\n }\n for (const rotor of rotors) rotor.userData.joint = { type: 'continuous', axis: [0, 1, 0] }\n jaw.userData.joint = { type: 'revolute', axis: [1, 0, 0], limits: [-0.18, 0.22] }\n for (const leg of legs)\n for (const node of [leg.upper, leg.lower, leg.foot])\n node.userData.joint = { type: 'revolute', axis: [1, 0, 0], limits: [-0.25, 0.25] }\n }\n if (pass === 'optimization') {\n // Share byte-identical geometry and index repeated face vertices without changing any surface.\n const fingerprint = (geometry: THREE.BufferGeometry) => {\n let h1 = 2166136261,\n h2 = 5381,\n length = 0\n const arrays = Object.keys(geometry.attributes)\n .sort()\n .map((k) => geometry.attributes[k]!.array)\n if (geometry.index) arrays.push(geometry.index.array)\n for (const array of arrays) {\n const bytes = new Uint8Array(array.buffer, array.byteOffset, array.byteLength)\n length += bytes.length\n for (const value of bytes) {\n h1 = Math.imul(h1 ^ value, 16777619)\n h2 = Math.imul(h2, 33) ^ value\n }\n }\n return `${Object.keys(geometry.attributes).sort().join('-')}:${length}:${h1 >>> 0}:${h2 >>> 0}`\n }\n const cache = new Map<string, THREE.BufferGeometry>()\n const discarded = new Set<THREE.BufferGeometry>()\n let beforeMeshes = 0\n root.traverse((object) => {\n if (!(object instanceof THREE.Mesh)) return\n beforeMeshes++\n const original = object.geometry,\n key = fingerprint(original)\n let shared = cache.get(key)\n if (!shared) {\n shared = mergeVertices(original, 1e-6)\n shared.clearGroups()\n cache.set(key, shared)\n }\n object.geometry = shared\n discarded.add(original)\n })\n // Batch only repeated copies of the SAME primitive within a part. Unlike a material-wide\n // merge, this keeps mechanically overlapping stators, housings and rails as separate solids.\n const containers: THREE.Object3D[] = []\n root.traverse((object) => {\n if (object.children.some((child) => child instanceof THREE.Mesh)) containers.push(object)\n })\n for (const container of containers) {\n const buckets = new Map<string, THREE.Mesh[]>()\n for (const child of container.children) {\n if (!(child instanceof THREE.Mesh) || Array.isArray(child.material)) continue\n // Closely spaced teeth keep their own closed solids and semantic boundaries.\n if (child.name.includes('tooth-')) continue\n // 桨叶必须保持独立网格:无人机适配层要按名字重命名成 PROP_*_Blade_*,\n // DroneRig 靠它们驱动自转/收纳叠拢(合并后三叶变一个整体网格,无法驱动)。\n if (child.name.includes('blade-')) continue\n // 电量灯珠同理:适配层要把左侧 4 颗升格为 LED_BatteryCharge_01..04 交给灯光系统驱动。\n if (child.name.includes('power-led-')) continue\n const key = child.geometry.uuid + child.material.uuid\n const bucket = buckets.get(key) ?? []\n bucket.push(child)\n buckets.set(key, bucket)\n }\n let batch = 0\n for (const copies of buckets.values()) {\n if (copies.length < 2) continue\n const transformed = copies.map((copy) => {\n copy.updateMatrix()\n return copy.geometry.clone().applyMatrix4(copy.matrix)\n })\n const merged = mergeGeometries(transformed, false)\n transformed.forEach((geometry) => geometry.dispose())\n if (!merged) throw new Error('Repeated drone geometry could not be batched.')\n const object = new THREE.Mesh(merged, copies[0]!.material)\n object.name = `${container.name}-repeated-${batch++}`\n object.castShadow = true\n object.receiveShadow = true\n object.userData = {\n ...copies[0]!.userData,\n sourceMeshes: copies.map((copy) => copy.name),\n repeatedSolidCount: copies.length,\n }\n copies.forEach((copy) => container.remove(copy))\n container.add(object)\n }\n }\n // Share the resulting repeated arrangements across the four identical motor stations, too.\n const finalCache = new Map<string, THREE.BufferGeometry>()\n let afterMeshes = 0\n root.traverse((object) => {\n if (!(object instanceof THREE.Mesh)) return\n afterMeshes++\n const key = fingerprint(object.geometry),\n shared = finalCache.get(key)\n if (shared && shared !== object.geometry) {\n discarded.add(object.geometry)\n object.geometry = shared\n } else finalCache.set(key, object.geometry)\n })\n const retained = new Set(finalCache.values())\n discarded.forEach((geometry) => {\n if (!retained.has(geometry)) geometry.dispose()\n })\n root.userData.optimization = {\n beforeMeshes,\n afterMeshes,\n uniqueGeometries: finalCache.size,\n method:\n 'shared buffers; indexed vertices; repeated non-intersecting primitives batched within each part',\n decimated: false,\n }\n }\n root.userData.sculptRuntime = {\n nodes,\n sockets,\n parts,\n rotors,\n jaw,\n legs,\n parentIds,\n colliders: parts.map((p) => ({ id: p.name, ...p.userData.collider })),\n destructionGroups: parts.map((p) => ({\n id: p.name,\n parentId: parentIds[p.name],\n parts: [p.name],\n seam: 'mechanical-mount',\n })),\n }\n root.userData.buildPass = pass\n root.userData.source =\n 'User supplied mechanical-beast concept images; revision-v6 sculpted mechanical husky head'\n root.userData.proportions = {\n bodyLengthScale,\n bodyLength: 2.5 * bodyLengthScale,\n headShiftZ,\n armShiftZ,\n }\n root.userData.design =\n 'Mechanical husky with upright ears and closed muzzle, elongated body, four tri-blade rotors'\n root.updateMatrixWorld(true)\n return root\n}\n\n// ═══════════════════════ 无人机链路适配层 ═══════════════════════\n//\n// 把上面的\"纯造形\"哈士奇改造成 DroneFly 语义机体。与积木机体(已移除)相同的三条\n// 隐性契约在这里兑现:\n// 1. 语义 ID 全量补挂(部件自检 50/50),功能性节点(CTRL_*)真正可驱动;\n// 2. 前向对齐:哈士奇头朝 +Z,链路约定 -Z → 适配层绕竖轴翻转 180°;\n// 3. 触地点交给 normalizeModel(全模型最低点=机足),适配层不手工对地。\n// 所有补挂的网格/材质都在模型树内,随 DroneFly.destroy() 一并释放。\n\nimport type { ArmPosition } from './drone-rig'\n\n/** 建一个命名节点并挂到父级 */\nfunction huskyNode(parent: THREE.Object3D, name: string, position?: THREE.Vector3): THREE.Object3D {\n const node = new THREE.Object3D()\n node.name = name\n if (position) node.position.copy(position)\n parent.add(node)\n return node\n}\n\n/** 适配层自用材质(风格对齐哈士奇的石墨/蓝玻璃,不引用建模段闭包里的材质) */\ninterface HuskyAdapterMaterials {\n housing: THREE.MeshStandardMaterial\n glass: THREE.MeshStandardMaterial\n lens: THREE.MeshStandardMaterial\n led: THREE.MeshStandardMaterial\n decal: THREE.MeshStandardMaterial\n all: THREE.Material[]\n}\n\nfunction createHuskyAdapterMaterials(): HuskyAdapterMaterials {\n const housing = new THREE.MeshStandardMaterial({\n name: 'husky-gimbal-housing',\n color: 0x303540,\n roughness: 0.34,\n metalness: 0.76,\n })\n const glass = new THREE.MeshStandardMaterial({\n name: 'husky-sensor-glass',\n color: 0x0c2233,\n roughness: 0.16,\n metalness: 0.7,\n emissive: 0x039eff,\n emissiveIntensity: 0.55,\n side: THREE.DoubleSide,\n })\n const lens = new THREE.MeshStandardMaterial({\n name: 'husky-optical-lens',\n color: 0x05102c,\n roughness: 0.09,\n metalness: 0.55,\n })\n // 灯珠底色:DroneLights 装配时会就地换成自发光材质\n const led = new THREE.MeshStandardMaterial({\n name: 'husky-led-base',\n color: 0x14171c,\n roughness: 0.3,\n metalness: 0.1,\n })\n const decal = new THREE.MeshStandardMaterial({\n name: 'husky-decal-plate',\n color: 0x4c5563,\n roughness: 0.5,\n metalness: 0.3,\n })\n return { housing, glass, lens, led, decal, all: [housing, glass, lens, led, decal] }\n}\n\n/**\n * 造一台「机械哈士奇」语义机体:建模(createDroneModel)+ 语义适配。\n * 返回的模型可直接交给 DroneFly.initialize()(缺省机体的来源);\n * 所有几何/材质由模型自己持有,随 DroneFly.destroy() 一起释放。\n */\nexport function createHuskyDroneModel(): THREE.Group {\n const materials = createHuskyAdapterMaterials()\n const inner = createDroneModel()\n\n const model = new THREE.Group()\n model.name = '机械哈士奇无人机'\n\n // —— 顶层骨架:与默认 GLB 的双根约定一致 ——\n // CTRL_DJI_Root 施加机体俯仰/横滚;RIG_DJI_Arms_Props 承载机臂折叠/旋翼自转。\n // 两根同在模型原点且都不带旋转,DroneRig 对两根施加相同姿态 → 机体与机臂同步刚转。\n const controlRoot = huskyNode(model, 'CTRL_DJI_Root')\n const rigRoot = huskyNode(model, 'RIG_DJI_Arms_Props')\n\n // 前向翻转:哈士奇头朝 +Z,链路约定 -Z。绕竖轴转 180°,原点不动,\n // 因此机身/腿/头的世界坐标只做 (x,z) → (-x,-z),y 不变。\n const oriented = huskyNode(controlRoot, 'BLOCK_Orientation')\n oriented.rotation.y = Math.PI\n oriented.add(inner)\n model.updateMatrixWorld(true)\n const world = (object: THREE.Object3D) => object.getWorldPosition(new THREE.Vector3())\n\n // —— 机臂 / 电机 / 旋翼:四角按\"转后世界象限\"归位成 FL/FR/RL/RR ——\n // 哈士奇原角标 fl 在 (+Z 机头、-X),翻转 180° 后落在 +X/-Z = FrontRight,以此类推。\n for (const corner of ['fl', 'fr', 'rl', 'rr'] as const) {\n const armPart = inner.getObjectByName(`arm-${corner}`)\n const rotorPart = inner.getObjectByName(`rotor-${corner}`)\n const motorPart = inner.getObjectByName(`motor-${corner}`)\n if (!armPart || !rotorPart || !motorPart) continue\n\n const hinge = world(armPart) // 折叠铰链(机身角上)\n const prop = world(rotorPart) // 桨心\n const position: ArmPosition =\n `${prop.z < 0 ? 'Front' : 'Rear'}${prop.x < 0 ? 'Left' : 'Right'}` as ArmPosition\n\n // 折叠节点:局部 -Z 指向\"铰链→桨心\"水平方向(DroneRig 由该向量反推折叠角)\n const dir = new THREE.Vector3(prop.x - hinge.x, 0, prop.z - hinge.z).normalize()\n const fold = huskyNode(rigRoot, `CTRL_Arm_${position}_Fold`, hinge)\n fold.rotation.y = -Math.PI / 2 - Math.atan2(dir.z, dir.x)\n // 整条臂(含电机/旋翼/护圈子树)按世界位姿收养进折叠节点\n fold.attach(armPart)\n // 自检项:机臂网格(语义名,空组)\n huskyNode(fold, `ARM_${position}`)\n\n // 电机钟罩(固定外壳,DroneRig.buildProps 会把它从自转子树摘出挂回折叠节点)\n motorPart.name = `MOTOR_${position}_RotorBell`\n // 自转节点:整个旋翼组件(rotor 组)原地升格;出厂相位角随之保留\n rotorPart.name = `CTRL_Prop_${position}_Spin`\n // 桨毂网格对齐语义名(部件表核对项\"转子桨毂\")\n const hubMesh = rotorPart.getObjectByName(`rotor-${corner}-hub`)\n if (hubMesh) hubMesh.name = `ROTOR_${position}`\n // 三叶桨:前两片进 DroneRig 桨叶表(可自转/收纳叠拢),第三片留在自转节点下随转\n for (const [index, name] of [`rotor-${corner}-blade-0`, `rotor-${corner}-blade-1`].entries()) {\n const blade = model.getObjectByName(name)\n if (blade) blade.name = `PROP_${position}_Blade_${index + 1}`\n }\n }\n\n // —— 起落架腿:四条机械腿的三关节补挂语义名,供 DroneRig 的收腿功能驱动 ——\n // 角标同样按\"转后世界象限\"映射(fl→FrontRight,以此类推,与机臂一致)。\n // 收腿姿态角(髋 +65°/膝 −145°/踝 +45°)由 DroneRig 的 LEG_FOLD_ANGLES 定义;\n // 没有 LEG_* 节点的模型(真机 GLB)收腿功能自动停用。\n const legCornerToPosition: Record<string, ArmPosition> = {\n fl: 'FrontRight',\n fr: 'FrontLeft',\n rl: 'RearRight',\n rr: 'RearLeft',\n }\n for (const [corner, position] of Object.entries(legCornerToPosition)) {\n for (const [sourceName, joint] of [\n [`leg-upper-${corner}`, 'Hip'],\n [`leg-lower-${corner}`, 'Knee'],\n [`foot-${corner}`, 'Ankle'],\n ] as const) {\n const node = inner.getObjectByName(sourceName)\n if (node) node.name = `LEG_${position}_${joint}`\n }\n }\n\n // —— 云台:哈士奇没有云台,在机头前下方新建三轴 + 载荷 ——\n // 枢轴越过下颚前缘(z -1.72,下巴伸到约 -1.55):FPV 前视不被狗下巴挡住;\n // 载荷盒底保持在机腹之上,全机最低点仍是机足,不抢触地点。\n const gimbalYaw = huskyNode(controlRoot, 'CTRL_Gimbal_Yaw', new THREE.Vector3(0, -0.5, -1.72))\n const gimbalPitch = huskyNode(gimbalYaw, 'CTRL_Gimbal_Pitch')\n const gimbalRoll = huskyNode(gimbalPitch, 'CTRL_Gimbal_Roll')\n const pod = huskyNode(gimbalRoll, 'GIMBAL_BlackCameraPod')\n // 减震臂:从载荷顶连到机腹\n const neck = new THREE.Mesh(new THREE.CylinderGeometry(0.055, 0.055, 0.55, 14), materials.housing)\n neck.name = 'GIMBAL_DampingArm'\n neck.position.set(0, 0.3, 0.02)\n pod.add(neck)\n const housing = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.34, 0.46), materials.housing)\n housing.name = 'GIMBAL_CameraHousing'\n housing.position.set(0, -0.02, -0.06)\n pod.add(housing)\n const lensFrame = new THREE.Mesh(\n new THREE.CylinderGeometry(0.135, 0.15, 0.16, 24),\n materials.housing,\n )\n lensFrame.name = 'GIMBAL_CameraLensFrame'\n lensFrame.rotation.x = Math.PI / 2\n lensFrame.position.set(0, -0.02, -0.3)\n pod.add(lensFrame)\n const lens = new THREE.Mesh(new THREE.CircleGeometry(0.105, 24), materials.lens)\n lens.name = 'SENSOR_Glass_GimbalLens'\n lens.rotation.y = Math.PI\n lens.position.set(0, -0.02, -0.385)\n pod.add(lens)\n const sideRing = new THREE.Mesh(new THREE.TorusGeometry(0.105, 0.02, 8, 20), materials.lens)\n sideRing.name = 'GIMBAL_Camera_SideRing_Right'\n sideRing.rotation.y = Math.PI / 2\n sideRing.position.set(0.225, -0.02, -0.08)\n pod.add(sideRing)\n\n // —— 视觉玻璃:五块单面 Plane(法线朝向即雷达判向,与积木机同规则) ——\n // 位置贴哈士奇的背脊/机腹表面,坐标全部用转后系(机头 = -Z)。\n const glassGroup = huskyNode(controlRoot, 'SRC_SensorGlass_Group')\n const addGlass = (\n name: string,\n w: number,\n h: number,\n position: THREE.Vector3,\n orientation: 'front' | 'up' | 'down',\n ) => {\n const mesh = new THREE.Mesh(new THREE.PlaneGeometry(w, h), materials.glass)\n mesh.name = name\n mesh.position.copy(position)\n if (orientation === 'front') mesh.rotation.y = Math.PI\n if (orientation === 'up') mesh.rotation.x = -Math.PI / 2\n if (orientation === 'down') mesh.rotation.x = Math.PI / 2\n glassGroup.add(mesh)\n return mesh\n }\n addGlass('SENSOR_Glass_Front_Left', 0.26, 0.15, new THREE.Vector3(-0.3, 0.72, -1.32), 'front')\n addGlass('SENSOR_Glass_Front_Right', 0.26, 0.15, new THREE.Vector3(0.3, 0.72, -1.32), 'front')\n addGlass('SENSOR_Glass_Side_Left', 0.24, 0.15, new THREE.Vector3(-0.42, 0.795, 0.1), 'up')\n addGlass('SENSOR_Glass_Side_Right', 0.24, 0.15, new THREE.Vector3(0.42, 0.795, 0.1), 'up')\n addGlass('SENSOR_Glass_Downward', 0.3, 0.22, new THREE.Vector3(0, -0.752, 0.28), 'down')\n // 前视壳体:前脸玻璃外侧的两块深灰传感舱\n const frontHousingGeometry = new THREE.BoxGeometry(0.2, 0.13, 0.07)\n for (const [name, x] of [\n ['SENSOR_FrontHousing_Left', -0.31],\n ['SENSOR_FrontHousing_Right', 0.31],\n ] as const) {\n const mesh = new THREE.Mesh(frontHousingGeometry, materials.housing)\n mesh.name = name\n mesh.position.set(x, 0.72, -1.34)\n glassGroup.add(mesh)\n }\n\n // —— 尾灯:机尾两侧小灯珠(DroneLights 会把它们收养进两条后臂折叠节点) ——\n const tailGroup = huskyNode(controlRoot, 'SRC_TailStatusLED_Group')\n for (const [name, x] of [\n ['LED_Status_Tail_Left', -2.2],\n ['LED_Status_Tail_Right', 2.2],\n ] as const) {\n const anchor = new THREE.Vector3(x, 0.15, 2.3)\n const bead = new THREE.Mesh(new THREE.SphereGeometry(0.038, 12, 10), materials.led)\n bead.name = name\n bead.position.copy(anchor)\n tailGroup.add(bead)\n const light = new THREE.PointLight(0xffc14d, 0, 0)\n light.name = name.replace('LED_Status_Tail', 'LIGHT_StatusLED')\n light.position.copy(anchor)\n tailGroup.add(light)\n }\n\n // —— 电量灯:哈士奇机身两侧各有 4 颗绿色 power-led;把左侧一列升格为电量灯 ——\n for (let index = 0; index < 4; index += 1) {\n const bead = model.getObjectByName(`chassis-power-led-l-${index}`)\n if (!bead) continue\n bead.name = `LED_BatteryCharge_0${index + 1}`\n const anchor = world(bead)\n const light = new THREE.PointLight(0xffc14d, 0, 0)\n light.name = `LIGHT_BatteryLED_0${index + 1}`\n light.position.copy(anchor)\n controlRoot.add(light)\n }\n\n // —— 电源键 / 贴花 / 语义空组(部件自检用) ——\n const button = new THREE.Mesh(new THREE.CylinderGeometry(0.055, 0.055, 0.03, 16), materials.decal)\n button.name = 'BUTTON_Power'\n button.position.set(0.3, 0.8, -0.55)\n controlRoot.add(button)\n const decals = huskyNode(controlRoot, 'SRC_Decals_Group')\n const decal = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.01, 0.18), materials.decal)\n decal.name = 'DECAL_Text_Labels'\n decal.position.set(-0.3, 0.8, -0.55)\n decals.add(decal)\n // 机身主体 / 黑色下壳:哈士奇的对应体块叫 chassis/belly,语义名补成空组供自检核对\n huskyNode(controlRoot, 'BODY_Main_Fuselage')\n huskyNode(controlRoot, 'BODY_Black_Underside_SensorHousing')\n huskyNode(controlRoot, 'SRC_BlackHousing_Group')\n huskyNode(controlRoot, 'SRC_GeometryRoot')\n // 底部辅助灯的模型自带点光源(DroneLights 吸附到机腹下方做降落补光)\n const assist = new THREE.PointLight(0xffc14d, 0, 0)\n assist.name = 'LIGHT_BottomAssist'\n assist.position.set(0, -0.66, 0.28)\n controlRoot.add(assist)\n\n return model\n}\n","/**\n * 飞行沙盒的\"装配层 + 主循环\":把仿真内核(drone-sim)、模型机械(drone-rig)、场景辅助(drone-world)、\n * 灯光(drone-lights)、雷达(drone-radar)接成一个可交互的三维飞行场景。\n *\n * 职责边界:本文件只做\"推进仿真 + 把仿真状态映射到场景与相机\";飞行物理在 drone-sim、\n * 模型骨骼动画在 drone-rig,各子系统实现各归其模块。\n *\n * 对外导出 DroneFly(宿主类)、DroneFlyInitializeOptions(初始化配置)、DRONE_MODEL_URL(真机机体模型路径)、\n * statusPatternFor(灯语映射)、DRONE_MAX_SPEED_METERS_PER_SECOND 及相机/拍照/录像类型,并转出 drone-sim / mission-editor 的公共类型。\n * 非直觉约定:仿真用固定步长 1/60 推进、每帧只调一次 syncPose 写入场景;非 orbit 视角不得调 controls.update();拍照/录像共用云台取景相机位姿;\n * initialize 不传 modelUrl 时用内置「机械哈士奇」机体(createHuskyDroneModel)而非去 /models 下找文件。\n */\nimport * as THREE from 'three'\nimport { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'\nimport type { DronePanelHost } from './drone-control'\nimport { createHuskyDroneModel } from './drone-husky-model'\nimport type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'\nimport type { WebGPURenderer } from 'three/webgpu'\nimport { DroneLights, statusPatternFor } from './drone-lights'\nimport type { AuxLightMode, BatteryLightMode, StatusLightKey } from './drone-lights'\nimport { DroneRig, type RigPartReport } from './drone-rig'\nimport { DroneRadar, type RadarAim, type RadarSnapshot } from './drone-radar'\nimport { DEFAULT_OBSTACLES, DroneWorld, type ObstacleSpec } from './drone-world'\nimport { MissionEditor, type MissionEditState } from './mission-editor'\nimport {\n DroneSim,\n DRONE_SPEC,\n FLIGHT_MODES,\n NEUTRAL_STICK,\n buildMissionExport,\n exportMissionText,\n type DroneSnapshot,\n type ChecklistItem,\n type FaultFlags,\n type FlightMode,\n type FlightPhase,\n type MissionConfig,\n type MissionExport,\n type MissionExportFormat,\n type MissionSnapshot,\n type MissionWaypoint,\n type SimConfig,\n type StickState,\n} from './drone-sim'\n// 仅类型:采集契约定义在 drone-capture,用 import type 引 —— 采集服务不引任何渲染实现,\n// 依赖方向始终是「后端 → 契约」单向,没有运行时循环。\nimport type { CaptureCameraState, CaptureViewOverride } from './drone-capture'\n\n/**\n * 默认真机机体模型路径(DJI Mini 4 Pro 重命名版)。\n * 资产不随库打包,需使用方自行提供 —— 所以它**不再是 initialize() 的缺省值**:\n * 不传 modelUrl 时用的是零依赖的内置积木机体,想要真机模型就把这个常量传进去。\n */\nexport const DRONE_MODEL_URL = '/models/djiair_renamed.glb'\nconst DRONE_SIZE_METERS = 2.4\n/** 机型最快档位速度,用于界面提示 */\nexport const DRONE_MAX_SPEED_METERS_PER_SECOND = FLIGHT_MODES.sport.horizontalSpeed\n\n/** 旧版方向键 API(仅保留给外部复用) */\nexport type DroneDirection = 'up' | 'down' | 'left' | 'right'\n/** 纯升降方向 */\nexport type DroneVerticalDirection = 'up' | 'down'\n/** 纯偏航(左右转向)方向 */\nexport type DroneRotationDirection = 'left' | 'right'\n\nexport type {\n DroneSnapshot,\n FaultFlags,\n FlightMode,\n MissionConfig,\n MissionSnapshot,\n MissionWaypoint,\n SimConfig,\n StickState,\n} from './drone-sim'\n// 航线序列化(类型 + 实现)已下沉到 drone-sim(纯逻辑层):Cesium 后端复用同一份实现时,\n// 不会把 three 拖进它的运行时依赖。这里原样再导出,公共 API(`from 'drone-camera'`)不变。\nexport type { MissionExport, MissionExportFormat } from './drone-sim'\nexport { buildMissionExport, exportMissionText, findMissionInsertIndex, missionToCsv } from './drone-sim'\nexport type { MissionDragMode, MissionEditState, MissionEditorHost } from './mission-editor'\n\n/** 相机视角模式:orbit=观察者(轨道控制器),follow=跟随,fpv=机载(云台视角) */\nexport type CameraMode = 'orbit' | 'follow' | 'fpv'\n\n/** 面板用的相机模式列表(含中文标签) */\nexport const CAMERA_MODE_LIST: Array<{ key: CameraMode; label: string }> = [\n { key: 'orbit', label: '观察者' },\n { key: 'follow', label: '跟随' },\n { key: 'fpv', label: '机载' },\n]\n\n/**\n * 云台增稳档位 —— **模型不参与稳像,全部在相机侧反向抵消**:\n * - `full`(默认):姿态 + 位置双解耦。取景相机的位置与朝向都由飞控真值直接解出\n * (`机体平移 + R_y(航向)·挂点` / `R_y(航向+云台偏航)·R_x(云台俯仰)⊗安装偏置`),\n * **完全不读骨骼链世界变换**。于是模型那边照旧物理真实(云台关节仍在做反向补偿、\n * 仍有一阶跟随与行程夹紧),但那些\"拟真副作用\"一个都传不到画面里 —— 机载视角即\n * 等效于真机的三轴机械云台:机身怎么倾斜,镜头都稳稳对着目标。\n * - `physical`:相机改为**采样骨骼链**(位置取云台真实世界位姿),于是镜头会跟着\n * 机体倾斜绕横滚轴画弧(镜头挂在重心下方约 0.14 m,侧倾 27° 即横移 ±6.7 cm),\n * 云台关节的滞后与行程夹紧也会原样入画。想要\"裸机硬核真实感\"时用。\n */\nexport type GimbalStabilization = 'full' | 'physical'\n\n/** 云台增稳可调项(见 DroneFly.configureGimbal) */\nexport interface GimbalStabilizationOptions {\n /** 增稳档位,默认 'full' */\n stabilization?: GimbalStabilization\n /**\n * 云台关节一阶跟随时间常数(秒):0 = 立即到位,默认 0.02(20 ms)。\n * 只作用于**模型外观**(云台看起来像无刷电机一样有响应时间);\n * `full` 档下相机不采样关节,所以不会因为滞后而晃。\n */\n response?: number\n /** 电子增稳强度 0~1:0 = 关闭,默认 0(解耦后目标姿态已无噪声,低通只会加滞后) */\n smoothing?: number\n}\n\nconst FIXED_STEP = 1 / 60\nconst MAX_STEPS_PER_FRAME = 240\nconst FOLLOW_DISTANCE = 7.5\nconst FOLLOW_HEIGHT = 2.8\n/** 相机镜头位置:沿云台光轴从云台中心前移,避免把云台自身拍进画面 */\nconst GIMBAL_CAMERA_OFFSET = 0.12\n/** 动态偏移余量(米):相机越过载荷前端后再往外挪这么多,贴着外壳边缘也不穿帮 */\nconst GIMBAL_CAMERA_CLEARANCE_MARGIN = 0.02\n/**\n * 电子增稳(EIS)时间常数上限(秒):fpvSmoothing=1 时相机姿态低通的时间常数。\n * 真机是\"云台没盖住的微米级偏移由电子防抖在裁切范围内修正\",这里等价成对\n * 相机姿态做一阶低通,把残余高频微抖滤掉。\n */\nconst FPV_SMOOTH_TAU = 0.08\n/** 云台关节一阶跟随时间常数默认值(秒):无刷电机实际响应量级 */\nconst DEFAULT_GIMBAL_RESPONSE = 0.02\n/**\n * 电子增稳默认强度(0~1):默认关闭。`full` 档下相机姿态已由飞控真值直出(无噪声),\n * 再叠低通只会引入滞后 —— 那本身就是\"晃\"的来源之一。需要额外柔化时再调高。\n */\nconst DEFAULT_FPV_SMOOTHING = 0\n/**\n * 机臂展开/收拢的一阶缓动速率(1/秒):9 ≈ 0.5 s 内完成,\n * 让\"上电 → 展臂 → 桨叶张开 → 可起飞\"整条流程控制在 1 秒内。\n */\nconst ARM_FOLD_RATE = 9\n/**\n * 起落架腿收放的一阶缓动速率(1/秒):2.5 ≈ 0.6 s 走完,比机臂慢半拍 ——\n * 收腿发生在\"已离地\"之后,从容一点更像真机起落架。\n */\nconst LEG_DEPLOY_RATE = 2.5\n/**\n * 起落架腿收放高度阈(米,AGL):高于收、低于放。\n * 起飞爬过该高度开始收;降落/手动下降到该高度以下就开始放,\n * 保证触地瞬间机足已经张开(地面静止永远是放下态,不影响触地配平)。\n */\nconst LEG_RETRACT_ALTITUDE = 0.6\n/** 拍照等待渲染的兜底时限(毫秒),超时按失败处理 */\nconst PHOTO_TIMEOUT_MS = 2000\n/** 录像参数:帧率 / 码率 / 分片间隔 */\nconst RECORD_FPS = 30\nconst RECORD_BITRATE = 8_000_000\nconst RECORD_TIMESLICE_MS = 500\n/** 录制收尾兜底时限(毫秒):部分环境可能不派发 onstop */\nconst RECORD_STOP_TIMEOUT_MS = 1500\n\n/** 录像容器候选:优先 VP9,退回 VP8/webm,再交给浏览器默认 */\nconst RECORD_MIME_CANDIDATES = ['video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm']\n\n/** 单张照片(云台相机取景) */\nexport interface PhotoShot {\n /** PNG data URL,可直接用于 <img> 或下载 */\n dataUrl: string\n width: number\n height: number\n /** 与该 PNG 同一渲染帧的取景相机快照。 */\n camera?: CaptureCameraState\n}\n\n/** 相机位姿快照:取景帧(拍照 / 录像)渲染完必须还回去 */\ninterface CameraRestore {\n position: THREE.Vector3\n quaternion: THREE.Quaternion\n zoom: number\n}\n\nfunction pickRecordMime(): string | undefined {\n if (typeof MediaRecorder === 'undefined' || typeof MediaRecorder.isTypeSupported !== 'function')\n return undefined\n return RECORD_MIME_CANDIDATES.find((mime) => MediaRecorder.isTypeSupported(mime))\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\n/** DroneFly.initialize() 的可选配置 */\nexport interface DroneFlyInitializeOptions {\n /**\n * 机体模型 GLB 地址(URL 或相对路径)。\n * **不传 = 用内置的「积木无人机」**(程序化几何体拼出来的四轴,零资产依赖,\n * 见 `createHuskyDroneModel`);需要真机模型时再传,例如 `DRONE_MODEL_URL`。\n * 自定义模型不必与默认模型尺寸/原点一致 —— 载入后会自动缩放到 2.4m 级、\n * 水平居中并底面对齐地面(见 normalizeModel),因此可直接传任意 glb。\n */\n modelUrl?: string\n /**\n * 场景里的障碍物(建筑 / 灯杆 / 天桥 / 树…… 都是 AABB 盒),同一份数据既生成可视网格,\n * 又喂给飞控做避障判定 —— 换了坐标,画面和碰撞会一起变。\n * 缺省用内置的 `DEFAULT_OBSTACLES`(起飞点 30 米内的 7 个参考物);\n * 传 `[]` 得到一块纯空地(只剩地面网格与返航点标记);也可传自己的 `ObstacleSpec[]` 整体替换。\n * 只想临时藏起来(保留碰撞)用 `fly.world?.setObstaclesVisible(false)`。\n */\n obstacles?: ObstacleSpec[]\n}\n\n/**\n * 飞行沙盒的装配层:把飞行仿真内核、模型机械、场景辅助与灯光接到一起。\n *\n * 每帧固定步长推进仿真(与渲染帧率解耦,时间倍速下也能保持稳定),\n * 然后只做\"把仿真状态映射到三维场景\"这一件事,保证画面与遥测显示的永远是同一份状态。\n */\nexport class DroneFly implements DronePanelHost {\n camera: THREE.PerspectiveCamera | null\n scene: THREE.Scene<THREE.Object3DEventMap> | null\n renderer: WebGPURenderer | null\n controls: OrbitControls<THREE.Camera> | null\n model: THREE.Group | null = null\n lights: DroneLights | null = null\n rig: DroneRig | null = null\n world: DroneWorld | null = null\n radar: DroneRadar | null = null\n /** 雷达射线可视化开关(雷达实例可能晚于页面初始化创建,先缓存再补上);默认不显示 */\n private radarBeamsVisible = false\n /** 辅助灯锥形光束开关;默认不显示 */\n private auxBeamVisible = false\n readonly sim = new DroneSim()\n /**\n * 引擎标识。DroneFly 是 three.js 实现,与 CesiumDroneAdapter 的 `engine = 'cesium'`\n * 构成判别字段 —— 宿主用同一份 `createDroneControl({ engine })` 装配,拿到实例后\n * 还能靠它 `if (drone.engine === 'three')` 访问引擎特有成员。\n */\n readonly engine = 'three' as const\n cameraMode: CameraMode = 'orbit'\n /** 云台增稳档位(见 GimbalStabilization);默认位置 + 姿态双解耦 */\n gimbalStabilization: GimbalStabilization = 'full'\n /** 云台关节一阶跟随时间常数(秒),0 = 立即到位;仅影响模型外观,初值同步给 rig */\n gimbalResponse = DEFAULT_GIMBAL_RESPONSE\n /** 电子增稳强度 0~1,0 = 关闭(默认) */\n fpvSmoothing = DEFAULT_FPV_SMOOTHING\n /**\n * 取景**俯仰**覆盖(全景分层用,见 `setCaptureView`):非 null 时取景相机临时偏离\n * \"云台当前俯仰\"。只影响取景(拍照 / 录像 / 采集快照),不动飞控与机体姿态。\n *\n * 这里没有水平偏航是有意的 —— 水平覆盖靠**机体原地自转**(见 `spinBodyTo`),\n * 云台 Pan 机械行程转不出 180°(理由见 `drone-panorama` 的文件头)。\n */\n private captureView: CaptureViewOverride | null = null\n\n private readonly loader = new GLTFLoader()\n private groundOffsetY = 0\n private accumulator = 0\n private destroyed = false\n private cameraTarget = new THREE.Vector3(0, 0, 0)\n /** 观察者视角是否把控制点自动跟到飞机上(框航线 / 场景编辑时关掉,见 frameMission) */\n private cameraAutoFollow = true\n private readonly cameraScratch = new THREE.Object3D()\n private readonly dronePosition = new THREE.Vector3()\n private readonly cameraShift = new THREE.Vector3()\n private readonly followPosition = new THREE.Vector3()\n private readonly followLookAt = new THREE.Vector3()\n private readonly forwardVector = new THREE.Vector3()\n /** 电子增稳:相机姿态的低通状态(避免切视角时从旧位姿慢慢滑过来) */\n private readonly fpvQuat = new THREE.Quaternion()\n /** 姿态解耦档下,由飞控真值解出的期望相机世界朝向(不读骨骼链) */\n private readonly fpvTargetQuat = new THREE.Quaternion()\n /** 全景成片的**固定光心**:挂点世界高度暂存(见 applyGimbalCameraPose 的 fixedCenter) */\n private readonly captureCenter = new THREE.Vector3()\n private fpvQuatReady = false\n /** 上一帧的帧间隔(秒):拍照/录像取景帧复用,保证平滑状态连续 */\n private lastFrameDelta = 1 / 60\n private lightOverride: StatusLightKey | null = null\n /** 机臂目标折叠度:1 = 收纳(出厂收纳状态,起飞前必须先展开) */\n private armFoldTarget = 1\n /** 航线可视化签名:只有航线内容变化时才重建三维标记(每帧重建代价太大) */\n private missionSignature = ''\n /** 航点/航线图层显隐(面板开关,重建标记后要重新贴回) */\n private missionVisible = true\n /** 航点标记 / 航线折线各自的开关(拆开控制,重建标记后要重新贴回) */\n private missionMarkersVisible = true\n private missionPathVisible = true\n /** 场景内航点编辑器(需要画布,headless / 无渲染器时为 null) */\n missionEditor: MissionEditor | null = null\n /** 场景编辑开关(界面意图;编辑器实例可能晚于页面初始化创建,先缓存再补上) */\n private missionEditEnabled = false\n /** 场景编辑选中的航点索引(-1 = 未选中) */\n private missionEditSelected = -1\n /** 待取景的拍照请求:渲染前摆相机、渲染后拷屏并还原(见 onBeforeRender / onAfterRender) */\n private photoRequest: {\n resolve: (shot: PhotoShot | null) => void\n timer: number\n /** 取景前的相机位姿(onBeforeRender 填,onAfterRender 还原) */\n restore: CameraRestore | null\n /** 在取景相机参与渲染前锁存,避免截图后读取到下一帧状态。 */\n captureCamera: CaptureCameraState | null\n } | null = null\n /** 云台相机取景用的独立相机(拍照不影响主相机对象本身) */\n private readonly captureCamera = new THREE.PerspectiveCamera()\n /** 屏录:sim.recording 是状态来源,这里负责真正的 MediaRecorder */\n private recorder: MediaRecorder | null = null\n private recordChunks: Blob[] = []\n private recordStream: MediaStream | null = null\n /** 手动抓帧轨道(captureStream(0)):录到什么由 captureRecordingFrame 那一帧云台取景决定 */\n private captureTrack: CanvasCaptureMediaStreamTrack | null = null\n /** 录制专用离屏画布的上下文:每帧只往里搬一帧云台取景,主画布上的用户视角永远不会混进录像 */\n private recordContext: CanvasRenderingContext2D | null = null\n /** 进入机载视角前的观察者缩放,退出时归还(变焦只在机载取景下生效) */\n private savedZoom: number | null = null\n /** 录像收尾回调:UI 用它落盘(引擎不直接触发下载) */\n onRecordingReady: ((blob: Blob, seconds: number) => void) | null = null\n\n /** 飞行总控:接管场景/相机/渲染器/控制器,装配仿真内核与机体机械 */\n constructor(\n scene: THREE.Scene<THREE.Object3DEventMap> | null,\n camera: THREE.PerspectiveCamera | null,\n renderer: WebGPURenderer | null,\n controls: OrbitControls<THREE.Camera> | null,\n ) {\n this.scene = scene\n this.camera = camera\n this.renderer = renderer\n this.controls = controls\n }\n\n /**\n * 初始化无人机:装配机体模型,并挂入机械 / 灯光 / 场景 / 雷达 / 编辑器各子系统,\n * 最后做一次初始同步。可重复调用前需先 dispose(本方法不自动清理旧模型)。\n *\n * 机体来源二选一:\n * - `options.modelUrl` 给了 → 加载该 GLB;\n * - 没给 → 用内置「积木无人机」(程序化几何体,零资产依赖)。\n *\n * @param options.modelUrl 机体 GLB 路径;缺省用内置积木机体。\n * @param options.obstacles 场景障碍物规格(缺省用内置 7 个参考物)。\n */\n async initialize(options: DroneFlyInitializeOptions = {}): Promise<void> {\n if (!this.scene || this.destroyed) return\n\n // 没传模型路径就不再默认去 /models 下找一个未必存在的文件:直接用内置积木机体,\n // 保证\"零资产\"环境下也能完整跑通(归一化 / 机械 / 灯光 / 雷达走的都是同一条装配路径)。\n let model: THREE.Group\n if (options.modelUrl) {\n const gltf = await this.loader.loadAsync(options.modelUrl)\n if (this.destroyed) {\n this.disposeModel(gltf.scene)\n return\n }\n model = gltf.scene\n model.name = 'DJI Mini 4 Pro'\n } else {\n model = createHuskyDroneModel()\n }\n this.normalizeModel(model)\n // 模型已归一到\"底面对齐 y=0\",记录其基准高度作为地面偏移;机体高度一律加这个偏移(见 syncPose / updateCamera)\n this.groundOffsetY = model.position.y\n model.traverse((object) => {\n const mesh = object as THREE.Mesh\n if (!mesh.isMesh) return\n mesh.castShadow = true\n mesh.receiveShadow = true\n })\n this.scene.add(model)\n this.model = model\n\n // 机械件与灯光\n this.rig = new DroneRig(model)\n this.rig.gimbalResponse = this.gimbalResponse\n this.lights = new DroneLights()\n this.lights.attach(model, this.scene)\n this.lights.setBeamVisible(this.auxBeamVisible)\n\n // 场景辅助(地面网格 / 返航点 / 障碍物 / 航迹):障碍物规格由调用方决定,缺省才是内置那 7 个\n this.world = new DroneWorld(this.scene, options.obstacles ?? DEFAULT_OBSTACLES)\n this.sim.obstacles = this.world.collisionBoxes\n\n // 场景内航点编辑:直接抓三维场景里的航点拖动(需要画布与相机,headless 环境下自动跳过)\n const canvas = this.renderer?.domElement\n if (canvas && this.camera) {\n this.missionEditor = new MissionEditor({\n canvas,\n camera: this.camera,\n world: this.world,\n canEdit: () => this.canMissionSceneEdit(),\n getWaypoints: () => this.sim.mission.map((waypoint) => ({ ...waypoint })),\n getSelected: () => this.missionEditSelected,\n setSelected: (index) => this.setMissionSelected(index),\n commitWaypoints: (waypoints, selected) => {\n // ⚠️ 这里故意不刷新 missionSignature:航点数变了必须让 syncPose 整层重建\n // (拖动那种\"只改一个航点\"的路径走 updateMissionWaypoint,才刷新签名)\n if (!this.sim.setMission(waypoints)) return\n this.setMissionSelected(selected)\n },\n moveWaypoint: (index, patch) => this.updateMissionWaypoint(index, patch),\n setOrbitEnabled: (enabled) => {\n if (this.controls) this.controls.enabled = enabled && this.isOrbitView\n },\n getLimits: () => ({\n // 用 ceiling 而不是 config 原值:unlimitedFlight 打开时是 Infinity,编辑器的夹紧自动失效\n maxAltitude: this.sim.altitudeCeiling,\n maxDistance: this.sim.distanceCeiling,\n homeX: this.sim.homePosition.x,\n homeZ: this.sim.homePosition.z,\n }),\n newWaypointTemplate: (index) => this.newWaypointTemplate(index),\n log: (level, text) => this.sim.pushEvent(level, text),\n })\n this.missionEditor.setEnabled(this.missionEditEnabled)\n }\n\n // 前视测距雷达(起点/方向取自模型的前向视觉玻璃节点,上电后工作)\n if (this.scene) {\n this.radar = new DroneRadar(this.scene)\n this.radar.bindSensors(model)\n this.radar.setVisible(this.radarBeamsVisible)\n }\n\n // 起飞前检查里的\"模型侧\"项目:模型自检只做提示,不拦起飞(缺几个装饰件不影响飞控)\n const allPartsFound = this.rig.parts.every((part) => part.found)\n this.sim.externalChecks = [\n {\n id: 'model',\n label: '模型部件自检',\n ok: allPartsFound,\n detail: this.modelHealthText(),\n blocking: false,\n },\n {\n id: 'arms',\n label: '机臂已展开',\n ok: this.rig.armFold < 0.02,\n detail: this.rig.armFold < 0.02 ? '已展开' : '机臂处于收纳状态',\n blocking: true,\n },\n ]\n this.syncPose(0)\n }\n\n private modelHealthText(): string {\n const missing = this.rig?.parts.filter((part) => !part.found) ?? []\n if (missing.length === 0) return `ID 表部件全部就位(${this.rig?.parts.length ?? 0} 项)`\n return `缺失 ${missing.length} 项:${missing.map((item) => item.id).join('、')}`\n }\n\n /** 每帧推进:渲染帧率下用固定步长跑仿真,再用同一份状态驱动画面 */\n /**\n * 每帧推进:固定步长跑仿真,与渲染帧率解耦。\n * 低帧率机器(或无头环境)不会让仿真变慢 —— 用累加器补帧;挂起后的超长帧直接丢弃积压。\n */\n animate(deltaSeconds?: number): void {\n const delta = Math.max(0, Math.min(deltaSeconds ?? 1 / 60, 2))\n this.lastFrameDelta = delta\n const timeScale = clamp(this.sim.config.timeScale, 1, 20)\n const scaled = delta * timeScale\n this.accumulator = Math.min(this.accumulator + scaled, MAX_STEPS_PER_FRAME * FIXED_STEP)\n let steps = 0\n while (this.accumulator >= FIXED_STEP && steps < MAX_STEPS_PER_FRAME) {\n this.sim.step(FIXED_STEP)\n this.accumulator -= FIXED_STEP\n steps += 1\n }\n\n this.syncPose(delta)\n this.updateCamera(delta)\n this.pushTrail()\n // 仿真侧关掉了录像(重置/降落等):收尾并交出成片\n if (this.recorder && !this.sim.recording) void this.stopRecording()\n }\n\n /** 把仿真状态写进三维场景 */\n private syncPose(delta: number): void {\n const sim = this.sim\n const x = sim.position.x\n const y = this.groundOffsetY + sim.position.y\n const z = sim.position.z\n this.rig?.applyBodyPose({\n x,\n y,\n z,\n headingDeg: sim.heading,\n pitchDeg: sim.tiltPitch,\n rollDeg: sim.tiltRoll,\n })\n this.rig?.setGimbalAttitude(\n sim.gimbalPitch,\n sim.gimbalRoll,\n sim.gimbalYaw,\n sim.tiltPitch,\n sim.tiltRoll,\n // 关节按一阶惯性跟随(模拟无刷电机响应);delta=0(初始化同步)时立即到位\n delta,\n )\n // 目视转速用真实帧时间,不跟时间倍速走,否则高倍速下桨叶会糊成一片;\n // 传入混控后的四电机转速表,每个桨按自己的负荷转(差动可见)\n this.rig?.updateProps(sim.motorSpeeds ?? sim.motorLoad, delta)\n if (this.rig) {\n const fold = this.rig.armFold\n this.rig.setArmFold(fold + (this.armFoldTarget - fold) * Math.min(1, delta * ARM_FOLD_RATE))\n // 桨叶叠拢/张开:机臂完全展开后才缓缓张开(真机节奏)\n this.rig.updateBlades(delta)\n sim.armFold = this.rig.armFold\n const armCheck = sim.externalChecks.find((item) => item.id === 'arms')\n if (armCheck) {\n const unfolded = this.rig.armFold < 0.02\n if (!unfolded) {\n armCheck.ok = false\n armCheck.detail = '机臂处于收纳状态'\n } else if (this.rig.bladeOpen < 0.98) {\n armCheck.ok = false\n armCheck.detail = '螺旋桨展开中'\n } else {\n armCheck.ok = true\n armCheck.detail = '已展开'\n }\n }\n // 起落架腿:高于阈值(且已离地)收起,低空/地面放下 —— 只有带 LEG_* 节点的\n // 机体(哈士奇)有腿,真机 GLB 自动 no-op。迟滞靠单阈值 + 缓动天然形成。\n const legTarget = sim.airborne && sim.position.y > LEG_RETRACT_ALTITUDE ? 0 : 1\n const legDeploy = this.rig.legDeploy\n this.rig.setLegDeploy(\n legDeploy + (legTarget - legDeploy) * Math.min(1, delta * LEG_DEPLOY_RATE),\n )\n }\n\n // 灯光:状态灯语跟随飞行状态,除非面板手动指定\n const lights = this.lights\n if (lights) {\n lights.setFlying(sim.airborne)\n lights.setBatteryLevel(sim.batteryPercent)\n lights.setStatusPattern(this.lightOverride ?? statusPatternFor(sim))\n lights.update(delta)\n // 状态灯骨骼绑定:首次在展开态把灯珠挂进机臂折叠节点(记录绑定位姿),\n // 之后每帧按绑定位姿刚体同步 —— 灯珠像焊在机臂末端一样随折叠/展开运动,\n // 即使被热更新重建等外力挪动过也会自动归位\n if (this.rig && this.rig.armFold < 0.005) {\n lights.attachStatusToArms(\n this.rig.getArmFoldNode('RearLeft'),\n this.rig.getArmFoldNode('RearRight'),\n )\n }\n lights.syncStatusToArms()\n }\n\n const home = sim.homePosition\n this.world?.setHome(home.x, home.z)\n\n // 航线任务可视化:航点标记 / 航线折线只在航线内容变化时重建,当前目标每帧贴高亮\n const world = this.world\n if (world) {\n const signature = this.buildMissionSignature()\n if (signature !== this.missionSignature) {\n this.missionSignature = signature\n world.setMissionWaypoints(\n sim.mission.map((waypoint) => ({\n x: waypoint.x,\n z: waypoint.z,\n altitude: waypoint.altitude,\n })),\n home,\n )\n // 重建出来的是一整套新对象,显隐与高亮状态得重新贴回去\n world.setMissionVisible(this.missionVisible)\n world.setMissionWaypointsVisible(this.missionMarkersVisible)\n world.setMissionPathVisible(this.missionPathVisible)\n world.setMissionSelection(this.missionEditSelected)\n }\n const active = sim.missionIndex >= 0 ? (sim.mission[sim.missionIndex] ?? null) : null\n world.setMissionActive(\n active ? sim.missionIndex : -1,\n active ? { x: active.x, z: active.z, altitude: active.altitude } : null,\n )\n }\n\n // 场景编辑:不可编辑(切到机载视角 / 任务开始执行)时自动收摊\n this.missionEditor?.update()\n\n // 测距雷达:上电即工作,镜头可转向(默认看正前方/正上方),视场随机体姿态转动\n if (this.radar && this.world && this.model) {\n this.radar.update(this.model, this.world, delta, sim.phase !== 'powerOff')\n }\n }\n\n /** 测距雷达最新读数:前视双镜头 + 上视双孔(Vue 面板 100ms 轮询) */\n getRadarSnapshot(): RadarSnapshot {\n return (\n this.radar?.getSnapshot() ?? {\n detecting: false,\n left: null,\n right: null,\n range: 18,\n upLeft: null,\n upRight: null,\n upRange: 15,\n aim: 'forward',\n }\n )\n }\n\n /** 切换镜头瞄准:forward = 转向正前方/正上方;sensor = 沿镜头(玻璃面)朝向 */\n setRadarAim(aim: RadarAim): void {\n this.radar?.setAim(aim)\n }\n\n /** 切换雷达射线可视化(前视 + 上视,读数不受影响) */\n setRadarBeamsVisible(visible: boolean): void {\n this.radarBeamsVisible = visible\n this.radar?.setVisible(visible)\n }\n\n /** 当前雷达射线可视化是否显示(探针用) */\n isRadarBeamsVisible(): boolean {\n return this.radar?.isBeamsVisible() ?? this.radarBeamsVisible\n }\n\n /** 切换底部辅助灯的锥形光束显隐(不影响辅助灯照明) */\n setAuxBeamVisible(visible: boolean): void {\n this.auxBeamVisible = visible\n this.lights?.setBeamVisible(visible)\n }\n\n /** 当前辅助灯锥形光束是否绘制(探针用) */\n isAuxBeamVisible(): boolean {\n return this.lights?.isBeamVisible() ?? this.auxBeamVisible\n }\n\n private pushTrail(): void {\n const sim = this.sim\n if (!this.world) return\n if (sim.airborne) {\n this.world.pushTrail(sim.position.x, this.groundOffsetY + sim.position.y, sim.position.z)\n }\n }\n\n // ————————————————————————————— 相机 —————————————————————————————\n\n /** 切换相机视角;进出机载视角时保存/归还轨道视角的缩放(变焦只在机载下生效) */\n setCameraMode(mode: CameraMode): void {\n const previous = this.cameraMode\n this.cameraMode = mode\n if (this.controls) this.controls.enabled = mode === 'orbit'\n // 变焦只在机载取景下生效:进入时记下观察者视角的缩放,离开时归还,\n // 这样轨道视角的滚轮缩放不会被相机变焦覆盖\n if (this.camera && mode === 'fpv' && previous !== 'fpv') this.savedZoom = this.camera.zoom\n // 进机载视角时重置电子增稳状态,否则会从上一段视角的位姿慢慢滑过来\n if (mode === 'fpv') this.fpvQuatReady = false\n if (this.camera && mode !== 'fpv' && previous === 'fpv') {\n this.camera.zoom = this.savedZoom ?? 1\n this.camera.updateProjectionMatrix()\n this.savedZoom = null\n }\n if (mode === 'orbit') {\n // 回到观察者视角时保留当前朝向,只把控制点交还给轨道控制器\n this.cameraTarget.set(\n this.sim.position.x,\n this.groundOffsetY + this.sim.position.y,\n this.sim.position.z,\n )\n this.controls?.target.copy(this.cameraTarget)\n }\n }\n\n /** 当前是否为观察者(轨道控制)视角;非轨道模式下渲染循环不得调用 controls.update(),否则其每帧 lookAt(target) 会覆盖机载/跟随朝向 */\n get isOrbitView(): boolean {\n return this.cameraMode === 'orbit'\n }\n\n /** 直接把无人机移动到目标位置并同步观察点/朝向 */\n flyTo(target: { x?: number; y?: number; z?: number; heading?: number; pitch?: number; roll?: number }): void {\n const next = { ...this.sim.position }\n if (target.x !== undefined) next.x = target.x\n if (target.y !== undefined) next.y = Math.max(0, target.y)\n if (target.z !== undefined) next.z = target.z\n this.sim.position.x = next.x\n this.sim.position.y = next.y\n this.sim.position.z = next.z\n if (target.heading !== undefined) this.sim.heading = target.heading\n if (target.pitch !== undefined) this.sim.tiltPitch = clamp(target.pitch, -45, 45)\n if (target.roll !== undefined) this.sim.tiltRoll = clamp(target.roll, -45, 45)\n this.cameraTarget.set(this.sim.position.x, this.groundOffsetY + this.sim.position.y, this.sim.position.z)\n if (this.controls) {\n this.controls.target.copy(this.cameraTarget)\n this.controls.update()\n }\n this.syncPose(0)\n }\n\n /** 直接调整镜头/云台变焦倍数(1~4x),并同步当前机载视角 */\n zoomTo(zoom: number): void {\n this.sim.setZoom(zoom)\n if (this.camera && this.cameraMode !== 'orbit') {\n if (this.camera.zoom !== this.sim.cameraZoom) {\n this.camera.zoom = this.sim.cameraZoom\n this.camera.updateProjectionMatrix()\n }\n }\n }\n\n /**\n * 把相机摆到云台镜片位置并取云台朝向(机载视角与拍照共用):\n * 位置 = 云台挂点沿航向前移 offset,朝向 = 云台光轴(已被三轴解算锁水平)。\n *\n * offset 默认按模型实测:云台载荷(相机外壳)沿光轴的前端距离 + 余量。\n * 写死的小偏移只适配\"外壳刚好在枢轴前一点点\"的模型;换一个载荷更长的\n * 模型(如 dji_mini4_pro.glb,壳体前端在枢轴前 0.25m),相机会泡进外壳,\n * 画面正中变成一颗对着的镜头(看着相机而不是从相机往外看)。\n *\n * 位姿来源按 gimbalStabilization 分档:'full' 时位置与朝向都由飞控真值解算(不读骨骼链,\n * 与模型姿态完全解耦);'physical' 时采样骨骼链世界变换(镜头随机体侧倾横移,保留裸机真实感)。\n */\n private applyGimbalCameraPose(\n camera: THREE.PerspectiveCamera,\n options: {\n offset?: number\n zoom?: number\n delta?: number\n live?: boolean\n fixedCenter?: boolean\n instant?: boolean\n } = {},\n ): boolean {\n const rig = this.rig\n const ok = rig?.getGimbalCameraTransform(this.cameraScratch) ?? false\n if (!ok || !rig) return false\n this.cameraScratch.updateMatrixWorld(true)\n const clearance = rig.getGimbalCameraClearance()\n const offset =\n options.offset ??\n (clearance != null ? clearance + GIMBAL_CAMERA_CLEARANCE_MARGIN : GIMBAL_CAMERA_OFFSET)\n const stabilized = this.gimbalStabilization === 'full'\n // 取景俯仰:全景分层给了覆盖时用覆盖值(绝对值),否则沿用云台俯仰。\n // ⚠️ `live`(实时机载画面)**不吃覆盖**:覆盖只属于成片。否则全景扫描每拍一张,\n // 实时画面就被拽到那一层的俯仰再弹回来 —— 用户看到的就是\"一闪一闪\"(扫描时\n // 每张取景都会暂时改角度)。水平方向本来就不在这里做手脚(靠机体自转),俯仰同样只给成片。\n const overridePitch = options.live === true ? undefined : this.captureView?.pitch\n const viewPitch = overridePitch ?? this.sim.gimbalPitch\n const viewYaw = this.sim.gimbalYaw\n // 成片光心**固定在空间中一点**(机体自转轴正上方、云台挂点高度):\n // 拼接器的成立前提是\"相机绕光心纯旋转\"(见 drone-stitch 文件头)。而取景相机\n // 平时挂在机头、还沿光轴前移一米级 —— 机体原地自转一周,光心扫出一个大圆,\n // 对 1m 高的近地场景视差以\"度\"计,羽化平均直接把重叠区抹成糊(实测清晰度只剩 8.6%)。\n // 全景扫描(captureView 非 null)时把光心钉死,这一条假设就严格成立,拼出来才是锐的。\n const fixedCenter =\n options.fixedCenter === true &&\n stabilized &&\n this.captureView !== null &&\n rig.getGimbalCameraMount(this.captureCenter, 0, 0)\n const decoupled =\n !fixedCenter && stabilized && rig.getGimbalCameraMount(camera.position, offset, viewPitch)\n if (fixedCenter) {\n camera.position.set(this.sim.position.x, this.captureCenter.y, this.sim.position.z)\n } else if (!decoupled) {\n this.forwardVector.set(0, 0, -1).applyQuaternion(this.cameraScratch.quaternion).normalize()\n camera.position.copy(this.cameraScratch.position).addScaledVector(this.forwardVector, offset)\n }\n // 朝向:解耦档直接取\"引擎解出的期望姿态\"(不经骨骼链),否则沿用云台节点世界朝向\n const oriented =\n stabilized && rig.getGimbalCameraOrientation(this.fpvTargetQuat, viewPitch, 0, viewYaw)\n const source = oriented ? this.fpvTargetQuat : this.cameraScratch.quaternion\n // 可选电子增稳(对相机姿态做一阶低通,滤掉残余高频微抖)\n const delta = options.delta ?? 0\n const tau = this.fpvSmoothing * FPV_SMOOTH_TAU\n if (!options.instant && tau > 0 && delta > 0) {\n if (!this.fpvQuatReady) {\n this.fpvQuat.copy(source)\n this.fpvQuatReady = true\n } else {\n this.fpvQuat.slerp(source, 1 - Math.exp(-delta / tau))\n }\n camera.quaternion.copy(this.fpvQuat)\n } else {\n this.fpvQuat.copy(source)\n this.fpvQuatReady = true\n camera.quaternion.copy(source)\n }\n if (options.zoom !== undefined && Math.abs(camera.zoom - options.zoom) > 1e-3) {\n camera.zoom = options.zoom\n camera.updateProjectionMatrix()\n }\n return true\n }\n\n /**\n * 调云台增稳参数(机载视角稳定性调优入口)。\n *\n * ```ts\n * fly.configureGimbal({ stabilization: 'physical', response: 0.05, smoothing: 0 })\n * ```\n */\n configureGimbal(options: GimbalStabilizationOptions = {}): void {\n if (options.stabilization) this.gimbalStabilization = options.stabilization\n if (options.response !== undefined) {\n this.gimbalResponse = Math.max(0, options.response)\n if (this.rig) this.rig.gimbalResponse = this.gimbalResponse\n }\n if (options.smoothing !== undefined) this.fpvSmoothing = clamp(options.smoothing, 0, 1)\n }\n\n /**\n * 设置取景**俯仰**覆盖 —— 全景扫描(180° / 360°)按俯仰层逐张取景的入口;传 `null` 复位。\n *\n * ```ts\n * fly.setCaptureView({ pitch: -15 }) // 这一张按俯视 15° 取景(替代云台俯仰)\n * const shot = await fly.requestPhoto()\n * fly.setCaptureView(null) // 复位\n * ```\n *\n * 语义:俯仰**替代**云台俯仰(绝对值)。只影响取景相机,机体、飞控与用户视角都不动 ——\n * 与\"稳像 = 模型不动、相机侧解耦\"同一套思路。\n *\n * ⚠️ **没有水平偏航**是有意的:水平方向靠 `spinBodyTo` 转机体,云台 Pan 机械行程\n * 转不出 180°(理由见 `drone-panorama` 文件头)。\n *\n * ⚠️ 俯仰覆盖依赖姿态解耦档(`gimbalStabilization === 'full'`,默认)。\n * `physical` 档的相机姿态采自骨骼链(保留裸机画弧),叠加不了绝对俯仰,此时覆盖不生效。\n * 采集侧(`drone-capture`)在每次取景后会自动复位,单独调用时记得自己收尾。\n */\n setCaptureView(override: CaptureViewOverride | null): void {\n this.captureView = override ?? null\n }\n\n /**\n * 让**机体原地自转**到绝对方位角 `headingDeg`(度,罗盘角);`null` 停止自转。\n *\n * 全景的水平网格走这条通道(见 `drone-panorama`):Mini 4 Pro 云台 Pan 只有 ±30°,\n * 180° 视野必须靠机体自己转。转的是机身、不是相机 —— 所以 FPV 画面里的世界会跟着转,\n * 而机体的 x/z 位置由定点保持锁住(\"原地\"的含义)。\n */\n spinBodyTo(headingDeg: number | null): void {\n this.sim.spinTo(headingDeg)\n }\n\n /** 机体当前方位角(度,罗盘角)—— 全景扫描用它判断\"转到位了没\" */\n getBodyHeading(): number {\n return this.sim.heading\n }\n\n /** 记下当前相机位姿,供取景帧渲染完还原 */\n private snapshotCamera(): CameraRestore | null {\n const camera = this.camera\n if (!camera) return null\n return {\n position: camera.position.clone(),\n quaternion: camera.quaternion.clone(),\n zoom: camera.zoom,\n }\n }\n\n /** 把相机位姿还回去 —— 取景帧渲染完必须调用 */\n private restoreCamera(restore: CameraRestore): void {\n const camera = this.camera\n if (!camera) return\n camera.position.copy(restore.position)\n camera.quaternion.copy(restore.quaternion)\n if (Math.abs(camera.zoom - restore.zoom) > 1e-3) {\n camera.zoom = restore.zoom\n camera.updateProjectionMatrix()\n }\n camera.updateMatrixWorld(true)\n }\n\n /**\n * 取景帧渲染时的场景可见性:位置标记类辅助物 + **机体自身**都退场。\n *\n * 辅助物(返航点光柱 / 航点光柱 / 航迹 / 地面网格)不该出现在照片里,这批早就有;\n * 机体自身是全景**固定光心**带来的新问题 —— 光心钉在自转轴上以后,相机就在机体怀里,\n * 机壳 / 机臂 / 桨叶会进画面,再被多张重复拼进天底。真机全景同样拍不到自己,\n * 成片退场是干净的做法(实时画面不受影响:还原后那一帧渲的还是完整机体)。\n */\n private setCaptureFrameVisible(visible: boolean): void {\n this.world?.setCaptureHelpersVisible(visible)\n if (this.model) this.model.visible = visible\n // 雷达波束挂在场景上(不随 model 隐藏),单独摘;恢复时按用户开关的语义还原\n if (this.radar) this.radar.setVisible(visible ? this.radarBeamsVisible : false)\n }\n\n // ————————————————————————————— 录制帧 —————————————————————————————\n\n /**\n * 录制帧 —— 交给录制器的画面恒为云台取景,与用户此刻在哪个视角无关。\n *\n * 真机只有一个云台相机,录制内容就该是它拍的;而用户盯着的画面可能是观察者\n * 视角,两者不是一回事。做法是每帧走一遍**和拍照完全相同**的取景路径\n * (摆相机 → 渲染 → 同一任务内把这一帧搬进录制画布),再还原相机位姿。\n *\n * 录制流挂在独立的离屏画布上而不是主画布:主画布随后还要渲染用户视角,而画布\n * 捕获是按\"绘制之后\"抓帧的,直接抓主画布会把用户视角录进去。离屏画布的内容\n * 只有云台取景,污染不了。\n *\n * 由渲染循环调用(在 controls.update 之后、onBeforeRender 之前)。未录制时零开销。\n */\n captureRecordingFrame(): void {\n const track = this.captureTrack\n const context = this.recordContext\n const camera = this.camera\n const source = this.renderer?.domElement as HTMLCanvasElement | undefined\n if (!track || !context || !camera || !source || !this.renderer || !this.scene) return\n // 尺寸跟住主画布,窗口变化时录像不留黑边\n if (context.canvas.width !== source.width || context.canvas.height !== source.height) {\n context.canvas.width = source.width\n context.canvas.height = source.height\n }\n if (this.cameraMode === 'fpv') {\n // 相机此刻就在云台上(updateCamera 刚摆好):这一帧渲出来直接搬走\n this.setCaptureFrameVisible(false)\n this.renderer.render(this.scene, camera)\n this.setCaptureFrameVisible(true)\n context.drawImage(source, 0, 0)\n track.requestFrame()\n return\n }\n const restore = this.snapshotCamera()\n if (\n !this.applyGimbalCameraPose(camera, {\n zoom: this.sim.cameraZoom,\n delta: this.lastFrameDelta,\n live: true,\n })\n ) {\n // 云台还不可用(模型没载完):这一帧没有可录内容,下一帧再说\n return\n }\n camera.updateMatrixWorld(true)\n this.setCaptureFrameVisible(false)\n this.renderer.render(this.scene, camera)\n this.setCaptureFrameVisible(true)\n context.drawImage(source, 0, 0)\n track.requestFrame()\n if (restore) this.restoreCamera(restore)\n }\n\n // ————————————————————————————— 拍照 —————————————————————————————\n\n /**\n * 拍照:用云台相机取景拍一张 PNG。\n *\n * 实现是\"渲染前换相机位姿 → 渲染后同一任务内拷屏\":不依赖离屏渲染目标\n * (WebGPU/WebGL 后端行为一致),拷屏与渲染在同一个任务里完成,画面不会闪帧。\n * 上一张尚未取回时返回 null,避免连拍踩踏。\n */\n requestPhoto(): Promise<PhotoShot | null> {\n if (!this.renderer || !this.camera || !this.rig) return Promise.resolve(null)\n if (this.photoRequest) return Promise.resolve(null)\n return new Promise<PhotoShot | null>((resolve) => {\n const timer = window.setTimeout(() => {\n if (this.photoRequest) {\n this.photoRequest = null\n resolve(null)\n }\n }, PHOTO_TIMEOUT_MS)\n this.photoRequest = { resolve, timer, restore: null, captureCamera: null }\n })\n }\n\n /**\n * 取景相机(云台镜头)的位姿与内参快照 —— 虚拟影像采集用(见 drone-capture 的\n * `VirtualCaptureProvider`)。与 CesiumDroneAdapter.getCaptureCamera 同义。\n *\n * 三个要点:\n * - 记录的是**取景相机**,不是机身 —— 云台朝向可能与机体朝向差几十度;\n * - 位姿与拍照走**同一条取景路径**(`applyGimbalCameraPose`),只是渲到一台临时相机上,\n * 因此不触发渲染、不动主相机、也不影响用户当前视角;\n * - 坐标系换成库内约定的 `local-enu`(x 东 / y 北 / z 上):three 沙盒内部是\n * x 东 / y 上 / z 南,由 ENU 基向量在 three 世界系里的朝向构成旋转再左乘即可。\n *\n * 模型 / 云台未就绪时返回 null(调用方据此跳过本次采集)。\n */\n getCaptureCamera(): CaptureCameraState | null {\n const rig = this.rig\n const camera = this.camera\n const renderer = this.renderer\n if (!rig || !camera || !renderer) return null\n const scratch = this.captureCamera\n // delta 传 0:取当前真值对应的期望位姿,不参与电子增稳的低通(那会引入一帧滞后)。\n // fixedCenter 与拍照帧同一开关:录进影像条目的光心 = 成片真正的光心(全景扫描时是固定点)\n if (\n !this.applyGimbalCameraPose(scratch, {\n zoom: this.sim.cameraZoom,\n delta: 0,\n fixedCenter: true,\n })\n )\n return null\n\n const canvas = renderer.domElement as HTMLCanvasElement\n const width = canvas.width\n const height = canvas.height\n if (!width || !height) return null\n\n // 内参:成片用的是渲染相机的投影参数 + 取景时套上的数字变焦\n const zoom = this.sim.cameraZoom > 0 ? this.sim.cameraZoom : 1\n const fovY = 2 * Math.atan(Math.tan((camera.fov * Math.PI) / 360) / zoom)\n const fy = height / 2 / Math.tan(fovY / 2)\n const fx = fy // 像素正方\n const fovX = 2 * Math.atan(width / (2 * fx))\n\n // 位姿:three 世界系 → local-enu。ENU 基向量在世界系里是\n // 东 (1,0,0) / 北 (0,0,-1) / 上 (0,1,0),三者的旋转矩阵的逆把世界向量换进 ENU。\n const enuFromWorld = new THREE.Quaternion()\n .setFromRotationMatrix(\n new THREE.Matrix4().makeBasis(\n new THREE.Vector3(1, 0, 0),\n new THREE.Vector3(0, 0, -1),\n new THREE.Vector3(0, 1, 0),\n ),\n )\n .invert()\n const rotation = scratch.quaternion.clone().premultiply(enuFromWorld)\n\n const sim = this.sim\n const toEnu = (x: number, y: number, z: number): [number, number, number] => [x, -z, y]\n const degrees = (radians: number): number => (radians * 180) / Math.PI\n\n return {\n pose: {\n position: toEnu(scratch.position.x, scratch.position.y, scratch.position.z),\n rotation: [rotation.x, rotation.y, rotation.z, rotation.w],\n coordinateFrame: 'local-enu',\n },\n intrinsics: {\n width,\n height,\n fx,\n fy,\n cx: width / 2,\n cy: height / 2,\n horizontalFovDeg: degrees(fovX),\n verticalFovDeg: degrees(fovY),\n calibrationSource: 'known',\n },\n frame: { width, height },\n drone: {\n position: toEnu(sim.position.x, sim.position.y, sim.position.z),\n heading: sim.heading,\n tiltPitch: sim.tiltPitch,\n tiltRoll: sim.tiltRoll,\n gimbalPitch: sim.gimbalPitch,\n gimbalYaw: sim.gimbalYaw,\n },\n }\n }\n\n /** 渲染前钩子(渲染循环在 controls.update 之后、render 之前调用):本帧按云台相机取景 */\n onBeforeRender(): void {\n const request = this.photoRequest\n const camera = this.camera\n if (!request || !camera) return\n // 先记下当前位姿:取景帧渲染完立刻还原,轨道视角不会被拽到云台位置\n request.restore = this.snapshotCamera()\n // 返航点光柱 / 航迹是给用户看的位置标记,不该出现在照片里(机腹正下方的\n // 3m 光柱离镜头只有几厘米,每一张都会带一根竖线,全景拼接后逐列复制)\n this.setCaptureFrameVisible(false)\n this.applyGimbalCameraPose(camera, {\n zoom: this.sim.cameraZoom,\n delta: this.lastFrameDelta,\n fixedCenter: true,\n // 静态照片必须使用元数据记录的精确姿态,不能沿用实时画面 / 录像的平滑过渡。\n instant: true,\n })\n camera.updateMatrixWorld(true)\n request.captureCamera = this.getCaptureCamera()\n }\n\n /** 渲染后钩子:与渲染同一任务内拷屏,然后把相机位姿还原 */\n onAfterRender(): void {\n const request = this.photoRequest\n if (!request) return\n this.photoRequest = null\n window.clearTimeout(request.timer)\n const shot = this.grabCanvas()\n if (shot && request.captureCamera) shot.camera = request.captureCamera\n // 成片才计数:sim.takePhoto 负责 photoCount +1 与事件日志(实际成像由引擎层负责,见其注释)\n if (shot) this.sim.takePhoto()\n if (request.restore) this.restoreCamera(request.restore)\n this.setCaptureFrameVisible(true)\n // 补渲一帧用户视角:刚才那一帧画布上留的是云台取景,直接把还原后的位姿渲回去,\n // 观察者 / 跟随视角就不会\"闪\"一帧云台画面(Cesium 侧同款修法)。\n // 走裸 render 不经宿主后期管线 —— 与录像路径同一已知差异,仅此一帧。\n if (this.scene && this.renderer && this.camera) {\n this.renderer.render(this.scene, this.camera)\n }\n request.resolve(shot)\n }\n\n /** 把当前画布内容拷进离屏 canvas(同一任务内执行,拿到的是刚渲染的那一帧) */\n private grabCanvas(): PhotoShot | null {\n const source = this.renderer?.domElement as HTMLCanvasElement | undefined\n if (!source || !source.width || !source.height) return null\n const target = document.createElement('canvas')\n target.width = source.width\n target.height = source.height\n const context = target.getContext('2d')\n if (!context) return null\n context.drawImage(source, 0, 0)\n return { dataUrl: target.toDataURL('image/png'), width: target.width, height: target.height }\n }\n\n // ————————————————————————————— 录像 —————————————————————————————\n\n /** 当前是否正在屏录(以 MediaRecorder 实例是否存在为准) */\n get isRecording(): boolean {\n return this.recorder !== null\n }\n\n /** 开始屏录(录云台取景,与用户当前视角无关)。返回 false = 当前环境不支持 */\n startRecording(): boolean {\n if (this.recorder) return true\n const canvas = this.renderer?.domElement as HTMLCanvasElement | undefined\n if (\n !canvas ||\n typeof MediaRecorder === 'undefined' ||\n typeof canvas.captureStream !== 'function'\n )\n return false\n // 录制源用独立离屏画布:每帧只往里搬一帧云台取景,主画布上用户视角的渲染\n // 永远不会污染录制流(画布捕获按\"绘制之后\"抓帧,直接抓主画布会录到用户视角)。\n const recordCanvas = document.createElement('canvas')\n recordCanvas.width = canvas.width || 1\n recordCanvas.height = canvas.height || 1\n const recordContext = recordCanvas.getContext('2d')\n if (!recordContext) return false\n let recorder: MediaRecorder\n let stream: MediaStream\n try {\n // captureStream(0) = 手动抓帧:录到什么由渲染循环里搬进这张画布的云台帧决定。\n stream = recordCanvas.captureStream(0)\n const track = stream.getVideoTracks()[0] as CanvasCaptureMediaStreamTrack | undefined\n if (track && typeof track.requestFrame === 'function') {\n this.captureTrack = track\n } else {\n // 少数环境不支持手动抓帧:退回按帧率自动采样这张画布(内容仍是云台取景)\n stream.getTracks().forEach((item) => item.stop())\n stream = recordCanvas.captureStream(RECORD_FPS)\n this.captureTrack = null\n }\n const mime = pickRecordMime()\n recorder = mime\n ? new MediaRecorder(stream, { mimeType: mime, videoBitsPerSecond: RECORD_BITRATE })\n : new MediaRecorder(stream)\n } catch {\n this.captureTrack = null\n return false\n }\n this.recordContext = recordContext\n this.recordChunks = []\n this.recordStream = stream\n recorder.ondataavailable = (event) => {\n if (event.data && event.data.size > 0) this.recordChunks.push(event.data)\n }\n recorder.start(RECORD_TIMESLICE_MS)\n this.recorder = recorder\n this.sim.setRecordingState(true)\n return true\n }\n\n /** 结束屏录:收尾成 Blob 并交给 onRecordingReady(未录制时直接返回 null) */\n stopRecording(): Promise<Blob | null> {\n const recorder = this.recorder\n if (!recorder) {\n this.sim.setRecordingState(false)\n return Promise.resolve(null)\n }\n this.recorder = null\n const seconds = this.sim.recordSeconds\n this.sim.setRecordingState(false)\n return new Promise<Blob | null>((resolve) => {\n let settled = false\n const finish = (): void => {\n if (settled) return\n settled = true\n window.clearTimeout(timer)\n const chunks = this.recordChunks\n this.recordChunks = []\n this.recordStream?.getTracks().forEach((track) => track.stop())\n this.recordStream = null\n this.captureTrack = null\n this.recordContext = null\n const blob = chunks.length\n ? new Blob(chunks, { type: recorder.mimeType || 'video/webm' })\n : null\n if (blob) this.onRecordingReady?.(blob, seconds)\n resolve(blob)\n }\n // 部分环境可能不派发 onstop,兜底收尾\n const timer = window.setTimeout(finish, RECORD_STOP_TIMEOUT_MS)\n recorder.onstop = finish\n try {\n recorder.stop()\n } catch {\n finish()\n }\n })\n }\n\n /** 按当前相机模式摆放相机:机载=云台位姿,跟随=平滑尾随,观察者=交给轨道控制器 */\n private updateCamera(delta: number): void {\n const camera = this.camera\n if (!camera) return\n const sim = this.sim\n const dronePosition = this.dronePosition.set(\n sim.position.x,\n this.groundOffsetY + sim.position.y,\n sim.position.z,\n )\n\n if (this.cameraMode === 'fpv') {\n // 相机位姿 = 云台光轴(含变焦);取景逻辑与拍照共用同一套。\n // live: 实时画面不吃\"取景俯仰覆盖\"(那属于成片),否则全景扫描时机载视角每拍一帧闪一下\n this.applyGimbalCameraPose(camera, { zoom: this.sim.cameraZoom, delta, live: true })\n return\n }\n\n if (this.cameraMode === 'follow') {\n const rad = (sim.heading * Math.PI) / 180\n this.forwardVector.set(Math.sin(rad), 0, -Math.cos(rad))\n this.followPosition.copy(dronePosition).addScaledVector(this.forwardVector, -FOLLOW_DISTANCE)\n this.followPosition.y += FOLLOW_HEIGHT\n camera.position.lerp(this.followPosition, Math.min(1, delta * 4))\n this.followLookAt.copy(dronePosition)\n this.followLookAt.y += 0.6\n camera.lookAt(this.followLookAt)\n return\n }\n\n // 观察者:轨道控制器接管,但控制点平滑跟住飞机,避免飞出视野后找不回来\n if (this.cameraAutoFollow) {\n this.cameraTarget.lerp(dronePosition, Math.min(1, delta * 2.2))\n if (this.controls) {\n this.cameraShift.subVectors(this.cameraTarget, this.controls.target)\n this.controls.target.add(this.cameraShift)\n camera.position.add(this.cameraShift)\n }\n } else if (this.controls) {\n // 框住航线 / 场景编辑期间不跟飞机:否则刚摆好的画面会被\"跟到飞机\"慢慢拖走\n this.cameraTarget.copy(this.controls.target)\n }\n }\n\n /** 复位相机:回到观察者视角、还原缩放,并把机位重置到飞机斜后上方 */\n resetCamera(): void {\n if (!this.camera || !this.controls) return\n this.setCameraMode('orbit')\n this.cameraAutoFollow = true\n if (this.camera.zoom !== 1) {\n this.camera.zoom = 1\n this.camera.updateProjectionMatrix()\n }\n const dronePosition = new THREE.Vector3(\n this.sim.position.x,\n this.groundOffsetY + this.sim.position.y,\n this.sim.position.z,\n )\n this.camera.position.set(dronePosition.x + 6, dronePosition.y + 3.5, dronePosition.z + 7)\n this.controls.target.copy(dronePosition)\n this.cameraTarget.copy(dronePosition)\n }\n\n /**\n * 把观察者相机拉到\"一眼看全整条航线\"的位置(编辑航线前先框住航线)。\n *\n * 航点动辄 30~40 米高、水平铺开几十米,默认机位贴着飞机,根本看不到航线 ——\n * 所以框住一次比让用户自己滚轮找半天更实际。按包围球的垂直/水平视场双向取距离,\n * 保证竖着铺开的航线也不会被裁掉。\n */\n frameMission(): boolean {\n const camera = this.camera\n const controls = this.controls\n if (!camera || !controls) return false\n const home = this.sim.homePosition\n const first = this.sim.mission[0]\n let minX = home.x\n let maxX = home.x\n let minY = first ? Math.max(1, first.altitude) : DRONE_SPEC.takeoffAltitude\n let maxY = minY\n let minZ = home.z\n let maxZ = home.z\n const include = (x: number, y: number, z: number): void => {\n minX = Math.min(minX, x)\n maxX = Math.max(maxX, x)\n minY = Math.min(minY, y)\n maxY = Math.max(maxY, y)\n minZ = Math.min(minZ, z)\n maxZ = Math.max(maxZ, z)\n }\n for (const waypoint of this.sim.mission) {\n include(waypoint.x, waypoint.altitude, waypoint.z)\n }\n // 地面也要进画面:航点的水平位置在地面上是有意义的参照\n include(minX, 0, minZ)\n include(maxX, 0, maxZ)\n\n const center = new THREE.Vector3((minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2)\n center.y += this.groundOffsetY\n const radius = Math.max(12, 0.5 * Math.hypot(maxX - minX, maxY - minY, maxZ - minZ))\n const halfVertical = THREE.MathUtils.degToRad(camera.fov / 2)\n const halfHorizontal = Math.atan(Math.tan(halfVertical) * Math.max(0.2, camera.aspect))\n const distance =\n Math.max(radius / Math.tan(halfVertical), radius / Math.tan(halfHorizontal)) * 1.35\n\n this.setCameraMode('orbit')\n // 关掉\"控制点跟飞机\":否则摆好的画面会在几秒内被拖回飞机身上\n this.cameraAutoFollow = false\n if (camera.zoom !== 1) {\n camera.zoom = 1\n camera.updateProjectionMatrix()\n }\n const direction = new THREE.Vector3(-0.42, 0.55, 0.72).normalize()\n camera.position.copy(center).addScaledVector(direction, distance)\n controls.target.copy(center)\n this.cameraTarget.copy(center)\n camera.lookAt(center)\n camera.updateMatrixWorld(true)\n controls.update()\n return true\n }\n\n // ————————————————————————————— 飞行控制 —————————————————————————————\n\n /** 当前飞行阶段(真值取 sim.phase):未上电 / 自检 / 待机 / 电机已启动 / 飞行中 / 返航 / 降落… */\n get phase(): FlightPhase {\n return this.sim.phase\n }\n\n /** 是否已离地(离地 5cm 以上且已上电) */\n get airborne(): boolean {\n return this.sim.airborne\n }\n\n /** 当前飞行模式:Cine / Normal / Sport */\n get flightMode(): FlightMode {\n return this.sim.mode\n }\n\n /** 当前高度(米,相对起飞点) */\n get altitude(): number {\n return this.sim.position.y\n }\n\n /** 起飞前检查清单(含宿主注入的外部项);面板据此提示\"还差什么才能起飞\" */\n get preflightChecks(): ChecklistItem[] {\n return this.sim.checklist\n }\n\n /** 上电:自检 → 预热 → 待机(仅未上电 / 已停桨时受理) */\n powerOn(): void {\n this.sim.powerOn()\n }\n\n /** 一键起飞:地面待机时自动启动电机,再爬升到 1.2 米;返回是否受理 */\n takeOff(): boolean {\n return this.sim.autoTakeOff()\n }\n\n /** 自动降落(会先中止正在执行的航线);返回是否受理 */\n land(): boolean {\n return this.sim.startLanding()\n }\n\n /** 智能返航(需在空中,会先中止航线);返回是否受理 */\n returnHome(reason = '用户触发返航'): boolean {\n return this.sim.startRth(reason)\n }\n\n // ————————————————————————————— 操作透传 —————————————————————————————\n\n /** 写摇杆连续量(摇杆优先于键盘;松杆由飞控刹停悬停) */\n setStick(stick: Partial<StickState>): void {\n this.sim.setStick(stick)\n }\n\n /** 摇杆立刻回中(等同松开遥控器杆) */\n resetStick(): void {\n this.sim.setStick({ ...NEUTRAL_STICK })\n }\n\n /** 键盘 / 方向键输入直接映射成摇杆行程(美国手 Mode 2) */\n setMoveDirection(direction: DroneDirection | null): void {\n this.sim.setStick({\n pitch: direction === 'up' ? 1 : direction === 'down' ? -1 : 0,\n roll: direction === 'right' ? 1 : direction === 'left' ? -1 : 0,\n })\n }\n\n /** 设垂直方向指令 -1(降)~0(保持)~1(升),面板/键盘写入 */\n setVerticalDirection(direction: DroneVerticalDirection | null): void {\n this.sim.setStick({ throttle: direction === 'up' ? 1 : direction === 'down' ? -1 : 0 })\n }\n\n /** 设偏航指令 -1(左转)~0~1(右转) */\n setRotationDirection(direction: DroneRotationDirection | null): void {\n this.sim.setStick({ yaw: direction === 'right' ? 1 : direction === 'left' ? -1 : 0 })\n }\n\n /** 切飞行挡位:平稳 C / 普通 N / 运动 S */\n setMode(mode: FlightMode): void {\n this.sim.setMode(mode)\n }\n\n /** 改仿真配置(限速 / 限高限距开关 / 风 / 电量阈值等,立即生效) */\n setConfig(patch: Partial<SimConfig>): void {\n this.sim.setConfig(patch)\n }\n\n /** 注入/清除故障标志(测失效保护用) */\n setFaults(patch: Partial<FaultFlags>): void {\n this.sim.faults = { ...this.sim.faults, ...patch }\n }\n\n /** 设机臂折叠目标 0(展开)~1(收纳),桨叶同步收拢 */\n setArmFoldTarget(fold: number): void {\n this.armFoldTarget = fold > 0.5 ? 1 : 0\n }\n\n /** 当前机臂折叠量 0~1(插值中的实时值) */\n get armFoldTargetValue(): number {\n return this.armFoldTarget\n }\n\n /**\n * 展开 / 收拢机臂。\n *\n * 折叠量的取值是**反的**(0 = 完全展开,1 = 完全收纳,见 DroneRig),这里包一层语义化接口,\n * 免得调用方每次都要在脑子里翻转一次。飞行中收拢会被机臂检查项拦下(与真机一致)。\n */\n setArmsFolded(folded: boolean): void {\n this.setArmFoldTarget(folded ? 1 : 0)\n }\n\n /** 机臂当前是否处于\"已收拢\"的目标状态(展开 = false) */\n get armsFolded(): boolean {\n return this.armFoldTargetValue > 0.5\n }\n\n /**\n * 起落架腿当前是否放下(收起 = false)。\n * 收放由飞行高度自动驱动(高于 LEG_RETRACT_ALTITUDE 收、低于放),\n * 只有带 LEG_* 节点的机体(哈士奇)有腿,真机 GLB 恒为 true。\n */\n get legsDeployed(): boolean {\n return (this.rig?.legDeploy ?? 1) > 0.5\n }\n\n /** 设云台俯仰角(度,范围见 DRONE_SPEC) */\n setGimbalPitch(pitch: number): void {\n this.sim.setGimbalPitch(pitch)\n }\n\n /** 云台增量微调(度;俯仰/偏航分别给) */\n nudgeGimbal(deltaPitch: number, deltaYaw = 0): void {\n this.sim.nudgeGimbal(deltaPitch, deltaYaw)\n }\n\n /** 清空飞行航迹 */\n clearTrail(): void {\n this.world?.clearTrail()\n }\n\n // ————————————————————————————— 航线任务 —————————————————————————————\n\n /** 启动航线任务(地面时先自动起飞,到 1.2 米后自动接上航线) */\n startMission(): boolean {\n return this.sim.startMission()\n }\n\n /** 暂停执行中的航线(原地悬停,可再续飞) */\n pauseMission(reason?: string): boolean {\n return this.sim.pauseMission(reason)\n }\n\n /** 续飞被暂停的航线 */\n resumeMission(): boolean {\n return this.sim.resumeMission()\n }\n\n /** 中止航线并留在原地悬停 */\n stopMission(reason?: string): boolean {\n return this.sim.stopMission(reason)\n }\n\n /** 下发航点列表(执行中会被拒绝,与真机\"上传后不可改\"一致) */\n setMission(waypoints: Array<Partial<MissionWaypoint>>): boolean {\n return this.sim.setMission(waypoints)\n }\n\n /** 改航线参数(高度/速度/航向模式等,下一次起飞生效) */\n setMissionConfig(patch: Partial<MissionConfig>): void {\n this.sim.setMissionConfig(patch)\n }\n\n /** 航线参数恢复默认 */\n resetMissionToDefault(): void {\n this.sim.resetMissionToDefault()\n }\n\n /** 取航线只读快照(航点/Home/进度) */\n getMissionSnapshot(): MissionSnapshot {\n return this.sim.snapshot().mission\n }\n\n /**\n * 把当前航线整理成可序列化的导出数据(只读,随时可调,执行中也行)。\n *\n * 序列化实现已下沉到 drone-sim(纯逻辑层),Cesium 后端复用同一函数;\n * 航程按\"返航点起飞 → 依次过点\"的三维折线累计,耗时按每点自己的速度(0 = 巡航速度)\n * 加上到点悬停折算,和引擎执行时的口径一致。\n */\n getMissionExport(): MissionExport {\n return buildMissionExport(\n this.sim.mission.map((waypoint) => ({ ...waypoint })),\n { ...this.sim.missionConfig },\n this.sim.homePosition,\n )\n }\n\n /**\n * 导出航线文本。`json` 是完整存档(含坐标约定、任务参数、概览统计,可再喂回 `setMission`),\n * `csv` 只出航点表 —— 表头中文、CRLF 换行,Excel / WPS 双击即可打开。\n */\n exportMission(format: MissionExportFormat = 'json'): string {\n return exportMissionText(this.getMissionExport(), format)\n }\n\n /** 航点标记 / 航线折线的总显隐(不影响任务执行) */\n setMissionVisible(visible: boolean): void {\n this.missionVisible = visible\n this.world?.setMissionVisible(visible)\n }\n\n /** 航线是否可见 */\n isMissionVisible(): boolean {\n return this.world?.isMissionVisible() ?? this.missionVisible\n }\n\n /** 只开关航点标记 */\n setMissionWaypointsVisible(visible: boolean): void {\n this.missionMarkersVisible = visible\n this.world?.setMissionWaypointsVisible(visible)\n }\n\n /** 航线航点标记是否可见 */\n isMissionWaypointsVisible(): boolean {\n return this.world?.isMissionWaypointsVisible() ?? this.missionMarkersVisible\n }\n\n /** 只开关航线折线 */\n setMissionPathVisible(visible: boolean): void {\n this.missionPathVisible = visible\n this.world?.setMissionPathVisible(visible)\n }\n\n /** 航线路径线是否可见 */\n isMissionPathVisible(): boolean {\n return this.world?.isMissionPathVisible() ?? this.missionPathVisible\n }\n\n /** three 后端是本地米坐标系,没有地理坐标;经纬度切换开关因此不显示 */\n supportsGeoCoordinates(): boolean {\n return false\n }\n\n /** three 后端无经纬高:返回 null(面板始终走 XYZ 显示) */\n getWaypointGeo(_index: number): { lon: number; lat: number; height: number } | null {\n return null\n }\n\n /** three 后端是本地米坐标系,无地理锚点,经纬度编辑不可用;接口占位返回 false */\n setWaypointGeo(_index: number, _geo: { lon: number; lat: number; height: number }): boolean {\n return false\n }\n\n // ————————————————————————————— 场景内航点编辑 —————————————————————————————\n\n /** 航点标记 / 航线折线的重建签名(只跟返航点与航点位置有关) */\n private buildMissionSignature(): string {\n const home = this.sim.homePosition\n return `${home.x}|${home.z}#${this.sim.mission\n .map((waypoint) => `${waypoint.x}|${waypoint.z}|${waypoint.altitude}`)\n .join(';')}`\n }\n\n /** 场景编辑的硬门槛:观察者视角 + 航线未在执行(含\"已下发等待起飞到位\") */\n private canMissionSceneEdit(): boolean {\n return this.isOrbitView && this.sim.missionStatus === 'idle' && !this.sim.missionPendingStart\n }\n\n /** 开关「场景内编辑航点」 */\n setMissionEditEnabled(enabled: boolean): void {\n this.missionEditEnabled = enabled\n this.missionEditor?.setEnabled(enabled)\n if (!enabled) {\n this.setMissionSelected(-1)\n } else if (!this.canMissionSceneEdit()) {\n this.sim.pushEvent('warn', '场景编辑需要先切到观察者视角,且航线未在执行')\n } else if (this.sim.mission.length > 0) {\n // 默认机位贴着飞机,航点全在画面外 —— 开编辑时先把整条航线框进画面\n this.frameMission()\n }\n }\n\n /** 航线编辑模式是否开启 */\n isMissionEditEnabled(): boolean {\n return this.missionEditEnabled\n }\n\n /** 场景编辑:选中某个航点(-1 取消选中) */\n setMissionSelected(index: number): void {\n this.missionEditSelected = index\n this.world?.setMissionSelection(index)\n }\n\n /** 当前选中的航点索引(无选中返回 null) */\n getMissionSelected(): number {\n return this.missionEditSelected\n }\n\n /** 场景编辑的实时状态(界面 100ms 轮询) */\n getMissionEditState(): MissionEditState {\n const editor = this.missionEditor\n if (!editor) {\n return {\n enabled: this.missionEditEnabled,\n active: false,\n selectedIndex: this.missionEditSelected,\n hoverIndex: -1,\n dragging: false,\n mode: null,\n }\n }\n const state = editor.getState()\n // 选中态以装配层为准(编辑器只是读)\n return { ...state, selectedIndex: this.missionEditSelected }\n }\n\n /**\n * 原位更新一个航点(场景拖拽每帧调用):改内核 + 只挪这一根三维标记。\n *\n * 关键在于同步刷新签名 —— 否则下一帧 syncPose 发现\"签名变了\"会把整层标记重建一遍,\n * 拖动时就会一顿一顿地闪。\n */\n updateMissionWaypoint(index: number, patch: Partial<MissionWaypoint>): MissionWaypoint | null {\n const applied = this.sim.updateMissionWaypoint(index, patch)\n if (!applied) return null\n this.world?.updateMissionWaypoint(index, {\n x: applied.x,\n z: applied.z,\n altitude: applied.altitude,\n })\n this.missionSignature = this.buildMissionSignature()\n return applied\n }\n\n /** 场景编辑:按索引删除一个航点 */\n deleteMissionWaypoint(index: number): boolean {\n if (!this.canMissionSceneEdit()) return false\n if (!this.sim.removeMissionWaypoint(index)) return false\n this.setMissionSelected(Math.min(index, this.sim.mission.length - 1))\n this.sim.pushEvent('info', `场景编辑:已删除航点 ${index + 1}`)\n return true\n }\n\n /** 场景编辑:以飞机当前位置新增一个航点(高度取飞机当前高度) */\n addMissionWaypointAtDrone(): number {\n if (!this.canMissionSceneEdit()) return -1\n const position = this.sim.position\n const index = this.sim.addMissionWaypoint({\n x: Math.round(position.x),\n z: Math.round(position.z),\n altitude: Math.max(1, Math.round(position.y)),\n speed: this.sim.missionConfig.autoSpeed,\n })\n if (index < 0) return -1\n this.setMissionSelected(index)\n this.sim.pushEvent('info', `场景编辑:已在飞机当前位置新增航点 ${index + 1}`)\n return index\n }\n\n /** 新航点的默认参数:高度与速度继承相邻航点,避免插出一个贴地点 */\n private newWaypointTemplate(index: number): Partial<MissionWaypoint> {\n const list = this.sim.mission\n const neighbour = list[Math.min(index, list.length - 1)] ?? list[list.length - 1]\n return {\n altitude: neighbour?.altitude ?? 30,\n speed: neighbour?.speed ?? this.sim.missionConfig.autoSpeed,\n hoverSeconds: neighbour?.hoverSeconds ?? 0,\n }\n }\n\n /** 取机体机械位姿报告(机臂折叠/桨叶角/云台角/起落架,调试用) */\n getRigReport(): RigPartReport[] {\n return this.rig?.parts ?? []\n }\n\n /** 取全量真值快照(位置/姿态/电池/云台/任务…,面板与 MCP 都用它) */\n getSnapshot(): DroneSnapshot {\n return this.sim.snapshot()\n }\n\n /** 取灯光真值快照(各灯模式/亮度/当前色) */\n getLightsSnapshot() {\n return this.lights?.getSnapshot() ?? null\n }\n\n /** 兼容旧接口:直接返回仿真遥测 */\n getTelemetry() {\n return {\n speedMetersPerSecond: Math.hypot(this.sim.velocity.x, this.sim.velocity.z),\n altitudeMeters: this.sim.position.y,\n isFlying: this.sim.airborne,\n }\n }\n\n // —— 灯光手动覆盖(面板调试用) ——\n\n /** 设状态灯灯语(覆盖默认模式,如 breathing / blink) */\n setStatusLightPattern(key: StatusLightKey): void {\n this.lightOverride = key\n this.lights?.setStatusPattern(key)\n }\n\n /** 清除状态灯覆盖,回到按状态自动的灯语 */\n clearStatusLightOverride(): void {\n this.lightOverride = null\n this.lights?.setStatusPattern(statusPatternFor(this.sim))\n }\n\n /** 当前状态灯覆盖(无覆盖返回 null) */\n get statusLightOverride(): StatusLightKey | null {\n return this.lightOverride\n }\n\n /** 设电池灯模式(off / level / blink) */\n setBatteryLightMode(mode: BatteryLightMode): void {\n this.lights?.setBatteryMode(mode)\n }\n\n /** 设电池灯亮度 0~1 */\n setBatteryLightLevel(levelPercent: number): void {\n this.sim.forceBatteryLevel(levelPercent)\n this.lights?.setBatteryLevel(levelPercent)\n }\n\n /** 设补光灯模式(off / on / auto) */\n setAuxLightMode(mode: AuxLightMode): void {\n this.lights?.setAuxLightMode(mode)\n }\n\n /** 销毁飞行沙盒:停渲染循环、拆面板、释放场景与渲染器 */\n destroy(): void {\n this.destroyed = true\n // 录制中直接收摊(页面卸载不触发下载)\n if (this.recorder) {\n try {\n this.recorder.stop()\n } catch {\n /* 已停止 */\n }\n this.recorder = null\n }\n this.recordStream?.getTracks().forEach((track) => track.stop())\n this.recordStream = null\n this.captureTrack = null\n this.recordContext = null\n this.recordChunks = []\n this.onRecordingReady = null\n this.missionEditor?.destroy()\n this.missionEditor = null\n this.rig?.destroy()\n this.rig = null\n this.lights?.destroy()\n this.lights = null\n this.world?.destroy()\n this.world = null\n this.radar?.destroy()\n this.radar = null\n if (this.model) {\n this.disposeModel(this.model)\n this.scene?.remove(this.model)\n }\n this.model = null\n }\n\n private normalizeModel(model: THREE.Group): void {\n const initialBounds = new THREE.Box3().setFromObject(model)\n const size = initialBounds.getSize(new THREE.Vector3())\n const largestDimension = Math.max(size.x, size.y, size.z)\n if (largestDimension > 0) model.scale.setScalar(DRONE_SIZE_METERS / largestDimension)\n\n const bounds = new THREE.Box3().setFromObject(model)\n const center = bounds.getCenter(new THREE.Vector3())\n model.position.x -= center.x\n model.position.z -= center.z\n model.position.y -= bounds.min.y\n }\n\n private disposeModel(model: THREE.Group): void {\n model.traverse((object) => {\n const mesh = object as THREE.Mesh\n if (!mesh.isMesh) return\n mesh.geometry.dispose()\n const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]\n materials.forEach((material) => material.dispose())\n })\n }\n}\n\n// statusPatternFor(灯语与飞行状态的映射)已搬到 drone-lights.ts:它是纯逻辑(只读 DroneSim),\n// 不依赖 three,搬过去之后 Cesium 侧机体表现层就能直接复用,不必 import 整个 three 装配层。\n// 这里原样再导出,`@/lib` 的既有 API 与导入路径完全不变。\nexport { statusPatternFor }\n","/**\n * Cesium 侧的机体表现层 —— 桨叶自转 + 机体灯光。\n *\n * 与 three 版的 DroneRig + DroneLights 对位:同一套语义节点、同一套转速公式、\n * 同一份灯语数据,只是把\"写 three 场景图\"换成\"写 Cesium Model 的节点矩阵 / 挂 CustomShader\"。\n *\n * Cesium 的 Model 是\"死\"的:GLB 里四颗电机关节不会自己转,灯珠也只是灰壳\n * (模型自带的 KHR_lights_punctual 扩展 Cesium 不支持,由 drone-cesium.ts 在加载期剥离)。\n * 本层用两个机制把表现力补齐到 three 版的水准:\n *\n * 1. 桨叶自转:每帧把「绕电机轴的自转」写进 Spin_Bone 关节矩阵——\n * node.matrix = originalMatrix × R(axisLocal, angle)\n * 这是实测唯一能传导到子节点的通道(setter 内部置 _userAnimationDirty,\n * 场景图当帧强制重建)。⚠️ 桨叶并不挂在 CTRL_Prop_*_Spin 下面:自转节点\n * 的子树是「两片桨叶折叠骨节」+「CTRL_Prop_*_Spin(其下才是 ROTOR 桨毂与\n * MOTOR_*_RotorBell 钟罩)」两拨兄弟,只写 CTRL 会漏掉桨叶。\n *\n * 2. 钟罩反向补偿:电机钟罩(磁钢外壳)是**固定件**,不能跟着转子转\n * (three 版靠把钟罩从自转子树摘出、挂回自转节点的父级实现;Cesium 的\n * glTF 层级改不了,只能解矩阵)。每帧给钟罩写一个\"刚好抵消自转\"的矩阵:\n * M = CTRL.original × ROTOR.original (自转节点 → 钟罩父级 的出厂链)\n * B = M⁻¹ · R(axis,θ)⁻¹ · M · B0 (R 定义在自转节点局部系)\n * 于是钟罩世界矩阵逐元素等于出厂值 —— 不是\"看起来没动\",而是数学上恒定。\n *\n * 3. 机体灯光:**模型自带的灯珠 Mesh 本体发光**(CustomShader 按 positionMC 判定\n * 片元落在哪个灯珠几何内,把 uniform 颜色写进 emissive —— 与 three 版\n * \"复用灯珠网格 + 换自发光材质\"同构;Cesium 不支持单节点换材质,shader 是唯一通道)。\n * 灯位/半径从 GLB 灯珠网格顶点离线实测,位置由几何自证,不存在手摆锚点。\n * 灯语数据与 three 版完全同源——statusPatternFor(sim) + STATUS_PATTERNS 时间轴、\n * batteryLedPlan(batteryPercent)、辅助灯「飞行中且离地 ≤8m 自动点亮」。\n * 两个后端在同样飞行状态下闪一样的灯。\n * ⚠️ 不要再走\"自建 billboard 灯珠\"路线:锚点坐标系(ENU↔Y-up)极易错位,\n * 且与灯珠 Mesh 位置永远差着一层手摆常量 —— 已按约定删除。\n *\n * 非 DJI 机体(无 PROP_*_Spin_Bone 语义节点)整体 no-op,与 DroneRig 的降级纪律一致。\n */\nimport { PROP_SPIN_SIGN, type ArmPosition } from './drone-rig'\nimport { STATUS_PATTERNS, batteryLedPlan, statusPatternFor } from './drone-lights'\nimport type { DroneSim } from './drone-sim'\n\ntype CesiumNS = Record<string, any>\n\nconst ARM_POSITIONS: ArmPosition[] = ['FrontLeft', 'FrontRight', 'RearLeft', 'RearRight']\n\n/**\n * 电机轴在「自转节点(PROP_*_Spin_Bone)局部系」里的表示,**运行时从模型算**,\n * 不写死常量:\n * axisLocal = normalize( linear(CTRL_Prop_*_Spin 出厂矩阵) · (0,1,0) )\n *\n * 依据:电机轴就是 CTRL_Prop_*_Spin 的局部 +Y(实测钟罩侧壁点绕该轴的半径\n * 离散度 MAD≈0.95mm,而绕模型竖直轴则散成 47~74mm —— 后者根本不是它的对称轴)。\n * CTRL 的出厂旋转是绕 X 的 90°,故 axisLocal 恰好是 (0,0,1)(Blender 的局部 Z\n * 即 three 的局部 Y,与 drone-rig.ts 的注释对得上)。\n *\n * ⚠️ 历史坑:这里曾写死四组常量,而那是把 CTRL 局部 +Y 的**世界方向**多换了一次\n * 轴(Blender Z-up ↔ three Y-up),实际指向模型竖直轴,和真电机轴差 15.27°\n * (前臂)/18.52°(后臂)—— 桨盘会绕着偏 15° 的轴进动,钟罩看起来就在\"转\"。\n */\nfunction spinAxisFor(C: CesiumNS, ctrlOriginal: CesiumNS): CesiumNS | null {\n const column = new C.Cartesian4()\n C.Matrix4.getColumn(ctrlOriginal, 1, column)\n const axis = C.Cartesian3.fromCartesian4(column, new C.Cartesian3())\n if (C.Cartesian3.magnitude(axis) < 1e-9) return null\n return C.Cartesian3.normalize(axis, axis)\n}\n\n/**\n * 灯珠几何(GLB 单位,Y-up):从 dji_mini4_pro.glb 的**灯珠网格顶点**离线实测\n * (质心 + 最大半径,脚本 tmp-shots/probe-glb-led-precise.py)。\n *\n * 这就是对齐 three 版「复用模型自带灯珠网格」约定的 Cesium 落地:\n * CustomShader 用它判定\"这个片元是不是灯珠\"→ 灯珠 Mesh 本体发光,位置由几何自证。\n * 曾经的 ANCHOR_* 手摆锚点 + billboard 灯珠方案已删除 —— 坐标轴(ENU↔Y-up)一错\n * 灯就飞到机身上方,且与 Mesh 位置永远差一层手摆常量(用户看到的\"红球绿球\")。\n */\nconst LED_GEO = {\n statusLeft: { center: [-8.64, 0.912, 9.765] as const, radius: 0.5 },\n statusRight: { center: [8.643, 0.911, 9.767] as const, radius: 0.5 },\n battery: [\n { center: [-0.527, 2.085, 4.811] as const, radius: 0.1 },\n { center: [-0.176, 2.084, 4.813] as const, radius: 0.1 },\n { center: [0.178, 2.084, 4.813] as const, radius: 0.1 },\n { center: [0.53, 2.085, 4.812] as const, radius: 0.1 },\n ],\n // 辅助灯照明面 = 下视玻璃(three 版 DroneLights 的同一锚点),不是 LIGHT_BottomAssist 空节点\n aux: { center: [0, -2.445, -0.927] as const, radius: 0.37 },\n} as const\n\n/** 电量灯颜色(与 drone-lights 的 BATTERY_COLOR 同值;那边未导出,此处按同值复刻) */\nconst BATTERY_COLOR = 0x9fffcb\n/** 底部辅助灯颜色(与 drone-lights 辅助灯盘的自发光同色:暖白) */\nconst AUX_COLOR = 0xffe9c0\n/** 辅助灯自动档的离地门限(米),与 drone-lights 的 AUTO_LIGHT_MAX_AGL_METERS 一致 */\nconst AUTO_LIGHT_MAX_AGL_METERS = 8\n\ninterface SpinJoint {\n position: ArmPosition\n node: CesiumNS\n /** 出厂矩阵(setter 的语义:写 originalMatrix 等价于\"撤销用户动画\") */\n original: CesiumNS\n /** 每关节独享的两块矩阵:节点矩阵按引用被 Cesium 持有,绝不能跨关节共用 */\n trs: CesiumNS\n product: CesiumNS\n axis: CesiumNS\n angle: number\n spinning: boolean\n /** 固定钟罩的反向补偿(缺节点时为 null → 该臂钟罩会跟着转,但不会崩) */\n bell: BellCompensation | null\n}\n\n/**\n * 电机钟罩(磁钢外壳)的反向补偿数据。\n * 每帧给钟罩写 K·B0,K = M⁻¹·R⁻¹·M —— 把自转在钟罩自己的父级坐标系里抵消掉,\n * 于是钟罩世界矩阵恒等于出厂值(与 three 版\"把钟罩挂到不动骨节下\"等价)。\n */\ninterface BellCompensation {\n node: CesiumNS\n /** 出厂矩阵 */\n original: CesiumNS\n /** M = CTRL_Prop_*_Spin.original × ROTOR_*.original */\n chain: CesiumNS\n invChain: CesiumNS\n /** 逐帧中间量(全预先分配,稳态零分配) */\n inv: CesiumNS\n conj: CesiumNS\n kernel: CesiumNS\n product: CesiumNS\n spinning: boolean\n}\n\n/** 闪烁相位:与 drone-lights 的 blinkPhase 同式 */\nfunction blinkPhase(timeMs: number, onMs: number, offMs: number): boolean {\n return timeMs % (onMs + offMs) < onMs\n}\n\n/**\n * 停转复位:把节点矩阵交还 setter 的\"清除用户动画\"分支 —— 恢复出厂矩阵并清\n * userAnimated,场景图不再每帧强制重建。自转节点与钟罩必须成对复位。\n */\nfunction stopJoint(joint: SpinJoint): void {\n if (joint.spinning) {\n joint.node.matrix = undefined\n joint.spinning = false\n }\n const bell = joint.bell\n if (bell?.spinning) {\n bell.node.matrix = undefined\n bell.spinning = false\n }\n}\n\nexport class CesiumDroneRig {\n private readonly sim: DroneSim\n private readonly C: CesiumNS\n\n private joints: SpinJoint[] = []\n /** 灯珠发光 shader(attach 时创建一次;每帧只改 uniform 颜色) */\n private lightShader: CesiumNS | null = null\n /** 当前绑定的模型(detach 时要摘掉 customShader) */\n private modelRef: CesiumNS | null = null\n /** 当前机体是否有语义节点(只有 DJI 机体启用本层) */\n private enabled = false\n private timeMs = 0\n\n /**\n * @param sim 飞控内核(与适配器共用同一个实例,读到的永远是同一份真值)\n * @param cesium Cesium 命名空间;缺省取 window.Cesium(全局脚本注入方式)\n */\n /** Cesium 机体表现层:把 DroneSim 真值映射到 Model 关节(桨叶自转/起落架/灯光) */\n constructor(sim: DroneSim, cesium?: CesiumNS) {\n this.sim = sim\n const injected = cesium ?? (window as unknown as { Cesium?: CesiumNS }).Cesium\n if (!injected) {\n throw new Error('[drone-camera] CesiumDroneRig 需要 Cesium 命名空间(请先加载 Cesium.js)')\n }\n this.C = injected\n }\n\n // ───────────────────────────── 挂载 / 拆卸 ─────────────────────────────\n\n /**\n * 绑定新机体:重绑自转关节、挂灯珠发光 shader。旧机体的关节矩阵先复位。\n */\n attach(model: CesiumNS): void {\n this.detach()\n const C = this.C\n this.enabled = typeof model.getNode === 'function' && !!model.getNode('PROP_FrontLeft_Spin_Bone')\n if (!this.enabled) return\n this.modelRef = model\n\n // —— 自转关节 ——\n this.joints = []\n for (const position of ARM_POSITIONS) {\n const node = model.getNode(`PROP_${position}_Spin_Bone`)\n const ctrl = model.getNode(`CTRL_Prop_${position}_Spin`)\n // 自转节点 + 轴参照节点缺一不可:轴要从 CTRL 的出厂矩阵推\n if (!node || !ctrl) continue\n const axis = spinAxisFor(C, ctrl.originalMatrix)\n if (!axis) continue\n\n // —— 钟罩反向补偿:需要 CTRL/ROTOR/钟罩三个节点 ——\n const rotor = model.getNode(`ROTOR_${position}`)\n const bellNode = model.getNode(`MOTOR_${position}_RotorBell`)\n let bell: BellCompensation | null = null\n if (rotor && bellNode) {\n const chain = C.Matrix4.multiply(\n ctrl.originalMatrix,\n rotor.originalMatrix,\n new C.Matrix4(),\n )\n bell = {\n node: bellNode,\n original: bellNode.originalMatrix,\n chain,\n invChain: C.Matrix4.inverse(chain, new C.Matrix4()),\n inv: new C.Matrix4(),\n conj: new C.Matrix4(),\n kernel: new C.Matrix4(),\n product: new C.Matrix4(),\n spinning: false,\n }\n }\n\n this.joints.push({\n position,\n node,\n original: node.originalMatrix,\n trs: new C.Matrix4(),\n product: new C.Matrix4(),\n axis,\n angle: 0,\n spinning: false,\n bell,\n })\n }\n\n // —— 灯组:CustomShader 让模型自带的灯珠 Mesh 本体发光(与 three 版\"复用灯珠\n // 网格 + 自发光材质\"同构;Cesium 不支持单节点换材质,只能走 shader)。\n // 不挂任何自建灯珠/billboard —— 位置由灯珠几何自证 ——\n this.attachLightShader(model)\n }\n\n /**\n * 灯珠发光 shader:按 positionMC(GLB 原始 Y-up 坐标,实测见 probe-pmc-axis)判定\n * 片元是否落在灯珠网格内,是则把 uniform 颜色写进 emissive。\n * 灭灯 = uniform 为 0 = emissive 0 = 原贴图灯罩本色,与 three 版\"换自发光材质\"一致。\n * 每帧只改 uniform 值,shader 本体在 attach 时创建一次。\n */\n private attachLightShader(model: CesiumNS): void {\n const C = this.C\n const g = LED_GEO\n const uniform = () => ({ type: C.UniformType.VEC3, value: new C.Cartesian3(0, 0, 0) })\n const fmt = (c: readonly number[]) => `vec3(${c[0]}, ${c[1]}, ${c[2]})`\n this.lightShader = new C.CustomShader({\n lightingModel: C.LightingModel.PBR,\n uniforms: {\n u_statusL: uniform(),\n u_statusR: uniform(),\n u_batt0: uniform(),\n u_batt1: uniform(),\n u_batt2: uniform(),\n u_batt3: uniform(),\n u_aux: uniform(),\n },\n fragmentShaderText: `\n void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {\n vec3 p = fsInput.attributes.positionMC;\n vec3 e = vec3(0.0);\n float t = 0.0; // 0 = 灯区中心,1 = 灯区边缘(供亮度梯度用)\n if (distance(p, ${fmt(g.statusLeft.center)}) < ${g.statusLeft.radius}) { e = u_statusL; t = distance(p, ${fmt(g.statusLeft.center)}) / ${g.statusLeft.radius}; }\n else if (distance(p, ${fmt(g.statusRight.center)}) < ${g.statusRight.radius}) { e = u_statusR; t = distance(p, ${fmt(g.statusRight.center)}) / ${g.statusRight.radius}; }\n else if (distance(p, ${fmt(g.battery[0].center)}) < ${g.battery[0].radius}) { e = u_batt0; t = distance(p, ${fmt(g.battery[0].center)}) / ${g.battery[0].radius}; }\n else if (distance(p, ${fmt(g.battery[1].center)}) < ${g.battery[1].radius}) { e = u_batt1; t = distance(p, ${fmt(g.battery[1].center)}) / ${g.battery[1].radius}; }\n else if (distance(p, ${fmt(g.battery[2].center)}) < ${g.battery[2].radius}) { e = u_batt2; t = distance(p, ${fmt(g.battery[2].center)}) / ${g.battery[2].radius}; }\n else if (distance(p, ${fmt(g.battery[3].center)}) < ${g.battery[3].radius}) { e = u_batt3; t = distance(p, ${fmt(g.battery[3].center)}) / ${g.battery[3].radius}; }\n else if (distance(p, ${fmt(g.aux.center)}) < ${g.aux.radius}) { e = u_aux; t = distance(p, ${fmt(g.aux.center)}) / ${g.aux.radius}; }\n // 点亮时只留自发光:必须同时关掉底色(diffuse)与高光(specular)——\n // Cesium PBR 的最终色 = 底色×光照 + 高光 + emissive,灯珠网格底图是灰白灯罩\n // (且被太阳照亮),红色 emissive 叠上去就被冲成粉色(观感\"不够红\")。\n // three 版是整颗换成\"底黑 + emissive\"的自发光材质,这里等价地把两项清零。\n //\n // 亮度梯度(发光感的来源):边缘保持纯灯色(强度 1.0,深红不发粉),\n // 中心按 core² 过曝到 ~3.2 倍形成亮核 —— 均匀提亮只会整块变粉,\n // \"深色边缘 + 过曝亮核\"的径向梯度才是 LED 点亮的观感。\n if (any(greaterThan(e, vec3(0.0)))) {\n float core = 1.0 - clamp(t, 0.0, 1.0);\n material.diffuse = vec3(0.0);\n material.specular = vec3(0.0);\n material.emissive = e * (1.0 + 2.2 * core * core);\n }\n }`,\n })\n model.customShader = this.lightShader\n }\n\n /** 拆卸:复位关节矩阵(含钟罩补偿)、摘掉发光 shader 与光晕集合 */\n detach(): void {\n for (const j of this.joints) stopJoint(j)\n this.joints = []\n if (this.modelRef && this.lightShader) {\n // shader 是挂在整个 Model 上的,换机体/销毁时必须摘掉,\n // 否则新机体(无灯位语义)会沿用旧模型的灯位判据\n try {\n this.modelRef.customShader = undefined\n } catch {\n /* 旧版 Cesium 无此属性时静默 */\n }\n }\n this.lightShader = null\n this.modelRef = null\n this.enabled = false\n }\n\n /** 停止每帧同步并解除与 primitive 的绑定 */\n dispose(): void {\n this.detach()\n }\n\n // ───────────────────────────── 每帧更新 ─────────────────────────────\n\n /**\n * 每帧驱动。调用时机:adapter 已推进 sim 并写完 model.modelMatrix ——\n * 桨叶关节矩阵直接改模型内部节点,光晕位置复用同一份世界矩阵。\n */\n update(deltaSeconds: number): void {\n if (!this.enabled) return\n // 与 drone-lights 同款截断:标签页挂起恢复时 delta 大跳,不让灯语时间轴跳变\n const dt = Math.max(0, Math.min(deltaSeconds, 0.1))\n this.timeMs += dt * 1000\n this.updateProps(dt)\n this.updateLights()\n }\n\n /**\n * 桨叶自转。转速公式与 three 版 DroneRig.updateProps 完全一致:\n * 每分钟转速 = (3.8 + 负载×7) 转/秒,对角反转由 PROP_SPIN_SIGN 给出,\n * 负载表直接取 DroneSim 的四电机混控输出(悬停 ≈ 1,前飞时后桨快前桨慢)。\n */\n private updateProps(dt: number): void {\n const C = this.C\n const speeds = this.sim.motorSpeeds\n for (const j of this.joints) {\n const speed = Math.min(1.15, Math.max(0, speeds[j.position] ?? 0))\n if (speed <= 0.01) {\n // 停转:传 undefined 走 setter 的\"清除用户动画\"分支(恢复出厂矩阵 + 清 userAnimated,\n // 场景图不再每帧强制重建);写 originalMatrix 反而会一直置脏。\n // 钟罩必须一起复位,否则会留着上一帧的反向补偿量、看上去歪一个角。\n stopJoint(j)\n continue\n }\n j.angle =\n (j.angle + ((3.8 + speed * 7) * dt * PROP_SPIN_SIGN[j.position] * Math.PI * 2)) %\n (Math.PI * 2)\n const rot = C.Matrix3.fromQuaternion(C.Quaternion.fromAxisAngle(j.axis, j.angle))\n C.Matrix4.fromRotationTranslation(rot, C.Cartesian3.ZERO, j.trs)\n // 关节矩阵 = 出厂矩阵 × 绕电机轴的自转(后乘 = 关节局部系,与 three 版前乘父系轴等价)\n j.node.matrix = C.Matrix4.multiply(j.original, j.trs, j.product)\n j.spinning = true\n const bell = j.bell\n if (bell) {\n // K = M⁻¹ · Rot⁻¹ · M:把自转在钟罩父级坐标系里原样抵消,\n // 钟罩世界矩阵逐元素等于出厂值(不是\"看起来没动\",是数学上恒定)\n C.Matrix4.inverseTransformation(j.trs, bell.inv)\n C.Matrix4.multiply(bell.invChain, bell.inv, bell.conj)\n C.Matrix4.multiply(bell.conj, bell.chain, bell.kernel)\n bell.node.matrix = C.Matrix4.multiply(bell.kernel, bell.original, bell.product)\n bell.spinning = true\n }\n }\n }\n\n /** 灯光:灯珠 Mesh 本体发光(只写 shader uniform),灯语时序与 three 版同源 */\n private updateLights(): void {\n // —— 灯语时间轴(three 版 applyStatusPattern 的同款展开):状态灯 = [左色, 右色] ——\n const pattern = STATUS_PATTERNS[statusPatternFor(this.sim)]\n const loopMs = pattern.steps.reduce((sum, s) => sum + s.onMs + s.offMs, 0)\n let t = loopMs > 0 ? this.timeMs % loopMs : 0\n let colors = pattern.steps[0]?.colors ?? [0x000000]\n let statusOn = false\n for (const s of pattern.steps) {\n if (t < s.onMs) {\n colors = s.colors\n statusOn = true\n break\n }\n if (t < s.onMs + s.offMs) {\n colors = s.colors\n statusOn = false\n break\n }\n t -= s.onMs + s.offMs\n }\n const statusColors = [colors[0] ?? 0x000000, colors[1] ?? colors[0] ?? 0x000000] as const\n\n // —— 电量灯:按电量分级点亮 + 末颗低电闪烁(three 版 applyBattery 的 level 档) ——\n const plan = batteryLedPlan(this.sim.batteryPercent)\n const batteryStates = [0, 1, 2, 3].map(\n (i) =>\n i < plan.lit ||\n (plan.blinkIndex !== null && i === plan.blinkIndex && blinkPhase(this.timeMs, 240, 240)),\n )\n\n // —— 辅助灯:飞行中且离地 ≤8m 自动点亮(three 版 auto 档同款门控) ——\n const auxOn = this.sim.airborne && this.sim.position.y <= AUTO_LIGHT_MAX_AGL_METERS\n\n // —— 灯珠 Mesh 发光:把每盏灯的当前颜色写进 shader uniform(灭 = 0 = 原贴图本色) ——\n // ⚠️ hex 是 sRGB,Cesium 把 emissive uniform 当**线性**值用,输出前会做 sRGB 编码\n // (czm_gammaCorrect)—— 直接写 sRGB 数值,线性小分量(如 0xff3b30 的绿 0.23)经编码\n // 回到屏幕变成 ~0.5,红灯就被\"提亮\"成粉色。必须先做 sRGB→线性转换,与 three 版\n // ColorManagement.setHex 的往返等价,屏幕色才和 hex 一致。\n const u = this.lightShader?.uniforms\n if (u) {\n const setColor = (name: string, hex: number, on: boolean) => {\n const v = u[name].value as CesiumNS\n if (on) {\n v.x = srgbToLinear(((hex >> 16) & 0xff) / 255)\n v.y = srgbToLinear(((hex >> 8) & 0xff) / 255)\n v.z = srgbToLinear((hex & 0xff) / 255)\n } else {\n v.x = 0\n v.y = 0\n v.z = 0\n }\n }\n setColor('u_statusL', statusColors[0], statusOn)\n setColor('u_statusR', statusColors[1], statusOn)\n batteryStates.forEach((lit, i) => setColor(`u_batt${i}`, BATTERY_COLOR, lit))\n setColor('u_aux', AUX_COLOR, auxOn)\n }\n }\n}\n\n/** sRGB → 线性(IEC 61966-2-1 传递函数);Cesium 的 emissive/PBR 全程按线性计算 */\nfunction srgbToLinear(c: number): number {\n return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)\n}\n","/**\n * Cesium 航线表现层 + 场景内航点编辑器 —— 与 three 版 `mission-editor.ts`(拖动求解)\n * 和 `drone-world.ts`(航点标记/航线折线)合起来对应的那一层。\n *\n * ─── 为什么不直接复用 three 的 MissionEditor ────────────────────────────\n * three 的编辑器整层建立在 THREE.Raycaster × 场景图拾取之上(射线打光柱/地面环),\n * 而 Cesium 侧没有场景图可打:拾取走 `scene.pick`(实体)、求交走相机射线。\n * 交互约定则**逐条对齐**(拖拽=水平面内移动、Shift=改高、Alt=细调、双击地面插入、\n * Delete=删除、Esc=取消选中、3px 阈值区分点击与拖动、插入位用最小绕路法)——\n * 两个后端的\"航线编辑手感\"因此一致,用户换渲染器不需要重新学。\n *\n * ─── 拖拽数学:屏幕空间增量,而不是射线求交 ─────────────────────────────\n * Cesium 的 `pickEllipsoid` 只打椭球(有地形时会和可视地面不一致)、`pickPosition`\n * 依赖深度缓冲(无地形时不可用),所以这里不跟\"指针命中的世界点\"较劲,改用\n * **屏幕像素增量 → ENU 米增量**的换算:\n *\n * 1. 取拖拽起点处\"每像素多少米\":mpp = 2·dist·tan(fovy/2) / 画布高,\n * dist = 相机到该航点的距离(fovy 取 `camera.frustum.fovy`);\n * 2. 相机的右向(`rightWC`)与前向(`directionWC`)投影到 ENU 水平面(东/北),\n * 得到屏幕 x/y 轴在水平面上的单位方向;\n * 3. 屏幕增量 (dx, dy) 按 mpp 放大、沿这两个方向分解,得到 ENU 位移;\n * ENU 东 = sim +x,北 = sim −z(sim 的 z 向南为正)。\n *\n * 这样拖拽全程只依赖相机与画布,不依赖地形/拾取,任何视角下都不会跳点。\n * 改高度(Shift)更简单:dy·mpp 直接当米数(上拖加高)。\n *\n * ─── 事件拦截:容器捕获段 + stopPropagation ────────────────────────────\n * Cesium 的相机漫游监听挂在 canvas 上,且**注册得比本编辑器早**(viewer 先建)。\n * 若只在 canvas 上挂自己的 pointerdown,同目标监听器按注册序执行,Cesium 的\n * 先跑、相机已经开始转。所以本编辑器把指针事件挂在 `viewer.container` 的\n * **捕获段**:事件自窗口下行时先经过容器,命中航点要拖走时 `stopPropagation()`,\n * Cesium 的 canvas 监听器根本收不到这个事件,相机纹丝不动;点空白处则放行,\n * 观察者视角的平移/缩放完全不受影响。\n *\n * ─── 拾取:屏幕投影就近判定,而不是 scene.pick ────────────────────────────\n * 实测(Cesium 1.144)entity point 被 `scene.pick` 命中时返回的是批量图元包装对象\n * `{ primitive, collection, id }`(collection 里混着全部点图元、id 也不是实体 id),\n * 拿不到\"哪个航点\"。所以拾取改为几何法:把每个航点投到屏幕\n * (`cartesianToCanvasCoordinates`),指针落在标记 PICK_RADIUS_PX 内取最近的那个。\n * 好处:不依赖 Cesium 的拾取语义与深度剔除,合成事件与真实点击走同一条路,\n * 行为完全确定;代价是点被建筑挡住时也点得到(航点标记本就常显,可接受)。\n *\n * ─── 实体约定 ──────────────────────────────────────────────────────────\n * - 航点:`dc-mission-wp-<i>`(point + label,序号从 1 开始);\n * - 航线:`dc-mission-path`(polyline,PolylineGlow 材质);\n * - 返航点:`dc-mission-home`(point + label,常驻);\n * - 状态配色:默认青、选中琥珀、执行目标红、悬停放大一圈。\n * 位置只在\"航点签名变化\"时重写(拖动单点走 moveWaypoint 后签名也变),不逐帧刷。\n */\nimport { findMissionInsertIndex } from './drone-sim'\nimport type { DroneSim, MissionWaypoint } from './drone-sim'\nimport type { MissionDragMode, MissionEditLimits, MissionEditState } from './mission-editor'\n\n/** Cesium 由 /Cesium/Cesium.js 以全局脚本注入,这里只做一层薄封装取引用 */\ntype CesiumNS = Record<string, any>\n\n/** 航点实体 id 前缀(完整 id = 前缀 + 序号) */\nconst ENTITY_WAYPOINT_PREFIX = 'dc-mission-wp-'\n/** 航线折线实体 id */\nconst ENTITY_PATH_ID = 'dc-mission-path'\n/** 返航点实体 id */\nconst ENTITY_HOME_ID = 'dc-mission-home'\n\n/** 拖动判定阈值(像素):小于它算\"点击选中\"而不是拖动(与 three 版一致) */\nconst DRAG_THRESHOLD_PX = 3\n/** 航点拾取半径(像素):指针落在标记这么大的范围内算命中(就近取最近) */\nconst PICK_RADIUS_PX = 14\n/** 默认吸附步长(米);按住 Alt 时用微调步长(与 three 版一致) */\nconst SNAP_STEP = 1\nconst SNAP_STEP_FINE = 0.2\n\n/** 状态配色(与 dmp 面板的 CSS 变量同语言:accent 青 / warn 琥珀 / danger 红) */\nconst COLOR_WAYPOINT = '#38bdf8'\nconst COLOR_WAYPOINT_OUTLINE = '#04121f'\nconst COLOR_WAYPOINT_SELECTED = '#ffc53d'\nconst COLOR_WAYPOINT_ACTIVE = '#ff5f5f'\nconst COLOR_PATH = '#38bdf8'\nconst COLOR_HOME = '#3ddc97'\n/** 序号标签配色 */\nconst COLOR_LABEL = '#e9eff9'\nconst COLOR_LABEL_BG = 'rgba(4, 18, 31, 0.62)'\nconst COLOR_HOME_LABEL = '#bfe9d6'\n\n/**\n * 指针捕获的安全封装(与 mission-editor.ts 同名辅助函数同一形态,本地复制:\n * 本文件不能值导入 three 侧模块,否则把 three 拖进 Cesium 运行时依赖)。\n *\n * ⚠️ Chromium 起 setPointerCapture / releasePointerCapture 返回 **Promise**:\n * 指针已失效时是异步 reject —— 调用处的 try/catch 捕不到,会冒成未处理 rejection。\n */\nfunction capturePointer(canvas: HTMLCanvasElement, pointerId: number): void {\n try {\n const result: unknown = canvas.setPointerCapture(pointerId)\n if (result && typeof (result as Promise<void>).catch === 'function') {\n ;(result as Promise<void>).catch(() => {})\n }\n } catch {\n /* 指针已释放,同步抛错 */\n }\n}\n\nfunction releasePointer(canvas: HTMLCanvasElement, pointerId: number): void {\n try {\n const result: unknown = canvas.releasePointerCapture(pointerId)\n if (result && typeof (result as Promise<void>).catch === 'function') {\n ;(result as Promise<void>).catch(() => {})\n }\n } catch {\n /* 没捕获过,同步抛错 */\n }\n}\n\n/**\n * 编辑器依赖的宿主接口(由 CesiumDroneAdapter 实现):\n * 航点数据以仿真内核为权威源,编辑器只通过它读写,不含任何渲染细节。\n */\nexport interface CesiumMissionHost {\n /** 飞行内核:读 missionIndex(执行目标)等真值 */\n readonly sim: DroneSim\n /** 当前是否可编辑:观察者视角 + 航线未在执行 */\n canEdit(): boolean\n /** 航点列表(权威源 = 仿真内核) */\n getWaypoints(): MissionWaypoint[]\n /** 宿主查询当前选中航点索引(无选中返回 -1) */\n getSelected(): number\n /** 宿主同步选中航点(编辑器点击/面板列表双向联动) */\n setSelected(index: number): void\n /** 整表提交(新增 / 删除) */\n commitWaypoints(waypoints: Array<Partial<MissionWaypoint>>, selected: number): void\n /** 拖动中的单点实时更新,返回夹紧后的航点(限高限距由宿主/内核生效) */\n moveWaypoint(index: number, patch: Partial<MissionWaypoint>): MissionWaypoint | null\n /** 新航点的默认参数(高度与速度继承相邻航点) */\n newWaypointTemplate(index: number): Partial<MissionWaypoint>\n /** 取编辑限制(限高/限距/返航点,供夹紧与插入位计算) */\n getLimits(): MissionEditLimits\n /** sim 本地米坐标(x 东 / z 南 / y 为 AGL)→ ECEF 世界坐标 */\n localToWorld(x: number, y: number, z: number): CesiumNS\n /** ECEF 世界坐标 → sim 本地水平坐标(双击拾取用);打不中返回 null */\n worldToLocalXZ(world: CesiumNS): { x: number; z: number } | null\n /** 拖拽期间收起 / 恢复 Cesium 相机漫游输入 */\n setCameraInputEnabled(enabled: boolean): void\n /** 编辑日志出口(宿主接到 sim.pushEvent) */\n log(level: 'info' | 'warn', text: string): void\n}\n\n/** 拖动状态:屏幕空间增量法只需要起点坐标与起点时的航点值 */\ninterface DragState {\n index: number\n mode: MissionDragMode\n pointerId: number\n startClientX: number\n startClientY: number\n startX: number\n startZ: number\n startAltitude: number\n moved: boolean\n /** 起点处的东 / 北单位向量(世界坐标,拖动全程不变) */\n east: CesiumNS\n north: CesiumNS\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\nfunction snap(value: number, step: number): number {\n return Math.round(value / step) * step\n}\n\n/**\n * CSS 颜色串 → Cesium.Color。\n *\n * ⚠️ 必须转:实测 entity 图形属性(point.color / label.fillColor / outlineColor 等)\n * **不吃 CSS 字符串** —— 赋值后 Property 里存的是原样字符串,可视化器按 Color 取用时\n * 得到垃圾值,点会渲染成纯黑、标签直接不显示(红色对照点用 Color.RED 则完全正常)。\n * 所以所有颜色在进实体前统一过一遍本函数。\n */\nfunction cssColor(C: CesiumNS, css: string): CesiumNS {\n return C.Color.fromCssColorString(css)\n}\n\n/**\n * Cesium 航线编辑器:航点/航线实体渲染 + 场景内拖拽编辑。\n *\n * 用法:构造传入宿主 → 每帧 `update()` → 面板开关 `setEnabled()` → 卸载 `destroy()`。\n * 交互约定与 three 版 MissionEditor 一致,见文件头。\n */\nexport class CesiumMissionEditor {\n private readonly host: CesiumMissionHost\n private readonly C: CesiumNS\n private readonly viewer: CesiumNS\n /** 事件挂在 viewer.container 的捕获段(见文件头\"事件拦截\") */\n private readonly container: HTMLElement\n private readonly canvas: HTMLCanvasElement\n private enabled = false\n private hoverIndex = -1\n private drag: DragState | null = null\n /** 在空白处按下:抬手时若没拖动过就取消选中 */\n private emptyClick: { x: number; y: number } | null = null\n /** 上次同步过的航点签名:没变就不碰实体 */\n private waypointSignature = ''\n /** 上次同步过的返航点(起飞后才固定,之前可能还是 0,0) */\n private homeKey = ''\n /** 状态视觉(选中/执行目标)上次的值,变了才改实体 */\n private lastSelected = -2\n private lastActive = -2\n private lastHover = -2\n private disposed = false\n\n constructor(host: CesiumMissionHost, C: CesiumNS, viewer: CesiumNS) {\n this.host = host\n this.C = C\n this.viewer = viewer\n this.container = viewer.container as HTMLElement\n this.canvas = viewer.canvas as HTMLCanvasElement\n // 捕获段挂容器:先于 canvas 上的 Cesium 漫游监听器拿到事件,才能拦掉拖拽时的相机转动\n this.container.addEventListener('pointerdown', this.onPointerDown, { capture: true })\n this.container.addEventListener('pointermove', this.onPointerMove, { capture: true })\n this.container.addEventListener('pointerup', this.onPointerUp, { capture: true })\n this.container.addEventListener('pointercancel', this.onPointerUp, { capture: true })\n this.container.addEventListener('pointerleave', this.onPointerLeave, { capture: true })\n this.container.addEventListener('dblclick', this.onDoubleClick, { capture: true })\n window.addEventListener('keydown', this.onKeyDown)\n }\n\n // ───────────────────────────── 开关与状态 ─────────────────────────────\n\n /** 编辑模式是否开启(开启时点场景可加/拖航点) */\n get isEnabled(): boolean {\n return this.enabled\n }\n\n /** 开关编辑模式;关闭时取消拖动、归还相机输入、清悬停 */\n setEnabled(enabled: boolean): void {\n if (this.enabled === enabled) return\n this.enabled = enabled\n if (!enabled) {\n this.cancelDrag()\n this.hoverIndex = -1\n this.canvas.style.cursor = ''\n }\n }\n\n /** 编辑器对外状态快照(面板\"场景内编辑\"区块据此渲染) */\n getEditState(): MissionEditState {\n return {\n enabled: this.enabled,\n active: this.editable(),\n selectedIndex: this.host.getSelected(),\n hoverIndex: this.hoverIndex,\n dragging: this.drag !== null,\n mode: this.drag?.mode ?? null,\n }\n }\n\n private editable(): boolean {\n return this.enabled && this.host.canEdit()\n }\n\n // ───────────────────────────── 每帧同步 ─────────────────────────────\n\n /** 每帧调用:签名变了才重建实体位置,选中/执行目标变化才改配色 */\n update(): void {\n if (this.disposed) return\n this.syncEntities()\n this.syncStateVisuals()\n }\n\n /** 航点签名(坐标串)或返航点变了才动实体,避免逐帧重写 */\n private syncEntities(): void {\n const waypoints = this.host.getWaypoints()\n const signature = waypoints\n .map((wp) => `${wp.x.toFixed(2)},${wp.z.toFixed(2)},${wp.altitude.toFixed(2)}`)\n .join(';')\n const home = this.host.sim.homePosition\n const homeKey = `${home.x.toFixed(2)},${home.z.toFixed(2)}`\n if (signature === this.waypointSignature && homeKey === this.homeKey) return\n this.waypointSignature = signature\n this.homeKey = homeKey\n this.rebuildWaypointEntities(waypoints)\n this.rebuildPathEntity(waypoints)\n this.rebuildHomeEntity(home)\n // 实体换了,状态视觉的\"上次值\"作废,强制重刷一次\n this.lastSelected = -2\n this.lastActive = -2\n this.lastHover = -2\n }\n\n private rebuildWaypointEntities(waypoints: MissionWaypoint[]): void {\n // 先摘掉多出来的实体(航点被删)\n for (let index = waypoints.length; ; index += 1) {\n const stale = this.viewer.entities.getById(ENTITY_WAYPOINT_PREFIX + index)\n if (!stale) break\n this.viewer.entities.remove(stale)\n }\n waypoints.forEach((waypoint, index) => {\n const id = ENTITY_WAYPOINT_PREFIX + index\n const position = this.host.localToWorld(waypoint.x, waypoint.altitude, waypoint.z)\n const existing = this.viewer.entities.getById(id)\n if (existing) {\n existing.position = position\n return\n }\n this.viewer.entities.add({\n id,\n position,\n // point 而不是 billboard:零贴图依赖,且天然参与 scene.pick(点击选中靠它)\n point: {\n pixelSize: 9,\n color: cssColor(this.C, COLOR_WAYPOINT),\n outlineColor: cssColor(this.C, COLOR_WAYPOINT_OUTLINE),\n outlineWidth: 2,\n disableDepthTestDistance: Number.POSITIVE_INFINITY,\n },\n label: {\n text: String(index + 1),\n font: '12px sans-serif',\n pixelOffset: { x: 12, y: -12 },\n fillColor: cssColor(this.C, COLOR_LABEL),\n showBackground: true,\n backgroundColor: cssColor(this.C, COLOR_LABEL_BG),\n disableDepthTestDistance: Number.POSITIVE_INFINITY,\n },\n })\n })\n }\n\n private rebuildPathEntity(waypoints: MissionWaypoint[]): void {\n const existing = this.viewer.entities.getById(ENTITY_PATH_ID)\n if (waypoints.length < 2) {\n if (existing) this.viewer.entities.remove(existing)\n return\n }\n const positions = waypoints.map((wp) => this.host.localToWorld(wp.x, wp.altitude, wp.z))\n if (existing) {\n // 直接赋值数组即可:Cesium 的属性描述符会把它包成 ConstantProperty\n existing.polyline.positions = positions\n return\n }\n this.viewer.entities.add({\n id: ENTITY_PATH_ID,\n polyline: {\n positions,\n width: 2,\n // 发光材质:地球上一条淡青色的航线,和 dmp 面板 accent 色同语言\n material: new this.C.PolylineGlowMaterialProperty({\n glowPower: 0.25,\n color: cssColor(this.C, COLOR_PATH),\n }),\n disableDepthTestDistance: Number.POSITIVE_INFINITY,\n },\n })\n }\n\n private rebuildHomeEntity(home: { x: number; z: number }): void {\n const existing = this.viewer.entities.getById(ENTITY_HOME_ID)\n const position = this.host.localToWorld(home.x, 0, home.z)\n if (existing) {\n existing.position = position\n return\n }\n this.viewer.entities.add({\n id: ENTITY_HOME_ID,\n position,\n point: {\n pixelSize: 7,\n color: cssColor(this.C, COLOR_HOME),\n outlineColor: cssColor(this.C, COLOR_WAYPOINT_OUTLINE),\n outlineWidth: 2,\n disableDepthTestDistance: Number.POSITIVE_INFINITY,\n },\n label: {\n text: '返航点',\n font: '11px sans-serif',\n pixelOffset: { x: 10, y: -10 },\n fillColor: cssColor(this.C, COLOR_HOME_LABEL),\n showBackground: true,\n backgroundColor: cssColor(this.C, COLOR_LABEL_BG),\n disableDepthTestDistance: Number.POSITIVE_INFINITY,\n },\n })\n }\n\n /** 选中 / 执行目标 / 悬停 → 颜色与大小(只在变化时写实体) */\n private syncStateVisuals(): void {\n const selected = this.host.getSelected()\n // missionIndex = 当前目标航点;-1 = 无(未执行 / 已停止)\n const active = this.host.sim.missionStatus === 'idle' ? -1 : this.host.sim.missionIndex\n if (selected === this.lastSelected && active === this.lastActive && this.hoverIndex === this.lastHover) {\n return\n }\n this.lastSelected = selected\n this.lastActive = active\n this.lastHover = this.hoverIndex\n const count = this.host.getWaypoints().length\n for (let index = 0; index < count; index += 1) {\n const entity = this.viewer.entities.getById(ENTITY_WAYPOINT_PREFIX + index)\n if (!entity?.point) continue\n if (index === active) {\n entity.point.color = cssColor(this.C, COLOR_WAYPOINT_ACTIVE)\n entity.point.pixelSize = 12\n } else if (index === selected) {\n entity.point.color = cssColor(this.C, COLOR_WAYPOINT_SELECTED)\n entity.point.pixelSize = 12\n } else if (index === this.hoverIndex) {\n entity.point.color = cssColor(this.C, COLOR_WAYPOINT)\n entity.point.pixelSize = 11\n } else {\n entity.point.color = cssColor(this.C, COLOR_WAYPOINT)\n entity.point.pixelSize = 9\n }\n }\n }\n\n // ───────────────────────────── 显隐控制 ─────────────────────────────\n\n /** 航点标记 + 航线折线总显隐(由宿主的 setMissionVisible 转发) */\n setVisible(visible: boolean): void {\n const count = this.host.getWaypoints().length\n for (let index = 0; index < count; index += 1) {\n const entity = this.viewer.entities.getById(ENTITY_WAYPOINT_PREFIX + index)\n if (entity) entity.show = visible\n }\n const path = this.viewer.entities.getById(ENTITY_PATH_ID)\n if (path) path.show = visible\n const home = this.viewer.entities.getById(ENTITY_HOME_ID)\n if (home) home.show = visible\n }\n\n /** 航点标记显隐(不影响任务执行) */\n setWaypointsVisible(visible: boolean): void {\n const count = this.host.getWaypoints().length\n for (let index = 0; index < count; index += 1) {\n const entity = this.viewer.entities.getById(ENTITY_WAYPOINT_PREFIX + index)\n if (entity) entity.show = visible\n }\n }\n\n /** 航线折线显隐 */\n setPathVisible(visible: boolean): void {\n const path = this.viewer.entities.getById(ENTITY_PATH_ID)\n if (path) path.show = visible\n }\n\n // ───────────────────────────── 指针交互 ─────────────────────────────\n\n private readonly onPointerDown = (event: PointerEvent): void => {\n if (!this.editable() || event.button !== 0) return\n const index = this.pickWaypoint(event)\n if (index < 0) {\n // 空白处:交给 Cesium 漫游(不拦截),抬手没拖动就当取消选中\n this.emptyClick = { x: event.clientX, y: event.clientY }\n return\n }\n const waypoint = this.host.getWaypoints()[index]\n if (!waypoint) return\n this.emptyClick = null\n this.host.setSelected(index)\n this.drag = this.createDrag(event, index, waypoint)\n this.host.setCameraInputEnabled(false)\n this.canvas.style.cursor = event.shiftKey ? 'ns-resize' : 'grabbing'\n capturePointer(this.canvas, event.pointerId)\n // 命中航点要拖走:拦住事件,Canvas 上的 Cesium 漫游监听器收不到,相机不转\n event.stopPropagation()\n event.preventDefault()\n }\n\n private readonly onPointerMove = (event: PointerEvent): void => {\n if (!this.enabled) return\n const drag = this.drag\n if (!drag) {\n const hover = this.editable() ? this.pickWaypoint(event) : -1\n if (hover !== this.hoverIndex) {\n this.hoverIndex = hover\n this.canvas.style.cursor = hover >= 0 ? 'grab' : ''\n }\n return\n }\n if (!drag.moved) {\n const moved = Math.hypot(event.clientX - drag.startClientX, event.clientY - drag.startClientY)\n if (moved < DRAG_THRESHOLD_PX) return\n drag.moved = true\n }\n this.applyDrag(event, drag)\n }\n\n private readonly onPointerUp = (event: PointerEvent): void => {\n releasePointer(this.canvas, event.pointerId)\n const emptyClick = this.emptyClick\n this.emptyClick = null\n const drag = this.drag\n if (!drag) {\n if (\n emptyClick &&\n Math.hypot(event.clientX - emptyClick.x, event.clientY - emptyClick.y) < DRAG_THRESHOLD_PX\n ) {\n this.host.setSelected(-1)\n }\n return\n }\n this.drag = null\n this.host.setCameraInputEnabled(true)\n this.canvas.style.cursor = this.hoverIndex >= 0 ? 'grab' : ''\n if (!drag.moved) return\n const waypoint = this.host.getWaypoints()[drag.index]\n if (!waypoint) return\n this.host.log(\n 'info',\n `场景编辑:航点 ${drag.index + 1} 已更新为 E ${waypoint.x.toFixed(0)} / S ${waypoint.z.toFixed(0)} / 高度 ${waypoint.altitude.toFixed(1)} m`,\n )\n }\n\n private readonly onPointerLeave = (): void => {\n if (!this.drag && this.hoverIndex !== -1) {\n this.hoverIndex = -1\n this.canvas.style.cursor = ''\n }\n }\n\n /** 双击地面新增航点(双击已有航点不处理,避免误插一个重合点) */\n private readonly onDoubleClick = (event: MouseEvent): void => {\n if (!this.editable() || event.button !== 0) return\n if (this.pickWaypoint(event) >= 0) return\n const world = this.pickEllipsoid(event)\n const local = world ? this.host.worldToLocalXZ(world) : null\n if (!local) return\n const limits = this.host.getLimits()\n // 瞄到地平线附近时拾取点会跑到几公里外,先按限距拉回来(与 three 版同款夹紧)\n const offsetX = local.x - limits.homeX\n const offsetZ = local.z - limits.homeZ\n const distance = Math.hypot(offsetX, offsetZ)\n let x = local.x\n let z = local.z\n if (distance > limits.maxDistance) {\n const scale = limits.maxDistance / distance\n x = limits.homeX + offsetX * scale\n z = limits.homeZ + offsetZ * scale\n }\n x = snap(x, SNAP_STEP)\n z = snap(z, SNAP_STEP)\n\n const list = this.host.getWaypoints()\n const at = findMissionInsertIndex(list, x, z, limits.homeX, limits.homeZ)\n const template = this.host.newWaypointTemplate(at)\n const next: Array<Partial<MissionWaypoint>> = list.map((waypoint) => ({ ...waypoint }))\n next.splice(at, 0, { ...template, x, z })\n this.host.commitWaypoints(next, at)\n this.host.log('info', `场景编辑:在 E ${x.toFixed(0)} / S ${z.toFixed(0)} 新增航点 ${at + 1}(双击地面新增)`)\n // 拦住 Cesium 的双击默认行为(会把相机怼到拾取目标上)\n event.stopPropagation()\n event.preventDefault()\n }\n\n private readonly onKeyDown = (event: KeyboardEvent): void => {\n if (!this.enabled) return\n const target = event.target as HTMLElement | null\n if (target && ['INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName)) return\n if (event.key === 'Escape') {\n this.host.setSelected(-1)\n return\n }\n if (event.key !== 'Delete' && event.key !== 'Backspace') return\n const index = this.host.getSelected()\n if (index < 0) return\n // 浏览器默认会拿 Backspace 当\"后退\",先拦掉\n event.preventDefault()\n this.deleteWaypoint(index)\n }\n\n /** 删除一个航点(面板按钮与 Delete 键共用) */\n deleteWaypoint(index: number): boolean {\n if (!this.host.canEdit()) return false\n const list = this.host.getWaypoints()\n if (index < 0 || index >= list.length) return false\n const next = list.filter((_, i) => i !== index).map((waypoint) => ({ ...waypoint }))\n this.host.commitWaypoints(next, Math.min(index, next.length - 1))\n this.host.log('info', `场景编辑:已删除航点 ${index + 1}`)\n return true\n }\n\n // ───────────────────────────── 拖拽求解 ─────────────────────────────\n\n private createDrag(\n event: { clientX: number; clientY: number; shiftKey: boolean; pointerId: number },\n index: number,\n waypoint: MissionWaypoint,\n ): DragState {\n const C = this.C\n // 东 / 北单位向量:由 localToWorld 的两个微差推出(ENU 框架与高程基准都藏在宿主体内)\n const east = C.Cartesian3.subtract(\n this.host.localToWorld(1, 0, 0),\n this.host.localToWorld(0, 0, 0),\n new C.Cartesian3(),\n )\n const north = C.Cartesian3.subtract(\n this.host.localToWorld(0, 0, -1),\n this.host.localToWorld(0, 0, 0),\n new C.Cartesian3(),\n )\n return {\n index,\n mode: event.shiftKey ? 'altitude' : 'move',\n pointerId: event.pointerId,\n startClientX: event.clientX,\n startClientY: event.clientY,\n startX: waypoint.x,\n startZ: waypoint.z,\n startAltitude: waypoint.altitude,\n moved: false,\n east,\n north,\n }\n }\n\n private applyDrag(event: PointerEvent, drag: DragState): void {\n const step = event.altKey ? SNAP_STEP_FINE : SNAP_STEP\n const limits = this.host.getLimits()\n if (drag.mode === 'altitude') {\n // 改高度:屏幕纵向增量直接当米(上拖加高),不依赖任何求交\n const altitude = drag.startAltitude - (event.clientY - drag.startClientY) * this.metersPerPixel(drag)\n this.host.moveWaypoint(drag.index, {\n altitude: clamp(snap(altitude, step), 1, limits.maxAltitude),\n })\n return\n }\n const delta = this.screenDeltaToLocal(\n event.clientX - drag.startClientX,\n event.clientY - drag.startClientY,\n drag,\n )\n if (!delta) return\n this.host.moveWaypoint(drag.index, { x: snap(delta.x, step), z: snap(delta.z, step) })\n }\n\n /** 屏幕像素增量 → ENU 米增量(再换算成 sim 的 x 东 / z 南) */\n private screenDeltaToLocal(\n dx: number,\n dy: number,\n drag: DragState,\n ): { x: number; z: number } | null {\n const C = this.C\n const cam = this.viewer.camera\n const mpp = this.metersPerPixel(drag)\n // 相机右向 / 前向投影到 ENU 水平面:垂直俯视时水平分量退化,直接放弃本次更新\n const project = (vector: CesiumNS): { e: number; n: number } | null => {\n const e = C.Cartesian3.dot(vector, drag.east)\n const n = C.Cartesian3.dot(vector, drag.north)\n const length = Math.hypot(e, n)\n return length < 1e-6 ? null : { e: e / length, n: n / length }\n }\n const right = project(cam.rightWC)\n const forward = project(cam.directionWC)\n if (!right || !forward) return null\n // 屏幕右移 = 相机右向;屏幕上移(dy<0)= 相机前向\n const east = (right.e * dx - forward.e * dy) * mpp\n const northMeters = (right.n * dx - forward.n * dy) * mpp\n // ENU 东 = sim +x;北 = sim −z(sim 的 z 向南为正)\n return { x: drag.startX + east, z: drag.startZ - northMeters }\n }\n\n /** 拖拽起点处\"一个屏幕像素\"对应多少米 */\n private metersPerPixel(drag: DragState): number {\n const C = this.C\n const cam = this.viewer.camera\n const target = this.host.localToWorld(drag.startX, drag.startAltitude, drag.startZ)\n const distance = C.Cartesian3.distance(cam.positionWC, target)\n // 视锥在 distance 处的可见高度 = 2·tan(fovy/2)·distance;再按画布像素高度折算\n const height = 2 * Math.tan(cam.frustum.fovy / 2) * distance\n const rect = this.canvas.getBoundingClientRect()\n return rect.height > 0 ? height / rect.height : 0.2\n }\n\n // ───────────────────────────── 拾取 ─────────────────────────────\n\n /**\n * 屏幕坐标 → 命中的航点序号;没命中返回 -1。\n *\n * 几何法:每个航点投到屏幕,取指针 PICK_RADIUS_PX 内最近的那个(见文件头\"拾取\")。\n * 相机背后的航点投影会 undefined / 跑飞,一律跳过。\n */\n private pickWaypoint(event: { clientX: number; clientY: number }): number {\n const rect = this.canvas.getBoundingClientRect()\n if (rect.width <= 0 || rect.height <= 0) return -1\n const px = event.clientX - rect.left\n const py = event.clientY - rect.top\n const waypoints = this.host.getWaypoints()\n let best = -1\n let bestDistance = PICK_RADIUS_PX\n for (let index = 0; index < waypoints.length; index += 1) {\n const waypoint = waypoints[index]!\n const screen = this.viewer.scene.cartesianToCanvasCoordinates(\n this.host.localToWorld(waypoint.x, waypoint.altitude, waypoint.z),\n )\n if (!screen) continue\n const distance = Math.hypot(screen.x - px, screen.y - py)\n if (distance < bestDistance) {\n bestDistance = distance\n best = index\n }\n }\n return best\n }\n\n /** 屏幕坐标 → 椭球上的世界点(双击插入用;瞄到天空返回 undefined) */\n private pickEllipsoid(event: { clientX: number; clientY: number }): CesiumNS | undefined {\n return this.viewer.camera.pickEllipsoid(this.toWindowPosition(event))\n }\n\n /** client 坐标 → Cesium 的窗口坐标(相对画布左上角) */\n private toWindowPosition(event: { clientX: number; clientY: number }): CesiumNS {\n const rect = this.canvas.getBoundingClientRect()\n return new this.C.Cartesian2(event.clientX - rect.left, event.clientY - rect.top)\n }\n\n private cancelDrag(): void {\n if (!this.drag) return\n this.drag = null\n this.host.setCameraInputEnabled(true)\n }\n\n // ───────────────────────────── 销毁 ─────────────────────────────\n\n destroy(): void {\n if (this.disposed) return\n this.disposed = true\n this.container.removeEventListener('pointerdown', this.onPointerDown, { capture: true })\n this.container.removeEventListener('pointermove', this.onPointerMove, { capture: true })\n this.container.removeEventListener('pointerup', this.onPointerUp, { capture: true })\n this.container.removeEventListener('pointercancel', this.onPointerUp, { capture: true })\n this.container.removeEventListener('pointerleave', this.onPointerLeave, { capture: true })\n this.container.removeEventListener('dblclick', this.onDoubleClick, { capture: true })\n window.removeEventListener('keydown', this.onKeyDown)\n this.cancelDrag()\n // 实体逐个摘掉(entities.remove 会释放对应的 primitive)\n const waypointCount = this.host.getWaypoints().length\n for (let index = 0; index < waypointCount; index += 1) {\n const entity = this.viewer.entities.getById(ENTITY_WAYPOINT_PREFIX + index)\n if (entity) this.viewer.entities.remove(entity)\n }\n for (const id of [ENTITY_PATH_ID, ENTITY_HOME_ID]) {\n const entity = this.viewer.entities.getById(id)\n if (entity) this.viewer.entities.remove(entity)\n }\n this.waypointSignature = ''\n this.homeKey = ''\n }\n}\n","/**\n * Photogrammetry helpers that do not depend on Cesium, Three, the DOM, or Node.\n *\n * The capture side stores Cesium positions in ECEF and local positions in ENU.\n * This module keeps that convention explicit so a reconstruction worker can use\n * the same values as the browser without importing a renderer.\n */\n\nexport type Vector3 = [number, number, number]\nexport type Quaternion = [number, number, number, number]\n\nexport interface CameraPoseLike {\n position: Vector3\n rotation: Quaternion\n coordinateFrame: 'ecef' | 'local-enu' | 'project-local' | string\n}\n\nexport interface ColmapCameraPose {\n /** Camera centre in the reconstruction local frame, metres. */\n position: Vector3\n /** Camera-local-to-world quaternion in COLMAP/OpenCV axes, [x,y,z,w]. */\n rotation: Quaternion\n /** World-to-camera quaternion for COLMAP images.txt, [w,x,y,z]. */\n qvec: [number, number, number, number]\n /** World-to-camera translation for COLMAP images.txt: -R * position. */\n tvec: Vector3\n coordinateFrame: 'local-enu' | 'project-local'\n rotationConvention: 'camera-local-to-world; COLMAP +X right, +Y down, +Z forward'\n}\n\n/** WGS84 geodetic coordinate. Angles are degrees and altitude is metres. */\nexport interface GeodeticCoordinate {\n longitude: number\n latitude: number\n altitude?: number\n}\n\n/** WGS84 ellipsoid constants used by all ECEF/ENU conversions. */\nexport const WGS84 = {\n semiMajorAxis: 6378137,\n inverseFlattening: 298.257223563,\n get flattening(): number {\n return 1 / this.inverseFlattening\n },\n get eccentricitySquared(): number {\n const f = this.flattening\n return f * (2 - f)\n },\n} as const\n\nexport interface EcefEnuFrame {\n /** Origin expressed as the input geodetic coordinate. */\n readonly origin: GeodeticCoordinate\n /** Origin expressed in ECEF metres. */\n readonly ecefOrigin: Vector3\n /** Unit axes expressed in ECEF: east, north, up. */\n readonly east: Vector3\n readonly north: Vector3\n readonly up: Vector3\n /** Quaternion rotating ENU vectors into ECEF vectors, [x,y,z,w]. */\n readonly enuToEcefRotation: Quaternion\n}\n\nconst DEG = Math.PI / 180\nconst RAD = 180 / Math.PI\n\nfunction finite(value: number): boolean {\n return Number.isFinite(value)\n}\n\nfunction assertVector(value: Vector3, name: string): void {\n if (!value.every(finite)) throw new RangeError(`${name} must contain finite numbers`)\n}\n\nfunction normalizeVector(value: Vector3): Vector3 {\n const length = Math.hypot(value[0], value[1], value[2])\n if (!(length > 0) || !finite(length)) throw new RangeError('Cannot normalize a zero vector')\n return [value[0] / length, value[1] / length, value[2] / length]\n}\n\nfunction dot(a: Vector3, b: Vector3): number {\n return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]\n}\n\nfunction cross(a: Vector3, b: Vector3): Vector3 {\n return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]\n}\n\n/** Convert WGS84 longitude/latitude/altitude to Earth-centred Earth-fixed metres. */\nexport function geodeticToEcef(coordinate: GeodeticCoordinate): Vector3 {\n const { longitude, latitude } = coordinate\n const altitude = coordinate.altitude ?? 0\n if (![longitude, latitude, altitude].every(finite)) {\n throw new RangeError('Geodetic coordinates must contain finite numbers')\n }\n if (latitude < -90 || latitude > 90) throw new RangeError('Latitude must be in [-90, 90]')\n\n const lon = longitude * DEG\n const lat = latitude * DEG\n const sinLat = Math.sin(lat)\n const cosLat = Math.cos(lat)\n const radius = WGS84.semiMajorAxis / Math.sqrt(1 - WGS84.eccentricitySquared * sinLat * sinLat)\n return [\n (radius + altitude) * cosLat * Math.cos(lon),\n (radius + altitude) * cosLat * Math.sin(lon),\n (radius * (1 - WGS84.eccentricitySquared) + altitude) * sinLat,\n ]\n}\n\n/** Convert ECEF metres to WGS84 longitude/latitude/altitude. */\nexport function ecefToGeodetic(position: Vector3): GeodeticCoordinate {\n assertVector(position, 'ECEF position')\n const [x, y, z] = position\n const longitude = Math.atan2(y, x) * RAD\n const horizontal = Math.hypot(x, y)\n const a = WGS84.semiMajorAxis\n const e2 = WGS84.eccentricitySquared\n const b = a * Math.sqrt(1 - e2)\n\n if (x === 0 && y === 0 && z === 0) {\n throw new RangeError('The ECEF origin has no defined geodetic coordinate')\n }\n\n // The polar case has no meaningful longitude, but zero is a stable and useful\n // value for manifests. Bowring's initial latitude followed by a few Newton\n // iterations is accurate to sub-millimetres for normal drone altitudes.\n if (horizontal < 1e-12) {\n const latitude = Math.sign(z) * 90\n const altitude = Math.abs(z) - b\n return { longitude: 0, latitude, altitude }\n }\n\n const theta = Math.atan2(z * a, horizontal * b)\n const sinTheta = Math.sin(theta)\n const cosTheta = Math.cos(theta)\n let latitude = Math.atan2(\n z + (e2 * b * sinTheta * sinTheta * sinTheta) / (1 - e2),\n horizontal - e2 * a * cosTheta * cosTheta * cosTheta,\n )\n for (let i = 0; i < 8; i += 1) {\n const sinLat = Math.sin(latitude)\n const radius = a / Math.sqrt(1 - e2 * sinLat * sinLat)\n const altitude = horizontal / Math.cos(latitude) - radius\n const next = Math.atan2(z, horizontal * (1 - (e2 * radius) / (radius + altitude)))\n if (Math.abs(next - latitude) < 1e-13) {\n latitude = next\n break\n }\n latitude = next\n }\n const sinLat = Math.sin(latitude)\n const radius = a / Math.sqrt(1 - e2 * sinLat * sinLat)\n const altitude = horizontal / Math.cos(latitude) - radius\n return { longitude, latitude: latitude * RAD, altitude }\n}\n\n/** Build an ENU frame at a WGS84 geodetic origin. ENU is [east, north, up]. */\nexport function createEcefEnuFrame(origin: GeodeticCoordinate): EcefEnuFrame {\n if (![origin.longitude, origin.latitude, origin.altitude ?? 0].every(finite)) {\n throw new RangeError('ENU origin must contain finite numbers')\n }\n const lon = origin.longitude * DEG\n const lat = origin.latitude * DEG\n const sinLon = Math.sin(lon)\n const cosLon = Math.cos(lon)\n const sinLat = Math.sin(lat)\n const cosLat = Math.cos(lat)\n const east: Vector3 = [-sinLon, cosLon, 0]\n const north: Vector3 = [-sinLat * cosLon, -sinLat * sinLon, cosLat]\n const up: Vector3 = [cosLat * cosLon, cosLat * sinLon, sinLat]\n return {\n origin: { longitude: origin.longitude, latitude: origin.latitude, altitude: origin.altitude ?? 0 },\n ecefOrigin: geodeticToEcef(origin),\n east,\n north,\n up,\n enuToEcefRotation: quaternionFromMatrix3([east, north, up]),\n }\n}\n\n/** Convert an ECEF point to ENU metres relative to a frame. */\nexport function ecefToEnu(position: Vector3, frame: EcefEnuFrame): Vector3 {\n assertVector(position, 'ECEF position')\n const delta: Vector3 = [\n position[0] - frame.ecefOrigin[0],\n position[1] - frame.ecefOrigin[1],\n position[2] - frame.ecefOrigin[2],\n ]\n return [dot(delta, frame.east), dot(delta, frame.north), dot(delta, frame.up)]\n}\n\n/** Convert ENU metres relative to a frame to an ECEF point. */\nexport function enuToEcef(position: Vector3, frame: EcefEnuFrame): Vector3 {\n assertVector(position, 'ENU position')\n return [\n frame.ecefOrigin[0] + frame.east[0] * position[0] + frame.north[0] * position[1] + frame.up[0] * position[2],\n frame.ecefOrigin[1] + frame.east[1] * position[0] + frame.north[1] * position[1] + frame.up[1] * position[2],\n frame.ecefOrigin[2] + frame.east[2] * position[0] + frame.north[2] * position[1] + frame.up[2] * position[2],\n ]\n}\n\nfunction normalizeQuaternion(q: Quaternion): Quaternion {\n const length = Math.hypot(q[0], q[1], q[2], q[3])\n if (!(length > 0) || !finite(length)) throw new RangeError('Quaternion must be finite and non-zero')\n return [q[0] / length, q[1] / length, q[2] / length, q[3] / length]\n}\n\nfunction quaternionMultiply(a: Quaternion, b: Quaternion): Quaternion {\n return [\n a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],\n a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],\n a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],\n a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],\n ]\n}\n\nfunction quaternionConjugate(q: Quaternion): Quaternion {\n return [-q[0], -q[1], -q[2], q[3]]\n}\n\n/** Convert a column-basis 3×3 rotation matrix to an [x,y,z,w] quaternion. */\nfunction quaternionFromMatrix3(columns: [Vector3, Vector3, Vector3]): Quaternion {\n const [x, y, z] = columns\n const m00 = x[0], m01 = y[0], m02 = z[0]\n const m10 = x[1], m11 = y[1], m12 = z[1]\n const m20 = x[2], m21 = y[2], m22 = z[2]\n const trace = m00 + m11 + m22\n let q: Quaternion\n if (trace > 0) {\n const s = Math.sqrt(trace + 1) * 2\n q = [(m21 - m12) / s, (m02 - m20) / s, (m10 - m01) / s, s / 4]\n } else if (m00 > m11 && m00 > m22) {\n const s = Math.sqrt(1 + m00 - m11 - m22) * 2\n q = [s / 4, (m01 + m10) / s, (m02 + m20) / s, (m21 - m12) / s]\n } else if (m11 > m22) {\n const s = Math.sqrt(1 + m11 - m00 - m22) * 2\n q = [(m01 + m10) / s, s / 4, (m12 + m21) / s, (m02 - m20) / s]\n } else {\n const s = Math.sqrt(1 + m22 - m00 - m11) * 2\n q = [(m02 + m20) / s, (m12 + m21) / s, s / 4, (m10 - m01) / s]\n }\n return normalizeQuaternion(q)\n}\n\n/** Convert an ENU orientation quaternion to the same orientation in ECEF. */\nexport function enuQuaternionToEcef(rotation: Quaternion, frame: EcefEnuFrame): Quaternion {\n return normalizeQuaternion(quaternionMultiply(frame.enuToEcefRotation, normalizeQuaternion(rotation)))\n}\n\n/** Convert an ECEF orientation quaternion to the same orientation in ENU. */\nexport function ecefQuaternionToEnu(rotation: Quaternion, frame: EcefEnuFrame): Quaternion {\n return normalizeQuaternion(\n quaternionMultiply(quaternionConjugate(frame.enuToEcefRotation), normalizeQuaternion(rotation)),\n )\n}\n\n/**\n * Camera-local axis conventions used by common reconstruction/rendering tools.\n * `colmap` and `opencv` use +X right, +Y down, +Z forward. Three's camera uses\n * +X right, +Y up, -Z forward. Cesium's camera uses +X right, +Y up, -Z forward\n * for its view direction; Cesium HPR values still need their own heading/pitch\n * semantics and should not be written directly as a COLMAP quaternion.\n */\nexport type CameraAxisConvention = 'colmap' | 'opencv' | 'three' | 'cesium'\n\nexport interface CameraWorldBasis {\n /** Camera +X axis in the world (usually the view's right vector). */\n right: Vector3\n /** Camera visual up axis in the world. */\n up: Vector3\n /** Camera optical forward direction in the world. */\n forward: Vector3\n}\n\ntype Matrix3 = [Vector3, Vector3, Vector3]\n\nconst CAMERA_AXES: Record<CameraAxisConvention, Matrix3> = {\n colmap: [[1, 0, 0], [0, 1, 0], [0, 0, 1]],\n opencv: [[1, 0, 0], [0, 1, 0], [0, 0, 1]],\n three: [[1, 0, 0], [0, -1, 0], [0, 0, -1]],\n cesium: [[1, 0, 0], [0, -1, 0], [0, 0, -1]],\n}\n\n/**\n * Build a camera-local-to-world quaternion from rendered camera basis vectors.\n * This is useful for Cesium, where `directionWC`/`upWC` are available at the\n * exact frame that is copied to a PNG. The returned quaternion uses the chosen\n * camera convention and is safe to write to a manifest.\n */\nexport function cameraQuaternionFromBasis(\n basis: CameraWorldBasis,\n convention: CameraAxisConvention = 'three',\n): Quaternion {\n assertVector(basis.right, 'camera right')\n assertVector(basis.up, 'camera up')\n assertVector(basis.forward, 'camera forward')\n const right = normalizeVector(basis.right)\n const up = normalizeVector(basis.up)\n const forward = normalizeVector(basis.forward)\n const columns: Matrix3 =\n convention === 'colmap' || convention === 'opencv'\n ? [right, [-up[0], -up[1], -up[2]], forward]\n : [right, up, [-forward[0], -forward[1], -forward[2]]]\n return quaternionFromMatrix3(columns)\n}\n\nfunction transposeMultiply(a: Matrix3, b: Matrix3): Matrix3 {\n const result: Matrix3 = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]\n for (let c = 0; c < 3; c += 1) {\n const column = b[c]\n if (!column) continue\n result[c] = [dot(a[0] ?? [0, 0, 0], column), dot(a[1] ?? [0, 0, 0], column), dot(a[2] ?? [0, 0, 0], column)]\n }\n return result\n}\n\n/** Convert a camera pose quaternion while preserving its optical axis meaning. */\nexport function convertCameraQuaternion(\n rotation: Quaternion,\n from: CameraAxisConvention,\n to: CameraAxisConvention,\n): Quaternion {\n const source = CAMERA_AXES[from]\n const target = CAMERA_AXES[to]\n if (!source || !target) throw new RangeError(`Unknown camera axis convention: ${String(from)} → ${String(to)}`)\n if (from === to) return normalizeQuaternion(rotation)\n const targetToSource = transposeMultiply(source, target)\n return normalizeQuaternion(quaternionMultiply(normalizeQuaternion(rotation), quaternionFromMatrix3(targetToSource)))\n}\n\n/**\n * Convert a captured pose into the local camera convention expected by a\n * COLMAP/OpenCV reconstruction worker. This is the explicit ECEF→ENU and\n * camera-axis boundary; callers must not write Cesium HPR values directly to\n * a COLMAP pose file.\n */\nexport function cameraPoseToColmap(\n pose: CameraPoseLike,\n options: {\n enuFrame?: EcefEnuFrame\n sourceCameraConvention?: CameraAxisConvention\n } = {},\n): ColmapCameraPose {\n assertVector(pose.position, 'camera position')\n const sourceConvention = options.sourceCameraConvention ?? 'cesium'\n let position: Vector3\n let rotation = normalizeQuaternion(pose.rotation)\n if (pose.coordinateFrame === 'ecef') {\n if (!options.enuFrame) throw new RangeError('enuFrame is required to convert an ECEF camera pose')\n position = ecefToEnu(pose.position, options.enuFrame)\n rotation = ecefQuaternionToEnu(rotation, options.enuFrame)\n } else {\n if (pose.coordinateFrame !== 'local-enu' && pose.coordinateFrame !== 'project-local') {\n throw new RangeError(`Unsupported camera coordinate frame: ${pose.coordinateFrame}`)\n }\n position = [...pose.position]\n }\n const cameraToWorld = convertCameraQuaternion(rotation, sourceConvention, 'colmap')\n const worldToCamera = quaternionConjugate(cameraToWorld)\n const cameraCentre = quaternionMultiply(\n quaternionMultiply(worldToCamera, [position[0], position[1], position[2], 0]),\n cameraToWorld,\n )\n return {\n position,\n rotation: cameraToWorld,\n qvec: [worldToCamera[3], worldToCamera[0], worldToCamera[1], worldToCamera[2]],\n tvec: [-cameraCentre[0], -cameraCentre[1], -cameraCentre[2]],\n coordinateFrame: pose.coordinateFrame === 'project-local' ? 'project-local' : 'local-enu',\n rotationConvention: 'camera-local-to-world; COLMAP +X right, +Y down, +Z forward',\n }\n}\n\n/** Minimal 3D Tiles JSON shape used by validation and loading helpers. */\nexport interface TilesetNode {\n boundingVolume?: { box?: unknown; region?: unknown; sphere?: unknown }\n geometricError?: number\n refine?: 'ADD' | 'REPLACE' | string\n transform?: number[]\n content?: { uri?: string; url?: string; [key: string]: unknown }\n /** 3D Tiles 1.1 may attach more than one content payload to a node. */\n contents?: Array<{ uri?: string; url?: string; [key: string]: unknown }>\n children?: TilesetNode[]\n [key: string]: unknown\n}\n\nexport interface TilesetJson {\n asset?: { version?: string; [key: string]: unknown }\n geometricError?: number\n root?: TilesetNode\n properties?: Record<string, unknown>\n [key: string]: unknown\n}\n\nexport interface TilesetValidationStats {\n nodes: number\n contentUris: number\n boundingVolumes: number\n maxDepth: number\n}\n\nexport interface TilesetValidationResult {\n valid: boolean\n errors: string[]\n warnings: string[]\n stats: TilesetValidationStats\n}\n\nfunction finiteArray(value: unknown, length: number): value is number[] {\n return Array.isArray(value) && value.length === length && value.every((entry) => typeof entry === 'number' && finite(entry))\n}\n\nfunction validateBoundingVolume(volume: TilesetNode['boundingVolume'], path: string, result: TilesetValidationResult): void {\n if (!volume || typeof volume !== 'object') {\n result.errors.push(`${path}.boundingVolume is required`)\n return\n }\n const keys = ['box', 'region', 'sphere'].filter((key) => Object.prototype.hasOwnProperty.call(volume, key))\n if (keys.length !== 1) {\n result.errors.push(`${path}.boundingVolume must contain exactly one of box, region, sphere`)\n return\n }\n const key = keys[0]\n const lengths = { box: 12, region: 6, sphere: 4 }\n if (!finiteArray(volume[key as keyof typeof volume], lengths[key as keyof typeof lengths])) {\n result.errors.push(`${path}.boundingVolume.${key} must contain ${lengths[key as keyof typeof lengths]} finite numbers`)\n } else {\n result.stats.boundingVolumes += 1\n }\n}\n\n/** Validate the structural parts Cesium needs before attempting a tileset load. */\nexport function validateTileset(tileset: unknown): TilesetValidationResult {\n const result: TilesetValidationResult = {\n valid: false,\n errors: [],\n warnings: [],\n stats: { nodes: 0, contentUris: 0, boundingVolumes: 0, maxDepth: 0 },\n }\n if (!tileset || typeof tileset !== 'object') {\n result.errors.push('tileset must be an object')\n return result\n }\n const value = tileset as TilesetJson\n const version = value.asset?.version\n if (typeof version !== 'string' || !/^1(?:\\.0|\\.1)$/.test(version)) {\n result.errors.push('asset.version must be \"1.0\" or \"1.1\"')\n }\n if (typeof value.geometricError !== 'number' || !finite(value.geometricError) || value.geometricError < 0) {\n result.errors.push('geometricError must be a finite non-negative number')\n }\n if (!value.root || typeof value.root !== 'object' || Array.isArray(value.root)) {\n result.errors.push('root is required')\n } else {\n const walk = (node: TilesetNode, path: string, depth: number): void => {\n if (!node || typeof node !== 'object' || Array.isArray(node)) {\n result.errors.push(`${path} must be an object`)\n return\n }\n result.stats.nodes += 1\n result.stats.maxDepth = Math.max(result.stats.maxDepth, depth)\n if (typeof node.geometricError !== 'number' || !finite(node.geometricError) || node.geometricError < 0) {\n result.errors.push(`${path}.geometricError must be a finite non-negative number`)\n }\n validateBoundingVolume(node.boundingVolume, path, result)\n if (node.transform !== undefined && !finiteArray(node.transform, 16)) {\n result.errors.push(`${path}.transform must contain 16 finite numbers`)\n }\n const uri = node.content?.uri ?? node.content?.url\n if (uri !== undefined) {\n if (typeof uri !== 'string' || !uri.trim()) result.errors.push(`${path}.content.uri must be a non-empty string`)\n else result.stats.contentUris += 1\n }\n if (node.contents !== undefined) {\n if (!Array.isArray(node.contents)) {\n result.errors.push(`${path}.contents must be an array`)\n } else {\n node.contents.forEach((content, index) => {\n const contentUri = content?.uri ?? content?.url\n if (typeof contentUri !== 'string' || !contentUri.trim()) {\n result.errors.push(`${path}.contents[${index}].uri must be a non-empty string`)\n } else {\n result.stats.contentUris += 1\n }\n })\n }\n }\n if (node.children !== undefined) {\n if (!Array.isArray(node.children)) result.errors.push(`${path}.children must be an array`)\n else node.children.forEach((child, index) => walk(child, `${path}.children[${index}]`, depth + 1))\n }\n }\n walk(value.root, 'root', 0)\n if (result.stats.contentUris === 0) result.warnings.push('tileset has no content URI; it may contain only external children')\n }\n result.valid = result.errors.length === 0\n return result\n}\n\nexport interface CesiumTilesetLoader<T> {\n /** Adapter supplied by the host, typically Cesium3DTileset.fromUrl. */\n load(url: string): Promise<T> | T\n}\n\nexport interface CesiumTilesetLoadResult<T> {\n url: string\n tileset: T\n validation?: TilesetValidationResult\n}\n\n/**\n * Load a tileset through an injected Cesium adapter. No Cesium symbol is\n * imported or referenced, so this helper also works in workers and tests.\n */\nexport async function loadCesiumTileset<T>(\n url: string,\n loader: CesiumTilesetLoader<T> | ((url: string) => Promise<T> | T),\n options: { validate?: (url: string) => Promise<TilesetValidationResult> | TilesetValidationResult } = {},\n): Promise<CesiumTilesetLoadResult<T>> {\n if (typeof url !== 'string' || !url.trim()) throw new TypeError('A non-empty tileset URL is required')\n const validation = options.validate ? await options.validate(url) : undefined\n if (validation && !validation.valid) {\n throw new Error(`Invalid 3D Tiles tileset: ${validation.errors.join('; ')}`)\n }\n const load = typeof loader === 'function' ? loader : loader.load.bind(loader)\n return { url, tileset: await load(url), validation }\n}\n\n/** Verbose alias for applications that prefer the generic name. */\nexport const loadTilesetForCesium = loadCesiumTileset\n\nexport interface ReconstructionTaskPaths {\n captureDir: string\n imagesDir: string\n manifestPath: string\n validationPath: string\n reconstructionDir: string\n colmapDir: string\n sparseDir: string\n denseDir: string\n meshDir: string\n outputDir: string\n tilesetPath: string\n reportPath: string\n}\n\nexport interface ReconstructionTask {\n captureId: string\n createdAt: string\n paths: ReconstructionTaskPaths\n coordinateFrame: 'ecef' | 'local-enu' | 'project-local'\n status: 'created' | 'running' | 'succeeded' | 'failed'\n}\n\nexport interface ReconstructionReport {\n captureId: string\n status: 'succeeded' | 'failed' | 'partial'\n stage: 'validate-images' | 'feature-extraction' | 'matching' | 'sfm' | 'dense' | 'mesh' | 'tiles' | 'load'\n startedAt: string\n finishedAt?: string\n registeredImages?: number\n totalImages?: number\n tilesetUrl?: string\n errors: string[]\n warnings: string[]\n}\n\nfunction joinPath(...parts: string[]): string {\n const first = parts[0] ?? ''\n const absolute = /^[\\\\/]/.test(first)\n const joined = parts\n .filter((part) => part.length > 0)\n .join('/')\n .replace(/\\\\/g, '/')\n .replace(/\\/+/g, '/')\n const windowsDrive = /^[A-Za-z]:\\//.test(joined)\n if (absolute && !windowsDrive && !joined.startsWith('/')) return `/${joined}`\n return joined.replace(/\\/$/, '')\n}\n\n/** Create deterministic task directories without touching the filesystem. */\nexport function createReconstructionTask(\n dataRoot: string,\n captureId: string,\n options: { coordinateFrame?: ReconstructionTask['coordinateFrame']; createdAt?: string } = {},\n): ReconstructionTask {\n if (!captureId || /[\\\\/]/.test(captureId)) throw new TypeError('captureId must be a non-empty path segment')\n const captureDir = joinPath(dataRoot, 'captures', captureId)\n const reconstructionDir = joinPath(dataRoot, 'reconstruction', captureId)\n const outputDir = joinPath(dataRoot, 'outputs', captureId)\n return {\n captureId,\n createdAt: options.createdAt ?? new Date().toISOString(),\n coordinateFrame: options.coordinateFrame ?? 'local-enu',\n status: 'created',\n paths: {\n captureDir,\n imagesDir: joinPath(captureDir, 'images'),\n manifestPath: joinPath(captureDir, 'manifest.json'),\n validationPath: joinPath(captureDir, 'image-validation.json'),\n reconstructionDir,\n colmapDir: joinPath(reconstructionDir, 'colmap'),\n sparseDir: joinPath(reconstructionDir, 'sparse'),\n denseDir: joinPath(reconstructionDir, 'dense'),\n meshDir: joinPath(reconstructionDir, 'mesh'),\n outputDir,\n tilesetPath: joinPath(outputDir, 'tileset.json'),\n reportPath: joinPath(reconstructionDir, 'reconstruction-report.json'),\n },\n }\n}\n\n/** Basic consistency checks for a reconstruction report loaded from disk. */\nexport function validateReconstructionReport(report: unknown): string[] {\n const errors: string[] = []\n if (!report || typeof report !== 'object') return ['report must be an object']\n const value = report as Partial<ReconstructionReport>\n if (!value.captureId || typeof value.captureId !== 'string') errors.push('captureId is required')\n if (!['succeeded', 'failed', 'partial'].includes(value.status ?? '')) errors.push('status is invalid')\n if (!value.startedAt || typeof value.startedAt !== 'string') errors.push('startedAt is required')\n if (!Array.isArray(value.errors)) errors.push('errors must be an array')\n if (!Array.isArray(value.warnings)) errors.push('warnings must be an array')\n if (value.registeredImages !== undefined && (!Number.isInteger(value.registeredImages) || value.registeredImages < 0)) {\n errors.push('registeredImages must be a non-negative integer')\n }\n if (value.totalImages !== undefined && (!Number.isInteger(value.totalImages) || value.totalImages < 0)) {\n errors.push('totalImages must be a non-negative integer')\n }\n if (\n value.registeredImages !== undefined &&\n value.totalImages !== undefined &&\n value.registeredImages > value.totalImages\n ) {\n errors.push('registeredImages cannot exceed totalImages')\n }\n return errors\n}\n","/**\n * Cesium 版飞行适配器 —— 用 Cesium 自己的实体系统渲染机体,飞行逻辑仍走库里的 DroneSim。\n *\n * 与 three 版的 DroneFly 是同一组接口(DronePanelHost + MissionPanelHost)的两种实现:\n * 控制面板 / 航线面板 / 鹰眼图 / 键盘完全不区分后端,插到哪个适配器上就控制哪台飞机,\n * 面板代码一行都不用改。两端各带 `engine` 判别字段('three' / 'cesium'),\n * 由 `createDroneControl({ engine })` 统一装配(见 drone-engine.ts)。\n *\n * 模块划分(与 three 侧一一对应):\n * - 本文件 —— 适配器:坐标换轴、每帧写 modelMatrix、相机接管、拍照/录像、航线任务 API;\n * - drone-cesium-rig —— 机体表现层:桨叶自转 + 钟罩反向补偿 + 机体灯光;\n * - drone-cesium-mission —— 航线表现层:航点标记 / 航线折线 / 场景内拖拽编辑。\n *\n * ─── 坐标系换算(全部风险都在这里,改之前先读完) ───────────────────────────\n *\n * DroneSim 世界系 Cesium ENU 系 Cesium 世界系\n * x 向东 ──▶ x 向东\n * y 向上(AGL) ──▶ z 向上 ──▶ enuToFixed(锚点) × ENU\n * z 向南 ──▶ y 向北(= -z)\n *\n * · 轴序不是简单搬运:x/z 要换轴,且南向取负 —— 漏掉这一步无人机会沿南北轴反向飞。\n * · 航向:sim.heading 是罗盘角(正北 0°、顺时针增加),与 Cesium HeadingPitchRoll 同语义,直接换算;\n * · 俯仰:Cesium 的 HeadingPitchRoll 是同一个旋转,但参照轴相反 —— **模型**前向是局部 -x、\n * **相机**前向是局部 +x,所以模型 tiltPitch(正=抬头)要取负、相机 gimbalPitch(正=抬头)直传;\n * · 横滚:两者都以\"右压坡\"为正,同号。\n *\n * ─── 高度基准 ──────────────────────────────────────────────────────────\n * sim.position.y 是相对起飞点的高度(AGL),所以锚点必须落在真实地形面上:\n * 构造时用 sampleTerrainMostDetailed 采样锚点地形高度,否则无人机会沉进地里或浮在半空。\n *\n * ─── 机体加载:GLB 净化(为什么需要) ───────────────────────────────────\n * 真机 GLB(DJI Mini 4 Pro 导出)把 `KHR_lights_punctual` 写进了 **extensionsRequired**,\n * 而 Cesium 不支持该扩展 → `Model.fromGltfAsync` 直接以\n * \"Unsupported glTF Extension: KHR_lights_punctual\" 拒载(整个机体不出来,且异常被吞)。\n * 处理方式**不修改磁盘上的资产**(shared asset 被改会让 three 侧的 GLTFLoader 拿到\n * 半剥离的扩展引用而崩),而是在加载期做内存净化:\n * fetch → 复制 ArrayBuffer → 只在 JSON chunk 里摘掉 extensionsRequired 里的该扩展\n * → Blob URL 交给 Cesium\n * 其余(extensionsUsed / lights 数组 / 节点引用)原样保留 —— 实测摘掉 required 一项\n * Cesium 即通过,three 侧读到的仍是完整文件。可用 `stripRequiredExtensions` 覆盖清单。\n */\nimport { DroneSim, NEUTRAL_STICK, buildMissionExport, exportMissionText } from './drone-sim'\nimport type {\n ChecklistItem,\n DroneSnapshot,\n FlightMode,\n MissionConfig,\n MissionExport,\n MissionExportFormat,\n MissionSnapshot,\n MissionWaypoint,\n StickState,\n} from './drone-sim'\nimport type { DronePanelHost, MissionPanelHost } from './drone-control'\n// 仅类型:`CameraMode` / `PhotoShot` 定义在 three 装配层里,用 `import type` 引,\n// 编译期即被擦除,不会把 three 拖进 Cesium 路径的运行时依赖。\nimport type { CameraMode, PhotoShot } from './drone-fly'\n// 同样是纯类型:采集模块只声明契约(不引实现),编译期即被擦除,\n// 于是 drone-capture 与两套渲染后端之间不会形成运行时循环依赖。\nimport type { CaptureCameraState, CaptureViewOverride } from './drone-capture'\nimport { CesiumDroneRig } from './drone-cesium-rig'\nimport { CesiumMissionEditor } from './drone-cesium-mission'\nimport type { CesiumMissionHost } from './drone-cesium-mission'\nimport type { MissionEditLimits, MissionEditState } from './mission-editor'\nimport { cameraQuaternionFromBasis } from './drone-photogrammetry'\n\n/** Cesium 由 /Cesium/Cesium.js 以全局脚本注入,这里只做一层薄封装取引用 */\ntype CesiumNS = Record<string, any>\n\n/** 可选机体规格。scale 把模型最大边归一到 2.4 m(与 three 版沙盒同尺寸),lift 让模型底面对齐放置点 */\nexport interface DroneModelSpec {\n key: string\n label: string\n url: string\n scale: number\n /** 模型自身前向与 ENU 北向的夹角修正(度),由实测定,见文件尾注释 */\n yawOffsetDeg: number\n /** 模型原点到底面的距离(米),用于让机腹贴地而不是原点贴地 */\n lift: number\n /**\n * 云台相机沿光轴前移的距离(米,自机体原点起算)。\n * 离线按 GLB 顶点实测「光轴走廊内壳体(含云台壳)最前点 + 2cm 余量」得出:\n * 相机必须探出壳体,否则 GLB 材质多为双面,在壳内会把机身内壁拍进画面。\n * 桨叶/机臂不算在内 —— FPV 边缘露出桨尖是真机也有的现象。\n */\n gimbalOffsetM: number\n /** 云台相机相对机体原点的高度(米):镜头玻璃的实测中线高度 */\n gimbalLiftM: number\n}\n\n/** 目标最大边尺寸(米)。与 three 版 normalizeModel 保持一致,换渲染器不换体量感 */\nconst TARGET_MAX_EDGE_M = 2.4\n\n/**\n * 机体目录。scale / lift / gimbal* 由 GLB 包围盒与顶点实测得出(见下方注释里的原始数据):\n * - DJI Mini 4 Pro :31.758 × 9.959 × 25.236,min y = -5.034;云台镜头玻璃在机体系 z -0.590 m、y -0.03 m,壳体最前 -0.62 m\n * - CesiumDrone : 3.964 × 1.120 × 4.668,min y = -0.171;镜头在 z +1.127 m、y +0.19 m\n * (两机型在 glTF 里的机头方向相反:DJI 朝 -z、CesiumDrone 朝 +z —— 运行时偏移\n * 一律沿「飞行方向」施加,与模型自身轴向无关,所以这里只存距离不存符号)\n */\nexport const DRONE_MODELS: DroneModelSpec[] = [\n {\n key: 'dji',\n label: 'DJI Mini 4 Pro',\n url: '/models/dji_mini4_pro.glb',\n scale: TARGET_MAX_EDGE_M / 31.758,\n // Blender 导出的 GLB 前向(+Z)与 Cesium HPR 的前向约定差 90°,实测 heading=0 时机头朝西,+90 拉回正北\n yawOffsetDeg: 90,\n lift: 5.034 * (TARGET_MAX_EDGE_M / 31.758),\n gimbalOffsetM: 0.64,\n gimbalLiftM: -0.03,\n },\n {\n key: 'cesium',\n label: 'Cesium 默认无人机',\n url: '/Cesium/SampleData/models/CesiumDrone/CesiumDrone.glb',\n scale: TARGET_MAX_EDGE_M / 4.668,\n yawOffsetDeg: 0,\n lift: 0.171 * (TARGET_MAX_EDGE_M / 4.668),\n gimbalOffsetM: 1.15,\n gimbalLiftM: 0.19,\n },\n]\n\nexport interface CesiumDroneOptions {\n /** Cesium.Viewer 实例 */\n viewer: CesiumNS\n /**\n * Cesium 命名空间。缺省取 `window.Cesium`(配合 `loadCesiumScript()` 或页面里自引脚本);\n * 传进来可让同一页面挂多个互不干扰的 Cesium 版本 / 便于测试注入。\n */\n cesium?: CesiumNS\n /** 沙盒原点经纬度(度);高度缺省时自动采样地形 */\n lon?: number\n lat?: number\n /** 锚点海拔(米);不传则用 sampleTerrainMostDetailed 采样 */\n height?: number\n /** 初始机体下标(见 DRONE_MODELS),默认 0 */\n modelIndex?: number\n /** 起降点离地额外抬升(米),避免机腹与地形 z-fighting,默认 0.05 */\n groundClearance?: number\n /** 视角跟随距离(米),follow 模式用(相机在无人机正后方),默认 7.5(与 three 版一致) */\n followRange?: number\n /**\n * 加载期从 GLB 的 `extensionsRequired` 里摘掉的扩展清单\n * (见 DEFAULT_UNSUPPORTED_REQUIRED_EXTENSIONS,默认 ['KHR_lights_punctual']);\n * 传 `[]` 关闭净化,直接按原 URL 加载。\n */\n stripRequiredExtensions?: readonly string[]\n}\n\n/** 飞行中每帧推进仿真用的最大步长(秒):切标签页回来时防止一次跳太远 */\nconst MAX_STEP_SECONDS = 0.1\n\n// ── 跟随机位:与 three 版 updateCamera 的 FOLLOW_DISTANCE / FOLLOW_HEIGHT 同值 ──\n/** 相机在无人机正后方的水平距离(米) */\nconst FOLLOW_DISTANCE = 7.5\n/** 相机相对机体的高度(米) */\nconst FOLLOW_HEIGHT = 2.8\n/** 瞄准点相对机体中心的抬升(米):机身整体进画面,而不是对着机腹 */\nconst FOLLOW_AIM_LIFT = 0.6\n\n/** 等待 Cesium 取景瓦片稳定的最长时间;到时使用最近渲染帧而不是丢弃整张 */\nconst PHOTO_TIMEOUT_MS = 5000\n/** track.requestFrame 不可用时的兜底采样帧率 */\nconst RECORD_FPS = 30\nconst RECORD_BITRATE = 8_000_000\nconst RECORD_TIMESLICE_MS = 500\nconst RECORD_STOP_TIMEOUT_MS = 1500\n\n/** glTF 容器里的两个 chunk 类型 */\nconst CHUNK_JSON = 0x4e4f534a\nconst CHUNK_BIN = 0x004e4942\n\n/**\n * Cesium 不支持、但在真机 GLB 里被声明为 **required** 的 glTF 扩展。\n * 出现在 extensionsRequired 里会被 `Model.fromGltfAsync` 直接拒载\n * (\"Unsupported glTF Extension: ...\"),需要加载期摘掉。\n */\nexport const DEFAULT_UNSUPPORTED_REQUIRED_EXTENSIONS = ['KHR_lights_punctual']\n\n/**\n * 在内存里摘掉 JSON chunk 中 `extensionsRequired` 里的指定扩展,返回新的 GLB ArrayBuffer。\n *\n * 只动 JSON chunk 的这一个字段:MAGIC / version / BIN chunk 原样搬过去,\n * `extensionsUsed`、扩展本体(如 KHR_lights_punctual.lights)与节点上的扩展引用全部保留 ——\n * 于是 three 侧读同一份文件时行为完全不变(实测:摘 required 一项 Cesium 即通过,\n * 而连同 lights 数组一起删会让 three 的 GLTFLightsExtension 读到悬空引用而抛错)。\n *\n * @returns 新的 ArrayBuffer;若该文件本来就没有需要摘的扩展,返回 null(调用方继续用原 URL)\n */\nexport function stripRequiredExtensions(\n buffer: ArrayBuffer,\n strip: readonly string[] = DEFAULT_UNSUPPORTED_REQUIRED_EXTENSIONS,\n): ArrayBuffer | null {\n if (strip.length === 0 || buffer.byteLength < 20) return null\n const view = new DataView(buffer)\n if (view.getUint32(0, true) !== 0x46546c67) return null // 'glTF'\n if (view.getUint32(4, true) !== 2) return null\n\n // 走一遍 chunk 头,找出 JSON / BIN chunk 的位置与长度\n const total = view.getUint32(8, true)\n let jsonStart = -1\n let jsonLength = 0\n let binStart = -1\n let binLength = 0\n let cursor = 12\n const limit = Math.min(total, buffer.byteLength)\n while (cursor + 8 <= limit) {\n const length = view.getUint32(cursor, true)\n const type = view.getUint32(cursor + 4, true)\n const body = cursor + 8\n if (body + length > limit) break\n if (type === CHUNK_JSON && jsonStart < 0) {\n jsonStart = body\n jsonLength = length\n } else if (type === CHUNK_BIN && binStart < 0) {\n binStart = body\n binLength = length\n }\n cursor = body + length\n }\n if (jsonStart < 0) return null\n\n const jsonText = new TextDecoder('utf-8').decode(new Uint8Array(buffer, jsonStart, jsonLength))\n let json: { extensionsRequired?: string[]; [key: string]: unknown }\n try {\n json = JSON.parse(jsonText)\n } catch {\n return null\n }\n const required = json.extensionsRequired\n if (!Array.isArray(required)) return null\n const kept = required.filter((name) => !strip.includes(name))\n if (kept.length === required.length) return null // 无需改动 → 走原 URL,不复制\n\n if (kept.length > 0) json.extensionsRequired = kept\n else delete json.extensionsRequired\n\n // 重打包:JSON chunk 按 glTF 规范用空格(0x20)补齐到 4 字节对齐\n const encoded = new TextEncoder().encode(JSON.stringify(json))\n const paddedLength = (encoded.byteLength + 3) & ~3\n const binTail = binStart >= 0 ? binLength : 0\n const byteLength = 12 + 8 + paddedLength + (binTail ? 8 + binTail : 0)\n const out = new Uint8Array(byteLength)\n const outView = new DataView(out.buffer)\n outView.setUint32(0, 0x46546c67, true)\n outView.setUint32(4, 2, true)\n outView.setUint32(8, byteLength, true)\n outView.setUint32(12, paddedLength, true)\n outView.setUint32(16, CHUNK_JSON, true)\n out.fill(0x20, 20, 20 + paddedLength)\n out.set(encoded, 20)\n if (binTail) {\n const binChunk = 20 + paddedLength\n outView.setUint32(binChunk, binTail, true)\n outView.setUint32(binChunk + 4, CHUNK_BIN, true)\n out.set(new Uint8Array(buffer, binStart!, binTail), binChunk + 8)\n }\n return out.buffer\n}\n\nlet cesiumScriptPromise: Promise<void> | null = null\n\n/**\n * 按需注入 Cesium 全局脚本(默认 `/Cesium/Cesium.js`),幂等且并发安全。\n *\n * 库不打包 Cesium(它自带 WebGL 内核与 Workers,体量与构建方式都不适合打进库),\n * 由使用方把 Cesium 静态资源放到自有目录,再调本函数或在页面里自己引 script。\n * 脚本就绪后 `window.Cesium` 可用,直接传给 `createDroneControl({ engine: 'cesium', ... })`。\n */\nexport function loadCesiumScript(src = '/Cesium/Cesium.js'): Promise<void> {\n if (typeof window === 'undefined') {\n return Promise.reject(new Error('[drone-camera] loadCesiumScript 只能在浏览器里调用'))\n }\n const existing = (window as unknown as { Cesium?: CesiumNS }).Cesium\n if (existing) return Promise.resolve()\n if (cesiumScriptPromise) return cesiumScriptPromise\n cesiumScriptPromise = new Promise<void>((resolve, reject) => {\n const script = document.createElement('script')\n script.src = src\n script.onload = () => resolve()\n script.onerror = () => {\n cesiumScriptPromise = null\n reject(new Error(`[drone-camera] Cesium 静态资源加载失败:${src}`))\n }\n document.head.appendChild(script)\n })\n return cesiumScriptPromise\n}\n\nexport class CesiumDroneAdapter implements DronePanelHost, MissionPanelHost, CesiumMissionHost {\n readonly sim = new DroneSim()\n /** 引擎判别字段:与 DroneFly 的 `engine = 'three'` 成对(见 drone-engine.ts) */\n readonly engine = 'cesium' as const\n /** 当前视角模式;orbit = 交给 Cesium 自由漫游,follow / fpv 由本适配器每帧驱动相机 */\n cameraMode: CameraMode = 'orbit'\n\n private readonly C: CesiumNS\n private readonly viewer: CesiumNS\n private readonly enuToFixed: CesiumNS\n private readonly origin: { lon: number; lat: number; height: number }\n private readonly groundClearance: number\n private readonly followRange: number\n /** 加载期净化时的扩展剥离清单(见 stripRequiredExtensions) */\n private readonly stripExtensions: readonly string[]\n /** 当前机体若走了净化路径,记下 Blob URL 以便销毁时回收 */\n private sanitizedBlobUrl: string | null = null\n\n /** 场景里当前挂载的机体 —— 直接是 Cesium 的 Model primitive(不走 Entity,见 loadModel 注释) */\n private model: CesiumNS | null = null\n /** 机体表现层:桨叶自转 + 灯光(见 drone-cesium-rig 文件头) */\n private readonly rig: CesiumDroneRig\n /** 航线表现层 + 场景内编辑:航点标记 / 航线折线 / 拖拽(见 drone-cesium-mission 文件头) */\n private missionEditor: CesiumMissionEditor | null = null\n /** 模型加载序号:切换机体时作废仍在途的旧请求,避免晚到的结果盖掉新机体 */\n private loadToken = 0\n /** 复用的 4×4 世界矩阵(每帧重填,避免逐帧分配) */\n private readonly worldMatrix: CesiumNS\n private modelIndex: number\n private armFoldTarget = 0\n // ── 航线任务状态(与 three 版 DroneFly 同字段同语义:面板只读这些) ──\n /** 场景内编辑开关(面板勾选;\"当前确实可编辑\"另看 isOrbitView + 任务状态,见 canMissionSceneEdit) */\n private missionEditEnabled = false\n /** 场景/列表双向联动的选中航点索引 */\n private missionEditSelected = -1\n /** 航线总显隐 / 航点标记 / 航线折线(互不影响,与 three 版三个开关一致) */\n private missionVisible = true\n private missionMarkersVisible = true\n private missionPathVisible = true\n private recorder: MediaRecorder | null = null\n private recordChunks: Blob[] = []\n /** 录制专用离屏画布的上下文:每帧只往里搬一帧云台取景,主画布上的用户视角永远不会混进录像 */\n private recordContext: CanvasRenderingContext2D | null = null\n /** 手动抓帧轨道(captureStream(0)):录到什么由取景帧决定 */\n private captureTrack: CanvasCaptureMediaStreamTrack | null = null\n private recordStream: MediaStream | null = null\n /** 待取景的拍照请求:取景帧渲染完同任务内拷屏(见 captureGimbalFrame) */\n private photoPending: {\n resolve: (shot: PhotoShot | null) => void\n timer: number\n settledFrames: number\n forceCapture: boolean\n /** 在取景相机真正渲染前锁存,保证 PNG 与位姿来自同一帧。 */\n captureCamera: CaptureCameraState | null\n } | null = null\n /** 拍照稳定帧期间暂存的用户相机,完成或超时后还原 */\n private photoCameraRestore: {\n camera: ReturnType<CesiumDroneAdapter['snapshotCamera']> | null\n } | null = null\n /** 录像收尾回调:UI 用它落盘(引擎不直接触发下载);与 three 版同名同参 */\n onRecordingReady: ((blob: Blob, seconds: number) => void) | null = null\n private rafId: number | null = null\n private lastTime = 0\n private disposed = false\n /** Stop terrain sampling after an authorization failure so a bad Ion token cannot spam requests. */\n private terrainSamplingDisabled = false\n /** 相机是否被本适配器锁在无人机上(退出 follow/fpv 时要把 transform 还回去) */\n private cameraLocked = false\n /**\n * 取景**俯仰**覆盖(全景分层用,见 `setCaptureView`):非 null 时取景相机临时偏离\n * \"云台当前俯仰\"。只影响取景(拍照 / 录像 / 采集快照),不动飞控与机体姿态。\n *\n * 这里没有水平偏航是有意的 —— 水平覆盖靠**机体原地自转**(见 `spinBodyTo`),\n * 云台 Pan 机械行程转不出 180°(理由见 `drone-panorama` 的文件头)。\n */\n private captureView: CaptureViewOverride | null = null\n /** true while the Cesium camera is temporarily rendering the capture frame. */\n private captureFrameActive = false\n\n /** 宿主回调:模型切换完成后通知 UI 刷新按钮文案 */\n onModelChange: ((spec: DroneModelSpec) => void) | null = null\n\n /** Cesium 后端适配器:实现 DronePanelHost,面板/航线/鹰眼/键盘可直接用 */\n constructor(options: CesiumDroneOptions) {\n if (typeof window === 'undefined') {\n throw new Error('[drone-camera] CesiumDroneAdapter 需要浏览器环境')\n }\n const C = options.cesium ?? (window as unknown as { Cesium?: CesiumNS }).Cesium\n if (!C) {\n throw new Error(\n '[drone-camera] 未找到 Cesium:请先 await loadCesiumScript(\"/Cesium/Cesium.js\"),' +\n '或在 options 里显式传入 cesium 命名空间',\n )\n }\n this.C = C\n this.viewer = options.viewer\n this.groundClearance = options.groundClearance ?? 0.05\n this.followRange = options.followRange ?? FOLLOW_DISTANCE\n this.modelIndex = options.modelIndex ?? 0\n this.stripExtensions =\n options.stripRequiredExtensions ?? DEFAULT_UNSUPPORTED_REQUIRED_EXTENSIONS\n this.origin = {\n lon: options.lon ?? 116.391,\n lat: options.lat ?? 39.906,\n height: options.height ?? 0,\n }\n // 锚点 ENU 框架:本地米坐标 → ECEF 世界坐标的唯一入口\n this.enuToFixed = C.Transforms.eastNorthUpToFixedFrame(\n C.Cartesian3.fromDegrees(this.origin.lon, this.origin.lat, this.origin.height),\n )\n this.worldMatrix = new C.Matrix4()\n this.rig = new CesiumDroneRig(this.sim, C)\n this.installArmCheck()\n }\n\n // ───────────────────────────── 生命周期 ─────────────────────────────\n\n /**\n * 启动:采样锚点地形高度、加载机体、起循环。\n * 地形采样失败不阻断(退回给定的 height);不挂地形(椭球)时采样高度恒为 0,同样无碍。\n */\n async start(): Promise<void> {\n const C = this.C\n const provider = this.viewer.terrainProvider\n if (this.canSampleTerrain(provider)) {\n try {\n const carto = C.Cartographic.fromDegrees(this.origin.lon, this.origin.lat)\n const [sampled] = await C.sampleTerrainMostDetailed(provider, [carto])\n if (sampled && Number.isFinite(sampled.height)) {\n this.origin.height = sampled.height\n }\n } catch (error) {\n this.handleTerrainError(error)\n }\n }\n this.rebuildEnuFrame()\n void this.loadModel()\n // 航线表现层:航点标记 / 航线折线 / 场景内拖拽编辑(与机体加载异步并行,互不依赖)\n this.missionEditor = new CesiumMissionEditor(this, this.C, this.viewer)\n this.missionEditor.setEnabled(this.missionEditEnabled)\n this.applyMissionVisibility()\n this.loop(performance.now())\n }\n\n /** 销毁 viewer 与适配器,停止每帧同步 */\n destroy(): void {\n if (this.disposed) return\n this.disposed = true\n if (this.rafId !== null) cancelAnimationFrame(this.rafId)\n this.rafId = null\n if (this.recorder) {\n try {\n this.recorder.stop()\n } catch {\n /* 已停止 */\n }\n this.recorder = null\n }\n if (this.photoPending) {\n window.clearTimeout(this.photoPending.timer)\n const pending = this.photoPending\n this.photoPending = null\n pending.resolve(null)\n }\n if (this.photoCameraRestore?.camera) {\n this.restoreCamera(this.photoCameraRestore.camera)\n }\n this.photoCameraRestore = null\n this.captureFrameActive = false\n this.recordStream?.getTracks().forEach((track) => track.stop())\n this.recordStream = null\n this.captureTrack = null\n this.recordContext = null\n this.releaseCamera()\n this.rig.dispose()\n // 航线实体(航点/折线/返航点)与指针/键盘监听一并收掉\n this.missionEditor?.destroy()\n this.missionEditor = null\n if (this.model) {\n // primitives.remove 会连带 destroy primitive,几何与纹理一起释放\n this.viewer.scene.primitives.remove(this.model)\n this.model = null\n }\n this.releaseSanitizedUrl()\n this.recordChunks = []\n }\n\n /** 回收加载期净化产生的 Blob URL(否则整份 GLB 会一直被浏览器缓存持有) */\n private releaseSanitizedUrl(): void {\n if (this.sanitizedBlobUrl && typeof URL !== 'undefined') {\n URL.revokeObjectURL(this.sanitizedBlobUrl)\n }\n this.sanitizedBlobUrl = null\n }\n\n /**\n * 解析交给 Cesium 的模型地址:先按原 URL 取 ArrayBuffer,若 `extensionsRequired`\n * 里有 Cesium 不支持的扩展,就在内存里摘掉并用 Blob URL 代替(见文件头\"GLB 净化\")。\n *\n * 无扩展可摘(或净化关闭)时返回原 URL —— 零额外开销、零对象 URL 泄漏;\n * 取文件失败时也回退原 URL,让 Cesium 自己报出真正的网络/解析错误。\n */\n private async resolveModelUrl(url: string, token: number): Promise<string> {\n this.releaseSanitizedUrl()\n if (this.stripExtensions.length === 0 || typeof fetch !== 'function') return url\n try {\n const response = await fetch(url)\n if (!response.ok) return url\n const patched = stripRequiredExtensions(await response.arrayBuffer(), this.stripExtensions)\n if (this.disposed || token !== this.loadToken) return url\n if (!patched) return url\n this.sanitizedBlobUrl = URL.createObjectURL(\n new Blob([patched], { type: 'model/gltf-binary' }),\n )\n return this.sanitizedBlobUrl\n } catch {\n return url\n }\n }\n\n // ───────────────────────────── 机体 ─────────────────────────────\n\n /** 当前机体模型规格(名称/缩放/机头偏移等) */\n get modelSpec(): DroneModelSpec {\n return DRONE_MODELS[this.modelIndex] ?? DRONE_MODELS[0]!\n }\n\n /** 机体模型是否已挂载完成(加载是异步的,期间无人机不可见) */\n get modelLoaded(): boolean {\n return this.model !== null\n }\n\n /** 切换机体(按下标轮换或直接指定 key);返回切换后的规格 */\n setModel(keyOrIndex: string | number): DroneModelSpec {\n const idx =\n typeof keyOrIndex === 'number'\n ? keyOrIndex\n : DRONE_MODELS.findIndex((m) => m.key === keyOrIndex)\n if (idx < 0 || idx >= DRONE_MODELS.length) return this.modelSpec\n this.modelIndex = idx\n void this.loadModel()\n this.onModelChange?.(this.modelSpec)\n return this.modelSpec\n }\n\n /** 在目录里轮换到下一个机体 */\n cycleModel(): DroneModelSpec {\n return this.setModel((this.modelIndex + 1) % DRONE_MODELS.length)\n }\n\n /**\n * 加载机体并挂到 scene.primitives 上。\n *\n * 为什么不用 Entity + model:实测(Cesium 1.144)Entity.model 的 ModelVisualizer\n * 迟迟不产出可渲染/可拾取的 Model primitive(位置是 CallbackProperty 时尤甚),\n * 而手动 Model.fromGltfAsync + 每帧写 modelMatrix 立刻可见 —— 动态模型本就是\n * primitive 路线的推荐用法,还省掉了 Entity 的 Property 求值开销。\n */\n private async loadModel(): Promise<void> {\n const C = this.C\n const spec = this.modelSpec\n const token = ++this.loadToken\n // 净化后的 Blob URL(或原 URL)—— 摘掉 Cesium 不支持的 required 扩展,见文件头\n const url = await this.resolveModelUrl(spec.url, token)\n if (this.disposed || token !== this.loadToken) return\n let model: CesiumNS\n try {\n model = await C.Model.fromGltfAsync({\n url,\n scale: spec.scale,\n // 关掉像素下限,否则拉远看时模型会保持屏幕尺寸、比例失真\n minimumPixelSize: 0,\n })\n } catch {\n // 加载失败(404 / 解析错):保留旧机体,不打断飞行\n return\n }\n // 等待期间页面已销毁或又切了机体 → 丢弃这次的结果\n if (this.disposed || token !== this.loadToken) {\n if (!model.isDestroyed()) model.destroy()\n return\n }\n if (this.model) this.viewer.scene.primitives.remove(this.model)\n this.model = model\n this.viewer.scene.primitives.add(model)\n this.syncModelMatrix()\n // 重绑桨叶关节与灯组(非语义机体自动 no-op)。\n // ⚠️ 必须等 model.ready:fromGltfAsync 返回时 glTF 场景图还没建好,\n // 此时 getNode 一律返回 undefined,提前 attach 会把整层表现永久禁用。\n const bindRig = (): void => {\n if (!this.disposed && this.model === model) this.rig.attach(model)\n }\n if (model.ready) bindRig()\n else model.readyEvent.addEventListener(() => bindRig())\n }\n\n // ──────────────────────────── 坐标换算 ────────────────────────────\n\n private rebuildEnuFrame(): void {\n const C = this.C\n const next = C.Transforms.eastNorthUpToFixedFrame(\n C.Cartesian3.fromDegrees(this.origin.lon, this.origin.lat, this.origin.height),\n )\n // Matrix4 是数组型对象,逐元素拷贝保持引用稳定(enuToFixed 被多处闭包捕获)\n C.Matrix4.clone(next, this.enuToFixed)\n }\n\n /** sim 本地米坐标(y 为 AGL) → ECEF 世界坐标 */\n private computeWorldPosition(): CesiumNS {\n const C = this.C\n const sim = this.sim\n // ENU 三轴:东 = x,北 = -z(南向取负),上 = y\n const local = C.Cartesian3.fromElements(\n sim.position.x,\n -sim.position.z,\n sim.position.y + this.groundClearance + this.modelSpec.lift,\n )\n return C.Matrix4.multiplyByPoint(this.enuToFixed, local, new C.Cartesian3())\n }\n\n /**\n * 每帧把 sim 真值写进模型的世界矩阵。\n * headingPitchRollToFixedFrame(origin, hpr) 一步给出「ENU 平移 + 航向/俯仰/横滚旋转」,\n * 与 Entity 管线里 headingPitchRollQuaternion 的语义完全一致。\n */\n private syncModelMatrix(): void {\n if (!this.model) return\n const C = this.C\n const sim = this.sim\n const hpr = new C.HeadingPitchRoll(\n C.Math.toRadians(sim.heading + this.modelSpec.yawOffsetDeg),\n // HeadingPitchRoll 是同一个旋转,但模型前向(局部 -x)与相机前向(局部 +x)相反:\n // sim.tiltPitch 正 = 抬头,对模型要取负(相机侧则直传,见 applyGimbalCameraPose)\n C.Math.toRadians(-sim.tiltPitch),\n C.Math.toRadians(sim.tiltRoll),\n )\n this.model.modelMatrix = C.Transforms.headingPitchRollToFixedFrame(\n this.computeWorldPosition(),\n hpr,\n undefined,\n undefined,\n this.worldMatrix,\n )\n }\n\n /** 当前经纬高(渲染坐标系下的真实地理位置),供 HUD / 验证脚本读取 */\n getGeo(): { lon: number; lat: number; height: number } {\n const C = this.C\n const carto = C.Cartographic.fromCartesian(this.computeWorldPosition())\n return {\n lon: C.Math.toDegrees(carto.longitude),\n lat: C.Math.toDegrees(carto.latitude),\n height: carto.height,\n }\n }\n\n // ───────────────────────────── 主循环 ─────────────────────────────\n\n private loop = (now: number): void => {\n if (this.disposed) return\n const dt = this.lastTime === 0 ? 0 : Math.min(MAX_STEP_SECONDS, (now - this.lastTime) / 1000)\n this.lastTime = now\n // 固定调用顺序:先推进飞控状态机,再把真值写进场景(模型矩阵 → 桨叶/灯光 → 航线实体),\n // 然后按视角模式摆相机,最后抓拍照/录像的云台取景帧(见 captureGimbalFrame)\n this.sim.step(dt)\n this.syncModelMatrix()\n this.rig.update(dt)\n this.missionEditor?.update()\n this.applyCamera()\n this.captureGimbalFrame()\n this.rafId = requestAnimationFrame(this.loop)\n }\n\n // ───────────────────────────── 相机 ─────────────────────────────\n\n /** 切相机模式:orbit 环绕 / follow 跟随 / fpv 机载 */\n setCameraMode(mode: CameraMode): void {\n if (mode === this.cameraMode) return\n this.cameraMode = mode\n // 退出跟随时把相机交还给 Cesium 的漫游控制器;进入跟随/机载由 applyCamera 每帧接管\n if (mode === 'orbit') this.releaseCamera()\n }\n\n /** 退出跟随:把相机 transform 归位,交还给 Cesium 的漫游控制器 */\n private releaseCamera(): void {\n if (!this.cameraLocked) return\n this.viewer.camera.lookAtTransform(this.C.Matrix4.IDENTITY)\n this.cameraLocked = false\n }\n\n private applyCamera(): void {\n if (this.cameraMode === 'orbit') {\n this.releaseCamera()\n return\n }\n const C = this.C\n const cam = this.viewer.camera\n const pos = this.computeWorldPosition()\n const sim = this.sim\n if (this.cameraMode === 'follow') {\n // 与 three 版 updateCamera 同框位:相机在无人机**正后方** followRange、\n // 上方 FOLLOW_HEIGHT,瞄准点抬 FOLLOW_AIM_LIFT(机身整体进画面)。\n // ⚠️ HeadingPitchRange 的 heading 实测是「机位相对目标的方位角」:\n // 传 sim.heading + 180 会把相机摆到机头**前方**(画面正中是云台镜头),\n // 直接传 sim.heading 才落在机尾后方 —— 探针实测见 tmp-shots/probe-follow.mjs。\n const lift = FOLLOW_HEIGHT - FOLLOW_AIM_LIFT\n const aim = C.Cartesian3.add(\n pos,\n C.Matrix4.multiplyByPointAsVector(\n this.enuToFixed,\n new C.Cartesian3(0, 0, FOLLOW_AIM_LIFT),\n new C.Cartesian3(),\n ),\n new C.Cartesian3(),\n )\n cam.lookAt(\n aim,\n new C.HeadingPitchRange(\n C.Math.toRadians(sim.heading),\n -Math.atan2(lift, this.followRange),\n Math.hypot(this.followRange, lift),\n ),\n )\n this.cameraLocked = true\n return\n }\n // fpv:相机就架在云台载荷上,与拍照/录像共用同一条取景路径\n this.releaseCamera()\n this.applyGimbalCameraPose()\n }\n\n /**\n * 把相机摆到云台取景位姿 —— 机载视角、拍照、录像三者的唯一取景来源,\n * 与 three 版 applyGimbalCameraPose 同一语义:\n * 位置 = 机体原点沿**云台光轴**前移(水平分量按 cos(俯仰) 收缩、同时抬起 offset·sin(俯仰));\n * 姿态 = 航向(含云台偏航)与云台俯仰,横滚锁水平(three 版三轴增稳的 rollDeg 恒 0)。\n *\n * ⚠️ 符号约定(两端不同,别想当然):\n * · Cesium **相机**的 pitch 正 = 抬头(Quaternion.fromHeadingPitchRoll 里相机前向取局部 +x,\n * pitch 分量 Ry(-pitch) 作用后前向上扬),而 sim.gimbalPitch 正 = 抬头 → 同号直传;\n * · Cesium **模型**的 pitch 正 = 低头(机体前向是局部 -x,同一个旋转对它反号),\n * 而 sim.tiltPitch 正 = 抬头 → 要取负(见 syncModelMatrix)。\n * 实测铁证:云台 -30°(下视)时 pitch 直传画面朝地、取负则朝天(见 verify-gimbal-cam.mjs)。\n */\n private applyGimbalCameraPose(): void {\n const { destination, heading, pitch, roll } = this.gimbalCameraView()\n this.viewer.camera.setView({\n destination,\n orientation: { heading, pitch, roll },\n })\n }\n\n /**\n * 云台取景视点 —— 机载视角、拍照、录像、采集快照**共用同一个来源**。\n *\n * 把它单独抽出来是为了让「画面里看到的」与「采集记录下来的」不可能不一致:\n * 相机摆位与写入影像元数据的位姿读的是同一个函数。\n */\n private gimbalCameraView(): {\n destination: CesiumNS\n heading: number\n pitch: number\n roll: number\n } {\n const C = this.C\n const sim = this.sim\n const spec = this.modelSpec\n // 取景俯仰:全景分层给了覆盖时用覆盖值(绝对值),否则沿用云台俯仰。\n // 水平方向不在这里做手脚 —— 全景的水平覆盖靠 spinBodyTo 转机体(云台 Pan 行程不够),\n // 所以取景偏航恒等于\"机体航向 + 云台偏航真值\"。\n const viewPitch = this.captureView?.pitch ?? sim.gimbalPitch\n const viewYaw = sim.gimbalYaw\n const pitchRad = C.Math.toRadians(viewPitch)\n const headingRad = C.Math.toRadians(sim.heading)\n\n let offsetENU: CesiumNS\n if (this.captureView) {\n // 全景扫描:光心钉在机体自转轴上(与 three 版 fixedCenter 同义)。\n // 取景相机平时沿光轴前移,机体原地自转一周光心会扫出大圆,对近景产生显著视差,\n // 拼接器\"绕光心纯旋转\"的前提被破坏;全景分层俯仰时只转朝向、光心不动,才能保证\n // 相邻张重叠区可羽化融合,也避免下视时机身进画面。\n offsetENU = new C.Cartesian3(0, 0, spec.gimbalLiftM)\n } else {\n // 普通拍照/录像:光心随云台俯仰做弧线摆动,贴合真实镜头位置。\n offsetENU = new C.Cartesian3(\n Math.sin(headingRad) * spec.gimbalOffsetM * Math.cos(pitchRad),\n Math.cos(headingRad) * spec.gimbalOffsetM * Math.cos(pitchRad),\n spec.gimbalOffsetM * Math.sin(pitchRad) + spec.gimbalLiftM,\n )\n }\n const destination = C.Cartesian3.add(\n this.computeWorldPosition(),\n C.Matrix4.multiplyByPointAsVector(this.enuToFixed, offsetENU, new C.Cartesian3()),\n new C.Cartesian3(),\n )\n return {\n destination,\n // ⚠️ 见 applyGimbalCameraPose 的符号约定说明\n heading: C.Math.toRadians(sim.heading - viewYaw),\n pitch: pitchRad,\n // 三轴增稳:横滚锁水平(three 版 getGimbalCameraOrientation 的 rollDeg 恒 0)\n roll: 0,\n }\n }\n\n /**\n * 取景相机位姿与内参快照 —— 虚拟影像采集用(见 drone-capture 的 `VirtualCaptureProvider`)。\n *\n * 与 three 版同义:**记录的是取景相机(云台镜头),不是机身**;位姿与内参都从\n * `gimbalCameraView()` + 渲染相机的投影参数里读,不触发渲染、不改动任何相机状态。\n *\n * 坐标系用 `ecef`(Cesium 原生地固系),同时给出一份经纬高 —— 与真实照片的 GPS 字段同形。\n */\n getCaptureCamera(): CaptureCameraState | null {\n if (this.disposed || !this.modelLoaded) return null\n const C = this.C\n const canvas = this.viewer.canvas as HTMLCanvasElement | undefined\n const width = canvas?.width ?? 0\n const height = canvas?.height ?? 0\n if (!width || !height) return null\n\n const { destination, heading, pitch, roll } = this.gimbalCameraView()\n // HPR model quaternions use different axes from a view camera. Derive\n // both live snapshots and idle previews from the actual camera basis.\n const rendered = this.captureFrameActive ? this.viewer.camera : new C.Camera(this.viewer.scene)\n if (!this.captureFrameActive) {\n rendered.setView({ destination, orientation: { heading, pitch, roll } })\n }\n const right = rendered.rightWC\n const up = rendered.upWC\n const forward = rendered.directionWC\n const rotation = cameraQuaternionFromBasis({\n right: [right.x, right.y, right.z],\n up: [up.x, up.y, up.z],\n forward: [forward.x, forward.y, forward.z],\n }, 'cesium')\n const position = rendered.positionWC\n const carto = C.Cartographic.fromCartesian(position)\n // Read the rendered projection, including off-centre principal points.\n // Orthographic views cannot be represented by this PINHOLE contract.\n const projection = this.viewer.camera.frustum?.projectionMatrix\n if (!projection || Math.abs(projection[11] + 1) > 1e-8) return null\n const fx = projection[0] * width / 2\n const fy = projection[5] * height / 2\n const fovY = 2 * Math.atan(height / (2 * fy))\n const fovX = 2 * Math.atan(width / (2 * fx))\n const sim = this.sim\n const degrees = (radians: number): number => (radians * 180) / Math.PI\n\n return {\n pose: {\n position: [position.x, position.y, position.z],\n rotation,\n coordinateFrame: 'ecef',\n },\n intrinsics: {\n width,\n height,\n fx,\n fy,\n cx: width * (1 - projection[8]) / 2,\n cy: height * (1 + projection[9]) / 2,\n horizontalFovDeg: degrees(fovX),\n verticalFovDeg: degrees(fovY),\n calibrationSource: 'known',\n },\n frame: { width, height },\n drone: {\n // 与 three 版统一成 x 东 / y 北 / z 上(sim 内部是 x 东 / y 上 / z 南)\n position: [sim.position.x, -sim.position.z, sim.position.y],\n heading: sim.heading,\n tiltPitch: sim.tiltPitch,\n tiltRoll: sim.tiltRoll,\n gimbalPitch: sim.gimbalPitch,\n gimbalYaw: sim.gimbalYaw,\n },\n geo: {\n longitude: degrees(carto.longitude),\n latitude: degrees(carto.latitude),\n altitude: carto.height,\n },\n }\n }\n\n // ──────────────────── 取景帧(拍照 / 录像共用) ────────────────────\n\n /**\n * 记下相机当前位姿(取景帧渲染完原样还回):\n * 用 WC(World Coordinate)三元组存,不依赖相机此刻锁在哪个参考系里。\n */\n private snapshotCamera(): {\n position: CesiumNS\n direction: CesiumNS\n up: CesiumNS\n transform: CesiumNS\n } {\n const C = this.C\n const cam = this.viewer.camera\n return {\n position: C.Cartesian3.clone(cam.positionWC),\n direction: C.Cartesian3.clone(cam.directionWC),\n up: C.Cartesian3.clone(cam.upWC),\n transform: C.Matrix4.clone(cam.transform),\n }\n }\n\n /** 把相机位姿原样还回去 —— 取景帧渲染完必须调用(与 three 版 restoreCamera 同义) */\n private restoreCamera(snapshot: ReturnType<CesiumDroneAdapter['snapshotCamera']>): void {\n const C = this.C\n const cam = this.viewer.camera\n // 先回到世界系再写位姿:position/direction 的 setter 是按当前参考系解释的\n cam.lookAtTransform(C.Matrix4.IDENTITY)\n cam.setView({\n destination: snapshot.position,\n orientation: { direction: snapshot.direction, up: snapshot.up },\n })\n // 还回原来的锁定参考系(follow 模式下非单位阵);世界位姿在 _setTransform 里保持不变\n cam.lookAtTransform(snapshot.transform)\n }\n\n /**\n * 取景帧场景可见性:拍照/录像时机体自身与航线辅助物退场。\n *\n * 全景固定光心后,相机就在机体怀里,机壳/机臂/桨叶会进画面并被拼进全景;\n * 航线点/线也不该出现在照片里。与 three 版 `setCaptureFrameVisible` 同义,\n * 拍完按 `applyMissionVisibility()` 还原用户开关状态。\n */\n private setCaptureFrameVisible(visible: boolean): void {\n if (this.model) this.model.show = visible\n if (visible) {\n this.applyMissionVisibility()\n } else {\n this.missionEditor?.setVisible(false)\n }\n }\n\n /**\n * 取景帧 —— 拍照与录像共用同一条「云台取景」路径:录制/成片内容恒为云台相机拍的,\n * 与用户此刻在哪个视角无关(与 three 版 captureRecordingFrame 完全同义)。\n *\n * 做法:把 Cesium 相机临时搬到云台位姿并保持到地球瓦片就绪、连续渲染稳定帧,\n * 再拷进录制画布(或 toDataURL 成照片)→ 还原相机 → 再渲一帧把用户视角画回去。\n *\n * 两个必须:\n * - 渲染与拷屏必须同任务完成,否则 WebGL 合成后清空后台缓冲,拿到的是黑帧;\n * - 拷完必须补渲用户视角:取景帧留在主画布上,不补的话合成器会把云台画面\n * 呈现给用户(录制期间整屏闪帧)。\n * fpv 模式下相机本来就在云台上:用户视角与云台取景是同一个画面,\n * 省去搬移/还原/补渲,渲一帧直接用。\n */\n private captureGimbalFrame(): void {\n const photo = this.photoPending\n const recording = this.recorder !== null\n // 拍照超时可能在两次渲染帧之间触发;下一帧先还原临时相机与场景显隐。\n let restoredAfterTimeout = false\n if (!photo && this.photoCameraRestore) {\n if (this.photoCameraRestore.camera) this.restoreCamera(this.photoCameraRestore.camera)\n this.photoCameraRestore = null\n this.captureFrameActive = false\n this.setCaptureFrameVisible(true)\n restoredAfterTimeout = true\n }\n if (!photo && !recording) {\n if (restoredAfterTimeout) this.viewer.render()\n return\n }\n const fpv = this.cameraMode === 'fpv'\n const restore = fpv\n ? null\n : photo\n ? (this.photoCameraRestore?.camera ?? this.snapshotCamera())\n : this.snapshotCamera()\n if (photo && !this.photoCameraRestore) this.photoCameraRestore = { camera: restore }\n if (!fpv) this.applyGimbalCameraPose()\n this.setCaptureFrameVisible(false)\n if (photo) {\n this.captureFrameActive = true\n }\n this.viewer.render()\n if (photo) photo.captureCamera = this.getCaptureCamera()\n if (recording) this.pushRecordFrame()\n // 首帧 setView 后 Cesium 可能仍显示上一视角的地形/影像瓦片;保持取景位姿,\n // 直到地球瓦片就绪并连续渲染两帧,再拷贝画面。\n if (photo && !photo.forceCapture && this.viewer.scene.globe?.tilesLoaded === false) {\n photo.settledFrames = 0\n return\n }\n if (photo && photo.settledFrames < 2) {\n photo.settledFrames += 1\n return\n }\n if (photo) {\n this.photoPending = null\n window.clearTimeout(photo.timer)\n const shot = this.grabCanvas()\n if (shot && photo.captureCamera) shot.camera = photo.captureCamera\n // 成片才计数(与 three 版 onAfterRender 一致)\n if (shot) this.sim.takePhoto()\n photo.resolve(shot)\n }\n this.setCaptureFrameVisible(true)\n if (photo) {\n this.photoCameraRestore = null\n this.captureFrameActive = false\n }\n if (restore) {\n this.restoreCamera(restore)\n this.viewer.render()\n } else {\n // fpv 模式下取景帧就是用户视角:必须再渲一帧把机体显示回来,否则用户会看到一闪\n this.viewer.render()\n }\n }\n\n /** 把刚渲出来的主画布拷进录制画布,并通知抓帧轨道 */\n private pushRecordFrame(): void {\n const context = this.recordContext\n const source = this.viewer.canvas as HTMLCanvasElement | undefined\n if (!context || !source) return\n // 尺寸跟住主画布,窗口变化时录像不留黑边\n if (context.canvas.width !== source.width || context.canvas.height !== source.height) {\n context.canvas.width = source.width\n context.canvas.height = source.height\n }\n context.drawImage(source, 0, 0)\n this.captureTrack?.requestFrame()\n }\n\n /** 把当前画布内容拷进离屏 canvas 并转成 PNG(同一任务内执行,拿到的是刚渲染的那一帧) */\n private grabCanvas(): PhotoShot | null {\n const source = this.viewer.canvas as HTMLCanvasElement | undefined\n if (!source || !source.width || !source.height) return null\n const target = document.createElement('canvas')\n target.width = source.width\n target.height = source.height\n const context = target.getContext('2d')\n if (!context) return null\n context.drawImage(source, 0, 0)\n return { dataUrl: target.toDataURL('image/png'), width: target.width, height: target.height }\n }\n\n /** 视角复位:回到自由漫游,并把机位拉到无人机斜后上方 */\n resetCamera(): void {\n const C = this.C\n this.releaseCamera()\n this.cameraMode = 'orbit'\n const pos = this.computeWorldPosition()\n this.viewer.camera.flyToBoundingSphere(new C.BoundingSphere(pos, 6), {\n duration: 0.8,\n offset: new C.HeadingPitchRange(0, C.Math.toRadians(-28), 16),\n })\n }\n\n // ──────────────────────── 上位机指令透传 ────────────────────────\n\n /** 当前飞行相位(与 three 后端同一枚举) */\n get phase() {\n return this.sim.phase\n }\n /** 是否已离地 */\n get airborne(): boolean {\n return this.sim.airborne\n }\n /** 当前飞行挡位 */\n get flightMode(): FlightMode {\n return this.sim.mode\n }\n /** 相对起飞点高度(米) */\n get altitude(): number {\n return this.sim.position.y\n }\n /** 起飞前检查项列表(供面板展示) */\n get preflightChecks(): ChecklistItem[] {\n return this.sim.checklist\n }\n /** 机臂是否处于收纳态 */\n get armsFolded(): boolean {\n return this.armFoldTarget > 0.5\n }\n /** 是否正在录像 */\n get isRecording(): boolean {\n return this.recorder !== null\n }\n\n /** 上电自检(通过后才能起飞) */\n powerOn(): void {\n this.sim.powerOn()\n }\n /** 自动起飞到默认高度(地面待机相位才受理) */\n takeOff(): boolean {\n return this.sim.autoTakeOff()\n }\n /** 自动降落(中止进行中的航线) */\n land(): boolean {\n return this.sim.startLanding()\n }\n /** 智能返航(需在空中;先中止航线) */\n returnHome(reason = '用户触发返航'): boolean {\n return this.sim.startRth(reason)\n }\n /** 写摇杆连续量(与 three 后端同语义) */\n setStick(stick: Partial<StickState>): void {\n this.sim.setStick(stick)\n }\n /** 切飞行挡位 */\n setMode(mode: FlightMode): void {\n this.sim.setMode(mode)\n }\n /** 设云台俯仰角(度) */\n setGimbalPitch(pitch: number): void {\n this.sim.setGimbalPitch(pitch)\n }\n\n /** 直接把无人机移动到目标本地坐标位置,并保持离地高度不低于地形 */\n flyTo(target: { x?: number; y?: number; z?: number; heading?: number; pitch?: number; roll?: number }): void {\n const nextX = target.x ?? this.sim.position.x\n const nextY = target.y ?? this.sim.position.y\n const nextZ = target.z ?? this.sim.position.z\n this.sim.position.x = nextX\n this.sim.position.y = Math.max(0, nextY)\n this.sim.position.z = nextZ\n if (target.heading !== undefined) this.sim.heading = target.heading\n if (target.pitch !== undefined) {\n this.sim.tiltPitch = Math.max(-45, Math.min(45, target.pitch))\n }\n if (target.roll !== undefined) {\n this.sim.tiltRoll = Math.max(-45, Math.min(45, target.roll))\n }\n void this.ensureAltitudeAboveTerrain()\n }\n\n /** 设定相机变焦倍数(1~4x),保持和 three 后端一致 */\n zoomTo(zoom: number): void {\n this.sim.setZoom(zoom)\n if (this.cameraMode === 'follow') {\n const distance = Math.max(1.5, this.followRange / this.sim.cameraZoom)\n this.viewer.camera.lookAt(\n this.computeWorldPosition(),\n new this.C.HeadingPitchRange(\n this.C.Math.toRadians(this.sim.heading),\n -Math.atan2(FOLLOW_HEIGHT - FOLLOW_AIM_LIFT, distance),\n distance,\n ),\n )\n }\n }\n\n /** 在当前相对坐标处采样地形并确保无人机不钻进地形里 */\n private async ensureAltitudeAboveTerrain(): Promise<void> {\n const provider = this.viewer.terrainProvider\n if (!this.canSampleTerrain(provider)) return\n const C = this.C\n const local = C.Cartesian3.fromElements(this.sim.position.x, -this.sim.position.z, 0)\n const world = C.Matrix4.multiplyByPoint(this.enuToFixed, local, new C.Cartesian3())\n const carto = C.Cartographic.fromCartesian(world)\n try {\n const [sampled] = await C.sampleTerrainMostDetailed(provider, [carto])\n if (!sampled || !Number.isFinite(sampled.height)) return\n const minAltitude =\n sampled.height - this.origin.height - this.groundClearance - this.modelSpec.lift + 0.25\n if (this.sim.position.y < minAltitude) {\n this.sim.position.y = minAltitude\n }\n } catch (error) {\n this.handleTerrainError(error)\n }\n }\n\n private canSampleTerrain(provider: CesiumNS): boolean {\n return Boolean(\n !this.disposed &&\n !this.terrainSamplingDisabled &&\n provider &&\n provider.availability &&\n typeof provider.requestTileGeometry === 'function',\n )\n }\n\n private handleTerrainError(error: unknown): void {\n const source = error as {\n statusCode?: unknown\n response?: { status?: unknown }\n message?: unknown\n }\n const status = source?.statusCode ?? source?.response?.status\n const message = String(source?.message ?? error ?? '')\n if (status !== 401 && status !== 403 && !/\\b(?:401|403)\\b/.test(message)) return\n\n this.terrainSamplingDisabled = true\n try {\n this.viewer.terrainProvider = new this.C.EllipsoidTerrainProvider()\n } catch {\n // Keep the current provider if this Cesium build does not expose EllipsoidTerrainProvider.\n }\n }\n\n /**\n * 设置取景**俯仰**覆盖 —— 全景扫描(180° / 360°)按俯仰层逐张取景的入口;传 `null` 复位。\n * 与 three 版 `DroneFly.setCaptureView` 同语义:俯仰替代云台俯仰;只影响取景相机\n * (`gimbalCameraView` 一处生效),拍照 / 录像 / 采集快照三条路径自动同源。\n *\n * 没有水平偏航是有意的:水平覆盖靠 `spinBodyTo` 转机体(云台 Pan 行程不够)。\n */\n setCaptureView(override: CaptureViewOverride | null): void {\n this.captureView = override ?? null\n }\n\n /**\n * 让**机体原地自转**到绝对方位角 `headingDeg`(度,罗盘角);`null` 停止自转。\n * 与 three 版同语义,同样交给飞控内核(`sim.spinTo`)—— 两个后端的自转行为因此完全一致。\n */\n spinBodyTo(headingDeg: number | null): void {\n this.sim.spinTo(headingDeg)\n }\n\n /** 机体当前方位角(度,罗盘角)—— 全景扫描用它判断\"转到位了没\" */\n getBodyHeading(): number {\n return this.sim.heading\n }\n\n /**\n * 展开 / 收拢机臂。\n * Cesium 的 Model 没有 three 版的机械骨骼,这里只维护\"目标状态\"——\n * 它的唯一作用是驱动起飞前检查项,与真机\"不展臂不许起飞\"一致。\n */\n setArmsFolded(folded: boolean): void {\n this.armFoldTarget = folded ? 1 : 0\n this.refreshArmCheck()\n }\n\n /** 注入机臂检查项:三方渲染器下没有骨骼可查,直接用目标状态代替 */\n private installArmCheck(): void {\n if (this.sim.externalChecks.some((item) => item.id === 'arms')) return\n this.sim.externalChecks.push({\n id: 'arms',\n label: '机臂',\n ok: false,\n detail: '机臂处于收纳状态',\n blocking: true,\n })\n }\n\n private refreshArmCheck(): void {\n const item = this.sim.externalChecks.find((entry) => entry.id === 'arms')\n if (!item) return\n item.ok = this.armFoldTarget <= 0.5\n item.detail = item.ok ? '已展开' : '机臂处于收纳状态'\n }\n\n // ──────────────────────── 航线任务(MissionPanelHost) ────────────────────────\n //\n // 飞行内核(DroneSim)本来就带全套航线逻辑(启航四步 / goto / hover / 限高限距),\n // Cesium 侧要补的只是\"把面板需要的方法接上\"加\"航点怎么变成地球上的画面\"。\n // 方法与 three 版 DroneFly 同名同语义, MissionPanel 因此零改动换后端。\n\n /** 当前是否处于观察者(自由漫游)视角 —— 场景内编辑的前置条件 */\n get isOrbitView(): boolean {\n return this.cameraMode === 'orbit'\n }\n\n /** 启动航线任务(地面时先自动起飞,到 1.2 米后自动接上航线) */\n startMission(): boolean {\n return this.sim.startMission()\n }\n\n /** 暂停执行中的航线(原地悬停,可再续飞) */\n pauseMission(reason?: string): boolean {\n return this.sim.pauseMission(reason)\n }\n\n /** 续飞被暂停的航线 */\n resumeMission(): boolean {\n return this.sim.resumeMission()\n }\n\n /** 中止航线并留在原地悬停 */\n stopMission(reason?: string): boolean {\n return this.sim.stopMission(reason)\n }\n\n /** 下发航点列表(执行中会被内核拒绝,与真机\"上传后不可改\"一致) */\n setMission(waypoints: Array<Partial<MissionWaypoint>>): boolean {\n return this.sim.setMission(waypoints)\n }\n\n /** 改航线参数(巡航速度/机头朝向/过点方式/完成后动作/循环) */\n setMissionConfig(patch: Partial<MissionConfig>): void {\n this.sim.setMissionConfig(patch)\n }\n\n /** 航线参数与航点恢复出厂默认 */\n resetMissionToDefault(): void {\n this.sim.resetMissionToDefault()\n }\n\n /** 航线只读快照(航点/Home/进度) */\n getMissionSnapshot(): MissionSnapshot {\n return this.sim.snapshot().mission\n }\n\n /** 全量真值快照(面板据此渲染任务状态与阻塞原因) */\n getSnapshot(): DroneSnapshot {\n return this.sim.snapshot()\n }\n\n /** 把当前航线整理成可序列化的导出数据(实现下沉在 drone-sim,与 three 后端共用) */\n getMissionExport(): MissionExport {\n return buildMissionExport(\n this.sim.mission.map((waypoint) => ({ ...waypoint })),\n { ...this.sim.missionConfig },\n this.sim.homePosition,\n )\n }\n\n /** 导出航线文本(`json` 完整存档 / `csv` 纯航点表) */\n exportMission(format: MissionExportFormat = 'json'): string {\n return exportMissionText(this.getMissionExport(), format)\n }\n\n /** 场景内编辑状态(开关/是否激活/选中/悬停/拖拽中) */\n getMissionEditState(): MissionEditState {\n return (\n this.missionEditor?.getEditState() ?? {\n enabled: this.missionEditEnabled,\n active: false,\n selectedIndex: this.missionEditSelected,\n hoverIndex: -1,\n dragging: false,\n mode: null,\n }\n )\n }\n\n /** 场景内编辑开关(开启仍需\"当前确实可编辑\"才生效) */\n setMissionEditEnabled(enabled: boolean): void {\n this.missionEditEnabled = enabled\n this.missionEditor?.setEnabled(enabled)\n if (!enabled) this.setMissionSelected(-1)\n else if (this.sim.mission.length > 0 && this.canMissionSceneEdit()) {\n this.frameMission()\n }\n }\n\n /** 场景内编辑开关当前是否开着 */\n isMissionEditEnabled(): boolean {\n return this.missionEditEnabled\n }\n\n /** 当前选中的航点索引(无选中 -1) */\n getMissionSelected(): number {\n return this.missionEditSelected\n }\n\n /** 同步选中航点(列表点击与场景拾取双向联动) */\n setMissionSelected(index: number): void {\n this.missionEditSelected = index\n }\n\n /** 原位更新一个航点(场景拖拽每帧调用);内核会做限高 / 限距夹紧 */\n updateMissionWaypoint(index: number, patch: Partial<MissionWaypoint>): MissionWaypoint | null {\n return this.sim.updateMissionWaypoint(index, patch)\n }\n\n /** 场景编辑:按索引删除一个航点 */\n deleteMissionWaypoint(index: number): boolean {\n if (!this.canMissionSceneEdit()) return false\n if (!this.sim.removeMissionWaypoint(index)) return false\n this.setMissionSelected(Math.min(index, this.sim.mission.length - 1))\n this.sim.pushEvent('info', `场景编辑:已删除航点 ${index + 1}`)\n return true\n }\n\n /** 场景编辑:以飞机当前位置新增一个航点(高度取飞机当前高度) */\n addMissionWaypointAtDrone(): number {\n if (!this.canMissionSceneEdit()) return -1\n const position = this.sim.position\n const index = this.sim.addMissionWaypoint({\n x: Math.round(position.x),\n z: Math.round(position.z),\n altitude: Math.max(1, Math.round(position.y)),\n speed: this.sim.missionConfig.autoSpeed,\n })\n if (index < 0) return -1\n this.setMissionSelected(index)\n this.sim.pushEvent('info', `场景编辑:已在飞机当前位置新增航点 ${index + 1}`)\n return index\n }\n\n /** 新航点的默认参数:高度与速度继承相邻航点,避免插出一个贴地点 */\n newWaypointTemplate(index: number): Partial<MissionWaypoint> {\n const list = this.sim.mission\n const neighbour = list[Math.min(index, list.length - 1)] ?? list[list.length - 1]\n return {\n altitude: neighbour?.altitude ?? 30,\n speed: neighbour?.speed ?? this.sim.missionConfig.autoSpeed,\n hoverSeconds: neighbour?.hoverSeconds ?? 0,\n }\n }\n\n /** 航点标记 / 航线折线 / 返航点的总显隐(不影响任务执行) */\n setMissionVisible(visible: boolean): void {\n this.missionVisible = visible\n this.missionEditor?.setVisible(visible)\n }\n\n isMissionVisible(): boolean {\n return this.missionVisible\n }\n\n /** 航点标记显隐 */\n setMissionWaypointsVisible(visible: boolean): void {\n this.missionMarkersVisible = visible\n this.missionEditor?.setWaypointsVisible(visible)\n }\n\n isMissionWaypointsVisible(): boolean {\n return this.missionMarkersVisible\n }\n\n /** 航线折线显隐 */\n setMissionPathVisible(visible: boolean): void {\n this.missionPathVisible = visible\n this.missionEditor?.setPathVisible(visible)\n }\n\n isMissionPathVisible(): boolean {\n return this.missionPathVisible\n }\n\n /** 地理坐标(经纬高)在 Cesium 后端始终可用 */\n supportsGeoCoordinates(): boolean {\n return true\n }\n\n /**\n * 取某航点的地理坐标(经纬高,WGS84)。\n *\n * 航点在 sim 里是本地米坐标(x 东 / z 南 / altitude AGL),经 `localToWorld` 投到 ECEF 后\n * 再反算 Cartographic;海拔取椭球绝对高程(= 地形 + AGL),与真实照片的 GPS 字段同形。\n * 越界或点位退化(如地心)返回 null。\n */\n getWaypointGeo(index: number): { lon: number; lat: number; height: number } | null {\n const waypoint = this.sim.mission[index]\n if (!waypoint) return null\n const world = this.localToWorld(waypoint.x, waypoint.altitude, waypoint.z)\n const carto = this.C.Cartographic.fromCartesian(world)\n if (!carto || !Number.isFinite(carto.longitude) || !Number.isFinite(carto.latitude)) return null\n return {\n lon: this.C.Math.toDegrees(carto.longitude),\n lat: this.C.Math.toDegrees(carto.latitude),\n height: carto.height,\n }\n }\n\n /**\n * 用地理坐标原地改写一个航点:经纬高 → 本地米坐标的逆向换算。\n * `Cartesian3.fromDegrees` 投到 ECEF,再用 `worldToLocalXZ` 取回本地水平坐标(x 东 / z 南);\n * 海拔绝对高程减去锚点海拔即 AGL,交给 `updateMissionWaypoint` 夹紧到合法范围。\n * 越界 / 点位退化(地心)返回 false。\n */\n setWaypointGeo(index: number, geo: { lon: number; lat: number; height: number }): boolean {\n const waypoint = this.sim.mission[index]\n if (!waypoint) return false\n if (![geo.lon, geo.lat, geo.height].every((n) => Number.isFinite(n))) return false\n const C = this.C\n const world = C.Cartesian3.fromDegrees(geo.lon, geo.lat, geo.height)\n const local = this.worldToLocalXZ(world)\n if (!local) return false\n const altitude = Math.min(500, Math.max(1, geo.height - this.origin.height))\n return this.updateMissionWaypoint(index, { x: local.x, z: local.z, altitude }) !== null\n }\n\n /** 三个显隐开关的初值在编辑器创建后补一遍(构造早于 start) */\n private applyMissionVisibility(): void {\n this.missionEditor?.setVisible(this.missionVisible)\n this.missionEditor?.setWaypointsVisible(this.missionMarkersVisible)\n this.missionEditor?.setPathVisible(this.missionPathVisible)\n }\n\n /**\n * 把相机拉到\"一眼看全整条航线\"的位置(编辑航线前先框住航线)。\n *\n * 与 three 版 frameMission 同目的:航点动辄 30~40 米高、水平铺开几十米,\n * 默认机位贴着飞机,根本看不到航线。这里按航点 + 返航点的包围球算距离,\n * 保证竖着铺开的航线也不会被裁掉;只有观察者视角受理(机载/跟随下相机\n * 每帧被适配器接管,飞过去会被立刻拽回)。\n */\n frameMission(): boolean {\n if (!this.isOrbitView) return false\n const C = this.C\n const home = this.sim.homePosition\n const first = this.sim.mission[0]\n let minX = home.x\n let maxX = home.x\n let minY = first ? Math.max(1, first.altitude) : 1\n let maxY = minY\n let minZ = home.z\n let maxZ = home.z\n const include = (x: number, y: number, z: number): void => {\n minX = Math.min(minX, x)\n maxX = Math.max(maxX, x)\n minY = Math.min(minY, y)\n maxY = Math.max(maxY, y)\n minZ = Math.min(minZ, z)\n maxZ = Math.max(maxZ, z)\n }\n for (const waypoint of this.sim.mission) {\n include(waypoint.x, waypoint.altitude, waypoint.z)\n }\n // 地面也要进画面:航点的水平位置在地面上是有意义的参照\n include(minX, 0, minZ)\n include(maxX, 0, maxZ)\n\n const centerLocal = {\n x: (minX + maxX) / 2,\n y: (minY + maxY) / 2,\n z: (minZ + maxZ) / 2,\n }\n const radius = Math.max(12, 0.5 * Math.hypot(maxX - minX, maxY - minY, maxZ - minZ))\n const center = this.localToWorld(centerLocal.x, centerLocal.y, centerLocal.z)\n // 距离同时满足垂直/水平视场:按相机的 fovy 与画布宽高比取大者,再留 35% 余量\n const cam = this.viewer.camera\n const canvas = this.viewer.canvas as HTMLCanvasElement\n const halfVertical = cam.frustum.fovy / 2\n const aspect = canvas.clientWidth > 0 ? canvas.clientWidth / canvas.clientHeight : 1.6\n const halfHorizontal = Math.atan(Math.tan(halfVertical) * Math.max(0.2, aspect))\n const distance =\n Math.max(radius / Math.tan(halfVertical), radius / Math.tan(halfHorizontal)) * 1.35\n this.releaseCamera()\n this.viewer.camera.flyToBoundingSphere(new C.BoundingSphere(center, radius), {\n duration: 0.6,\n offset: new C.HeadingPitchRange(0, C.Math.toRadians(-32), distance),\n })\n return true\n }\n\n /** 场景内编辑的硬门槛:观察者视角 + 任务未在执行(与 three 版 canMissionSceneEdit 一致) */\n private canMissionSceneEdit(): boolean {\n return this.isOrbitView && this.sim.missionStatus === 'idle' && !this.sim.missionPendingStart\n }\n\n // ─────────────── 场景内编辑器宿主(CesiumMissionHost 实现) ───────────────\n\n canEdit(): boolean {\n return this.canMissionSceneEdit()\n }\n\n getWaypoints(): MissionWaypoint[] {\n return this.sim.mission.map((waypoint) => ({ ...waypoint }))\n }\n\n getSelected(): number {\n return this.missionEditSelected\n }\n\n setSelected(index: number): void {\n this.setMissionSelected(index)\n }\n\n /** 整表提交(双击插入 / 删除):内核会逐点归一化并重建实体 */\n commitWaypoints(waypoints: Array<Partial<MissionWaypoint>>, selected: number): void {\n if (!this.sim.setMission(waypoints)) return\n this.setMissionSelected(selected)\n }\n\n moveWaypoint(index: number, patch: Partial<MissionWaypoint>): MissionWaypoint | null {\n return this.updateMissionWaypoint(index, patch)\n }\n\n getLimits(): MissionEditLimits {\n return {\n // 用 ceiling 而不是 config 原值:unlimitedFlight 打开时是 Infinity,编辑器的夹紧自动失效\n maxAltitude: this.sim.altitudeCeiling,\n maxDistance: this.sim.distanceCeiling,\n homeX: this.sim.homePosition.x,\n homeZ: this.sim.homePosition.z,\n }\n }\n\n /**\n * sim 本地米坐标 → ECEF 世界坐标(航点实体定位用)。\n *\n * 与 computeWorldPosition(机体)的差别只在高程基准:机体要再抬\n * groundClearance + modelSpec.lift(让机腹贴地),而航点的 altitude 本身就是\n * **相对起飞点的 AGL**,直接加锚点海拔即是世界高。\n */\n localToWorld(x: number, y: number, z: number): CesiumNS {\n const C = this.C\n const local = C.Cartesian3.fromElements(x, -z, y + this.origin.height)\n return C.Matrix4.multiplyByPoint(this.enuToFixed, local, new C.Cartesian3())\n }\n\n /** ECEF 世界坐标 → sim 本地水平坐标(双击插入航点用) */\n worldToLocalXZ(world: CesiumNS): { x: number; z: number } | null {\n const C = this.C\n const inverse = C.Matrix4.inverse(this.enuToFixed, new C.Matrix4())\n if (!inverse) return null\n const local = C.Matrix4.multiplyByPoint(inverse, world, new C.Cartesian3())\n if (!local || !Number.isFinite(local.x) || !Number.isFinite(local.y)) return null\n // ENU 东 = sim +x;北 = sim −z(sim 的 z 向南为正)\n return { x: local.x, z: -local.y }\n }\n\n /** 拖拽期间收起 Cesium 的相机漫游输入,否则相机会跟着指针一起动 */\n setCameraInputEnabled(enabled: boolean): void {\n const controller = this.viewer.scene?.screenSpaceCameraController\n if (controller && 'enableInputs' in controller) controller.enableInputs = enabled\n }\n\n /** 编辑日志出口:接仿真内核的事件流(面板/控制台都看得到) */\n log(level: 'info' | 'warn', text: string): void {\n this.sim.pushEvent(level, text)\n }\n\n // ──────────────────────── 拍照 / 录像 ────────────────────────\n\n /**\n * 拍照:用云台相机取景拍一张 PNG(与用户当前视角无关,与 three 版同义)。\n *\n * 实现是登记一个待取景请求,渲染循环当帧把相机搬到云台位姿 → 渲染 → 同一任务内\n * 拷屏 → 还原(见 captureGimbalFrame)。上一张尚未取回时返回 null,避免连拍踩踏;\n * 地球瓦片未及时稳定时,最多等待 PHOTO_TIMEOUT_MS,之后使用最近渲染的取景帧,\n * 避免因流式地形持续加载而把整张照片丢弃。\n */\n async requestPhoto(): Promise<PhotoShot | null> {\n if (this.disposed || this.photoPending) return null\n return new Promise<PhotoShot | null>((resolve) => {\n const pending = {\n resolve,\n timer: 0,\n settledFrames: 0,\n forceCapture: false,\n captureCamera: null as CaptureCameraState | null,\n }\n pending.timer = window.setTimeout(() => {\n if (this.photoPending === pending) pending.forceCapture = true\n }, PHOTO_TIMEOUT_MS)\n this.photoPending = pending\n })\n }\n\n /** 开始屏录(录云台取景,与用户当前视角无关)。返回 false = 当前环境不支持 */\n startRecording(): boolean {\n if (this.recorder) return true\n const canvas = this.viewer.canvas as HTMLCanvasElement | undefined\n if (\n !canvas ||\n typeof MediaRecorder === 'undefined' ||\n typeof canvas.captureStream !== 'function'\n ) {\n return false\n }\n // 录制源用独立离屏画布:每帧只往里搬一帧云台取景,主画布上用户视角的渲染\n // 永远不会污染录制流。直接抓主画布(canvas.captureStream)录到的就是用户视角\n // —— 那正是\"录像没用云台拍\"的原因。\n const recordCanvas = document.createElement('canvas')\n recordCanvas.width = canvas.width || 1\n recordCanvas.height = canvas.height || 1\n const recordContext = recordCanvas.getContext('2d')\n if (!recordContext) return false\n let recorder: MediaRecorder\n let stream: MediaStream\n try {\n // captureStream(0) = 手动抓帧:录到什么由渲染循环里搬进这张画布的云台帧决定。\n stream = recordCanvas.captureStream(0)\n const track = stream.getVideoTracks()[0] as CanvasCaptureMediaStreamTrack | undefined\n if (track && typeof track.requestFrame === 'function') {\n this.captureTrack = track\n } else {\n // 少数环境不支持手动抓帧:退回按帧率自动采样这张画布(内容仍是云台取景)\n stream.getTracks().forEach((item) => item.stop())\n stream = recordCanvas.captureStream(RECORD_FPS)\n this.captureTrack = null\n }\n const mime = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')\n ? 'video/webm;codecs=vp9'\n : 'video/webm'\n recorder = new MediaRecorder(stream, { mimeType: mime, videoBitsPerSecond: RECORD_BITRATE })\n } catch {\n this.captureTrack = null\n return false\n }\n this.recordContext = recordContext\n this.recordStream = stream\n this.recordChunks = []\n recorder.ondataavailable = (event) => {\n if (event.data && event.data.size > 0) this.recordChunks.push(event.data)\n }\n recorder.start(RECORD_TIMESLICE_MS)\n this.recorder = recorder\n this.sim.setRecordingState(true)\n return true\n }\n\n /** 结束屏录:收尾成 Blob 并交给 onRecordingReady(未录制时直接返回 null) */\n stopRecording(): Promise<Blob | null> {\n const recorder = this.recorder\n this.recorder = null\n const seconds = this.sim.recordSeconds\n this.sim.setRecordingState(false)\n if (!recorder) return Promise.resolve(null)\n const chunks = this.recordChunks\n this.recordChunks = []\n return new Promise<Blob | null>((resolve) => {\n let settled = false\n const finish = (): void => {\n if (settled) return\n settled = true\n window.clearTimeout(timer)\n this.recordStream?.getTracks().forEach((track) => track.stop())\n this.recordStream = null\n this.captureTrack = null\n this.recordContext = null\n const blob = chunks.length\n ? new Blob(chunks, { type: recorder.mimeType || 'video/webm' })\n : null\n if (blob) this.onRecordingReady?.(blob, seconds)\n resolve(blob)\n }\n // 部分环境可能不派发 onstop,兜底收尾\n const timer = window.setTimeout(finish, RECORD_STOP_TIMEOUT_MS)\n recorder.onstop = finish\n try {\n recorder.stop()\n } catch {\n finish()\n }\n })\n }\n\n /** 回到\"上电前的干净状态\":停桨、归零位姿、复位飞控 */\n resetAll(): void {\n this.setStick({ ...NEUTRAL_STICK })\n this.sim.reset()\n }\n}\n","/**\n * 引擎装配层 —— 用 `engine` 字段选择渲染后端,一处配置换整套渲染实现。\n *\n * 库里有两套完整的飞行控制器,都实现同一个 `DronePanelHost` 接口:\n *\n * engine: 'three' → DroneFly three.js + WebGPURenderer 的飞行沙盒\n * engine: 'cesium' → CesiumDroneAdapter 地球(ENU/ECEF)坐标系下的飞行\n *\n * 两者的差别只在\"真值怎么变成画面\":飞行内核都是同一个 DroneSim,控制面板 /\n * 航线面板 / 鹰眼图 / 键盘控制完全不区分后端。所以选后端这件事只该发生一次 ——\n * 就在本工厂里,调用方拿到实例后只管用,不需要知道下面挂的是谁。\n *\n * ```ts\n * // three(自带场景)\n * const drone = await createDroneControl({\n * engine: 'three', scene, camera, renderer, controls, modelUrl: '/models/dji_mini4_pro.glb',\n * })\n *\n * // Cesium(先确保 /Cesium/Cesium.js 已就绪)\n * await loadCesiumScript('/Cesium/Cesium.js')\n * const drone = await createDroneControl({ engine: 'cesium', viewer, lon: 116.391, lat: 39.906 })\n *\n * // 之后再靠判别字段访问引擎特有成员\n * if (drone.engine === 'three') drone.setGimbalStabilization('physical')\n * ```\n *\n * `engine` 缺省为 `'three'`(与历史 API 一致:`new DroneFly(...)` + `initialize()`)。\n */\nimport type * as THREE from 'three'\nimport type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'\nimport type { WebGPURenderer } from 'three/webgpu'\n\nimport type { DronePanelHost } from './drone-control'\nimport { DroneFly, type DroneFlyInitializeOptions } from './drone-fly'\nimport { CesiumDroneAdapter, type CesiumDroneOptions } from './drone-cesium'\n\n/** 渲染后端标识 */\nexport type DroneEngine = 'three' | 'cesium'\n\n/**\n * three 后端参数:场景四件套 + `DroneFly.initialize()` 的全部选项\n * (modelUrl / obstacles)。\n */\nexport interface ThreeDroneEngineOptions extends DroneFlyInitializeOptions {\n /** 后端标识;缺省 'three' */\n engine?: 'three'\n scene: THREE.Scene<THREE.Object3DEventMap>\n camera: THREE.PerspectiveCamera\n renderer: WebGPURenderer\n controls?: OrbitControls<THREE.Camera> | null\n}\n\n/** Cesium 后端参数:本质就是 CesiumDroneOptions,只多一个必需的 engine 判别字段 */\nexport interface CesiumDroneEngineOptions extends CesiumDroneOptions {\n engine: 'cesium'\n}\n\n/** `createDroneControl()` 的参数(判别联合) */\nexport type DroneControlOptions = ThreeDroneEngineOptions | CesiumDroneEngineOptions\n\n/**\n * 引擎无关的控制器句柄 —— 两套实现都满足。\n *\n * 它是 `DronePanelHost`(面板 / 航线面板 / 鹰眼图真正依赖的那 22 项能力)\n * 加上一个 `engine` 判别字段,于是:\n * - 只做通用控制时,写 `(drone: DroneEngineHost) => void` 就够了,不必写联合类型;\n * - 需要引擎特有成员时,再用 `engine` 收窄到具体类。\n */\nexport interface DroneEngineHost extends DronePanelHost {\n readonly engine: DroneEngine\n}\n\n/** 按 `engine` 装配并初始化:three 走 DroneFly.initialize,Cesium 走 CesiumDroneAdapter.start */\nexport function createDroneControl(options: ThreeDroneEngineOptions): Promise<DroneFly>\nexport function createDroneControl(options: CesiumDroneEngineOptions): Promise<CesiumDroneAdapter>\nexport function createDroneControl(options: DroneControlOptions): Promise<DroneEngineHost>\nexport async function createDroneControl(\n options: DroneControlOptions,\n): Promise<DroneFly | CesiumDroneAdapter> {\n if (options.engine === 'cesium') {\n const adapter = new CesiumDroneAdapter(options)\n await adapter.start()\n return adapter\n }\n const { engine: _engine, scene, camera, renderer, controls, ...init } = options\n const fly = new DroneFly(scene, camera, renderer, controls ?? null)\n await fly.initialize(init)\n return fly\n}\n","/**\n * 全景拍摄(180° / 360°)—— 大疆式网格扫描。\n *\n * 真机全景不是一个快门拍下来的:机体与云台按**预设网格**转到一串方位,逐张曝光,\n * 相机内部把这一组照片拼成一张全景图。本模块复刻前半段(网格扫描 + 逐张落库),\n * 拼接交给外部(见文件末尾的 `PanoramaStitcher`)—— 库内不做图像配准与融合。\n *\n * ── 网格怎么施加:水平靠**机体原地自转**,垂直靠云台俯仰 ──────────────────\n *\n * ⚠️ 这是本模块唯一容易做错的地方。Mini 4 Pro 的云台 Pan(水平转)机械行程只有\n * **±30°**(本库 `DRONE_SPEC.gimbalYawRange` 按 ±5° 建模),而 180° 全景需要 ±90° 水平\n * 视野 —— 云台根本转不出来。所以真机的做法是:\n *\n * - **水平**:飞行器定点悬停、**空间坐标不动、原地自转机身**(Yaw),每转到一个方位停下来;\n * - **垂直**:云台只做俯仰分层(上 / 中 / 下),每层一张;\n * - 180°:7 个方位 × 3 个俯仰 = 21 张;\n * - 球形 360°:8 个方位;垂直层数按相机实际垂直视场自动计算,另拍天顶与天底。\n *\n * 一句话:**水平广角靠机身原地自转,垂直分层靠云台俯仰**。因此本模块的扫描循环\n * 是「先转机身到这一列 → 再按行拍俯仰」,而不是\"只转取景相机\"。老款 Mavic 2 有大角度\n * 云台 Pan,可以云台自己转着拍;Mini 系列不行。\n *\n * ── 三条设计约束 ──────────────────────────────────────────────────────\n *\n * - **只依赖 `CaptureService`**:转机身走 `spinBodyTo`(后端把目标航向交给飞控内核)、\n * 俯仰覆盖走 `setCaptureView`(见 `captureVirtualImage` 的 `options.view`)。\n * 模块本身既拿不到也不该拿渲染后端,换渲染器时一行都不用改。\n * - **每张都带全景定位**:写进 `CapturedImage.metadata.panorama`(组 id / 模式 / 序号 /\n * 总数 / 行列 / 该张的机身航向偏移与俯仰)。组内照片因此可被后端按 `group` 聚合、\n * 按 `index` 排序,单独一张也仍是完整的影像条目(有位姿、有内参、可单独校验)。\n * - **串行**:渲染后端的拍照是\"一帧一张\"的(上一张未取回时 `requestPhoto()` 会拒),\n * 所以扫描必须等一张落地再动下一个方位,否则会丢帧。\n *\n * ```ts\n * const pano = new PanoramaCapture(captureService)\n * pano.subscribe((p) => console.log(`${p.index}/${p.total}`))\n * const result = await pano.start(session.id, 'pano-360') // 垂直层数按实际视场自适应\n * pano.cancel() // 中途可停\n * ```\n */\nimport type { CaptureService, CapturedImage, PanoramaShotRef } from './drone-capture'\n\n/** 全景模式 */\nexport type PanoramaMode = 'pano-wide' | 'pano-180' | 'pano-360'\n\n/** 网格里的一个拍摄点 */\nexport interface PanoramaShotSpec {\n /** 组内序号(1 起,按扫描顺序:先扫完一个机身方位,再转下一个) */\n index: number\n /** 网格行列(0 起;row = 俯仰层, col = 机身水平方位) */\n row: number\n col: number\n /** 该张的**机身航向偏移**(度,相对本组起始航向;正值 = 向右/顺时针) */\n bodyYaw: number\n /** 该张的取景俯仰(度,相对水平面的绝对值) */\n pitch: number\n}\n\n/** 全景预设 */\nexport interface PanoramaPreset {\n mode: PanoramaMode\n label: string\n /** 纵向拍摄层数(含球形模式单独补拍的天顶/天底) */\n rows: number\n /** 机身水平方位数(每一列要原地自转一次) */\n cols: number\n /** 环拍网格每层的取景俯仰(度);按从上到下排列 */\n rowPitches: number[]\n /** 第一列的机身偏航(度,相对起始航向) */\n bodyYawStart: number\n /** 每列之间的机身偏航步进(度) */\n bodyYawStep: number\n /**\n * 扫描结束时机身应停在的偏航(度,相对起始航向)。\n * 180° 网格停在 +90°(就是最后一列);球形 360° 补完天底后回到起始航向。\n */\n finalBodyYaw: number\n /** 全部拍摄点(按扫描顺序:列优先) */\n shots: PanoramaShotSpec[]\n total: number\n}\n\n/** 无法读取相机内参时采用的保守垂直视场角 */\nconst FALLBACK_VERTICAL_FOV_DEG = 24\n/** 相邻垂直视场的最大中心间距比例;step <= FOV×0.6 即至少 40% 角度重叠 */\nconst MAX_VERTICAL_STEP_FOV_RATIO = 0.6\n\n/**\n * 广角采用 3×3 网格(9 张);180° 采用 7×3 网格(21 张);\n * 球形 360° 采用 8 个方位;俯仰层数按相机实际垂直视场自动计算,并补天顶/天底。\n *\n * - 广角:机身方位 -30°/0°/+30°,每个方位拍 +20°/0°/-20°,共 9 张;\n * - 180°:机身从 -90° 转到 +90°(共 180°),7 个方位 × 3 层 = 21 张,拍完就停在 +90°;\n * - 360° 球形:8 个方位(0°~315°,步进 45°);俯仰层根据实际垂直视场布满 -90°~+90°,\n * 相邻层中心距不超过垂直 FOV 的 60%,再补天顶/天底;拍完机身回到起始航向。\n *\n * 垂直环拍步长按实际垂直 FOV 计算:中心间距最大为 FOV×0.6,保留至少 40% 理论重叠,\n * 避免宽屏窄垂直视场下固定步长留下整圈未覆盖纬度带。\n * 额外的天顶与天底照片覆盖球面两极,而不是依赖合成器把最近边缘像素拉伸过去。\n * 360° 用 45° 方位步进;默认 Cesium 水平视场约 60°,仍有约 15° 水平重叠。\n */\nexport const PANORAMA_PRESETS: Record<PanoramaMode, PanoramaPreset> = {\n 'pano-wide': buildPreset('pano-wide', '广角', {\n bodyYawStart: -30,\n cols: 3,\n bodyYawStep: 30,\n rowPitches: [20, 0, -20],\n finalBodyYaw: 30,\n }),\n 'pano-180': buildPreset('pano-180', '180° 全景', {\n bodyYawStart: -90,\n cols: 7,\n bodyYawStep: 30,\n rowPitches: [20, -15, -50],\n // 收尾姿态 = 最后一列(+90°),不再多转\n finalBodyYaw: 90,\n }),\n 'pano-360': buildPreset('pano-360', '360° 全景', {\n bodyYawStart: 0,\n cols: 8,\n bodyYawStep: 45,\n rowPitches: sphericalPitchGrid(FALLBACK_VERTICAL_FOV_DEG),\n // 天顶/天底与首列同方位;拍完整球后机身回到起始航向\n finalBodyYaw: 0,\n poles: true,\n }),\n}\n\n/** 供 UI 渲染的稳定顺序 */\nexport const PANORAMA_MODE_LIST: PanoramaMode[] = ['pano-wide', 'pano-180', 'pano-360']\n\nfunction buildPreset(\n mode: PanoramaMode,\n label: string,\n spec: {\n bodyYawStart: number\n cols: number\n bodyYawStep: number\n rowPitches: number[]\n finalBodyYaw: number\n poles?: boolean\n },\n): PanoramaPreset {\n const shots: PanoramaShotSpec[] = []\n if (spec.poles) {\n shots.push({ index: shots.length + 1, row: 0, col: 0, bodyYaw: 0, pitch: 90 })\n }\n // 列优先:真机是\"转一个方位 → 上中下三张\",不是\"每个俯仰层转一圈\"\n for (let col = 0; col < spec.cols; col += 1) {\n const bodyYaw = normalizeYaw(spec.bodyYawStart + col * spec.bodyYawStep)\n spec.rowPitches.forEach((pitch, row) => {\n shots.push({ index: shots.length + 1, row: row + (spec.poles ? 1 : 0), col, bodyYaw, pitch })\n })\n }\n if (spec.poles) {\n shots.push({\n index: shots.length + 1,\n row: spec.rowPitches.length + 1,\n col: 0,\n bodyYaw: 0,\n pitch: -90,\n })\n }\n return {\n mode,\n label,\n rows: spec.rowPitches.length + (spec.poles ? 2 : 0),\n cols: spec.cols,\n rowPitches: [...spec.rowPitches],\n bodyYawStart: spec.bodyYawStart,\n bodyYawStep: spec.bodyYawStep,\n finalBodyYaw: spec.finalBodyYaw,\n shots,\n total: shots.length,\n }\n}\n\n/** 把角度收敛到 (-180, 180],读起来更直观(-90 而不是 270) */\nfunction normalizeYaw(deg: number): number {\n const wrapped = ((deg + 180) % 360) - 180\n return wrapped === -180 ? 180 : wrapped\n}\n\n/** 方位角归一到 [0, 360):罗盘角没有负数 */\nfunction normalizeHeading(deg: number): number {\n return ((deg % 360) + 360) % 360\n}\n\n/** 两个方位角之间的最小夹角(度,恒为非负) */\nfunction headingGap(a: number, b: number): number {\n return Math.abs(((a - b + 540) % 360) - 180)\n}\n\n/** 全景扫描状态 */\nexport type PanoramaState = 'idle' | 'running' | 'done' | 'cancelled' | 'failed'\n\n/** 扫描进度(面板订阅它做展示) */\nexport interface PanoramaProgress {\n state: PanoramaState\n mode: PanoramaMode\n /** 已完成张数 */\n index: number\n total: number\n /** 当前(或刚完成)的网格位置 */\n row: number\n col: number\n /** 当前列的机身航向偏移(度,相对起始航向) */\n bodyYaw: number\n pitch: number\n /** 组 id(开始拍摄后才有) */\n group: string | null\n message?: string\n}\n\n/** 一组全景照片(交给拼接器) */\nexport interface PanoramaGroup {\n id: string\n mode: PanoramaMode\n sessionId: string\n /** 按组内序号排序 */\n images: CapturedImage[]\n}\n\n/** 拼接产物(库里只搬运,不解释) */\nexport interface PanoramaStitchResult {\n /** 可直接显示的地址(blob: / http: / data:);以字符串打开时按标准整球解释 */\n url?: string\n /** 可选的覆盖热力图:红=无照片覆盖,黄=单张覆盖,绿=多张重叠 */\n diagnosticUrl?: string\n /** 覆盖热力图像素占比;未覆盖区域必须由拼接器补边,存在视差风险区域通常只被一张照片覆盖 */\n diagnosticCoverage?: { uncovered: number; single: number; overlap: number }\n /** 也可以直接给二进制,由宿主决定怎么用 */\n blob?: Blob\n width?: number\n height?: number\n name?: string\n /**\n * 有效覆盖比例(0~1) —— 拼接后画面里\"真的有像素\"的部分占比。\n * 库内自带拼接器(见 drone-stitch)会填;宿主自己的拼接器可以不填。\n * 拿它判断\"这组是不是拍漏了\"(比如中途没转到位,角落空一块)。\n */\n coverage?: number\n /** 局部全景带的球面范围;缺省表示标准 360°×180° 整球。 */\n equirectangular?: {\n thetaStart: number\n spanTheta: number\n phiTop: number\n spanPhi: number\n wrapsTheta: boolean\n }\n}\n\n/**\n * 拼接器接口 —— **库内不提供实现**,由宿主注入(后端服务、WASM 拼接库、离线工具都行)。\n * 整组拍完(未取消)后自动调用一次;返回 null 表示\"这次不拼\"。\n */\nexport interface PanoramaStitcher {\n /** 把一组已拍照片拼成等距柱状全景(按位姿反投影;覆盖不足返回 null) */\n stitch(group: PanoramaGroup): Promise<PanoramaStitchResult | null>\n}\n\n/** 一次扫描的最终结果 */\nexport interface PanoramaResult {\n group: string\n mode: PanoramaMode\n total: number\n /** 实际入库张数(成片失败会少于 total) */\n captured: number\n cancelled: boolean\n /** 没能完成的原因(机体没起飞 / 后端不支持自转 / 机身转不到位),成功时为 undefined */\n error?: string\n /** 拼接产物(注入过拼接器且未取消时才有) */\n stitched?: PanoramaStitchResult | null\n}\n\n/** `PanoramaCapture` 构造选项 */\nexport interface PanoramaCaptureOptions {\n /**\n * 每张之间的间隔(毫秒),默认 0(拍完立即转下一个方位)。\n * 想更贴近真机节奏、或给渲染后端留稳定时间时调大(真机每张约 0.5~1s)。\n */\n shotIntervalMs?: number\n /**\n * 等待机身转到位的最长时间(毫秒),默认 8000。\n * 超时即判定\"转不过去\"(没起飞、正在返航、速度倍率为 0),以失败收尾而不是硬拍。\n */\n spinTimeoutMs?: number\n /** 拼接器(可选) */\n stitcher?: PanoramaStitcher | null\n /** 整组拍完并完成拼接后的回调(宿主落盘 / 提示用) */\n onStitched?: (result: PanoramaStitchResult | null, group: PanoramaGroup) => void\n}\n\n/**\n * 判定\"机身已转到目标方位\"的容差(度)。\n * 取 0.5° 与飞控内核同值:航向是连续量,`turnToward` 的末段步长会缩到恰好落在目标上\n * (不会过冲),所以收得紧一点也能收敛,换来的是\"方位角与预设逐格一致\"这种干净结论。\n */\nconst SPIN_TOLERANCE_DEG = 0.5\n/** 等待机身自转时的轮询间隔(毫秒) */\nconst SPIN_POLL_MS = 16\n/** 默认的自转等待上限(毫秒):360° 一圈按 90°/s 约 4 s */\nconst DEFAULT_SPIN_TIMEOUT_MS = 8000\n/** 生成组 id 用的时间戳(本地时间,精确到秒) */\nfunction groupStamp(): string {\n const now = new Date()\n const pad = (value: number): string => String(value).padStart(2, '0')\n return (\n `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +\n `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`\n )\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/** 根据相机垂直 FOV 生成覆盖两极的俯仰层,相邻层中心距不超过 FOV 的 60%。 */\nexport function sphericalPitchGrid(verticalFovDeg: number): number[] {\n const fov = Number.isFinite(verticalFovDeg)\n ? Math.max(1, Math.min(179, verticalFovDeg))\n : FALLBACK_VERTICAL_FOV_DEG\n const intervals = Math.max(2, Math.ceil(180 / (fov * MAX_VERTICAL_STEP_FOV_RATIO)))\n const step = 180 / intervals\n return Array.from({ length: intervals - 1 }, (_, index) => 90 - step * (index + 1))\n}\n\nfunction sphericalPreset(verticalFovDeg: number): PanoramaPreset {\n return buildPreset('pano-360', '360° 全景', {\n bodyYawStart: 0,\n cols: 8,\n bodyYawStep: 45,\n rowPitches: sphericalPitchGrid(verticalFovDeg),\n finalBodyYaw: 0,\n poles: true,\n })\n}\n\n/**\n * 全景扫描器:先转机身到方位、再按俯仰层逐张取景拍摄,把每张登记进 `CaptureService` 的会话。\n *\n * 只依赖 `CaptureService` —— 转机身走 `spinBodyTo` / `getBodyHeading`,俯仰覆盖由服务在每张\n * 内部设置并复位(见 `CaptureService.captureVirtualImage` 的 `options.view`),\n * 这里既拿不到也不该拿渲染后端,于是换渲染器时本模块一行都不用改。\n */\nexport class PanoramaCapture {\n private readonly capture: CaptureService\n private readonly listeners = new Set<(progress: PanoramaProgress) => void>()\n private options: PanoramaCaptureOptions\n /** 取消令牌:自增即让正在跑的扫描退出 */\n private token = 0\n private current: PanoramaProgress | null = null\n\n /** 构造全景扫描器:按俯仰分层 × 机体自转覆盖 360° */\n constructor(capture: CaptureService, options: PanoramaCaptureOptions = {}) {\n this.capture = capture\n this.options = { ...options }\n }\n\n /** 是否正在扫描 */\n get running(): boolean {\n return this.current?.state === 'running'\n }\n\n /** 当前进度快照(未拍摄过返回 null) */\n get progress(): PanoramaProgress | null {\n return this.current ? { ...this.current } : null\n }\n\n /** 订阅进度变化;返回退订函数 */\n subscribe(listener: (progress: PanoramaProgress) => void): () => void {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n /** 设置 / 替换拼接器(传 null 关闭) */\n setStitcher(stitcher: PanoramaStitcher | null): void {\n this.options = { ...this.options, stitcher }\n }\n\n /** 取预设(UI 展示行列数 / 张数用) */\n static preset(mode: PanoramaMode, verticalFovDeg?: number): PanoramaPreset {\n return mode === 'pano-360' && verticalFovDeg !== undefined\n ? sphericalPreset(verticalFovDeg)\n : PANORAMA_PRESETS[mode]\n }\n\n /**\n * 开始一次扫描。已在扫描中、或会话不存在时返回 null(不排队,避免误触连发)。\n *\n * 成片失败(如后端某一帧相机未就绪)只计入 `captured` 的缺口,不中断整组 ——\n * 少一张的代价远小于让用户重拍一整组;但**机身转不到位**是另一回事:那说明网格根本没\n * 施加成功(没起飞 / 正在返航 / 速度倍率为 0),继续拍只会得到一堆同一方位的废片,\n * 所以立即以失败收尾。\n */\n async start(sessionId: string, mode: PanoramaMode): Promise<PanoramaResult | null> {\n if (this.running) return null\n const session = this.capture.getSession(sessionId)\n if (!session) return null\n const verticalFovDeg = this.capture.getCaptureIntrinsics()?.verticalFovDeg\n const preset =\n mode === 'pano-360'\n ? sphericalPreset(verticalFovDeg ?? FALLBACK_VERTICAL_FOV_DEG)\n : PANORAMA_PRESETS[mode]\n const group = `${mode}-${groupStamp()}`\n const token = (this.token += 1)\n\n let captured = 0\n // 进度里要播\"当前在哪一格\";shots 恒非空(预设至少一行一列),这里给个零值兜底\n let last: PanoramaShotSpec = preset.shots[0] ?? {\n index: 0,\n row: 0,\n col: 0,\n bodyYaw: 0,\n pitch: 0,\n }\n this.setProgress({\n state: 'running',\n mode,\n index: 0,\n total: preset.total,\n row: last.row,\n col: last.col,\n bodyYaw: last.bodyYaw,\n pitch: last.pitch,\n group,\n })\n\n const fail = (message: string): PanoramaResult => {\n this.capture.spinBodyTo(null)\n this.setProgress({\n state: 'failed',\n mode,\n index: captured,\n total: preset.total,\n row: last.row,\n col: last.col,\n bodyYaw: last.bodyYaw,\n pitch: last.pitch,\n group,\n message,\n })\n return { group, mode, total: preset.total, captured, cancelled: false, error: message }\n }\n\n // 水平网格只能靠机体自转:后端没有这个能力就没法拍,提前收场而不是拍一堆废片\n const startHeading = this.capture.getBodyHeading()\n if (!this.capture.canSpinBody || startHeading === null) {\n return fail('当前渲染后端不支持机体原地自转,无法完成全景水平网格')\n }\n\n let column = -1\n for (const shot of preset.shots) {\n if (this.token !== token) break\n\n // 换列了 —— 先让机身原地自转到这一列的方位,等它真的对准再拍\n if (shot.col !== column) {\n column = shot.col\n const target = normalizeHeading(startHeading + shot.bodyYaw)\n this.capture.spinBodyTo(target)\n const aligned = await this.awaitHeading(target, token)\n if (this.token !== token) break\n if (!aligned) return fail(`机身未能转到 ${target.toFixed(0)}°(需定点悬停中再拍)`)\n }\n\n last = shot\n // 先把\"正在拍哪一格\"播出去:进度条与取景角同步,而不是等成片回来才跳\n this.setProgress({\n state: 'running',\n mode,\n index: captured,\n total: preset.total,\n row: shot.row,\n col: shot.col,\n bodyYaw: shot.bodyYaw,\n pitch: shot.pitch,\n group,\n })\n const image = await this.capture.captureVirtualImage(sessionId, {\n view: { pitch: shot.pitch },\n panorama: panoramaRef(group, mode, shot, preset.total),\n })\n // 先计数再判取消:取消请求可能在等待成片时到达,而这张**已经入库**了,\n // 漏算会让\"入库张数\"与 result.captured 对不上(导出/日志都会少一张)\n if (image) captured += 1\n if (this.token !== token) break\n if (this.options.shotIntervalMs && this.options.shotIntervalMs > 0) {\n await delay(this.options.shotIntervalMs)\n }\n }\n\n const cancelled = this.token !== token\n const result: PanoramaResult = { group, mode, total: preset.total, captured, cancelled }\n\n // 收尾姿态:180° 网格停在 +90°(已到位,不再转);360° 网格把最后 30° 转满、回到起始航向。\n // 照片已经拍完了,所以这里转不到位不算失败(比如用户刚好触发返航),只是姿态没回正。\n if (!cancelled) {\n const finalHeading = normalizeHeading(startHeading + preset.finalBodyYaw)\n const current = this.capture.getBodyHeading()\n if (current === null || headingGap(current, finalHeading) > SPIN_TOLERANCE_DEG) {\n this.capture.spinBodyTo(finalHeading)\n await this.awaitHeading(finalHeading, token)\n }\n }\n // 停自转:目标留在那里的话,后面任何一次进入飞行态都会被它继续拽着转\n this.capture.spinBodyTo(null)\n\n // 整组拍完才谈拼接:取消的组是半成品,拼了也没意义\n if (!cancelled) {\n const stitcher = this.options.stitcher\n if (stitcher) {\n const built = this.buildGroup(sessionId, group, mode)\n if (built && built.images.length) {\n try {\n result.stitched = await stitcher.stitch(built)\n } catch (error) {\n this.setProgress({\n state: 'done',\n mode,\n index: captured,\n total: preset.total,\n row: last.row,\n col: last.col,\n bodyYaw: last.bodyYaw,\n pitch: last.pitch,\n group,\n message: `拼接失败:${(error as Error).message}`,\n })\n return result\n }\n this.options.onStitched?.(result.stitched ?? null, built)\n }\n }\n }\n\n this.setProgress({\n state: cancelled ? 'cancelled' : 'done',\n mode,\n index: captured,\n total: preset.total,\n row: last.row,\n col: last.col,\n bodyYaw: last.bodyYaw,\n pitch: last.pitch,\n group,\n message: cancelled\n ? `已取消(完成 ${captured}/${preset.total})`\n : `完成 ${captured}/${preset.total}`,\n })\n return result\n }\n\n /** 请求停止当前扫描(当前那一张会拍完再退出) */\n cancel(): void {\n if (!this.running) return\n this.token += 1\n this.setProgress({ ...(this.current as PanoramaProgress), state: 'cancelled' })\n }\n\n /** 按组 id 从会话里聚合已拍的照片(按序号排序)—— 拼接器与导出都用它 */\n buildGroup(sessionId: string, group: string, mode: PanoramaMode): PanoramaGroup | null {\n const session = this.capture.getSession(sessionId)\n if (!session) return null\n const images = session.images\n .filter((image) => image.metadata.panorama?.group === group)\n .sort((a, b) => (a.metadata.panorama?.index ?? 0) - (b.metadata.panorama?.index ?? 0))\n return { id: group, mode, sessionId, images }\n }\n\n /**\n * 等机身转到目标方位(轮询真值,而不是假设\"下一条命令必然到位\")。\n * 轮询间隔 16 ms、每个方位最多等 `spinTimeoutMs` —— 一帧的转角只有 1~2°,\n * 所以这里的等待粒度对结果没有影响,只影响进度播报的平滑度。\n */\n private async awaitHeading(target: number, token: number): Promise<boolean> {\n const timeout = this.options.spinTimeoutMs ?? DEFAULT_SPIN_TIMEOUT_MS\n const deadline = Date.now() + timeout\n for (;;) {\n const heading = this.capture.getBodyHeading()\n if (heading !== null && headingGap(heading, target) <= SPIN_TOLERANCE_DEG) return true\n if (this.token !== token) return false\n if (Date.now() >= deadline) return false\n await delay(SPIN_POLL_MS)\n }\n }\n\n private setProgress(progress: PanoramaProgress): void {\n this.current = progress\n for (const listener of this.listeners) listener({ ...progress })\n }\n}\n\n/** 组装写进影像元数据的全景定位 */\nfunction panoramaRef(\n group: string,\n mode: PanoramaMode,\n shot: PanoramaShotSpec,\n total: number,\n): PanoramaShotRef {\n return {\n group,\n mode,\n index: shot.index,\n total,\n row: shot.row,\n col: shot.col,\n bodyYaw: shot.bodyYaw,\n pitch: shot.pitch,\n }\n}\n","/**\n * 无人机控制面板(原生 DOM 实现):王者荣耀式 HUD —— 双虚拟摇杆 + 技能按钮 + 飞行控制面板。\n *\n * 布局(仿王者荣耀):\n * - 左右两根虚拟摇杆**脱离面板**,作为悬浮 HUD 挂在画面左下 / 右下角(控制罗盘左右各一个);\n * - 拍照 / 录像是「技能」按钮,悬浮在右轮盘上方(蓝圈 = 拍照,橙圈 = 录像),\n * 角标显示拍照张数 / 录制秒数(对应游戏里技能的buff计数);\n * - 面板本体只留飞行控制(上电/起飞/降落/返航/机臂/模式)+ 速度倍率(0~1000,数值即倍数)+\n * 视角切换 + 云台,停靠按 position 选项。\n *\n * 设计边界:\n * - 只用原生 DOM / CSS,不引入任何 UI 框架 —— 库整体保持与框架无关(Vue / React / 裸页面通用);\n * - 面板自身不持有状态:渲染时从 DroneFly / DroneSim 读,操作时调 DroneFly 方法,\n * 真值永远以引擎为准(见 sync)。宿主用别的方式改了视角或云台,面板下一轮同步就会跟上,不会脱节;\n * - 样式默认注入一份(类名统一 `dcp-` 前缀、配色走 CSS 变量),可用 injectStyles:false 关闭,由宿主自行配色。\n *\n * 非直觉约定:\n * - 面板以 200ms 轮询同步状态(可用 syncIntervalMs 调或关);拖动云台滑杆期间暂停回写,避免与用户输入打架;\n * - 键盘是屏上双摇杆的第二套输入:按键时对应那根杆会**同步推杆**(杆帽位移 + 高亮 + 读数),\n * 看起来就像有人在替你打杆;两套输入合成时\"摇杆优先\"(见 applyStick);\n * - 技能(拍照 / 录像)另有组合键 `Ctrl`(`Cmd`)+ `K` / `L`,按一次触发一次,与点技能钮同一路径;\n * - 摇杆按 DJI 遥控器(美国手 Mode 2)的杆位分工:左杆 = 上/下升降 + 左/右偏航,右杆 = 上/下前后 + 左/右横滚。\n * 两根杆都是**自回中**的:松手立刻归零并回到中心 —— 对应真机的\"松杆即停\",不会因为漏掉抬起事件一直飞;\n * - 摇杆用 Pointer Events + setPointerCapture 实现,鼠标 / 触摸 / 触控笔同一套代码,\n * 并且按 pointerId 分别跟踪,双指可以同时推两根杆(真机双手打杆);\n * - 悬浮层与面板是**两个兄弟节点**(都挂在 container 上):面板有 backdrop-filter,会把 fixed 后代的定位\n * 基准改成面板自身,所以摇杆不能塞进面板里;悬浮层 pointer-events:none、只留轮盘可点,不挡场景交互。\n */\nimport type { DronePanelHost } from './drone-control'\nimport { CAMERA_MODE_LIST } from './drone-fly'\nimport type { CameraMode, PhotoShot } from './drone-fly'\nimport {\n DEFAULT_SPEED_SCALE,\n FLIGHT_MODE_LIST,\n PHASE_LABELS,\n SPEED_SCALE_MAX,\n SPEED_SCALE_MIN,\n} from './drone-sim'\nimport type { FlightMode } from './drone-sim'\nimport { PANORAMA_MODE_LIST, PANORAMA_PRESETS, PanoramaCapture } from './drone-panorama'\nimport type { PanoramaMode, PanoramaProgress } from './drone-panorama'\nimport type { CaptureService } from './drone-capture'\n\n/** 拍摄模式:单张 / 广角 / 180° 全景 / 球形 360° */\ntype ShootMode = 'single' | PanoramaMode\n\n/** 全景每张之间的默认间隔(毫秒):贴近真机节奏,也给渲染后端留出稳定时间 */\nconst DEFAULT_PANORAMA_INTERVAL_MS = 120\n\n/** 面板停靠位置(相对挂载容器) */\nexport type DronePanelPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\n\n/** DroneControlPanel 构造选项 */\nexport interface DroneControlPanelOptions {\n /** 挂载容器;缺省挂到 document.body(此时面板与悬浮层改用 fixed 定位) */\n container?: HTMLElement | null\n /** 面板停靠位置,默认 'top-right';左右轮盘固定悬浮在画面两下角,不受此项影响 */\n position?: DronePanelPosition\n /** 面板标题,默认「无人机」 */\n title?: string\n /** 是否注入自带样式,默认 true;关闭后仅保留 `dcp-*` 类名,由宿主自行配色 */\n injectStyles?: boolean\n /** 云台俯仰快捷档位(度),默认 [-90, -30, -10, 0] */\n pitchPresets?: number[]\n /** 初始是否可见,默认 true */\n visible?: boolean\n /** 拍照完成回调;未提供且 autoDownload 为 true 时自动下载 PNG */\n onPhoto?: (shot: PhotoShot) => void\n /**\n * 采集服务(可选)。提供后面板出现「拍摄模式」区,可切换广角 / 180° / 360°:\n * 全景按预设网格逐张取景拍摄,每张都带全景分组信息登记进这个服务的当前会话。\n *\n * 与采集面板共用同一个实例时,全景照片会直接出现在那边的影像集里(见 demo 的接法)。\n */\n captureService?: CaptureService\n /** 是否启用全景拍摄(默认 true);没有 `captureService` 时不显示该区 */\n panorama?: boolean\n /** 全景每张之间的间隔(毫秒),默认 120 */\n panoramaShotIntervalMs?: number\n /** 录像完成回调;未提供且 autoDownload 为 true 时自动下载 webm */\n onRecording?: (blob: Blob, seconds: number) => void\n /** 无自定义回调时是否自动下载产物,默认 true */\n autoDownload?: boolean\n /** 状态轮询间隔(毫秒),默认 200;传 0 关闭自动同步、完全由宿主调 sync() */\n syncIntervalMs?: number\n /** 摇杆死区(0~1),默认 0.06;小于该幅值视为 0,抵住手指轻微抖动。作用于垂直通道(升降 / 前后) */\n stickDeadzone?: number\n /**\n * 横滚 / 偏航死区(0~1),默认 0.14。比垂直通道大一档 —— 触屏推杆时手指很难保证绝对竖直,\n * 轻微的侧向分量会被放大成持续横移和机头偏转,几分钟就飞出弧线。加大这一路死区等于给\n * 直线飞行留出一条\"安全走廊\",垂直通道不受影响、起手依然跟手。\n */\n stickLateralDeadzone?: number\n /**\n * 摇杆指数曲线 expo(0~1),默认 0.55,真机遥控器的 expo 手感:\n * 输出 = expo·v³ + (1-expo)·v —— 中位附近行程放慢(便于精确微调),\n * 末端仍能打满舵量(1 处恒等于 1),推到底的极速与转向权限不受影响。\n * 传 0 退回线性响应。\n */\n stickExpo?: number\n /**\n * 偏航角速度缩放(0~1],默认 0.6。左杆左右 / ←→ 键的转向输出乘这个系数 ——\n * 飞控的挡位角速度是照真机标的(普通挡 100°/s,满杆 3.6 秒转一圈),\n * 在屏幕遥控器上偏快;这里只压遥控器侧的输出,飞控与挡位表不动。\n * 传 1 退回真机原始速率。\n */\n yawRateScale?: number\n /**\n * 是否启用键盘控制,默认 true。键位(与屏上双摇杆等效,左手 WASD 对左杆、右手方向键对右杆):\n * `W/S` 升降 · `A/D` 转向(左杆) · `↑/↓` 前后 · `←/→` 横滚(右杆);`Q/E` 是转向的备用键。\n * 另外两组技能快捷键:`Ctrl`(macOS 上 `Cmd`)+ `K` 拍照、`Ctrl` + `L` 录像(开关,再按一次停止),\n * 等同于点右轮盘上的拍照 / 录像钮,长按不连发。\n * 键盘是开关量,内部做了渐进行程(按下渐推、松开渐回),不会突然满舵。\n * 焦点在输入框内时不拦截按键。\n */\n keyboard?: boolean\n /** 键盘推杆渐推时间(秒),默认 0.45;越大越柔和 */\n keyboardRiseSeconds?: number\n /** 键盘松手渐回时间(秒),默认 0.18 */\n keyboardFallSeconds?: number\n /** 是否启用移动端自适应(横屏紧凑布局 / 触控尺寸),默认 true */\n adaptive?: boolean\n}\n\nconst STYLE_ID = 'drone-control-panel-styles'\nconst DEFAULT_PITCH_PRESETS = [-90, -30, -10, 0]\n/**\n * 速度倍率滑杆的步长。\n * 行程 0 ~ 1000(数值即倍数),步长取 0.5 而不是 1 —— 倍率越小差异越明显,\n * 0.5×(半速)是很常用的一档,整数步长到不了它;0.5 步长下整数与 .5 都能取到。\n */\nconst SPEED_SCALE_STEP = 0.5\n/** 速度倍率快捷档位:半速 / 原速 / 2 倍 / 5 倍 / 10 倍(超过 10 倍靠滑杆或直接输入) */\nconst SPEED_SCALE_PRESETS: Array<{ value: number; label: string }> = [\n { value: 0.5, label: '0.5×' },\n { value: 1, label: '1×' },\n { value: 2, label: '2×' },\n { value: 5, label: '5×' },\n { value: 10, label: '10×' },\n]\n/** 云台俯仰可用范围,与 DRONE_SPEC 保持一致 */\nconst PITCH_MIN = -90\nconst PITCH_MAX = 60\n/** 摇杆死区:与 DroneSim 内部 0.06 的\"有效行程\"阈值对齐,免得面板给了量而飞控不认 */\nconst DEFAULT_STICK_DEADZONE = 0.06\n/** 横滚 / 偏航死区:比垂直通道大一档,给直线飞行留安全走廊 */\nconst DEFAULT_STICK_LATERAL_DEADZONE = 0.14\n/** 指数曲线:中位附近放慢、末端打满,DJI 遥控器的默认 expo 手感 */\nconst DEFAULT_STICK_EXPO = 0.55\n/** 偏航角速度缩放:真机挡位 100°/s 在屏幕遥控器上偏快,压到 60°/s 左右 */\nconst DEFAULT_YAW_RATE_SCALE = 0.6\n/** 键盘推杆渐推 / 渐回时间(秒) */\nconst DEFAULT_KEYBOARD_RISE = 0.45\nconst DEFAULT_KEYBOARD_FALL = 0.18\n/** 键盘渐进行程的刷新间隔(毫秒),~60Hz */\nconst KEYBOARD_TICK_MS = 16\n\n/** 摇杆四通道,键盘与摇杆合成时按这个顺序遍历 */\nconst STICK_CHANNELS = ['throttle', 'yaw', 'pitch', 'roll'] as const\ntype StickChannel = (typeof STICK_CHANNELS)[number]\n\n/**\n * 键盘键位 → 摇杆通道。\n * 左手 WASD 打左杆、右手方向键打右杆 —— 与屏上双摇杆的左右手分工对齐:\n * W/S 升降、A/D 转向(=左杆),↑/↓ 前后、←/→ 横滚(=右杆);Q/E 是转向的备用键。\n */\nconst KEY_BINDINGS: Record<string, { channel: StickChannel; sign: number }> = {\n KeyW: { channel: 'throttle', sign: 1 },\n KeyS: { channel: 'throttle', sign: -1 },\n KeyA: { channel: 'yaw', sign: -1 },\n KeyD: { channel: 'yaw', sign: 1 },\n ArrowUp: { channel: 'pitch', sign: 1 },\n ArrowDown: { channel: 'pitch', sign: -1 },\n ArrowLeft: { channel: 'roll', sign: -1 },\n ArrowRight: { channel: 'roll', sign: 1 },\n KeyQ: { channel: 'yaw', sign: -1 },\n KeyE: { channel: 'yaw', sign: 1 },\n}\n\n/**\n * 组合键快捷键:`Ctrl`(macOS 上 `Cmd`) + 键 → 面板动作。\n * 与上面的杆位键不是一回事 —— 杆位是\"按住持续输出\",快捷键是\"按一次触发一次\"(等同点技能钮),\n * 走的是同一个 takePhoto / toggleRecording,所以张数角标、录制计时、回调都跟点按钮完全一致。\n */\nconst KEY_SHORTCUTS: Record<string, PanelShortcut> = {\n KeyK: 'photo',\n KeyL: 'record',\n}\n/** 组合键能触发的动作;'record' 是开关(开始 / 停止在同一键上切换) */\ntype PanelShortcut = 'photo' | 'record'\n\n/** 焦点是否在输入类元素上(输入框 / 文本域 / 下拉 / 可编辑区):这些地方的按键归宿主,面板不接管 */\nfunction isTypingTarget(target: EventTarget | null): boolean {\n const el = target as HTMLElement | null\n if (!el) return false\n const tag = el.tagName\n return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable\n}\n\n/** 速度倍率写进输入框的文本:整数原样,小数去掉多余的 0(0.5 → \"0.5\",2.0 → \"2\") */\nfunction formatScale(value: number): string {\n if (!Number.isFinite(value)) return String(DEFAULT_SPEED_SCALE)\n return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(2)))\n}\n\n/** 摇杆所在的一侧,决定行程写进哪两个通道 */\ntype StickSide = 'left' | 'right'\n\n/** 摇杆内部句柄:几何缓存直接向 DOM 取,旋转 / 缩放后依然准 */\ninterface StickHandle {\n side: StickSide\n root: HTMLElement\n base: HTMLElement\n knob: HTMLElement\n read: HTMLElement\n /** 正在按住的指针 id(按 pointerId 跟踪,支持双指同时打两根杆) */\n pointerId: number | null\n /** 该杆当前行程(死区 + expo 之后的最终输出),与键盘合成时用 */\n vertical: number\n lateral: number\n /** 满行程对应的画布半径(px);测量缓存,键盘每帧刷杆帽时不必反复读布局 */\n radius: number\n /** 上次测量时的底盘边长(px),尺寸一变(换屏 / 旋转)就重测 radius */\n radiusSpan: number\n}\n\nconst PANEL_CSS = `\n.dcp, .dcp-sticks-float {\n --dcp-bg: rgba(15, 19, 27, 0.84);\n --dcp-border: rgba(255, 255, 255, 0.12);\n --dcp-text: #e9eff9;\n --dcp-muted: rgba(233, 239, 249, 0.58);\n --dcp-accent: #35a7ff;\n --dcp-on-accent: #04121f;\n --dcp-danger: #ff5f5f;\n --dcp-radius: 10px;\n /* 摇杆底盘直径与杆帽直径(杆帽按底盘等比缩放,横屏/小屏会整体调) */\n --dcp-stick: 92px;\n --dcp-knob: calc(var(--dcp-stick) * 0.42);\n /* 技能钮(拍照/录像)直径:跟着底盘等比缩放,但夹在可点范围内 */\n --dcp-skill: clamp(30px, calc(var(--dcp-stick) * 0.38), 38px);\n font: 12px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n user-select: none;\n -webkit-user-select: none;\n -webkit-tap-highlight-color: transparent;\n overscroll-behavior: contain;\n}\n.dcp {\n z-index: 20;\n /* 264px ≈ 任务面板(dmp 262px)同宽:230px 时预设行(0.5x/1x/2x/5x/10x 五连)、\n 四联分段控件挤到文字贴边;加宽只动行内空间,高度与块间距不变 */\n width: 264px;\n /* 高度封顶:面板加了拍摄模式区后内容超过 HUD 安全线(右下技能钮顶边距底 215px、\n 768 高的窗口里摇杆顶边距底 169px),必须让面板在矮视口里内部滚动,而不是压住摇杆 */\n max-height: min(66vh, 640px);\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n padding: 10px 12px 12px;\n border: 1px solid var(--dcp-border);\n border-radius: var(--dcp-radius);\n background: var(--dcp-bg);\n color: var(--dcp-text);\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);\n}\n/* 悬浮摇杆层:铺满容器但自身不接指针,只留轮盘/技能可点,不挡场景拖拽 */\n.dcp-sticks-float { inset: 0; z-index: 21; pointer-events: none; }\n.dcp[data-position=\"top-left\"] { top: 12px; left: 12px; }\n.dcp[data-position=\"top-right\"] { top: 12px; right: 12px; }\n.dcp[data-position=\"bottom-left\"] { bottom: 12px; left: 12px; }\n.dcp[data-position=\"bottom-right\"] { bottom: 12px; right: 12px; }\n.dcp button, .dcp input, .dcp-sticks-float button { touch-action: manipulation; }\n.dcp-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }\n.dcp-title { flex: 1; font-weight: 600; letter-spacing: 0.02em; }\n.dcp-icon {\n width: 20px; height: 20px; padding: 0; line-height: 18px;\n border: 1px solid var(--dcp-border); border-radius: 6px;\n background: transparent; color: var(--dcp-text); cursor: pointer; font-size: 12px;\n}\n.dcp-icon:hover { border-color: var(--dcp-accent); color: var(--dcp-accent); }\n.dcp-label { display: flex; justify-content: space-between; align-items: baseline; color: var(--dcp-muted); margin-bottom: 6px; }\n.dcp-label em { font-style: normal; color: var(--dcp-text); font-variant-numeric: tabular-nums; }\n/* 速度倍率:读数位放一个可直接输入的数字框(0~1000 的行程光靠拖滑杆到不了精确值) */\n.dcp-speed-val { display: inline-flex; align-items: center; gap: 2px; color: var(--dcp-text); }\n.dcp-speed-val i { font-style: normal; }\n.dcp-num {\n width: 56px; padding: 1px 4px; border-radius: 5px; text-align: right;\n border: 1px solid var(--dcp-border); background: rgba(255, 255, 255, 0.06);\n color: var(--dcp-text); font: inherit; font-variant-numeric: tabular-nums;\n /* 微调按钮在窄面板里太挤,且方向键已能微调,隐掉它 */\n -moz-appearance: textfield; appearance: textfield;\n}\n.dcp-num:focus { outline: none; border-color: var(--dcp-accent); }\n.dcp-num::-webkit-outer-spin-button,\n.dcp-num::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }\n.dcp-segmented { display: flex; gap: 4px; }\n.dcp-seg {\n flex: 1; min-width: 0; padding: 6px 0; border-radius: 7px; cursor: pointer;\n border: 1px solid var(--dcp-border); background: rgba(255, 255, 255, 0.04);\n color: var(--dcp-muted); font-size: 12px;\n}\n.dcp-seg:hover { color: var(--dcp-text); }\n.dcp-seg.is-active { background: var(--dcp-accent); border-color: var(--dcp-accent); color: var(--dcp-on-accent); font-weight: 600; }\n.dcp-wide {\n width: 100%; margin-top: 6px; padding: 6px 0; border-radius: 7px; cursor: pointer;\n border: 1px solid var(--dcp-border); background: rgba(255, 255, 255, 0.04); color: var(--dcp-text); font-size: 12px;\n}\n.dcp-wide:hover { border-color: var(--dcp-accent); color: var(--dcp-accent); }\n.dcp-range { width: 100%; margin: 2px 0 6px; accent-color: var(--dcp-accent); cursor: pointer; }\n.dcp-presets { display: flex; gap: 4px; }\n.dcp-preset {\n flex: 1; padding: 4px 0; border-radius: 6px; cursor: pointer; font-variant-numeric: tabular-nums;\n border: 1px solid var(--dcp-border); background: rgba(255, 255, 255, 0.04); color: var(--dcp-muted); font-size: 11px;\n}\n.dcp-preset:hover { color: var(--dcp-text); border-color: var(--dcp-accent); }\n.dcp-actions { display: flex; gap: 6px; }\n.dcp-flight-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }\n.dcp-flight {\n padding: 7px 0; border-radius: 7px; cursor: pointer; font-size: 12px;\n border: 1px solid var(--dcp-border); background: rgba(255, 255, 255, 0.06); color: var(--dcp-text);\n}\n.dcp-flight:hover:not(:disabled) { border-color: var(--dcp-accent); color: var(--dcp-accent); }\n.dcp-flight:disabled { opacity: 0.32; cursor: not-allowed; }\n.dcp-takeoff { border-color: rgba(53, 167, 255, 0.55); }\n.dcp-rth { border-color: rgba(255, 159, 67, 0.5); }\n.dcp-hint { margin: 6px 0 0; color: var(--dcp-muted); font-size: 11px; line-height: 1.4; }\n.dcp-hint:empty { display: none; }\n.dcp-note { min-height: 16px; margin: 8px 0 0; color: var(--dcp-muted); font-size: 11px; order: 7; }\n/* 键盘键位提示(手机横屏没键盘,媒体查询里会藏掉) */\n.dcp-keys { margin: 4px 0 0; color: var(--dcp-muted); font-size: 10px; line-height: 1.4; opacity: 0.72; order: 8; }\n.dcp-keys:empty { display: none; }\n.dcp-btn {\n flex: 1; padding: 7px 0; border-radius: 7px; cursor: pointer; font-size: 12px;\n border: 1px solid var(--dcp-border); background: rgba(255, 255, 255, 0.06); color: var(--dcp-text);\n}\n\n/* ── 主体:竖屏纵向流式;横屏(见媒体查询)改成两列网格 ── */\n.dcp-body {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n overscroll-behavior: contain;\n}\n/* display:contents 让各个控制块直接成为 .dcp-body 的 flex item */\n.dcp-mid { display: contents; }\n.dcp-blk { display: block; }\n.dcp-blk + .dcp-blk { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--dcp-border); }\n\n/* ── 悬浮双轮盘(仿王者荣耀:左下移动轮盘 + 右下技能轮盘) ── */\n.dcp-stick {\n position: absolute; pointer-events: auto;\n display: flex; flex-direction: column; align-items: center; gap: 4px;\n}\n.dcp-stick[data-stick=\"left\"] { left: 30px; bottom: 40px; }\n.dcp-stick[data-stick=\"right\"] { right: 30px; bottom: 40px; }\n.dcp-stick-base {\n position: relative;\n width: var(--dcp-stick); height: var(--dcp-stick);\n border-radius: 50%;\n border: 1px solid var(--dcp-border);\n background: radial-gradient(circle at 50% 32%, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.02) 62%, rgba(0, 0, 0, 0.22));\n box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.42), 0 6px 18px rgba(0, 0, 0, 0.3);\n cursor: grab;\n touch-action: none;\n -webkit-tap-highlight-color: transparent;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n}\n/* 十字刻度:让\"中心 / 行程极限\"一眼可见 */\n.dcp-stick-base::before,\n.dcp-stick-base::after { content: ''; position: absolute; left: 50%; top: 50%; background: var(--dcp-border); pointer-events: none; }\n.dcp-stick-base::before { width: calc(100% - 20px); height: 1px; transform: translate(-50%, -50%); }\n.dcp-stick-base::after { width: 1px; height: calc(100% - 20px); transform: translate(-50%, -50%); }\n.dcp-stick-base.is-active { cursor: grabbing; border-color: var(--dcp-accent); box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.42), inset 0 0 0 1px var(--dcp-accent), 0 6px 18px rgba(0, 0, 0, 0.3); }\n.dcp-stick-arrow { position: absolute; color: var(--dcp-muted); font-size: 9px; line-height: 1; pointer-events: none; }\n.dcp-stick-arrow[data-dir=\"up\"] { top: 3px; left: 50%; transform: translateX(-50%); }\n.dcp-stick-arrow[data-dir=\"down\"] { bottom: 3px; left: 50%; transform: translateX(-50%); }\n.dcp-stick-arrow[data-dir=\"left\"] { left: 3px; top: 50%; transform: translateY(-50%); }\n.dcp-stick-arrow[data-dir=\"right\"] { right: 3px; top: 50%; transform: translateY(-50%); }\n.dcp-stick-knob {\n position: absolute; left: 50%; top: 50%;\n width: var(--dcp-knob); height: var(--dcp-knob);\n margin: calc(var(--dcp-knob) / -2) 0 0 calc(var(--dcp-knob) / -2);\n border-radius: 50%;\n background: linear-gradient(180deg, #7b8798, #414b59);\n border: 1px solid rgba(255, 255, 255, 0.24);\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);\n transition: transform 0.14s ease-out;\n will-change: transform;\n}\n/* 按住时去掉回中动画,否则跟手会有一帧迟滞 */\n.dcp-stick-base.is-active .dcp-stick-knob { transition: none; }\n/* 键盘驱动:行程每 ~16ms 刷一次,过渡会拖在键位后面,同样去掉 */\n.dcp-stick.is-keyboard .dcp-stick-knob { transition: none; }\n.dcp-stick-cap { color: var(--dcp-muted); font-size: 10px; line-height: 1.25; text-align: center; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); }\n.dcp-stick-read { color: var(--dcp-muted); font-size: 10px; font-variant-numeric: tabular-nums; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); }\n.dcp-stick.is-active .dcp-stick-cap { color: var(--dcp-text); }\n.dcp-stick.is-active .dcp-stick-read { color: var(--dcp-accent); }\n\n/* ── 技能按钮(拍照/录像):坐在右轮盘正上方的技能位,不压盘面 ── */\n/* 技能容器:贴着底盘上沿(底边 = 底盘顶边 + 7px 间距),整体居中,两枚按钮左右排开。\n 用 bottom:100% 而不是负 top 偏移 —— 无论底盘放大到 100px 还是缩到 56px,\n 按钮永远在轮盘外,不会再盖住盘面;容器自身不接指针,点击只落在按钮上 */\n.dcp-skills {\n position: absolute; left: 50%; bottom: 100%;\n transform: translateX(-50%);\n margin-bottom: 7px;\n display: flex; align-items: flex-end; gap: 7px;\n pointer-events: none;\n}\n.dcp-skill {\n position: relative; z-index: 2;\n width: var(--dcp-skill); height: var(--dcp-skill); padding: 0;\n border-radius: 50%; cursor: pointer; pointer-events: auto;\n display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0;\n border: 1.5px solid rgba(53, 167, 255, 0.6);\n background: radial-gradient(circle at 50% 30%, rgba(53, 167, 255, 0.32), rgba(8, 18, 30, 0.92) 72%);\n color: var(--dcp-text);\n box-shadow: 0 3px 10px rgba(0, 0, 0, 0.42);\n transition: transform 0.1s ease, border-color 0.15s ease;\n}\n.dcp-skill:hover { border-color: var(--dcp-accent); }\n.dcp-skill:active { transform: scale(0.92); }\n/* 错开一格高度,斜向弧形排布(拍照左下 / 录像右上),不靠 transform 免得和按下态打架 */\n.dcp-skill-rec {\n margin-bottom: 5px;\n border-color: rgba(255, 159, 67, 0.6); background: radial-gradient(circle at 50% 30%, rgba(255, 159, 67, 0.3), rgba(26, 16, 6, 0.92) 72%);\n}\n.dcp-skill-rec:hover { border-color: #ffb267; }\n.dcp-skill-ico { font-size: 13px; line-height: 1; }\n.dcp-skill-lbl { font-size: 8px; line-height: 1.1; letter-spacing: 0.02em; }\n.dcp-skill-badge {\n position: absolute; right: -5px; top: -5px;\n min-width: 15px; padding: 0 3px; border-radius: 8px;\n background: var(--dcp-accent); color: var(--dcp-on-accent);\n font-size: 8px; line-height: 13px; font-weight: 700; font-variant-numeric: tabular-nums;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);\n display: none;\n}\n.dcp-skill-badge:not(:empty) { display: block; }\n.dcp-skill-rec .dcp-skill-badge { background: #ff9f43; color: #241300; }\n.dcp-ico-rec { width: 10px; height: 10px; border-radius: 50%; background: #ff5f5f; box-shadow: 0 0 0 2px rgba(255, 95, 95, 0.28); }\n.dcp-skill-rec.is-recording { border-color: var(--dcp-danger); }\n.dcp-skill-rec.is-recording .dcp-ico-rec { animation: dcp-rec-pulse 1s ease-in-out infinite; }\n@keyframes dcp-rec-pulse {\n 0%, 100% { opacity: 1; transform: scale(1); }\n 50% { opacity: 0.45; transform: scale(0.8); }\n}\n\n/* ── 手机横屏:面板贴底居中、嵌在左右轮盘之间(上方画面全留给取景),紧凑两列卡片 ── */\n@media (orientation: landscape) and (max-height: 620px) {\n /* 面板与悬浮轮盘共用同一杆径基准:面板左右偏移按它算,才能刚好让开轮盘和技能 */\n .dcp[data-adaptive=\"true\"],\n .dcp-sticks-float[data-adaptive=\"true\"] { --dcp-stick: clamp(64px, 22vh, 100px); }\n .dcp[data-adaptive=\"true\"][data-position] {\n top: auto; bottom: 8px;\n left: calc(var(--dcp-stick) + 62px);\n right: calc(var(--dcp-stick) + 62px);\n width: auto; max-width: none;\n padding: 5px 9px 6px;\n font-size: 11px;\n }\n .dcp[data-adaptive=\"true\"] .dcp-head { margin-bottom: 3px; }\n .dcp[data-adaptive=\"true\"] .dcp-title { font-size: 11px; }\n .dcp[data-adaptive=\"true\"] .dcp-body {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 4px 10px;\n align-content: start;\n max-height: 52vh;\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n }\n /* 横屏藏提示条(技能角标已承载拍照/录像反馈),把高度让给场景 */\n .dcp[data-adaptive=\"true\"] .dcp-note,\n .dcp[data-adaptive=\"true\"] .dcp-keys { display: none; }\n .dcp[data-adaptive=\"true\"] .dcp-blk {\n margin-top: 0; padding: 4px 6px; border-top: none;\n background: rgba(255, 255, 255, 0.03); border-radius: 8px;\n }\n /* 触控热区在横屏适度收窄:34px 会顶满屏高,28px 仍大于手指容差 */\n .dcp[data-adaptive=\"true\"] .dcp-seg,\n .dcp[data-adaptive=\"true\"] .dcp-wide,\n .dcp[data-adaptive=\"true\"] .dcp-flight,\n .dcp[data-adaptive=\"true\"] .dcp-btn,\n .dcp[data-adaptive=\"true\"] .dcp-preset { min-height: 28px; padding: 4px 0; font-size: 11px; }\n .dcp[data-adaptive=\"true\"] .dcp-label { margin-bottom: 3px; white-space: nowrap; }\n .dcp[data-adaptive=\"true\"] .dcp-label em { min-width: 0; overflow: hidden; }\n .dcp[data-adaptive=\"true\"] .dcp-seg,\n .dcp[data-adaptive=\"true\"] .dcp-wide { white-space: nowrap; overflow: hidden; }\n .dcp[data-adaptive=\"true\"] .dcp-flight-grid { grid-template-columns: repeat(3, 1fr); gap: 4px; }\n .dcp[data-adaptive=\"true\"] .dcp-segmented { gap: 3px; }\n .dcp[data-adaptive=\"true\"] .dcp-presets { gap: 3px; }\n .dcp[data-adaptive=\"true\"] .dcp-hint { margin: 3px 0 0; font-size: 10px; }\n .dcp[data-adaptive=\"true\"] .dcp-range { margin: 0 0 4px; }\n\n .dcp-sticks-float[data-adaptive=\"true\"] .dcp-stick[data-stick=\"left\"] { left: 20px; bottom: 26px; }\n .dcp-sticks-float[data-adaptive=\"true\"] .dcp-stick[data-stick=\"right\"] { right: 20px; bottom: 26px; }\n}\n\n/* ── 小屏横屏(如 iPhone SE):列宽吃紧,再降一档字号与杆径 ── */\n@media (orientation: landscape) and (max-height: 620px) and (max-width: 700px) {\n .dcp[data-adaptive=\"true\"],\n .dcp-sticks-float[data-adaptive=\"true\"] { --dcp-stick: clamp(56px, 20vh, 84px); }\n .dcp[data-adaptive=\"true\"] .dcp-seg,\n .dcp[data-adaptive=\"true\"] .dcp-wide,\n .dcp[data-adaptive=\"true\"] .dcp-flight,\n .dcp[data-adaptive=\"true\"] .dcp-btn,\n .dcp[data-adaptive=\"true\"] .dcp-preset { font-size: 10px; }\n .dcp[data-adaptive=\"true\"] .dcp-label { font-size: 10px; }\n}\n\n/* ── 手机竖屏:面板收一点宽度,轮盘略缩 ── */\n@media (max-width: 560px) and (orientation: portrait) {\n .dcp[data-adaptive=\"true\"] { width: min(86vw, 246px); }\n .dcp-sticks-float[data-adaptive=\"true\"] { --dcp-stick: 84px; }\n}\n\n/* ── 触控设备:把点击热区抬到 34px 以上,避免误触(技能钮只放到上限,不再撑大) ── */\n@media (pointer: coarse) {\n .dcp-seg, .dcp-wide, .dcp-flight, .dcp-btn, .dcp-preset { min-height: 34px; }\n .dcp, .dcp-sticks-float { --dcp-skill: clamp(34px, calc(var(--dcp-stick) * 0.4), 40px); }\n}\n\n/* 折叠 / 展开放最后:横屏那条 .dcp-body{display:grid} 与它同权重,靠源码顺序压过去 */\n.dcp[data-collapsed=\"true\"] .dcp-body { display: none; }\n`\n\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return\n if (document.getElementById(STYLE_ID)) return\n const style = document.createElement('style')\n style.id = STYLE_ID\n style.textContent = PANEL_CSS\n document.head.appendChild(style)\n}\n\nfunction escapeHtml(text: string): string {\n const map: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n }\n return text.replace(/[&<>\"']/g, (ch) => map[ch] ?? ch)\n}\n\nfunction triggerDownload(url: string, filename: string): void {\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = filename\n anchor.rel = 'noopener'\n document.body.appendChild(anchor)\n anchor.click()\n anchor.remove()\n}\n\n/** 生成文件名用的时间戳(本地时间,精确到秒) */\nfunction stamp(): string {\n const now = new Date()\n const pad = (value: number): string => String(value).padStart(2, '0')\n return (\n `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +\n `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`\n )\n}\n\n/**\n * 死区外重新归一:否则行程刚跨过死区时会从 0 直接跳到 deadzone,飞机起手就有一下顿挫。\n * 归一后仍是单调的 -1~1,只是把死区那一小段\"摊平\"了。\n */\nfunction applyDeadzone(value: number, deadzone: number): number {\n if (deadzone <= 0) return value\n if (Math.abs(value) <= deadzone) return 0\n return (value - Math.sign(value) * deadzone) / (1 - deadzone)\n}\n\n/**\n * 指数曲线(expo):真机遥控器用来\"软化中位\"的标准做法。\n *\n * out = sign(v) · (expo·|v|³ + (1-expo)·|v|)\n *\n * 指数项让中位附近的行程变慢(推 30% 只给约 17% 舵量),便于微调;\n * 一次项保证末端仍然线性推到满舵(v=1 恒等于 1),极速与转向权限不缩水。\n */\nfunction applyExpo(value: number, expo: number): number {\n if (expo <= 0) return value\n const e = Math.min(0.95, Math.max(0, expo))\n const magnitude = Math.abs(value)\n return Math.sign(value) * (e * magnitude ** 3 + (1 - e) * magnitude)\n}\n\n/** 行程夹到 ±1(摇杆圆轨与键盘叠加后都要落在合法行程内) */\nfunction clampUnit(value: number): number {\n return Math.max(-1, Math.min(1, value))\n}\n\n/** 摇杆读数:垂直 / 水平两路合成一行短文本,居中时给个明确提示 */\nfunction formatStickText(vertical: number, lateral: number): string {\n if (Math.abs(vertical) < 0.005 && Math.abs(lateral) < 0.005) return '居中'\n const mark = (value: number, positive: string, negative: string): string => {\n if (Math.abs(value) < 0.005) return '·0.00'\n return `${value > 0 ? positive : negative}${Math.abs(value).toFixed(2)}`\n }\n return `${mark(vertical, '↑', '↓')} ${mark(lateral, '→', '←')}`\n}\n\n/**\n * 无人机控制面板:王者荣耀式 HUD(左右悬浮轮盘 + 右轮盘技能) + 飞行控制面板。\n *\n * ```ts\n * const fly = new DroneFly(scene, camera, renderer, controls)\n * await fly.initialize({ modelUrl: '/models/dji_mini4_pro.glb' })\n * const panel = new DroneControlPanel(fly, { container: viewportEl, position: 'top-right' })\n * // 卸载:panel.destroy()\n * ```\n *\n * 按钮可用性由飞行阶段推导(见 sync):未上电时只有「上电」可点,待机/电机已启动才能「起飞」,\n * 离地后才能「降落」「返航」—— 与真机的操作次序一致,不会点出无效请求。\n * 方向由两根自回中虚拟摇杆给出(DJI 美国手 Mode 2),输出的是连续行程而不是开关量。\n *\n * 手感对齐真机遥控器:垂直通道死区 0.06、横滚/偏航死区 0.14(留出直线飞行走廊)、\n * 全通道叠加 expo 指数曲线(默认 0.55,中位变钝、末端仍打满),可用\n * `stickDeadzone` / `stickLateralDeadzone` / `stickExpo` 覆盖(传 0 即关闭)。\n */\nexport class DroneControlPanel {\n /** 面板根元素(宿主可追加类名做二次定制) */\n readonly root: HTMLDivElement\n /** 悬浮摇杆/技能层(面板的兄弟节点,铺满容器;宿主可追加类名做二次定制) */\n readonly hudLayer: HTMLDivElement\n\n private readonly fly: DronePanelHost\n private readonly opts: DroneControlPanelOptions\n private readonly presets: number[]\n private readonly container: HTMLElement\n private readonly abort = new AbortController()\n private readonly deadzone: number\n private readonly lateralDeadzone: number\n private readonly expo: number\n private readonly yawRateScale: number\n private readonly keyboardEnabled: boolean\n private readonly keyboardRise: number\n private readonly keyboardFall: number\n /** 当前按下的键盘键(存 event.code,不受输入法/大写锁定影响) */\n private readonly pressedKeys = new Set<string>()\n /** 键盘合成行程(带渐推渐回);与摇杆合成时\"摇杆优先\" */\n private readonly keyboardStick: Record<StickChannel, number> = {\n throttle: 0,\n yaw: 0,\n pitch: 0,\n roll: 0,\n }\n private keyboardTimer: number | null = null\n private lastKeyboardTick = 0\n private sticks: StickHandle[] = []\n private readonly refs: {\n modeLabel: HTMLElement\n flightState: HTMLElement\n powerButton: HTMLButtonElement\n takeoffButton: HTMLButtonElement\n landButton: HTMLButtonElement\n rthButton: HTMLButtonElement\n armDeployButton: HTMLButtonElement\n armFoldButton: HTMLButtonElement\n flightModeLabel: HTMLElement\n speedSlider: HTMLInputElement\n speedInput: HTMLInputElement\n preflight: HTMLElement\n pitchSlider: HTMLInputElement\n pitchValue: HTMLElement\n photoCount: HTMLElement\n recordButton: HTMLButtonElement\n recTime: HTMLElement\n note: HTMLElement\n /** 「拍摄模式」区的读数位(未启用全景时为 null) */\n shootLabel: HTMLElement | null\n }\n\n private timer: number | null = null\n private pitchDragging = false\n /** 正在拖速度倍率滑杆:期间不回写,免得和用户的手打架 */\n private speedDragging = false\n private photoBusy = false\n private visible: boolean\n private collapsed = false\n private destroyed = false\n /** 停止录像前抓的时长,用于回调与提示(stopRecording 后 sim.recordSeconds 会归零) */\n private lastRecordSeconds = 0\n /** 全景扫描器(未接采集服务时为 null) */\n private panorama: PanoramaCapture | null = null\n private panoramaUnsub: (() => void) | null = null\n /** 当前拍摄模式:单张 / 180° / 360° */\n private shootMode: ShootMode = 'single'\n /** 最近一次全景进度(面板只做展示) */\n private panoProgress: PanoramaProgress | null = null\n\n /** 构造控制面板并插入容器(可传 options 停靠位置/手感/键盘开关) */\n constructor(fly: DronePanelHost, options: DroneControlPanelOptions = {}) {\n if (typeof document === 'undefined') {\n throw new Error('[drone-camera] DroneControlPanel 需要浏览器环境(document),Node 侧请勿实例化')\n }\n this.fly = fly\n this.opts = options\n this.presets = options.pitchPresets ?? DEFAULT_PITCH_PRESETS\n this.deadzone = options.stickDeadzone ?? DEFAULT_STICK_DEADZONE\n this.lateralDeadzone = options.stickLateralDeadzone ?? DEFAULT_STICK_LATERAL_DEADZONE\n this.expo = options.stickExpo ?? DEFAULT_STICK_EXPO\n this.yawRateScale = Math.max(0, Math.min(1, options.yawRateScale ?? DEFAULT_YAW_RATE_SCALE))\n this.keyboardEnabled = options.keyboard !== false\n this.keyboardRise = Math.max(0.02, options.keyboardRiseSeconds ?? DEFAULT_KEYBOARD_RISE)\n this.keyboardFall = Math.max(0.02, options.keyboardFallSeconds ?? DEFAULT_KEYBOARD_FALL)\n this.container = options.container ?? document.body\n this.visible = options.visible ?? true\n\n if (options.injectStyles !== false) ensureStyles()\n\n // 全景扫描器要在**模板生成之前**就绪 —— 模板按 this.panorama 的有无决定渲不渲染\n // 「拍摄模式」区。它只依赖采集服务(取景方位由服务在每张内部施加并复位),不碰渲染后端。\n if (options.captureService && options.panorama !== false) {\n this.panorama = new PanoramaCapture(options.captureService, {\n shotIntervalMs: options.panoramaShotIntervalMs ?? DEFAULT_PANORAMA_INTERVAL_MS,\n })\n }\n\n // 面板用 absolute 定位到容器;容器若没有定位上下文则补一个 relative,否则会跑到页面左上角\n const host = options.container ?? null\n if (host && getComputedStyle(host).position === 'static') host.style.position = 'relative'\n // 容器缺省时用 fixed 贴视口,否则 absolute 贴容器\n const layerPosition = host ? 'absolute' : 'fixed'\n\n this.root = document.createElement('div')\n this.root.className = 'dcp'\n this.root.dataset.position = options.position ?? 'top-right'\n // 自适应开关交给 CSS:媒体查询只对 data-adaptive=\"true\" 生效,宿主想自己排版可关掉\n this.root.dataset.adaptive = String(options.adaptive !== false)\n this.root.style.position = layerPosition\n this.root.innerHTML = this.template()\n if (!this.visible) this.root.style.display = 'none'\n\n // 悬浮层与面板是兄弟节点:面板的 backdrop-filter 会改变 fixed 后代的定位基准,摇杆不能塞进面板\n this.hudLayer = document.createElement('div')\n this.hudLayer.className = 'dcp-sticks-float'\n this.hudLayer.dataset.adaptive = this.root.dataset.adaptive\n this.hudLayer.style.position = layerPosition\n this.hudLayer.innerHTML = this.hudTemplate()\n if (!this.visible) this.hudLayer.style.display = 'none'\n\n this.refs = {\n modeLabel: this.query('[data-role=\"mode-label\"]'),\n flightState: this.query('[data-role=\"flight-state\"]'),\n powerButton: this.query<HTMLButtonElement>('[data-role=\"power\"]'),\n takeoffButton: this.query<HTMLButtonElement>('[data-role=\"takeoff\"]'),\n landButton: this.query<HTMLButtonElement>('[data-role=\"land\"]'),\n rthButton: this.query<HTMLButtonElement>('[data-role=\"rth\"]'),\n armDeployButton: this.query<HTMLButtonElement>('[data-role=\"arm-deploy\"]'),\n armFoldButton: this.query<HTMLButtonElement>('[data-role=\"arm-fold\"]'),\n flightModeLabel: this.query('[data-role=\"flight-mode-label\"]'),\n speedSlider: this.query<HTMLInputElement>('[data-role=\"speed-scale\"]'),\n speedInput: this.query<HTMLInputElement>('[data-role=\"speed-scale-input\"]'),\n preflight: this.query('[data-role=\"preflight\"]'),\n pitchSlider: this.query<HTMLInputElement>('[data-role=\"pitch\"]'),\n pitchValue: this.query('[data-role=\"pitch-value\"]'),\n photoCount: this.queryIn(this.hudLayer, '[data-role=\"photo-count\"]'),\n recordButton: this.queryIn<HTMLButtonElement>(this.hudLayer, '[data-role=\"toggle-record\"]'),\n recTime: this.queryIn(this.hudLayer, '[data-role=\"rec-time\"]'),\n note: this.query('[data-role=\"note\"]'),\n // 全景区是条件渲染的(没接采集服务就没有),所以用可空查询\n shootLabel: this.root.querySelector<HTMLElement>('[data-role=\"shoot-label\"]'),\n }\n this.sticks = this.collectSticks()\n\n // 订阅进度(全景实例已在上面按需创建;回调只碰 DOM 与进度字段,构造期不会触发)\n if (this.panorama) {\n this.panoramaUnsub = this.panorama.subscribe((progress) => {\n this.panoProgress = progress\n this.syncShootMode()\n this.syncHudBadge()\n })\n }\n\n this.bind()\n this.sync()\n this.mount()\n }\n\n // ───────────────────────────── 生命周期 ─────────────────────────────\n\n /** 插入容器(构造时已自动调用一次;面板与悬浮层一起挂) */\n mount(): this {\n if (!this.destroyed && !this.root.isConnected) this.container.appendChild(this.root)\n if (!this.destroyed && !this.hudLayer.isConnected) this.container.appendChild(this.hudLayer)\n return this\n }\n\n /** 从容器移除但保留实例,可再次 mount */\n unmount(): this {\n this.root.remove()\n this.hudLayer.remove()\n return this\n }\n\n /** 显示 / 隐藏(面板与悬浮轮盘一起切换) */\n setVisible(visible: boolean): this {\n this.visible = visible\n this.root.style.display = visible ? '' : 'none'\n this.hudLayer.style.display = visible ? '' : 'none'\n return this\n }\n\n /** 当前是否可见 */\n isVisible(): boolean {\n return this.visible\n }\n\n /** 折叠 / 展开(只收面板,左右轮盘与技能保持可用 —— 游戏里也不收操作区) */\n setCollapsed(collapsed: boolean): this {\n this.collapsed = collapsed\n this.root.dataset.collapsed = String(collapsed)\n return this\n }\n\n /** 彻底销毁:解绑事件、停轮询、移除 DOM */\n destroy(): void {\n if (this.destroyed) return\n this.destroyed = true\n // 先松开摇杆与键盘:销毁时若还按着,行程会留在引擎里让飞机一直飞\n this.stopKeyboard()\n this.releaseKeyboard()\n this.releaseSticks()\n this.abort.abort()\n this.stopTimer()\n // 全景扫描:先停扫描(它还在逐张 await),再退订 —— 否则回调会打到已移除的 DOM 上\n this.panorama?.cancel()\n this.panoramaUnsub?.()\n this.panoramaUnsub = null\n this.root.remove()\n this.hudLayer.remove()\n }\n\n /**\n * 移除已注入的共享样式。\n * 多个面板共存时请等最后一个销毁后再调用,否则其余面板会掉样式。\n */\n static removeInjectedStyles(): void {\n if (typeof document === 'undefined') return\n document.getElementById(STYLE_ID)?.remove()\n }\n\n // ───────────────────────────── 状态同步 ─────────────────────────────\n\n /** 把引擎当前状态刷到 HUD 上(飞行阶段/按钮可用性 / 视角高亮 / 云台角度 / 技能角标 / 摇杆读数) */\n sync(): void {\n if (this.destroyed) return\n const fly = this.fly\n\n // 飞行:阶段 + 高度。按钮可用性直接由阶段推导,避免用户点出无效请求\n const phase = fly.phase\n this.refs.flightState.textContent = `${PHASE_LABELS[phase] ?? phase} · ${fly.altitude.toFixed(1)} m`\n const canPower = phase === 'powerOff' || phase === 'stopped'\n this.refs.powerButton.disabled = !canPower\n this.refs.takeoffButton.disabled = !(phase === 'standby' || phase === 'motorsOn')\n this.refs.landButton.disabled = !(fly.airborne || phase === 'takingOff')\n this.refs.rthButton.disabled = !(fly.airborne && phase !== 'rth')\n // 机臂:已处于目标状态就置灰;飞行中不得收拢(与真机一致)\n const folded = fly.armsFolded\n this.refs.armDeployButton.disabled = !folded\n this.refs.armFoldButton.disabled = folded || fly.airborne\n\n // 起飞前检查未过时说明卡在哪一项(如 GNSS 搜星),否则用户只会觉得\"点了没反应\"\n const blocked = fly.preflightChecks.filter((item) => item.blocking && !item.ok)\n this.refs.preflight.textContent = blocked.length\n ? `待通过:${blocked.map((item) => item.label).join('、')}`\n : ''\n this.refs.preflight.title = blocked.map((item) => `${item.label}:${item.detail}`).join('\\n')\n\n // 飞行模式:真值取 fly.flightMode\n const flightMode = fly.flightMode\n this.refs.flightModeLabel.textContent =\n FLIGHT_MODE_LIST.find((item) => item.key === flightMode)?.label ?? flightMode\n this.root.querySelectorAll<HTMLElement>('[data-action=\"set-flight-mode\"]').forEach((button) => {\n button.classList.toggle('is-active', button.dataset.mode === flightMode)\n })\n\n // 速度倍率:真值取 sim.speedScale(可能与界面控件不一致 —— 宿主/脚本改过就以下一轮同步为准)\n this.renderSpeedScale(fly.sim.speedScale)\n\n // 视角:真值取 fly.cameraMode\n const mode = fly.cameraMode\n this.refs.modeLabel.textContent =\n CAMERA_MODE_LIST.find((item) => item.key === mode)?.label ?? mode\n this.root.querySelectorAll<HTMLElement>('[data-action=\"set-mode\"]').forEach((button) => {\n button.classList.toggle('is-active', button.dataset.mode === mode)\n })\n\n // 云台:拖动滑杆期间不回写,否则会和用户的手打架\n if (!this.pitchDragging) {\n const pitch = Math.round(fly.sim.gimbalPitch)\n this.refs.pitchSlider.value = String(pitch)\n this.renderPitch(pitch)\n }\n\n // 摇杆读数:正被手指按住的那根以面板为准(回写会打架),其余跟随引擎真值\n this.syncStickReadout()\n\n this.syncShootMode()\n this.syncHudBadge()\n }\n\n /** 技能角标(游戏里的 buff 计数位):全景扫描中优先显示扫描进度,否则显示拍照张数 */\n private syncHudBadge(): void {\n const fly = this.fly\n const progress = this.panoProgress\n this.refs.photoCount.textContent =\n progress?.state === 'running'\n ? `${progress.index}/${progress.total}`\n : fly.sim.photoCount > 0\n ? String(fly.sim.photoCount)\n : ''\n const recording = fly.isRecording\n this.refs.recordButton.classList.toggle('is-recording', recording)\n this.refs.recTime.textContent = recording ? `${fly.sim.recordSeconds.toFixed(0)}s` : ''\n }\n\n /** 拍摄模式区:模式高亮 + 读数(空闲显示张数,扫描中/完成显示进度) */\n private syncShootMode(): void {\n if (!this.panorama) return\n const running = this.panorama.running\n this.root\n .querySelectorAll<HTMLButtonElement>('[data-action=\"set-shoot-mode\"]')\n .forEach((button) => {\n const isActive = button.dataset.mode === this.shootMode\n button.classList.toggle('is-active', isActive)\n // 扫描中锁模式:半途换模式会让\"这一组\"变成两种参数混拍\n button.toggleAttribute('disabled', running && !isActive)\n })\n const label = this.refs.shootLabel\n if (!label) return\n const progress = this.panoProgress\n const preset =\n this.shootMode === 'single'\n ? null\n : PanoramaCapture.preset(\n this.shootMode,\n this.opts.captureService?.getCaptureIntrinsics()?.verticalFovDeg,\n )\n if (progress && progress.mode === this.shootMode && progress.state !== 'idle') {\n const prefix =\n progress.state === 'running'\n ? '扫描中'\n : progress.state === 'done'\n ? '完成'\n : progress.state === 'failed'\n ? '未完成'\n : '已停止'\n label.textContent = `${prefix} ${progress.index}/${progress.total}`\n return\n }\n label.textContent = preset ? `${preset.total} 张` : '单张'\n }\n\n // ───────────────────────────── 内部实现 ─────────────────────────────\n\n private query<T extends HTMLElement = HTMLElement>(selector: string): T {\n return this.queryIn(this.root, selector)\n }\n\n private queryIn<T extends HTMLElement = HTMLElement>(scope: ParentNode, selector: string): T {\n const el = scope.querySelector<T>(selector)\n if (!el) throw new Error(`[drone-camera] 面板结构缺失:${selector}`)\n return el\n }\n\n /** 面板本体:飞行控制 + 模式/视角/云台(方向轮盘与拍照录像已拆到悬浮层) */\n private template(): string {\n const modes = CAMERA_MODE_LIST.map(\n (item) =>\n `<button type=\"button\" class=\"dcp-seg\" data-action=\"set-mode\" data-mode=\"${item.key}\">${item.label}</button>`,\n ).join('')\n const presets = this.presets\n .map(\n (value) =>\n `<button type=\"button\" class=\"dcp-preset\" data-action=\"preset-pitch\" data-pitch=\"${value}\">${value}°</button>`,\n )\n .join('')\n const flightModes = FLIGHT_MODE_LIST.map(\n (item) =>\n `<button type=\"button\" class=\"dcp-seg\" data-action=\"set-flight-mode\" data-mode=\"${item.key}\">${item.label}</button>`,\n ).join('')\n const speedPresets = SPEED_SCALE_PRESETS.map(\n (item) =>\n `<button type=\"button\" class=\"dcp-preset\" data-action=\"preset-speed\" data-scale=\"${item.value}\">${item.label}</button>`,\n ).join('')\n // 全景区只在接入采集服务时出现 —— 照片要有地方归档、能被导出,这个功能才成立\n const shootSection = this.panorama\n ? `<section class=\"dcp-blk\" data-blk=\"shoot\">\n <div class=\"dcp-label\">拍摄模式<em data-role=\"shoot-label\">单张</em></div>\n <div class=\"dcp-segmented\">\n <button type=\"button\" class=\"dcp-seg is-active\" data-action=\"set-shoot-mode\" data-mode=\"single\">单张</button>\n ${PANORAMA_MODE_LIST.map(\n (mode) =>\n `<button type=\"button\" class=\"dcp-seg\" data-action=\"set-shoot-mode\" data-mode=\"${mode}\">${PANORAMA_PRESETS[mode].label}</button>`,\n ).join('')}\n </div>\n <p class=\"dcp-hint\">悬停中拍摄:广角 3×3、180° 分层扫描、球形 360° 另补天顶/天底。</p>\n </section>`\n : ''\n\n return `\n <div class=\"dcp-head\">\n <span class=\"dcp-title\">${escapeHtml(this.opts.title ?? '无人机')}</span>\n <button type=\"button\" class=\"dcp-icon\" data-action=\"toggle-collapse\" title=\"收起 / 展开\" aria-label=\"收起或展开\">–</button>\n </div>\n <div class=\"dcp-body\">\n <div class=\"dcp-mid\">\n <section class=\"dcp-blk\" data-blk=\"flight\">\n <div class=\"dcp-label\">飞行状态<em data-role=\"flight-state\">--</em></div>\n <div class=\"dcp-flight-grid\">\n <button type=\"button\" class=\"dcp-flight\" data-action=\"power-on\" data-role=\"power\">上电</button>\n <button type=\"button\" class=\"dcp-flight dcp-takeoff\" data-action=\"takeoff\" data-role=\"takeoff\">起飞</button>\n <button type=\"button\" class=\"dcp-flight\" data-action=\"land\" data-role=\"land\">降落</button>\n <button type=\"button\" class=\"dcp-flight dcp-rth\" data-action=\"rth\" data-role=\"rth\">返航</button>\n <button type=\"button\" class=\"dcp-flight\" data-action=\"arm-deploy\" data-role=\"arm-deploy\">展开机臂</button>\n <button type=\"button\" class=\"dcp-flight\" data-action=\"arm-fold\" data-role=\"arm-fold\">收拢机臂</button>\n </div>\n <p class=\"dcp-hint\" data-role=\"preflight\"></p>\n </section>\n\n <section class=\"dcp-blk\" data-blk=\"mode\">\n <div class=\"dcp-label\">飞行模式<em data-role=\"flight-mode-label\">--</em></div>\n <div class=\"dcp-segmented\">${flightModes}</div>\n </section>\n\n <section class=\"dcp-blk\" data-blk=\"speed\">\n <div class=\"dcp-label\">\n 速度倍率\n <span class=\"dcp-speed-val\">\n <input class=\"dcp-num\" type=\"number\" data-role=\"speed-scale-input\"\n min=\"${SPEED_SCALE_MIN}\" max=\"${SPEED_SCALE_MAX}\" step=\"${SPEED_SCALE_STEP}\"\n value=\"${formatScale(DEFAULT_SPEED_SCALE)}\" title=\"0 ~ ${SPEED_SCALE_MAX},数值即倍数(1 = 真机原速)\" />\n <i>×</i>\n </span>\n </div>\n <input class=\"dcp-range\" type=\"range\" data-role=\"speed-scale\"\n min=\"${SPEED_SCALE_MIN}\" max=\"${SPEED_SCALE_MAX}\" step=\"${SPEED_SCALE_STEP}\"\n value=\"${formatScale(DEFAULT_SPEED_SCALE)}\" title=\"左右拖动;0 = 机体不动,1 = 真机原速\" />\n <div class=\"dcp-presets\">${speedPresets}</div>\n </section>\n\n <section class=\"dcp-blk\" data-blk=\"camera\">\n <div class=\"dcp-label\">相机视角<em data-role=\"mode-label\">--</em></div>\n <div class=\"dcp-segmented\">${modes}</div>\n <button type=\"button\" class=\"dcp-wide\" data-action=\"reset-camera\">复位观察视角</button>\n </section>\n\n <section class=\"dcp-blk\" data-blk=\"gimbal\">\n <div class=\"dcp-label\">云台俯仰<em data-role=\"pitch-value\">--</em></div>\n <input class=\"dcp-range\" type=\"range\" data-role=\"pitch\" min=\"${PITCH_MIN}\" max=\"${PITCH_MAX}\" step=\"1\" value=\"-10\" />\n <div class=\"dcp-presets\">${presets}</div>\n </section>\n\n ${shootSection}\n </div>\n <p class=\"dcp-note\" data-role=\"note\"></p>\n <p class=\"dcp-keys\">${this.keyboardEnabled ? '键盘 W/S 升降 · A/D 转向 · ↑/↓ 前后 · ←/→ 横滚 · Ctrl+K 拍照 · Ctrl+L 录像' : ''}</p>\n </div>\n `\n }\n\n /** 悬浮层:左右两根轮盘 + 右轮盘上的拍照/录像技能(位置关系由 CSS 钉死,旋转/缩放不散架) */\n private hudTemplate(): string {\n return `\n ${this.stickTemplate('left', '左杆 · 升降/偏航', [\n ['up', '升'],\n ['down', '降'],\n ['left', '左转'],\n ['right', '右转'],\n ])}\n <div class=\"dcp-stick\" data-stick=\"right\" title=\"右杆:上下前后、左右横滚(美国手 Mode 2),松杆自动回中\">\n <div class=\"dcp-stick-base\">\n <span class=\"dcp-stick-arrow\" data-dir=\"up\">前</span>\n <span class=\"dcp-stick-arrow\" data-dir=\"down\">后</span>\n <span class=\"dcp-stick-arrow\" data-dir=\"left\">左移</span>\n <span class=\"dcp-stick-arrow\" data-dir=\"right\">右移</span>\n <div class=\"dcp-stick-knob\"></div>\n <div class=\"dcp-skills\">\n <button type=\"button\" class=\"dcp-skill dcp-skill-photo\" data-action=\"photo\" title=\"拍照\">\n <span class=\"dcp-skill-ico\">📷</span>\n <span class=\"dcp-skill-lbl\">拍照</span>\n <span class=\"dcp-skill-badge\" data-role=\"photo-count\"></span>\n </button>\n <button type=\"button\" class=\"dcp-skill dcp-skill-rec\" data-action=\"toggle-record\" data-role=\"toggle-record\" title=\"开始 / 停止录像\">\n <span class=\"dcp-skill-ico dcp-ico-rec\"></span>\n <span class=\"dcp-skill-lbl\">录像</span>\n <span class=\"dcp-skill-badge\" data-role=\"rec-time\"></span>\n </button>\n </div>\n </div>\n <span class=\"dcp-stick-cap\">右杆 · 前后/横滚</span>\n <span class=\"dcp-stick-read\" data-role=\"stick-read\">居中</span>\n </div>\n `\n }\n\n /** 单根虚拟摇杆:底盘 + 十字刻度 + 四向提示 + 可按压的杆帽 + 实时读数 */\n private stickTemplate(\n side: StickSide,\n caption: string,\n marks: ReadonlyArray<readonly [string, string]>,\n ): string {\n const arrows = marks\n .map(([dir, label]) => `<span class=\"dcp-stick-arrow\" data-dir=\"${dir}\">${label}</span>`)\n .join('')\n const hint =\n side === 'left'\n ? '左杆:上下升降、左右偏航(美国手 Mode 2),松杆自动回中'\n : '右杆:上下前后、左右横滚(美国手 Mode 2),松杆自动回中'\n return `\n <div class=\"dcp-stick\" data-stick=\"${side}\" title=\"${hint}\">\n <div class=\"dcp-stick-base\">\n ${arrows}\n <div class=\"dcp-stick-knob\"></div>\n </div>\n <span class=\"dcp-stick-cap\">${caption}</span>\n <span class=\"dcp-stick-read\" data-role=\"stick-read\">居中</span>\n </div>\n `\n }\n\n private collectSticks(): StickHandle[] {\n return Array.from(this.hudLayer.querySelectorAll<HTMLElement>('.dcp-stick')).map((root) => {\n const base = root.querySelector<HTMLElement>('.dcp-stick-base')\n const knob = root.querySelector<HTMLElement>('.dcp-stick-knob')\n const read = root.querySelector<HTMLElement>('[data-role=\"stick-read\"]')\n if (!base || !knob || !read) throw new Error('[drone-camera] 面板摇杆结构缺失')\n return {\n side: root.dataset.stick as StickSide,\n root,\n base,\n knob,\n read,\n pointerId: null,\n vertical: 0,\n lateral: 0,\n radius: 0,\n radiusSpan: 0,\n }\n })\n }\n\n /** 事件委托挂在指定子树上(面板与悬浮层各一份,共用 handleAction) */\n private bindActionDelegate(scope: HTMLElement): void {\n scope.addEventListener(\n 'click',\n (event) => {\n const target = (event.target as HTMLElement | null)?.closest<HTMLElement>('[data-action]')\n if (target) this.handleAction(target.dataset.action ?? '', target)\n },\n { signal: this.abort.signal },\n )\n }\n\n private bind(): void {\n const { signal } = this.abort\n\n // 事件委托:面板与悬浮层各一份,减少句柄也便于整体解绑\n this.bindActionDelegate(this.root)\n this.bindActionDelegate(this.hudLayer)\n\n const slider = this.refs.pitchSlider\n slider.addEventListener(\n 'input',\n () => {\n const value = Number(slider.value)\n this.fly.setGimbalPitch(value)\n this.renderPitch(value)\n },\n { signal },\n )\n slider.addEventListener('pointerdown', () => (this.pitchDragging = true), { signal })\n // 指针可能在面板外抬起,所以监听挂在 window 上(随 signal 一起解绑)\n window.addEventListener('pointerup', () => (this.pitchDragging = false), { signal })\n\n // 速度倍率:滑杆拖动即时生效(0 ~ 1000,数值即倍数);数字框可直接输入精确值\n const speedSlider = this.refs.speedSlider\n speedSlider.addEventListener('input', () => this.applySpeedScale(speedSlider.value), { signal })\n speedSlider.addEventListener('pointerdown', () => (this.speedDragging = true), { signal })\n window.addEventListener('pointerup', () => (this.speedDragging = false), { signal })\n\n const speedInput = this.refs.speedInput\n speedInput.addEventListener(\n 'input',\n () => {\n // 用户正在删改(空串)或输入非法字符时保持原值不动,别把半截输入当成倍率下发\n if (speedInput.value.trim() === '') return\n this.applySpeedScale(speedInput.value)\n },\n { signal },\n )\n // 提交 / 失焦:把输入框归一(清掉 \"3%\"、\"9999\" 这类越界或非数字的残留)\n speedInput.addEventListener(\n 'change',\n () => {\n speedInput.value = formatScale(this.fly.sim.speedScale)\n this.sync()\n },\n { signal },\n )\n\n // 虚拟摇杆:pointerdown 落在底盘上就锁定这个指针,之后拖到哪儿都继续跟手;\n // move/up 挂在 window 上,是因为指针移出摇杆(甚至移出画面)时事件不再冒泡到悬浮层\n this.hudLayer.addEventListener('pointerdown', (event) => this.startStick(event), { signal })\n window.addEventListener('pointermove', (event) => this.moveStick(event), { signal })\n window.addEventListener('pointerup', (event) => this.endStick(event), { signal })\n window.addEventListener('pointercancel', (event) => this.endStick(event), { signal })\n\n this.bindKeyboard()\n\n if (this.opts.syncIntervalMs !== 0) {\n this.timer = window.setInterval(() => this.sync(), this.opts.syncIntervalMs ?? 200)\n }\n }\n\n // ── 摇杆:按下 / 拖动 / 抬起 ──\n\n private startStick(event: PointerEvent): void {\n // 技能钮(拍照/录像)现在是底盘的子节点,按它们不能把杆推出去,先把这条挡掉\n const hit = event.target as HTMLElement | null\n if (hit?.closest('.dcp-skill')) return\n const base = hit?.closest<HTMLElement>('.dcp-stick-base')\n if (!base) return\n const handle = this.sticks.find((item) => item.base === base)\n if (!handle) return\n // 阻止指针默认行为,避免按住时触发文本选择 / 拖拽 / 页面滚动\n event.preventDefault()\n handle.pointerId = event.pointerId\n handle.root.classList.add('is-active')\n try {\n // 捕获后即使手指滑出底盘也继续收到事件(触摸本身有隐式捕获,鼠标则靠这句)\n handle.base.setPointerCapture(event.pointerId)\n } catch {\n // 指针已失效(如合成事件):退化为普通拖动,不影响功能\n }\n this.updateStick(handle, event.clientX, event.clientY)\n }\n\n private moveStick(event: PointerEvent): void {\n const handle = this.sticks.find((item) => item.pointerId === event.pointerId)\n if (!handle) return\n event.preventDefault()\n this.updateStick(handle, event.clientX, event.clientY)\n }\n\n private endStick(event: PointerEvent): void {\n const handle = this.sticks.find((item) => item.pointerId === event.pointerId)\n if (!handle) return\n this.resetStick(handle)\n }\n\n /** 把指针位置换算成摇杆行程:超出圆行程就按半径夹到边界(圆轨,和真机的物理限位一致) */\n private updateStick(handle: StickHandle, clientX: number, clientY: number): void {\n const rect = handle.base.getBoundingClientRect()\n const radius = Math.max(1, (rect.width - handle.knob.offsetWidth) / 2)\n let dx = clientX - (rect.left + rect.width / 2)\n let dy = clientY - (rect.top + rect.height / 2)\n const distance = Math.hypot(dx, dy)\n if (distance > radius) {\n const ratio = radius / distance\n dx *= ratio\n dy *= ratio\n }\n handle.knob.style.transform = `translate(${dx.toFixed(1)}px, ${dy.toFixed(1)}px)`\n\n // 屏幕 y 轴向下为正,而\"推杆向前 / 上升\"要求正行程,所以垂直方向取反。\n // 顺序是 死区 → 归一 → expo:先滤掉中位抖动,再压平中段行程(见 applyExpo 注释)。\n // 横滚 / 偏航用更大的死区,手指侧滑带出的微小分量直接归零,飞机才走得直。\n const vertical = applyExpo(applyDeadzone(-dy / radius, this.deadzone), this.expo)\n const lateral = applyExpo(applyDeadzone(dx / radius, this.lateralDeadzone), this.expo)\n\n handle.vertical = vertical\n handle.lateral = lateral\n this.applyStick()\n handle.read.textContent = formatStickText(vertical, lateral)\n }\n\n /** 松杆 / 销毁:行程归零 + 杆帽回中。必须成对调用,否则摇杆会\"卡住\"让飞机一直飞 */\n private resetStick(handle: StickHandle): void {\n handle.pointerId = null\n handle.root.classList.remove('is-active', 'is-keyboard')\n handle.knob.style.transform = ''\n handle.vertical = 0\n handle.lateral = 0\n this.applyStick()\n handle.read.textContent = formatStickText(0, 0)\n // 键盘可能还按着(甚至按的就是这根杆):把杆帽交还给键盘状态,别停在居中的画面上\n this.syncKeyboardVisuals()\n }\n\n /**\n * 键盘 → 屏上摇杆:把渐进行程画到杆帽上(位移 + 高亮 + 读数),按键时看起来就像有人在替你打杆。\n * 手指正按着的杆跳过 —— 那根杆的视觉由 updateStick 跟手绘制,键盘不该来抢。\n */\n private syncKeyboardVisuals(): void {\n if (this.destroyed) return\n const kb = this.keyboardStick\n for (const handle of this.sticks) {\n if (handle.pointerId !== null) continue\n const vertical = handle.side === 'left' ? kb.throttle : kb.pitch\n const lateral = handle.side === 'left' ? kb.yaw : kb.roll\n const active = Math.abs(vertical) > 0.005 || Math.abs(lateral) > 0.005\n this.paintStick(handle, vertical, lateral, active)\n }\n }\n\n /**\n * 画一根杆的杆帽位置:行程 ±1 直接映射到圆轨半径(上 / 右为正),与 formatStickText 的方向约定一致。\n * 半径按底盘边长缓存 —— 键盘每 ~16ms 刷一次,不能每次都去读布局(会引发布局抖动);\n * 换屏 / 旋转导致杆径变化时,边长对不上会自动重测。\n */\n private paintStick(\n handle: StickHandle,\n vertical: number,\n lateral: number,\n active: boolean,\n ): void {\n const span = handle.base.offsetWidth\n if (span !== handle.radiusSpan) {\n handle.radiusSpan = span\n handle.radius = Math.max(1, (span - handle.knob.offsetWidth) / 2)\n }\n const dx = clampUnit(lateral) * handle.radius\n // 屏幕 y 轴向下为正,而\"推杆向前 / 上升\"要求向上,所以垂直方向取反(与 updateStick 同一套约定)\n const dy = -clampUnit(vertical) * handle.radius\n handle.knob.style.transform = `translate(${dx.toFixed(1)}px, ${dy.toFixed(1)}px)`\n handle.root.classList.toggle('is-keyboard', active)\n handle.root.classList.toggle('is-active', active)\n handle.read.textContent = formatStickText(vertical, lateral)\n }\n\n /**\n * 把「摇杆 + 键盘」合成后写进飞控 —— 唯一的 setStick 出口。\n *\n * 合成规则是**摇杆优先**:某根杆正被按住时,该杆的两个通道只听摇杆;\n * 没被按住的杆才交给键盘。于是双指打杆时键盘不会来抢,松开手指后键盘又能接手。\n * 偏航通道在这里统一乘 yawRateScale(真机挡位角速度在屏幕上偏快,见选项注释)。\n */\n private applyStick(): void {\n if (this.destroyed) return\n const left = this.sticks.find((item) => item.side === 'left')\n const right = this.sticks.find((item) => item.side === 'right')\n const holding = (handle: StickHandle | undefined) => handle != null && handle.pointerId !== null\n const kb = this.keyboardStick\n const throttle = holding(left) ? left!.vertical : kb.throttle\n const yaw = holding(left) ? left!.lateral : kb.yaw\n const pitch = holding(right) ? right!.vertical : kb.pitch\n const roll = holding(right) ? right!.lateral : kb.roll\n this.fly.setStick({\n throttle: clampUnit(throttle),\n pitch: clampUnit(pitch),\n roll: clampUnit(roll),\n yaw: clampUnit(yaw) * this.yawRateScale,\n })\n }\n\n // ── 键盘:与屏上双摇杆等价的第二套输入 ──\n\n private bindKeyboard(): void {\n if (!this.keyboardEnabled) return\n const { signal } = this.abort\n window.addEventListener('keydown', (event) => this.onKey(event, true), { signal })\n window.addEventListener('keyup', (event) => this.onKey(event, false), { signal })\n // 切走窗口 / 失焦时把所有键松开,否则回来时飞机会朝一个方向一直飞\n window.addEventListener('blur', () => this.releaseKeyboard(), { signal })\n this.lastKeyboardTick = performance.now()\n this.keyboardTimer = window.setInterval(() => this.tickKeyboard(), KEYBOARD_TICK_MS)\n }\n\n private onKey(event: KeyboardEvent, down: boolean): void {\n // 输入框 / 可编辑区里的按键归宿主(敲字不该顺带让飞机动作)\n if (isTypingTarget(event.target)) return\n\n // Ctrl / Cmd + K L:拍照 / 录像。只在按下那一次触发(长按连发的 keydown 与 keyup 都吃掉),\n // 这样按住 Ctrl+K 不会连拍。preventDefault 是为了让宿主页面的同名快捷键让位。\n if (event.ctrlKey || event.metaKey) {\n const shortcut = KEY_SHORTCUTS[event.code]\n if (!shortcut) return\n event.preventDefault()\n if (!down || event.repeat || event.altKey) return\n this.runShortcut(shortcut)\n return\n }\n\n if (!KEY_BINDINGS[event.code]) return\n // 其余组合键留给浏览器 / 宿主快捷键\n if (event.altKey) return\n if (down) this.pressedKeys.add(event.code)\n else this.pressedKeys.delete(event.code)\n // 方向键默认会滚动页面,按住时尤其明显\n event.preventDefault()\n }\n\n /** 跑一个组合键动作:与点技能钮同一条路径,保证角标 / 计时 / 回调行为一致 */\n private runShortcut(shortcut: PanelShortcut): void {\n if (this.destroyed) return\n if (shortcut === 'photo') {\n void this.takePhoto()\n return\n }\n void this.toggleRecording()\n }\n\n /** 把按键状态渐推/渐回成行程;有变化才写飞控,静止时零开销 */\n private tickKeyboard(): void {\n if (this.destroyed) return\n const now = performance.now()\n const delta = Math.min(0.1, Math.max(0, (now - this.lastKeyboardTick) / 1000))\n this.lastKeyboardTick = now\n if (delta <= 0) return\n\n // 同一帧可能同时按着同通道的两个键(如 ← 与 Q),按符号叠加后夹回 ±1\n const goal: Record<StickChannel, number> = { throttle: 0, yaw: 0, pitch: 0, roll: 0 }\n for (const code of this.pressedKeys) {\n const bind = KEY_BINDINGS[code]\n if (!bind) continue\n goal[bind.channel] = clampUnit(goal[bind.channel] + bind.sign)\n }\n\n let changed = false\n for (const channel of STICK_CHANNELS) {\n const current = this.keyboardStick[channel]\n const target = goal[channel]\n if (current === target) continue\n const step = delta / (target === 0 ? this.keyboardFall : this.keyboardRise)\n const next =\n Math.abs(target - current) <= step ? target : current + Math.sign(target - current) * step\n this.keyboardStick[channel] = next\n changed = true\n }\n if (!changed) return\n this.applyStick()\n // 杆帽同步跟着键位走:不然只有飞机在动、HUD 上两根杆一动不动,看着像没生效\n this.syncKeyboardVisuals()\n }\n\n /** 松开全部键盘行程(失焦 / 销毁) */\n private releaseKeyboard(): void {\n this.pressedKeys.clear()\n let changed = false\n for (const channel of STICK_CHANNELS) {\n if (this.keyboardStick[channel] !== 0) {\n this.keyboardStick[channel] = 0\n changed = true\n }\n }\n if (!changed) return\n this.applyStick()\n this.syncKeyboardVisuals()\n }\n\n private stopKeyboard(): void {\n if (this.keyboardTimer !== null) {\n window.clearInterval(this.keyboardTimer)\n this.keyboardTimer = null\n }\n }\n\n /** 两杆全部归零(销毁 / 外部打断时用) */\n private releaseSticks(): void {\n this.sticks.forEach((handle) => this.resetStick(handle))\n }\n\n /**\n * 摇杆读数对齐引擎真值。正被按住的杆跳过 —— 它在跟手,回写会与用户输入打架。\n */\n private syncStickReadout(): void {\n const stick = this.fly.sim.stick\n for (const handle of this.sticks) {\n if (handle.pointerId !== null) continue\n const vertical = handle.side === 'left' ? stick.throttle : stick.pitch\n // 读数是\"杆位\"语义:飞控里的偏航是缩放后的值,显示前还原回杆位\n const lateral = handle.side === 'left' ? stick.yaw / (this.yawRateScale || 1) : stick.roll\n handle.read.textContent = formatStickText(vertical, lateral)\n }\n }\n\n private stopTimer(): void {\n if (this.timer !== null) {\n window.clearInterval(this.timer)\n this.timer = null\n }\n }\n\n private renderPitch(value: number): void {\n this.refs.pitchValue.textContent = `${Math.round(value)}°`\n }\n\n /**\n * 下发速度倍率并同步两个控件。\n *\n * 真值以飞控为准(`sim.setSpeedScale` 内部夹到 0 ~ 1000),回写用夹紧后的值。\n * ⚠️ 数字框**聚焦期间不回写**:归一化会抹掉用户刚敲的小数点(\"2.\" → \"2\"),\n * 之后再敲 \"5\" 就成了 25;归一交给 change(失焦 / 回车)。\n */\n private applySpeedScale(raw: string | number): void {\n const value = typeof raw === 'number' ? raw : Number(raw)\n if (!Number.isFinite(value)) return\n this.fly.sim.setSpeedScale(value)\n const applied = this.fly.sim.speedScale\n if (!this.speedDragging) this.refs.speedSlider.value = String(applied)\n if (document.activeElement !== this.refs.speedInput) {\n this.refs.speedInput.value = formatScale(applied)\n }\n this.setNote(\n applied === 0\n ? '速度倍率 0×:机体不会移动(含自动起飞 / 降落)'\n : `速度倍率 ${formatScale(applied)}×`,\n )\n }\n\n /** 把倍率刷到滑杆与数字框上(轮询同步用;拖动滑杆 / 正在输入时跳过,免得和用户的手打架) */\n private renderSpeedScale(value: number): void {\n if (!this.speedDragging) this.refs.speedSlider.value = String(value)\n if (document.activeElement !== this.refs.speedInput) {\n this.refs.speedInput.value = formatScale(value)\n }\n }\n\n private setNote(text: string): void {\n this.refs.note.textContent = text\n }\n\n private handleAction(action: string, el: HTMLElement): void {\n if (this.destroyed) return\n\n switch (action) {\n case 'power-on':\n this.fly.powerOn()\n this.setNote('已上电:自检 + 预热完成后(待机)即可起飞')\n this.sync()\n break\n case 'takeoff':\n if (this.fly.takeOff()) this.setNote('起飞:自动上升至 1.2 米')\n else this.setNote('起飞未受理:需等自检/预热到「地面待机」,且起飞前检查通过')\n this.sync()\n break\n case 'land':\n if (this.fly.land()) this.setNote('开始自动降落')\n else this.setNote('降落未受理:需在空中或起飞爬升中')\n this.sync()\n break\n case 'rth':\n if (this.fly.returnHome('面板按钮')) this.setNote('已触发智能返航')\n else this.setNote('返航未受理:需在空中(已在返航中也会被忽略)')\n this.sync()\n break\n case 'arm-deploy':\n this.fly.setArmsFolded(false)\n this.setNote('机臂展开中,桨叶随后张开')\n this.sync()\n break\n case 'arm-fold':\n this.fly.setArmsFolded(true)\n this.setNote('机臂收拢中')\n this.sync()\n break\n case 'set-flight-mode': {\n const mode = el.dataset.mode as FlightMode | undefined\n if (!mode) return\n this.fly.setMode(mode)\n this.sync()\n break\n }\n case 'set-mode': {\n const mode = el.dataset.mode as CameraMode | undefined\n if (!mode) return\n this.fly.setCameraMode(mode)\n this.sync()\n break\n }\n case 'reset-camera':\n this.fly.resetCamera()\n this.sync()\n break\n case 'preset-pitch': {\n const pitch = Number(el.dataset.pitch)\n if (!Number.isFinite(pitch)) return\n this.fly.setGimbalPitch(pitch)\n this.sync()\n break\n }\n case 'preset-speed': {\n const scale = Number(el.dataset.scale)\n if (!Number.isFinite(scale)) return\n this.applySpeedScale(scale)\n this.sync()\n break\n }\n case 'photo':\n void this.takePhoto()\n break\n case 'set-shoot-mode': {\n if (this.panorama?.running) break\n const mode = el.dataset.mode as ShootMode | undefined\n if (!mode) break\n this.shootMode = mode\n this.syncShootMode()\n break\n }\n case 'toggle-record':\n void this.toggleRecording()\n break\n case 'toggle-collapse':\n this.setCollapsed(!this.collapsed)\n break\n default:\n break\n }\n }\n\n private async takePhoto(): Promise<void> {\n if (this.photoBusy || this.destroyed) return\n // 全景模式:拍照钮变成\"开始 / 停止整组扫描\"的开关(与大疆的拍摄模式语义一致)\n if (this.shootMode !== 'single') {\n await this.runPanorama()\n return\n }\n this.photoBusy = true\n this.setNote('拍照中…')\n try {\n const shot = await this.fly.requestPhoto()\n if (!shot) {\n this.setNote('拍照失败:相机未就绪')\n return\n }\n if (this.opts.onPhoto) this.opts.onPhoto(shot)\n else if (this.opts.autoDownload !== false)\n triggerDownload(shot.dataUrl, `drone-photo-${stamp()}.png`)\n this.setNote(`已拍摄 ${shot.width}×${shot.height}`)\n } catch (error) {\n this.setNote(`拍照异常:${(error as Error).message}`)\n } finally {\n this.photoBusy = false\n }\n }\n\n /**\n * 全景拍摄:点一次开始整组网格扫描,再点一次请求停止(见 `PanoramaCapture`)。\n * 面板只负责挑会话与播报 —— 网格、逐张取景、方位复位都在库里,这里不碰渲染后端。\n */\n private async runPanorama(): Promise<void> {\n const pano = this.panorama\n const service = this.opts.captureService\n if (!pano || !service) return\n if (pano.running) {\n pano.cancel()\n this.setNote('正在停止全景拍摄…')\n return\n }\n const mode = this.shootMode as PanoramaMode\n const preset = PanoramaCapture.preset(mode, service.getCaptureIntrinsics()?.verticalFovDeg)\n // 全景靠**机体原地自转**补水平视野(云台 Pan 只有 ±30°),所以必须悬停中拍:\n // 在地面或航线中自转不成立,真机也是这个前提。提前挡住,免得出 21 张同一方位的废片。\n if (this.fly.phase !== 'flying') {\n this.setNote('全景需要定点悬停中拍摄(请先起飞并停止航线)')\n return\n }\n // 没有会话就现建一个:全景是\"拍一组照片\",总得有地方归档\n const session =\n service.activeSession ??\n service.createSession({ source: 'virtual-camera', name: `全景-${stamp()}` })\n this.photoBusy = true\n this.setNote(`${preset.label} 扫描中 0/${preset.total}…`)\n try {\n const result = await pano.start(session.id, mode)\n if (!result) this.setNote('全景启动失败(可能已有扫描在进行)')\n else if (result.error) this.setNote(`全景未完成:${result.error}`)\n else if (result.cancelled) this.setNote(`全景已停止(${result.captured}/${result.total})`)\n else\n this.setNote(\n `${preset.label} 完成 ${result.captured}/${result.total}${result.stitched ? ' · 已拼接' : ''}`,\n )\n } catch (error) {\n this.setNote(`全景异常:${(error as Error).message}`)\n } finally {\n this.photoBusy = false\n this.sync()\n }\n }\n\n private async toggleRecording(): Promise<void> {\n if (this.destroyed) return\n\n if (this.fly.isRecording) {\n this.setNote('正在收尾…')\n this.lastRecordSeconds = this.fly.sim.recordSeconds\n const blob = await this.fly.stopRecording()\n if (!blob) {\n this.setNote('录像为空,未保存')\n } else {\n if (this.opts.onRecording) this.opts.onRecording(blob, this.lastRecordSeconds)\n else if (this.opts.autoDownload !== false)\n triggerDownload(URL.createObjectURL(blob), `drone-video-${stamp()}.webm`)\n this.setNote(`已保存 ${this.lastRecordSeconds.toFixed(1)}s`)\n }\n this.sync()\n return\n }\n\n if (!this.fly.startRecording()) {\n this.setNote('当前环境不支持录像(需要 MediaRecorder)')\n return\n }\n this.setNote('录制中(云台取景),停止后自动保存')\n this.sync()\n }\n}\n","/**\n * 航线规划面板(原生 DOM 实现):航点任务的「规划 → 执行 → 场景内编辑」一条龙界面。\n *\n * 迁移自 firstapp 的 `views/dji/dji-viewer.vue`「航线」分页,去掉 Vue 依赖后重写为原生 DOM,\n * 与 `drone-panel.ts`(飞行控制面板)同一套范式:只读引擎、只调引擎,面板自身不持有状态。\n *\n * 四个区块(与 firstapp 一一对应):\n * 1. **航线任务** —— 执行 / 暂停 / 继续 / 停止 + 状态角标 + 进度 + 启动被拒的原因;\n * 2. **任务参数** —— 巡航速度 / 机头朝向 / 过点方式 / 完成后动作 / 循环执行;\n * 3. **航点列表** —— 每个航点的 X/Z/高度/速度/悬停/云台俯仰/动作,增删改 + 示例航线 + 清空 + 导出;\n * 4. **场景内编辑** —— 开关三维场景里的拖拽编辑,含手势说明、选中状态、框住航线与图层显隐。\n *\n * 设计边界:\n * - 只用原生 DOM / CSS,不引入任何 UI 框架(库整体与框架无关,Vue / React / 裸页面通用);\n * - 面板不持有状态:渲染时从宿主(`MissionPanelHost`)读快照,操作时调宿主方法,真值永远以引擎为准\n * (见 sync)。宿主用别的方式改了航线,面板下一轮同步就会跟上;\n * - 样式默认注入一份(类名统一 `dmp-` 前缀、配色走 CSS 变量),可用 `injectStyles:false` 关闭。\n *\n * 非直觉约定:\n * - 以 100ms 轮询同步(可用 syncIntervalMs 调或传 0 关闭、完全由宿主调 sync());\n * - 航点数字输入框在**获得焦点期间不回写** —— 否则用户打到一半的 \"-\" / \"1.\" 会被引擎值抹掉,\n * 与 firstapp 里\"草稿行\"要解决的是同一个问题,这里用\"聚焦即不覆盖\"更省一层状态;\n * - 航点列表只在**数量变化**时重建 DOM,其余情况原位更新,避免每轮同步都打断正在编辑的输入框;\n * - 航线执行中航点列表整体 disabled(与真机\"航线上传后不可修改\"一致);**导出是只读操作,执行中照常可用**;\n * - 导出走宿主的 `exportMission()`,面板只负责落盘(或交给宿主的 `onExport`),序列化逻辑留在引擎侧。\n * - 场景内编辑另有硬门槛:需处于观察者视角且航线未在执行,由宿主判定,面板只展示结果。\n */\nimport { MISSION_STATUS_LABELS, isMissionDepartStage } from './drone-sim'\nimport type {\n MissionFinishAction,\n MissionHeadingMode,\n MissionPathMode,\n MissionStage,\n MissionWaypoint,\n} from './drone-sim'\n// 面板只依赖 DronePanelHost 同款的 MissionPanelHost 接口:three 的 DroneFly 与\n// CesiumDroneAdapter 都满足它,换渲染后端时本文件一行都不用改(见 drone-control.ts)。\nimport type { MissionPanelHost } from './drone-control'\nimport type { MissionExportFormat } from './drone-sim'\nimport type { MissionEditState } from './mission-editor'\n\n/** 面板停靠位置(相对挂载容器) */\nexport type MissionPanelPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\n\n/** 「导出航线」产出的载荷:宿主传了 `onExport` 就由宿主决定怎么保存(上传 / 入库 / 换文件名) */\nexport interface MissionExportPayload {\n format: MissionExportFormat\n /** 序列化好的完整文本(JSON 已带缩进,CSV 为中文表头 + CRLF) */\n text: string\n /** 建议文件名(含扩展名) */\n filename: string\n /** MIME 类型 */\n mime: string\n}\n\n/** MissionPanel 构造选项 */\nexport interface MissionPanelOptions {\n /** 挂载容器;缺省挂到 document.body(此时面板改用 fixed 定位) */\n container?: HTMLElement | null\n /** 面板停靠位置,默认 'top-left'(通常与右上角的飞行控制面板对开) */\n position?: MissionPanelPosition\n /** 面板标题,默认「航线规划」 */\n title?: string\n /** 是否注入自带样式,默认 true;关闭后仅保留 `dmp-*` 类名,由宿主自行配色 */\n injectStyles?: boolean\n /** 初始是否可见,默认 true */\n visible?: boolean\n /** 状态轮询间隔(毫秒),默认 100;传 0 关闭自动同步、完全由宿主调 sync() */\n syncIntervalMs?: number\n /** 是否渲染「场景内编辑」区块(需要画布与观察者视角),默认 true;传 false 退化成三块纯列表编辑 */\n sceneEdit?: boolean\n /** 是否启用移动端自适应(窄屏紧凑布局),默认 true */\n adaptive?: boolean\n /**\n * 停靠微调(像素),与 DroneCapturePanel 同语义:y 对 top 方位= marginTop(下移)、\n * 对 bottom 方位= marginBottom(上移);x 对 right 方位= marginRight(左移)、其余= marginLeft。\n * 用来避开宿主自己的边角 UI(如 Cesium 的工具条 / 其他面板)。\n */\n offset?: { x?: number; y?: number }\n /**\n * 「导出航线」的回调。**给了就由宿主接管保存**(上传服务器 / 写数据库 / 自定义文件名),\n * 面板不再触发浏览器下载;不给则默认下载到本地(JSON / CSV,文件名带时间戳)。\n */\n onExport?: (payload: MissionExportPayload) => void\n}\n\nconst STYLE_ID = 'drone-mission-panel-styles'\nconst DEFAULT_SYNC_MS = 100\n/** 巡航速度滑杆范围,与 DroneSim 的归一化保持一致 */\nconst SPEED_MIN = 1\nconst SPEED_MAX = 16\n/** 航点数字字段的取值范围(与内核的归一化保持一致) */\nconst FIELD_RANGES = {\n x: { min: -2000, max: 2000 },\n z: { min: -2000, max: 2000 },\n altitude: { min: 1, max: 500 },\n speed: { min: 0, max: 16 },\n hoverSeconds: { min: 0, max: 60 },\n gimbalPitch: { min: -90, max: 60 },\n} as const\ntype WaypointNumberField = keyof typeof FIELD_RANGES\n/** 航点到点动作(与 firstapp 的 MISSION_ACTIONS 一致) */\nconst WAYPOINT_ACTIONS: Array<{ key: MissionWaypoint['action']; label: string }> = [\n { key: 'none', label: '无动作' },\n { key: 'photo', label: '拍照' },\n]\n/** 任务结束动作(与 firstapp 的 MISSION_FINISH_ACTIONS 一致) */\nconst FINISH_ACTIONS: Array<{ key: MissionFinishAction; label: string }> = [\n { key: 'hover', label: '原地悬停' },\n { key: 'rth', label: '自动返航' },\n { key: 'land', label: '自动降落' },\n]\n/** 哪些飞行阶段允许启动航线(与 firstapp 的 canStartMission 一致) */\nconst MISSION_STARTABLE_PHASES = ['standby', 'motorsOn', 'takingOff', 'flying', 'rth']\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\nfunction escapeHtml(text: string): string {\n const map: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n }\n return text.replace(/[&<>\"']/g, (ch) => map[ch] ?? ch)\n}\n\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return\n if (document.getElementById(STYLE_ID)) return\n const style = document.createElement('style')\n style.id = STYLE_ID\n style.textContent = PANEL_CSS\n document.head.appendChild(style)\n}\n\n/**\n * 航线规划面板:航点任务的规划 / 执行 / 场景内编辑。\n *\n * ```ts\n * // three 后端(自带了场景,初始化后即是 MissionPanelHost)\n * const fly = await createDroneControl({ engine: 'three', scene, camera, renderer, controls })\n * const mission = new MissionPanel(fly, { container: viewportEl, position: 'top-left' })\n *\n * // Cesium 后端同理:适配器实现同一个 MissionPanelHost,面板代码完全一样\n * const adapter = await createDroneControl({ engine: 'cesium', viewer, lon: 116.391, lat: 39.906 })\n * const mission2 = new MissionPanel(adapter, { container: viewportEl, position: 'top-right' })\n * // 卸载:mission.destroy()\n * ```\n */\nexport class MissionPanel {\n /** 面板根元素(宿主可追加类名做二次定制) */\n readonly root: HTMLDivElement\n\n private readonly fly: MissionPanelHost\n private readonly opts: MissionPanelOptions\n private readonly container: HTMLElement\n private readonly abort = new AbortController()\n private timer: number | null = null\n private destroyed = false\n private visible: boolean\n /** 上一次构建航点列表时的航点数:变了才重建 DOM,否则原位更新(不打断输入) */\n private lastWaypointCount = -1\n private speedDragging = false\n /** 坐标显示模式:xyz = 本地米(E/S 偏移),geo = 经纬高(由支持的后端提供) */\n private coordMode: 'xyz' | 'geo' = 'xyz'\n /** 后端是否支持地理坐标(是否渲染\"经纬度 / XYZ\"切换开关) */\n private readonly geoSupported: boolean\n\n private readonly refs: {\n status: HTMLElement\n progress: HTMLElement\n block: HTMLElement\n start: HTMLButtonElement\n pause: HTMLButtonElement\n resume: HTMLButtonElement\n stop: HTMLButtonElement\n total: HTMLElement\n speed: HTMLInputElement\n speedValue: HTMLElement\n loop: HTMLInputElement\n count: HTMLElement\n fieldset: HTMLFieldSetElement\n list: HTMLUListElement\n add: HTMLButtonElement\n preset: HTMLButtonElement\n clear: HTMLButtonElement\n exportJson: HTMLButtonElement\n exportCsv: HTMLButtonElement\n lockHint: HTMLElement\n }\n\n /** 场景内编辑区块的元素(未渲染时为 null) */\n private readonly scene: {\n toggle: HTMLInputElement\n badge: HTMLElement\n status: HTMLElement\n deleteBtn: HTMLButtonElement\n addBtn: HTMLButtonElement\n selectFirst: HTMLButtonElement\n deselect: HTMLButtonElement\n showWaypoints: HTMLInputElement\n showPath: HTMLInputElement\n } | null\n\n /** 构造航线编辑面板(航点列表/参数/执行控制,200ms 同步真值) */\n constructor(fly: MissionPanelHost, options: MissionPanelOptions = {}) {\n if (typeof document === 'undefined') {\n throw new Error('[drone-camera] MissionPanel 需要浏览器环境(document),Node 侧请勿实例化')\n }\n this.fly = fly\n this.opts = options\n this.container = options.container ?? document.body\n this.visible = options.visible ?? true\n this.geoSupported = fly.supportsGeoCoordinates()\n\n if (options.injectStyles !== false) ensureStyles()\n\n // 面板用 absolute 定位到容器;容器若没有定位上下文则补一个 relative,否则会跑到页面左上角\n const host = options.container ?? null\n if (host && getComputedStyle(host).position === 'static') host.style.position = 'relative'\n const layerPosition = host ? 'absolute' : 'fixed'\n\n this.root = document.createElement('div')\n this.root.className = 'dmp'\n this.root.dataset.position = options.position ?? 'top-left'\n this.root.dataset.adaptive = String(options.adaptive !== false)\n this.root.style.position = layerPosition\n // 停靠微调用 margin:与 position 类里的 top/left/bottom/right 是两套独立机制,互不覆盖\n const offset = options.offset ?? {}\n if (offset.y) {\n const key = this.root.dataset.position.startsWith('bottom') ? 'marginBottom' : 'marginTop'\n this.root.style[key] = `${offset.y}px`\n }\n if (offset.x) {\n const key = this.root.dataset.position.endsWith('right') ? 'marginRight' : 'marginLeft'\n this.root.style[key] = `${offset.x}px`\n }\n this.root.innerHTML = this.template()\n if (!this.visible) this.root.style.display = 'none'\n\n this.refs = {\n status: this.query('[data-role=\"mission-status\"]'),\n progress: this.query('[data-role=\"mission-progress\"]'),\n block: this.query('[data-role=\"mission-block\"]'),\n start: this.query<HTMLButtonElement>('[data-action=\"mission-start\"]'),\n pause: this.query<HTMLButtonElement>('[data-action=\"mission-pause\"]'),\n resume: this.query<HTMLButtonElement>('[data-action=\"mission-resume\"]'),\n stop: this.query<HTMLButtonElement>('[data-action=\"mission-stop\"]'),\n total: this.query('[data-role=\"mission-total\"]'),\n speed: this.query<HTMLInputElement>('[data-role=\"mission-speed\"]'),\n speedValue: this.query('[data-role=\"mission-speed-value\"]'),\n loop: this.query<HTMLInputElement>('[data-role=\"mission-loop\"]'),\n count: this.query('[data-role=\"waypoint-count\"]'),\n fieldset: this.query<HTMLFieldSetElement>('[data-role=\"waypoint-fieldset\"]'),\n list: this.query<HTMLUListElement>('[data-role=\"waypoint-list\"]'),\n add: this.query<HTMLButtonElement>('[data-action=\"waypoint-add\"]'),\n preset: this.query<HTMLButtonElement>('[data-action=\"waypoint-preset\"]'),\n clear: this.query<HTMLButtonElement>('[data-action=\"waypoint-clear\"]'),\n exportJson: this.query<HTMLButtonElement>(\n '[data-action=\"waypoint-export\"][data-format=\"json\"]',\n ),\n exportCsv: this.query<HTMLButtonElement>(\n '[data-action=\"waypoint-export\"][data-format=\"csv\"]',\n ),\n lockHint: this.query('[data-role=\"waypoint-lock\"]'),\n }\n\n const sceneCard = this.root.querySelector<HTMLElement>('[data-blk=\"scene-edit\"]')\n this.scene = sceneCard\n ? {\n toggle: sceneCard.querySelector<HTMLInputElement>('[data-role=\"scene-edit-toggle\"]')!,\n badge: sceneCard.querySelector<HTMLElement>('[data-role=\"scene-edit-badge\"]')!,\n status: sceneCard.querySelector<HTMLElement>('[data-role=\"scene-edit-status\"]')!,\n deleteBtn: sceneCard.querySelector<HTMLButtonElement>('[data-action=\"scene-delete\"]')!,\n addBtn: sceneCard.querySelector<HTMLButtonElement>('[data-action=\"scene-add\"]')!,\n selectFirst: sceneCard.querySelector<HTMLButtonElement>(\n '[data-action=\"scene-select-first\"]',\n )!,\n deselect: sceneCard.querySelector<HTMLButtonElement>('[data-action=\"scene-deselect\"]')!,\n showWaypoints: sceneCard.querySelector<HTMLInputElement>(\n '[data-role=\"toggle-waypoints\"]',\n )!,\n showPath: sceneCard.querySelector<HTMLInputElement>('[data-role=\"toggle-path\"]')!,\n }\n : null\n\n this.bind()\n this.sync()\n this.mount()\n }\n\n // ───────────────────────────── 生命周期 ─────────────────────────────\n\n /** 插入容器(构造时已自动调用一次) */\n mount(): this {\n if (!this.destroyed && !this.root.isConnected) this.container.appendChild(this.root)\n return this\n }\n\n /** 从容器移除但保留实例,可再次 mount */\n unmount(): this {\n this.root.remove()\n return this\n }\n\n /** 显示 / 隐藏 */\n setVisible(visible: boolean): this {\n this.visible = visible\n this.root.style.display = visible ? '' : 'none'\n return this\n }\n\n /** 当前是否可见 */\n isVisible(): boolean {\n return this.visible\n }\n\n /** 彻底销毁:解绑事件、停轮询、移除 DOM */\n destroy(): void {\n if (this.destroyed) return\n this.destroyed = true\n this.abort.abort()\n this.stopTimer()\n this.root.remove()\n }\n\n /**\n * 移除已注入的共享样式。\n * 多个面板共存时请等最后一个销毁后再调用,否则其余面板会掉样式。\n */\n static removeInjectedStyles(): void {\n if (typeof document === 'undefined') return\n document.getElementById(STYLE_ID)?.remove()\n }\n\n // ───────────────────────────── 状态同步 ─────────────────────────────\n\n /** 把引擎当前状态刷到面板上(任务状态/按钮可用性/参数回显/航点列表/场景编辑) */\n sync(): void {\n if (this.destroyed) return\n const snap = this.fly.getSnapshot()\n const mission = snap.mission\n const editable = mission.status === 'idle'\n\n // ── 航线任务 ──\n this.refs.status.textContent = MISSION_STATUS_LABELS[mission.status] ?? mission.status\n this.refs.status.className = `dmp-badge${toneClass(mission.status)}`\n this.refs.progress.textContent = this.progressText(mission)\n this.refs.block.textContent = this.blockReason(snap, editable)\n\n const canStart =\n mission.status === 'idle' &&\n mission.total > 0 &&\n MISSION_STARTABLE_PHASES.includes(snap.phase)\n this.refs.start.disabled = !canStart\n this.refs.pause.disabled = mission.status !== 'running'\n this.refs.resume.disabled = mission.status !== 'paused'\n this.refs.stop.disabled = mission.status === 'idle' && snap.phase !== 'takingOff'\n\n // ── 任务参数 ──\n this.refs.total.textContent = `${mission.total} 个航点`\n if (!this.speedDragging) {\n this.refs.speed.value = String(mission.config.autoSpeed)\n this.renderSpeed(mission.config.autoSpeed)\n }\n this.writeChecked(this.refs.loop, mission.config.loop)\n this.markActive('[data-action=\"mission-heading\"]', mission.config.headingMode)\n this.markActive('[data-action=\"mission-path\"]', mission.config.pathMode)\n this.markActive('[data-action=\"mission-finish\"]', mission.config.finishAction)\n\n // ── 航点列表 ──\n if (mission.waypoints.length !== this.lastWaypointCount) {\n this.buildRows(mission.waypoints)\n this.lastWaypointCount = mission.waypoints.length\n }\n this.refs.count.textContent = `${mission.waypoints.length} 个`\n this.refs.fieldset.disabled = !editable\n this.refs.add.disabled = !editable\n this.refs.preset.disabled = !editable\n this.refs.clear.disabled = !editable\n // 导出是只读操作:执行中也能导,只有\"没有航点\"才没得导\n const noWaypoints = mission.waypoints.length === 0\n this.refs.exportJson.disabled = noWaypoints\n this.refs.exportCsv.disabled = noWaypoints\n this.refs.lockHint.hidden = editable\n this.syncRows(mission)\n\n // ── 场景内编辑 ──\n if (this.scene) this.syncScene(this.fly.getMissionEditState(), editable)\n }\n\n // ───────────────────────────── 内部实现:模板 ─────────────────────────────\n\n private query<T extends HTMLElement = HTMLElement>(selector: string): T {\n const el = this.root.querySelector<T>(selector)\n if (!el) throw new Error(`[drone-camera] 航线面板结构缺失:${selector}`)\n return el\n }\n\n private template(): string {\n const heading = [\n { value: 'auto', label: '指向下一航点' },\n { value: 'fixed', label: '锁定起始航向' },\n ]\n .map(\n (item) =>\n `<button type=\"button\" data-action=\"mission-heading\" data-value=\"${item.value}\">${item.label}</button>`,\n )\n .join('')\n const path = [\n { value: 'straight', label: '到点减速' },\n { value: 'curved', label: '曲线过点' },\n ]\n .map(\n (item) =>\n `<button type=\"button\" data-action=\"mission-path\" data-value=\"${item.value}\">${item.label}</button>`,\n )\n .join('')\n const finish = FINISH_ACTIONS.map(\n (item) =>\n `<button type=\"button\" data-action=\"mission-finish\" data-value=\"${item.key}\">${item.label}</button>`,\n ).join('')\n\n return `\n <div class=\"dmp-head\">\n <span class=\"dmp-title\">${escapeHtml(this.opts.title ?? '航线规划')}</span>\n <button type=\"button\" class=\"dmp-icon\" data-action=\"toggle-collapse\" title=\"收起 / 展开\" aria-label=\"收起或展开\">–</button>\n </div>\n <div class=\"dmp-body\">\n <section class=\"dmp-card\">\n <div class=\"dmp-card-head\"><span>航线任务</span><em class=\"dmp-badge\" data-role=\"mission-status\">--</em></div>\n <div class=\"dmp-btns\">\n <button type=\"button\" class=\"dmp-btn dmp-primary\" data-action=\"mission-start\">执行航线</button>\n <button type=\"button\" class=\"dmp-btn\" data-action=\"mission-pause\">暂停</button>\n <button type=\"button\" class=\"dmp-btn\" data-action=\"mission-resume\">继续</button>\n <button type=\"button\" class=\"dmp-btn dmp-wide\" data-action=\"mission-stop\">停止任务(原地悬停)</button>\n </div>\n <p class=\"dmp-progress\" data-role=\"mission-progress\"></p>\n <p class=\"dmp-block\" data-role=\"mission-block\" hidden></p>\n <p class=\"dmp-fine\">地面直接点「执行航线」会先自动起飞,到 1.2 米后自动接上航线;空中启动同样从首个航点走起。飞向首个航点与返航同款分段:先垂直调整到航点高度 → 水平飞过去 → 收到航点高度 → 机头对准航线方向,之后才开始执行航线。执行中拨动摇杆会暂停任务并交回手动控制。</p>\n </section>\n\n <section class=\"dmp-card\">\n <div class=\"dmp-card-head\"><span>任务参数</span><em data-role=\"mission-total\">0 个航点</em></div>\n <label class=\"dmp-range-row\">\n <span>巡航速度</span>\n <input type=\"range\" data-role=\"mission-speed\" min=\"${SPEED_MIN}\" max=\"${SPEED_MAX}\" step=\"0.5\" value=\"6\" />\n <strong data-role=\"mission-speed-value\">6.0 m/s</strong>\n </label>\n <p class=\"dmp-row-label\">机头朝向</p>\n <div class=\"dmp-seg dmp-seg-2\">${heading}</div>\n <p class=\"dmp-row-label\">过点方式</p>\n <div class=\"dmp-seg dmp-seg-2\">${path}</div>\n <p class=\"dmp-row-label\">全部完成后</p>\n <div class=\"dmp-seg dmp-seg-3\">${finish}</div>\n <label class=\"dmp-switch\"><input type=\"checkbox\" data-role=\"mission-loop\" /><span>循环执行(完成后回到第 1 个航点)</span></label>\n <p class=\"dmp-fine\">曲线过点保持巡航速度、距航点 3 米就切向下一点,所以会忽略航点悬停;直线模式按刹车距离提前收油,过点更稳。</p>\n </section>\n\n <section class=\"dmp-card\">\n <div class=\"dmp-card-head\"><span>航点列表</span><em data-role=\"waypoint-count\">0 个</em></div>\n ${this.coordModeTemplate()}\n <fieldset class=\"dmp-wp-fieldset\" data-role=\"waypoint-fieldset\">\n <ul class=\"dmp-wp-list\" data-role=\"waypoint-list\"></ul>\n </fieldset>\n <div class=\"dmp-seg dmp-seg-3\">\n <button type=\"button\" data-action=\"waypoint-add\">添加航点</button>\n <button type=\"button\" data-action=\"waypoint-preset\">示例航线</button>\n <button type=\"button\" data-action=\"waypoint-clear\">清空</button>\n </div>\n <div class=\"dmp-seg dmp-seg-2\">\n <button type=\"button\" data-action=\"waypoint-export\" data-format=\"json\" title=\"完整存档:航点 + 任务参数 + 概览统计,可再次载入\">导出 JSON</button>\n <button type=\"button\" data-action=\"waypoint-export\" data-format=\"csv\" title=\"纯航点表,Excel / WPS 可直接打开\">导出 CSV</button>\n </div>\n <p class=\"dmp-fine\">导出内容为当前航点与任务参数,坐标与列表一致(相对返航点的东 / 南向偏移)。</p>\n <p class=\"dmp-fine\" data-role=\"waypoint-lock\" hidden>任务执行中:停止任务之后才能修改航点(与真机「航线上传后不可改」一致)。</p>\n </section>\n\n ${this.sceneEditTemplate()}\n </div>\n `\n }\n\n /** 坐标显示模式切换段:后端支持地理坐标(Cesium)时出现,否则整段不渲染 */\n private coordModeTemplate(): string {\n if (!this.geoSupported) return ''\n return `\n <div class=\"dmp-seg dmp-seg-2 dmp-coord-mode\" data-blk=\"coord-mode\">\n <button type=\"button\" data-action=\"coord-mode\" data-value=\"xyz\" class=\"is-active\">XYZ</button>\n <button type=\"button\" data-action=\"coord-mode\" data-value=\"geo\">经纬度</button>\n </div>`\n }\n\n /** 「场景内编辑」区块;sceneEdit:false 时整块不渲染 */\n private sceneEditTemplate(): string {\n if (this.opts.sceneEdit === false) return ''\n return `\n <section class=\"dmp-card\" data-blk=\"scene-edit\">\n <div class=\"dmp-card-head\"><span>场景内编辑</span><em class=\"dmp-badge\" data-role=\"scene-edit-badge\">已关闭</em></div>\n <label class=\"dmp-switch\"><input type=\"checkbox\" data-role=\"scene-edit-toggle\" /><span>在三维场景里直接编辑航点</span></label>\n <ul class=\"dmp-gesture\">\n <li><b>拖拽航点</b><span>在它当前高度的水平面内移动</span></li>\n <li><b>Shift + 拖拽</b><span>改高度(按住 Alt 可关掉 1 米吸附)</span></li>\n <li><b>双击地面</b><span>在该处插入一个航点</span></li>\n <li><b>Delete</b><span>删除当前选中的航点,Esc 取消选中</span></li>\n </ul>\n <div class=\"dmp-status\" data-role=\"scene-edit-status\"></div>\n <div class=\"dmp-seg dmp-seg-3\">\n <button type=\"button\" data-action=\"scene-delete\">删除选中</button>\n <button type=\"button\" data-action=\"scene-add\">飞机位置新增</button>\n <button type=\"button\" data-action=\"scene-frame\">框住航线</button>\n </div>\n <div class=\"dmp-seg dmp-seg-2\">\n <button type=\"button\" data-action=\"scene-select-first\">选中第 1 个航点</button>\n <button type=\"button\" data-action=\"scene-deselect\">取消选中</button>\n </div>\n <div class=\"dmp-switch-row\">\n <label class=\"dmp-switch\"><input type=\"checkbox\" data-role=\"toggle-waypoints\" /><span>显示航点</span></label>\n <label class=\"dmp-switch\"><input type=\"checkbox\" data-role=\"toggle-path\" /><span>显示航线</span></label>\n </div>\n <p class=\"dmp-fine\">隐藏只是不画,不影响任务执行;编辑航点则要求航线未在执行,且处于观察者视角(机载视角下屏幕里没有可点的目标)。</p>\n </section>\n `\n }\n\n /** 单个航点行的 DOM(结构固定,值由 syncRows 原位写入);首行按坐标模式在 XYZ / 经纬高之间切换 */\n private rowTemplate(index: number): string {\n const actions = WAYPOINT_ACTIONS.map(\n (item) => `<option value=\"${item.key}\">${item.label}</option>`,\n ).join('')\n const firstRow =\n this.coordMode === 'geo'\n ? `<div class=\"dmp-wp-row dmp-wp-row-geo\">\n <label class=\"dmp-wp-field\"><span>经度</span><input class=\"dmp-wp-input\" type=\"number\" step=\"0.00000001\" data-field=\"lon\" /></label>\n <label class=\"dmp-wp-field\"><span>纬度</span><input class=\"dmp-wp-input\" type=\"number\" step=\"0.00000001\" data-field=\"lat\" /></label>\n <label class=\"dmp-wp-field dmp-wp-geo-h\"><span>高度</span><input class=\"dmp-wp-input\" type=\"number\" step=\"0.1\" data-field=\"geoHeight\" /></label>\n </div>`\n : `<div class=\"dmp-wp-row\">\n <label class=\"dmp-wp-field\"><span>X</span><input class=\"dmp-wp-input\" type=\"number\" step=\"2\" data-field=\"x\" /></label>\n <label class=\"dmp-wp-field\"><span>Z</span><input class=\"dmp-wp-input\" type=\"number\" step=\"2\" data-field=\"z\" /></label>\n <label class=\"dmp-wp-field\"><span>高度</span><input class=\"dmp-wp-input\" type=\"number\" step=\"1\" min=\"1\" data-field=\"altitude\" /></label>\n </div>`\n return `\n <li class=\"dmp-wp\" data-index=\"${index}\" data-action=\"waypoint-select\">\n <div class=\"dmp-wp-head\">\n <span class=\"dmp-wp-index\">${index + 1}</span>\n <span class=\"dmp-wp-coord\" data-role=\"wp-coord\"></span>\n <button type=\"button\" class=\"dmp-mini\" data-action=\"waypoint-remove\">删除</button>\n </div>\n ${firstRow}\n <div class=\"dmp-wp-row\">\n <label class=\"dmp-wp-field\"><span>速度</span><input class=\"dmp-wp-input\" type=\"number\" step=\"1\" min=\"0\" max=\"16\" data-field=\"speed\" /></label>\n <label class=\"dmp-wp-field\"><span>悬停</span><input class=\"dmp-wp-input\" type=\"number\" step=\"1\" min=\"0\" max=\"60\" data-field=\"hoverSeconds\" /></label>\n <label class=\"dmp-wp-field dmp-wp-gimbal\">\n <input type=\"checkbox\" data-field=\"gimbalOn\" />\n <span>云台</span>\n <input class=\"dmp-wp-input\" type=\"number\" step=\"5\" min=\"-90\" max=\"60\" data-field=\"gimbalPitch\" />\n </label>\n <label class=\"dmp-wp-field\"><span>动作</span><select class=\"dmp-wp-select\" data-field=\"action\">${actions}</select></label>\n </div>\n <em class=\"dmp-wp-flag\" data-role=\"wp-flag\" hidden>当前目标</em>\n </li>\n `\n }\n\n /** 重建整张航点列表(仅在航点数量变化时调用) */\n private buildRows(waypoints: MissionWaypoint[]): void {\n this.refs.list.innerHTML = waypoints.length\n ? waypoints.map((_, index) => this.rowTemplate(index)).join('')\n : '<li class=\"dmp-wp-empty\">暂无航点</li>'\n }\n\n /** 原位写入每个航点行的值:聚焦中的输入框跳过,避免抹掉用户打了一半的数 */\n private syncRows(mission: { waypoints: MissionWaypoint[]; index: number; status: string }): void {\n const rows = this.refs.list.querySelectorAll<HTMLLIElement>('li.dmp-wp')\n const selected = this.fly.getMissionSelected()\n rows.forEach((row, index) => {\n const waypoint = mission.waypoints[index]\n if (!waypoint) return\n row.classList.toggle('is-active', mission.status !== 'idle' && mission.index === index)\n row.classList.toggle('is-selected', selected === index)\n const coord = row.querySelector<HTMLElement>('[data-role=\"wp-coord\"]')\n if (coord) coord.textContent = this.waypointCoordText(waypoint, index)\n\n const numberInput = (field: WaypointNumberField): void => {\n const input = row.querySelector<HTMLInputElement>(`[data-field=\"${field}\"]`)\n if (input) this.writeValue(input, String(waypoint[field]))\n }\n if (this.coordMode === 'geo') {\n // geo 模式首行是经度/纬度/高度(绝对海拔)的可编辑框,值来自 getWaypointGeo\n const geo = this.fly.getWaypointGeo(index)\n if (geo) {\n const lonInput = row.querySelector<HTMLInputElement>('[data-field=\"lon\"]')\n if (lonInput) this.writeValue(lonInput, geo.lon.toFixed(8))\n const latInput = row.querySelector<HTMLInputElement>('[data-field=\"lat\"]')\n if (latInput) this.writeValue(latInput, geo.lat.toFixed(8))\n const hInput = row.querySelector<HTMLInputElement>('[data-field=\"geoHeight\"]')\n if (hInput) this.writeValue(hInput, geo.height.toFixed(1))\n }\n } else {\n numberInput('x')\n numberInput('z')\n numberInput('altitude')\n }\n numberInput('speed')\n numberInput('hoverSeconds')\n\n const gimbalOn = row.querySelector<HTMLInputElement>('[data-field=\"gimbalOn\"]')\n if (gimbalOn) this.writeChecked(gimbalOn, waypoint.gimbalPitch !== null)\n const gimbalPitch = row.querySelector<HTMLInputElement>('[data-field=\"gimbalPitch\"]')\n if (gimbalPitch) {\n this.writeValue(gimbalPitch, String(waypoint.gimbalPitch ?? -45))\n gimbalPitch.disabled = waypoint.gimbalPitch === null\n }\n const action = row.querySelector<HTMLSelectElement>('[data-field=\"action\"]')\n if (action) this.writeValue(action, waypoint.action)\n\n const flag = row.querySelector<HTMLElement>('[data-role=\"wp-flag\"]')\n if (flag) flag.hidden = mission.status === 'idle' || mission.index !== index\n })\n }\n\n // ───────────────────────────── 内部实现:事件 ─────────────────────────────\n\n private bind(): void {\n const { signal } = this.abort\n\n // 事件委托:整块面板一个 click 句柄,内部按 [data-action] 分发\n this.root.addEventListener(\n 'click',\n (event) => {\n const target = (event.target as HTMLElement | null)?.closest<HTMLElement>('[data-action]')\n if (target) this.handleAction(target.dataset.action ?? '', target)\n },\n { signal },\n )\n\n // 数字输入 / 下拉 / 勾选:统一在航点列表上委托\n this.refs.list.addEventListener('input', (event) => this.handleRowInput(event), { signal })\n this.refs.list.addEventListener('change', (event) => this.handleRowInput(event), { signal })\n\n // 巡航速度滑杆:拖动期间暂停回写,免得和用户的手打架\n this.refs.speed.addEventListener(\n 'input',\n () => {\n const value = Number(this.refs.speed.value)\n if (!Number.isFinite(value)) return\n this.fly.setMissionConfig({ autoSpeed: value })\n this.renderSpeed(value)\n },\n { signal },\n )\n this.refs.speed.addEventListener('pointerdown', () => (this.speedDragging = true), { signal })\n window.addEventListener('pointerup', () => (this.speedDragging = false), { signal })\n\n // 循环执行\n this.refs.loop.addEventListener(\n 'change',\n () => this.fly.setMissionConfig({ loop: this.refs.loop.checked }),\n { signal },\n )\n\n if (this.scene) {\n this.scene.toggle.addEventListener(\n 'change',\n () => {\n this.fly.setMissionEditEnabled(this.scene?.toggle.checked ?? false)\n this.sync()\n },\n { signal },\n )\n this.scene.showWaypoints.addEventListener(\n 'change',\n () => this.fly.setMissionWaypointsVisible(this.scene?.showWaypoints.checked ?? true),\n { signal },\n )\n this.scene.showPath.addEventListener(\n 'change',\n () => this.fly.setMissionPathVisible(this.scene?.showPath.checked ?? true),\n { signal },\n )\n }\n\n if (this.opts.syncIntervalMs !== 0) {\n this.timer = window.setInterval(\n () => this.sync(),\n this.opts.syncIntervalMs ?? DEFAULT_SYNC_MS,\n )\n }\n }\n\n private handleAction(action: string, target: HTMLElement): void {\n const fly = this.fly\n const rowIndex = (): number =>\n Number(target.closest<HTMLElement>('.dmp-wp')?.dataset.index ?? -1)\n switch (action) {\n case 'toggle-collapse':\n this.root.dataset.collapsed = String(this.root.dataset.collapsed !== 'true')\n break\n case 'mission-start':\n fly.startMission()\n break\n case 'mission-pause':\n fly.pauseMission('用户暂停')\n break\n case 'mission-resume':\n fly.resumeMission()\n break\n case 'mission-stop':\n fly.stopMission('用户停止')\n break\n case 'mission-heading':\n fly.setMissionConfig({ headingMode: target.dataset.value as MissionHeadingMode })\n break\n case 'mission-path':\n fly.setMissionConfig({ pathMode: target.dataset.value as MissionPathMode })\n break\n case 'mission-finish':\n fly.setMissionConfig({ finishAction: target.dataset.value as MissionFinishAction })\n break\n case 'waypoint-add':\n this.addWaypoint()\n break\n case 'waypoint-preset':\n this.selectWaypoint(-1)\n fly.resetMissionToDefault()\n break\n case 'waypoint-clear':\n this.selectWaypoint(-1)\n fly.setMission([])\n break\n case 'waypoint-export':\n this.exportMission(target.dataset.format === 'csv' ? 'csv' : 'json', target)\n break\n case 'waypoint-select':\n this.selectWaypoint(rowIndex())\n break\n case 'waypoint-remove':\n this.removeWaypoint(rowIndex())\n break\n case 'scene-delete':\n this.deleteSelected()\n break\n case 'scene-add':\n this.addAtDrone()\n break\n case 'scene-frame':\n fly.frameMission()\n break\n case 'scene-select-first':\n this.selectWaypoint(0)\n break\n case 'scene-deselect':\n this.selectWaypoint(-1)\n break\n case 'coord-mode':\n this.setCoordMode(target.dataset.value === 'geo' ? 'geo' : 'xyz')\n break\n default:\n return\n }\n this.sync()\n }\n\n /** 航点行内的字段编辑(数字 / 勾选 / 下拉共用一条路径) */\n private handleRowInput(event: Event): void {\n const field = (event.target as HTMLElement | null)?.closest<HTMLElement>('[data-field]')\n const index = Number((field?.closest('.dmp-wp') as HTMLElement | null)?.dataset.index ?? -1)\n if (!field || index < 0) return\n const name = field.dataset.field ?? ''\n\n if (name === 'gimbalOn') {\n const enabled = (field as HTMLInputElement).checked\n this.patchWaypoint(index, { gimbalPitch: enabled ? -45 : null })\n return\n }\n if (name === 'action') {\n const value = (field as HTMLSelectElement).value === 'photo' ? 'photo' : 'none'\n this.patchWaypoint(index, { action: value })\n return\n }\n // 经纬度 / 绝对海拔:geo 模式下的可编辑字段,走逆向换算\n if (name === 'lon' || name === 'lat' || name === 'geoHeight') {\n const raw = (event.target as HTMLInputElement).value.trim()\n this.patchWaypointGeo(index, name, raw)\n return\n }\n if (!(name in FIELD_RANGES)) return\n const key = name as WaypointNumberField\n const raw = (event.target as HTMLInputElement).value.trim()\n // 允许 \"-\" / \"1.\" 这类输入中间态留在框里,不往下发\n if (raw === '') return\n const value = Number(raw)\n if (!Number.isFinite(value)) return\n const range = FIELD_RANGES[key]\n this.patchWaypoint(index, {\n [key]: clamp(value, range.min, range.max),\n } as Partial<MissionWaypoint>)\n }\n\n // ───────────────────────────── 内部实现:动作 ─────────────────────────────\n\n /** 航点列表的权威来源是仿真内核,不是面板快照(快照 100ms 才刷一次,连续编辑会丢格) */\n private waypoints(): MissionWaypoint[] {\n return this.fly.sim.mission.map((waypoint) => ({ ...waypoint }))\n }\n\n private patchWaypoint(index: number, patch: Partial<MissionWaypoint>): void {\n const list = this.waypoints().map((waypoint, i) =>\n i === index ? { ...waypoint, ...patch } : waypoint,\n )\n this.fly.setMission(list)\n }\n\n /**\n * geo 模式下编辑经度 / 纬度 / 绝对海拔:取当前经纬高,只替换被改的那一项(其余保持),\n * 走后端 `setWaypointGeo` 做经纬高 → 本地米坐标的逆向换算。中间态(\"-\"/\"1.\")会被 `raw === ''`\n * 与 `!isFinite` 两道门槛拦下,不往下发;越界值就地夹紧,避免面板抖动。\n */\n private patchWaypointGeo(index: number, field: string, raw: string): void {\n if (raw === '') return\n const current = this.fly.getWaypointGeo(index)\n if (!current) return\n const value = Number(raw)\n if (!Number.isFinite(value)) return\n const next = { lon: current.lon, lat: current.lat, height: current.height }\n if (field === 'lon') next.lon = Math.min(180, Math.max(-180, value))\n else if (field === 'lat') next.lat = Math.min(90, Math.max(-90, value))\n else if (field === 'geoHeight') next.height = Math.min(9000, Math.max(0, value))\n this.fly.setWaypointGeo(index, next)\n }\n\n private addWaypoint(): void {\n const list = this.waypoints()\n const last = list[list.length - 1]\n const next: Partial<MissionWaypoint> = last\n ? { x: last.x + 12, z: last.z, altitude: last.altitude, speed: last.speed }\n : { x: 0, z: -30, altitude: 30, speed: 0, hoverSeconds: 0, gimbalPitch: null, action: 'none' }\n this.fly.setMission([...list, next])\n this.revealRow(this.fly.sim.mission.length - 1)\n }\n\n private removeWaypoint(index: number): void {\n if (index < 0) return\n this.fly.setMission(this.waypoints().filter((_, i) => i !== index))\n // 删掉的可能正是场景里选中的那个,索引会整体前移,直接取消选中最不容易出错\n this.selectWaypoint(-1)\n }\n\n /**\n * 导出航线(只读操作,任务执行中也允许)。\n *\n * 默认把序列化结果落成本地文件:`json` 是完整存档(坐标约定 + 任务参数 + 概览统计),\n * `csv` 是纯航点表(带 UTF-8 BOM,Excel / WPS 双击不乱码)。\n * 宿主传了 `onExport` 时改由宿主接管保存,这里只把文本递出去。\n */\n private exportMission(format: MissionExportFormat, button: HTMLElement): void {\n const text = this.fly.exportMission(format)\n const filename = missionFilename(format)\n const mime = format === 'csv' ? 'text/csv;charset=utf-8' : 'application/json'\n\n if (this.opts.onExport) {\n this.opts.onExport({ format, text, filename, mime })\n this.flashButton(button, '已交给宿主')\n return\n }\n\n const blob = new Blob([format === 'csv' ? `\\ufeff${text}` : text], { type: mime })\n const url = URL.createObjectURL(blob)\n const link = document.createElement('a')\n link.href = url\n link.download = filename\n link.rel = 'noopener'\n // Firefox 要求链接在文档里才会响应 click\n document.body.appendChild(link)\n link.click()\n link.remove()\n // 立刻 revoke 有概率让浏览器来不及取数据,留一拍\n window.setTimeout(() => URL.revokeObjectURL(url), 2000)\n this.flashButton(button, '已导出')\n }\n\n /** 按钮上的短反馈(导出完成 / 已交给宿主),1.4s 后恢复原文案 */\n private flashButton(button: HTMLElement, text: string): void {\n if (!button.dataset.label) button.dataset.label = button.textContent ?? ''\n button.textContent = text\n window.setTimeout(() => {\n if (button.dataset.label) button.textContent = button.dataset.label\n }, 1400)\n }\n\n /** 选中航点(-1 = 取消):面板列表与三维场景共用同一份选中态 */\n private selectWaypoint(index: number): void {\n this.fly.setMissionSelected(index)\n if (index >= 0) this.revealRow(index)\n }\n\n private deleteSelected(): void {\n const index = this.fly.getMissionSelected()\n if (index < 0) return\n this.fly.deleteMissionWaypoint(index)\n }\n\n private addAtDrone(): void {\n const index = this.fly.addMissionWaypointAtDrone()\n if (index >= 0) this.revealRow(index)\n }\n\n /** 从场景里选中时,把列表里对应的那一行滚进视野(列表有最大高度) */\n private revealRow(index: number): void {\n const row = this.refs.list.querySelector<HTMLElement>(`[data-index=\"${index}\"]`)\n row?.scrollIntoView({ block: 'nearest' })\n }\n\n // ───────────────────────────── 内部实现:渲染小件 ─────────────────────────────\n\n private renderSpeed(value: number): void {\n this.refs.speedValue.textContent = `${value.toFixed(1)} m/s`\n }\n\n /** 分段按钮的选中态:按 data-value 打标 */\n private markActive(selector: string, value: string): void {\n this.root.querySelectorAll<HTMLElement>(selector).forEach((button) => {\n button.classList.toggle('is-active', button.dataset.value === value)\n })\n }\n\n /** 写入输入框的值;聚焦中的框跳过(用户可能正在输入中间态) */\n private writeValue(input: HTMLInputElement | HTMLSelectElement, value: string): void {\n if (document.activeElement === input) return\n if (input.value !== value) input.value = value\n }\n\n /**\n * 写入勾选态;聚焦中的框跳过 —— 不只是为了不打架,还是正确性要求:\n * 航点行整行可点(选中航点),点击复选框会先冒泡到面板触发一次 sync;\n * 此刻浏览器把 checked 置为 true、但 input/change 事件尚未派发,若这里照写引擎值(false)\n * 就会把刚勾上的框拽回去,随后 change 又读到 false —— 勾选\"点了没反应\"。\n */\n private writeChecked(input: HTMLInputElement, checked: boolean): void {\n if (document.activeElement === input) return\n if (input.checked !== checked) input.checked = checked\n }\n\n private progressText(mission: {\n status: string\n stage: MissionStage\n index: number\n total: number\n progress: number\n etaSeconds: number\n }): string {\n if (mission.status === 'idle') {\n return mission.total ? `待执行 · 共 ${mission.total} 个航点` : '暂无航线'\n }\n const head = isMissionDepartStage(mission.stage)\n ? `启航(航点 1/${mission.total}) · ${this.stageLabel(mission.stage)}`\n : `第 ${mission.index + 1}/${mission.total} 个航点 · ${this.stageLabel(mission.stage)}`\n const percent = `${Math.round(mission.progress * 100)}%`\n const seconds = mission.etaSeconds\n const eta =\n seconds >= 60\n ? `${Math.floor(seconds / 60)} 分 ${Math.round(seconds % 60)} 秒`\n : `${seconds.toFixed(0)} 秒`\n return `${head} · ${percent} · 剩余 ${eta}`\n }\n\n private stageLabel(stage: string): string {\n const labels: Record<string, string> = {\n idle: '待执行',\n 'depart-climb': '垂直调整到首航点高度',\n 'depart-cruise': '水平飞向首个航点',\n 'depart-settle': '调整到航点高度',\n 'depart-align': '对准航线方向',\n goto: '飞向航点',\n hover: '航点悬停',\n }\n return labels[stage] ?? stage\n }\n\n /** 启动不了时把原因摆出来,别让按钮灰着却没有解释 */\n private blockReason(\n snap: {\n phase: string\n phaseLabel: string\n positionSource: string\n mission: { status: string; total: number }\n },\n editable: boolean,\n ): string {\n if (!editable) return ''\n const mission = snap.mission\n if (mission.total === 0) return '航线为空:先「添加航点」或「示例航线」'\n if (snap.positionSource === 'atti') return '当前无定位(姿态模式),航点任务需要 GNSS'\n if (!MISSION_STARTABLE_PHASES.includes(snap.phase)) {\n return `当前阶段(${snap.phaseLabel})无法启动航线`\n }\n return ''\n }\n\n private syncScene(state: MissionEditState, editable: boolean): void {\n const scene = this.scene\n if (!scene) return\n this.writeChecked(scene.toggle, state.enabled)\n\n const badge = !state.enabled\n ? { label: '已关闭', tone: '' }\n : state.dragging\n ? { label: '拖动中', tone: ' is-warn' }\n : state.active\n ? { label: '编辑中', tone: ' is-ok' }\n : { label: '待切换视角', tone: ' is-warn' }\n scene.badge.textContent = badge.label\n scene.badge.className = `dmp-badge${badge.tone}`\n\n // 状态行:优先报拖动动作,其次报选中/未选中,最后才提示\"为什么不能编\"\n const waypoint = state.selectedIndex >= 0 ? this.fly.sim.mission[state.selectedIndex] : null\n let text: string\n if (waypoint) {\n text = this.sceneCoordText(state.selectedIndex, waypoint)\n } else {\n text = '未选中航点(在场景里点一下航点光柱)'\n }\n if (state.dragging) {\n text += ` · ${state.mode === 'altitude' ? '调整高度中' : '移动中'}`\n } else if (state.enabled && !state.active) {\n text += ` · ${this.sceneBlockReason(editable)}`\n }\n scene.status.textContent = text\n\n scene.deleteBtn.disabled = state.selectedIndex < 0 || !editable\n scene.addBtn.disabled = !editable\n scene.selectFirst.disabled = this.fly.sim.mission.length === 0\n scene.deselect.disabled = state.selectedIndex < 0\n this.writeChecked(scene.showWaypoints, this.fly.isMissionWaypointsVisible())\n this.writeChecked(scene.showPath, this.fly.isMissionPathVisible())\n }\n\n private sceneBlockReason(editable: boolean): string {\n if (!editable) return '任务执行中不可编辑'\n if (!this.fly.isOrbitView) return '需切到观察者视角'\n return '当前不可编辑'\n }\n\n /**\n * 切换坐标显示模式(xyz 本地米 / geo 经纬高)。\n * geo 模式下航点行首行改为可编辑的「经度 / 纬度 / 高度」,因此切换时要重建行 DOM。\n */\n private setCoordMode(mode: 'xyz' | 'geo'): void {\n if (mode === this.coordMode) return\n this.coordMode = mode\n this.root\n .querySelectorAll<HTMLElement>('[data-action=\"coord-mode\"]')\n .forEach((button) => button.classList.toggle('is-active', button.dataset.value === mode))\n // 首行字段集变了,强制重建行(重建会丢失编辑焦点,但切模式是刻意操作,可接受)\n this.lastWaypointCount = -1\n this.sync()\n }\n\n /** 航点行的坐标徽标文字:xyz = E/S 偏移,geo = 经纬度(8 位小数) */\n private waypointCoordText(waypoint: MissionWaypoint, index: number): string {\n if (this.coordMode === 'geo') {\n const geo = this.fly.getWaypointGeo(index)\n if (geo) return `经度 ${geo.lon.toFixed(8)} / 纬度 ${geo.lat.toFixed(8)}`\n }\n return `E ${waypoint.x} / S ${waypoint.z}`\n }\n\n /** 场景编辑状态行的选中航点坐标文字(含海拔);geo 模式用绝对高程 */\n private sceneCoordText(index: number, waypoint: MissionWaypoint): string {\n if (this.coordMode === 'geo') {\n const geo = this.fly.getWaypointGeo(index)\n if (geo) {\n return `已选中 航点 ${index + 1} · 经度 ${geo.lon.toFixed(8)} / 纬度 ${geo.lat.toFixed(8)} / ${geo.height.toFixed(1)} m`\n }\n }\n return `已选中 航点 ${index + 1} · E ${waypoint.x} / S ${waypoint.z} / ${waypoint.altitude.toFixed(0)} m`\n }\n\n private stopTimer(): void {\n if (this.timer !== null) {\n window.clearInterval(this.timer)\n this.timer = null\n }\n }\n}\n\n/** 导出文件名:`mission-20261010-181530.json` —— 带本地时间戳,同一目录里多次导出不会互相覆盖 */\nfunction missionFilename(format: MissionExportFormat): string {\n const now = new Date()\n const pad = (value: number): string => String(value).padStart(2, '0')\n const stamp =\n `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +\n `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`\n return `mission-${stamp}.${format}`\n}\n\n/** 任务状态 → 角标配色 */\nfunction toneClass(status: string): string {\n if (status === 'running') return ' is-ok'\n if (status === 'paused') return ' is-warn'\n return ''\n}\n\nconst PANEL_CSS = `\n.dmp {\n --dmp-bg: rgba(15, 19, 27, 0.84);\n --dmp-card: rgba(255, 255, 255, 0.04);\n --dmp-border: rgba(255, 255, 255, 0.12);\n --dmp-text: #e9eff9;\n --dmp-muted: rgba(233, 239, 249, 0.58);\n --dmp-accent: #35a7ff;\n --dmp-on-accent: #04121f;\n --dmp-warn: #ffc53d;\n --dmp-danger: #ff5f5f;\n --dmp-ok: #3ddc97;\n --dmp-radius: 10px;\n /* 面板底部留出的避让高度:这套 HUD 布局里画面两下角是虚拟摇杆(底盘 92px + 上下留白),\n 面板别压上去 —— 摇杆悬浮层 z-index 更高、仍可点,但叠着既难看又容易误触。\n 宿主没有摇杆时可自行改小(例如 --dmp-clear-bottom: 24px) */\n --dmp-clear-bottom: 188px;\n font: 12px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n user-select: none;\n -webkit-user-select: none;\n -webkit-tap-highlight-color: transparent;\n overscroll-behavior: contain;\n z-index: 20;\n width: 300px;\n box-sizing: border-box;\n padding: 10px 12px 12px;\n border: 1px solid var(--dmp-border);\n border-radius: var(--dmp-radius);\n background: var(--dmp-bg);\n color: var(--dmp-text);\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);\n max-height: calc(100% - var(--dmp-clear-bottom));\n display: flex;\n flex-direction: column;\n}\n.dmp[data-position=\"top-left\"] { top: 12px; left: 12px; }\n.dmp[data-position=\"top-right\"] { top: 12px; right: 12px; }\n.dmp[data-position=\"bottom-left\"] { bottom: 12px; left: 12px; }\n.dmp[data-position=\"bottom-right\"] { bottom: 12px; right: 12px; }\n.dmp button, .dmp input, .dmp select { touch-action: manipulation; font: inherit; }\n.dmp-hidden, .dmp [hidden] { display: none !important; }\n\n.dmp-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }\n.dmp-title { flex: 1; font-weight: 600; letter-spacing: 0.4px; }\n.dmp-icon {\n width: 22px; height: 22px; flex: none; line-height: 1;\n border: 1px solid var(--dmp-border); border-radius: 6px;\n background: transparent; color: var(--dmp-text); cursor: pointer;\n}\n.dmp-icon:hover { background: rgba(255, 255, 255, 0.08); }\n.dmp[data-collapsed=\"true\"] .dmp-body { display: none; }\n\n.dmp-body { overflow-y: auto; min-height: 0; display: flex; flex-direction: column; gap: 8px; padding-right: 2px; }\n.dmp-body::-webkit-scrollbar { width: 6px; }\n.dmp-body::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.16); border-radius: 3px; }\n\n.dmp-card {\n padding: 8px 9px 9px;\n border: 1px solid var(--dmp-border);\n border-radius: 8px;\n background: var(--dmp-card);\n}\n.dmp-card-head { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }\n.dmp-card-head > span { flex: 1; font-weight: 600; }\n.dmp-card-head em { font-style: normal; font-size: 11px; color: var(--dmp-muted); }\n\n.dmp-badge {\n flex: none; padding: 1px 7px; border-radius: 999px;\n border: 1px solid var(--dmp-border);\n background: rgba(255, 255, 255, 0.06);\n color: var(--dmp-muted); font-size: 11px;\n}\n.dmp-badge.is-ok { color: var(--dmp-ok); border-color: rgba(61, 220, 151, 0.4); }\n.dmp-badge.is-warn { color: var(--dmp-warn); border-color: rgba(255, 197, 61, 0.4); }\n\n.dmp-btns { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }\n.dmp-btn {\n padding: 6px 8px; border-radius: 7px; cursor: pointer;\n border: 1px solid var(--dmp-border);\n background: rgba(255, 255, 255, 0.06);\n color: var(--dmp-text);\n}\n.dmp-btn:hover:not(:disabled) { background: rgba(255, 255, 255, 0.12); }\n.dmp-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n.dmp-btn.dmp-primary { background: var(--dmp-accent); border-color: var(--dmp-accent); color: var(--dmp-on-accent); font-weight: 600; }\n.dmp-btn.dmp-primary:hover:not(:disabled) { filter: brightness(1.08); }\n.dmp-btn.dmp-wide { grid-column: 1 / -1; }\n\n.dmp-progress { margin: 7px 0 0; color: var(--dmp-accent); font-size: 11px; }\n.dmp-block { margin: 5px 0 0; padding: 5px 7px; border-radius: 6px; background: rgba(255, 95, 95, 0.12); color: #ffb3b3; font-size: 11px; }\n.dmp-fine { margin: 6px 0 0; color: var(--dmp-muted); font-size: 11px; line-height: 1.45; }\n.dmp-row-label { margin: 7px 0 4px; color: var(--dmp-muted); font-size: 11px; }\n\n.dmp-range-row { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 8px; }\n.dmp-range-row > span { color: var(--dmp-muted); font-size: 11px; }\n.dmp-range-row strong { font-variant-numeric: tabular-nums; font-size: 11px; }\n.dmp-range-row input[type=\"range\"] { width: 100%; accent-color: var(--dmp-accent); }\n\n.dmp-seg { display: grid; gap: 4px; }\n.dmp-seg-2 { grid-template-columns: 1fr 1fr; }\n.dmp-seg-3 { grid-template-columns: repeat(3, 1fr); }\n.dmp-seg button {\n padding: 5px 4px; border-radius: 6px; cursor: pointer; font-size: 11px;\n border: 1px solid var(--dmp-border);\n background: rgba(255, 255, 255, 0.05);\n color: var(--dmp-text);\n}\n.dmp-seg button:hover:not(:disabled) { background: rgba(255, 255, 255, 0.11); }\n.dmp-seg button:disabled { opacity: 0.4; cursor: not-allowed; }\n.dmp-seg button.is-active { background: var(--dmp-accent); border-color: var(--dmp-accent); color: var(--dmp-on-accent); font-weight: 600; }\n.dmp-coord-mode { margin-bottom: 7px; }\n.dmp-coord-mode button { font-variant-numeric: tabular-nums; }\n\n.dmp-switch { display: flex; align-items: center; gap: 6px; margin-top: 7px; cursor: pointer; }\n.dmp-switch input { flex: none; accent-color: var(--dmp-accent); }\n.dmp-switch > span { font-size: 11px; }\n.dmp-switch-row { display: flex; gap: 14px; }\n\n.dmp-wp-fieldset { margin: 0; padding: 0; border: 0; min-width: 0; }\n.dmp-wp-fieldset:disabled { opacity: 0.55; }\n.dmp-wp-list { list-style: none; margin: 0; padding: 0; max-height: 240px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }\n.dmp-wp-list::-webkit-scrollbar { width: 6px; }\n.dmp-wp-list::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.16); border-radius: 3px; }\n.dmp-wp-empty { color: var(--dmp-muted); font-size: 11px; padding: 6px 2px; }\n.dmp-wp {\n position: relative; padding: 6px 6px 7px; border-radius: 7px; cursor: pointer;\n border: 1px solid var(--dmp-border);\n background: rgba(255, 255, 255, 0.03);\n}\n.dmp-wp.is-selected, .dmp-wp:hover { border-color: rgba(53, 167, 255, 0.5); }\n.dmp-wp.is-active { border-color: var(--dmp-ok); box-shadow: 0 0 0 1px rgba(61, 220, 151, 0.35) inset; }\n.dmp-wp-head { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; }\n.dmp-wp-index {\n flex: none; width: 18px; height: 18px; border-radius: 50%;\n display: inline-flex; align-items: center; justify-content: center;\n background: rgba(53, 167, 255, 0.2); color: var(--dmp-accent); font-size: 11px; font-weight: 600;\n}\n.dmp-wp-coord { flex: 1; color: var(--dmp-muted); font-size: 11px; font-variant-numeric: tabular-nums; }\n.dmp-mini {\n flex: none; padding: 2px 7px; border-radius: 5px; cursor: pointer; font-size: 11px;\n border: 1px solid var(--dmp-border);\n background: transparent; color: var(--dmp-danger);\n}\n.dmp-mini:hover { background: rgba(255, 95, 95, 0.14); }\n.dmp-wp-row { display: grid; grid-template-columns: 1fr 1fr 1.25fr; gap: 5px; margin-bottom: 5px; }\n.dmp-wp-row:last-of-type { margin-bottom: 0; grid-template-columns: repeat(2, 1fr); }\n/* geo 首行:经纬度各占半宽(8 位小数约 10 字符,半宽输入框放得下),高度独占整行 */\n.dmp-wp-row-geo { grid-template-columns: 1fr 1fr; }\n.dmp-wp-row-geo .dmp-wp-geo-h { grid-column: 1 / -1; }\n.dmp-wp-field { display: flex; align-items: center; gap: 4px; min-width: 0; }\n.dmp-wp-field > span { flex: none; color: var(--dmp-muted); font-size: 11px; }\n.dmp-wp-gimbal { grid-column: span 2; }\n.dmp-wp-input {\n width: 100%; min-width: 0; box-sizing: border-box;\n padding: 3px 4px; border-radius: 5px;\n border: 1px solid var(--dmp-border);\n background: rgba(0, 0, 0, 0.28); color: var(--dmp-text); font-size: 11px;\n font-variant-numeric: tabular-nums;\n}\n/* Chrome/Edge 的数字框默认带上下调节箭头,窄格里能吃掉十几像素 —— 藏掉它,\n 否则\"500\"这种三位数会裁得只剩\"5\",看起来就像列表高度和场景对不上 */\n.dmp-wp-input::-webkit-outer-spin-button,\n.dmp-wp-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }\n.dmp-wp-input[type='number'] { -moz-appearance: textfield; appearance: textfield; }\n.dmp-wp-input:focus { outline: none; border-color: var(--dmp-accent); }\n.dmp-wp-input:disabled { opacity: 0.45; }\n.dmp-wp-select {\n width: 100%; min-width: 0; box-sizing: border-box;\n padding: 3px 4px; border-radius: 5px;\n border: 1px solid var(--dmp-border);\n background: rgba(0, 0, 0, 0.28); color: var(--dmp-text); font-size: 11px;\n}\n.dmp-wp-flag {\n position: absolute; top: -6px; right: 8px;\n padding: 0 6px; border-radius: 999px; font-style: normal; font-size: 10px;\n background: var(--dmp-ok); color: #03231a; font-weight: 600;\n}\n.dmp-gesture { list-style: none; margin: 7px 0 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }\n.dmp-gesture li { display: flex; gap: 8px; font-size: 11px; }\n.dmp-gesture b { flex: none; width: 74px; color: var(--dmp-text); font-weight: 600; }\n.dmp-gesture span { color: var(--dmp-muted); }\n.dmp-status { margin: 7px 0; padding: 5px 7px; border-radius: 6px; background: rgba(255, 255, 255, 0.05); font-size: 11px; color: var(--dmp-muted); }\n\n@media (max-height: 620px) {\n .dmp[data-adaptive=\"true\"] { width: 232px; padding: 8px 9px 9px; }\n .dmp[data-adaptive=\"true\"] .dmp-wp-list { max-height: 168px; }\n .dmp[data-adaptive=\"true\"] .dmp-fine { display: none; }\n .dmp[data-adaptive=\"true\"] .dmp-gesture { display: none; }\n}\n`\n","import type { DroneFly } from './drone-fly'\nimport type { DroneSim, ObstacleBox } from './drone-sim'\n\n/**\n * 鹰眼图(俯视小地图)—— 原生 DOM + Canvas 2D,与 drone-panel / mission-panel 同一套范式:\n * 框架无关、`dcm-*` 类名、配色走 CSS 变量、构造即可挂载、destroy 彻底回收。\n *\n * ## 为什么不是\"开第二个 three.js 视口\"\n * 它只是一张**态势示意图**,不是真实几何的第二次渲染 —— 只读 fly.sim 的真值\n * (位置 / 航向 / 障碍 AABB / 航线 / 返航点)与 world 的航迹,自己投影成 2D。于是:\n *\n * 1. **零渲染器耦合**:WebGPU / WebGL / 无头环境都能跑,不碰 renderer、不碰相机、不碰场景;\n * 2. **不会混进成片**:第二视口方案要把小地图渲进同一个 renderer,拍照与录像抓的就是那帧,\n * 小地图会糊在成片角落;本方案出的是叠加层,与 renderer 输出天然分离;\n * 3. **不占用三维资源**:没有额外的正交相机、视锥、scissor 状态要维护。\n *\n * ## 更新节奏与面板不同\n * 面板 200ms 轮询读数足够,但小地图 5Hz 会一帧一格地跳。所以这里**自驱 requestAnimationFrame**\n * 每帧重绘 —— 几百个图元的 canvas 重绘成本可忽略,且与宿主渲染循环互不干扰\n * (`render()` 也对外公开,宿主想自己控制节拍可以调它)。\n *\n * ## 坐标映射是直给\n * sim 的约定是 x 向东、z 向南(与模型一致),所以\"北向上\"就是屏幕右 = +x、屏幕上 = -z:\n * `u = cx + (x - ox) · k`、`v = cy + (z - oz) · k`。不需要矩阵,也不需要碰相机。\n * 顺带一个好处:机体箭头的 canvas 旋转角**恰好等于 heading 弧度**(0° = 北 = 屏幕上方)。\n *\n * ## 朝向固定北向\n * 地图不旋转、只有机体箭头转 —— 这是 DJI 的默认读图方式:障碍物与返航点的相对方位\n * 不随飞行姿态变化,\"该往哪边飞\"判断更稳(机头朝上模式下车头一偏,整张图都跟着晃)。\n */\n\n/** 小地图吸附的容器角;bottom-center = 底边水平居中(两侧摇杆之间的空档) */\nexport type MiniMapPosition =\n 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'bottom-center'\n\nexport interface MiniMapOptions {\n /** 挂载容器;缺省挂到 document.body(贴视口),给了容器就贴容器(必要时自动补 relative) */\n container?: HTMLElement\n /** 吸附角,默认右下角(与控制面板右上、航线面板左上错开) */\n position?: MiniMapPosition\n /**\n * 角内偏移(像素):在默认 14px 边距之上再让出的距离。\n * 右下角常被 HUD 摇杆 + 技能钮占着,demo 里就是用它把小地图抬到轮盘上方。\n */\n offset?: { x?: number; y?: number }\n /** 画布边长(CSS 像素),默认 186 */\n size?: number\n /** 固定半量程(米);传了就关闭自动量程。缺省 null = 自动跟随 */\n range?: number | null\n /** 自动量程下限(米),默认 40 */\n minRange?: number\n /** 自动量程上限(米),默认 300 */\n maxRange?: number\n visible?: boolean\n /** 注入内置样式,默认 true;宿主自备样式可关掉 */\n injectStyles?: boolean\n /** 额外类名,追加在 `dcm` 之后 */\n className?: string\n showObstacles?: boolean\n showMission?: boolean\n showTrail?: boolean\n showFov?: boolean\n showGrid?: boolean\n}\n\n/** 探针/调试读数 */\nexport interface MiniMapStats {\n /** 当前实际半量程(米) */\n range: number\n /** 当前比例尺(像素 / 米) */\n scale: number\n obstacleCount: number\n waypointCount: number\n trailPoints: number\n /** 机载水平视场角(度);非机载视角返回 null */\n fovDeg: number | null\n /** 地图中心的世界坐标(= 机体位置) */\n centerX: number\n centerZ: number\n}\n\n/** 解析后的配色(从 CSS 变量读,宿主可整体换肤) */\ninterface MiniMapColors {\n grid: string\n ring: string\n obstacle: string\n obstacleEdge: string\n mission: string\n missionDone: string\n missionActive: string\n trail: string\n home: string\n drone: string\n fov: string\n text: string\n}\n\nconst STYLE_ID = 'drone-minimap-styles'\nconst DEG = Math.PI / 180\n/** 画布内边距(像素):给刻度环与角标留白 */\nconst MAP_PADDING = 10\n/** 自动量程档位(米):取不小于需求的最近一档,避免地图随飞行持续缩放抖动 */\nconst RANGE_STEPS = [40, 60, 80, 100, 150, 200, 300]\n/** 量程缓动速率(1/秒):换档时平滑过渡而不是硬切 */\nconst RANGE_EASE_RATE = 3.4\n/** 视场扇形半径占量程的比例(纯示意,不代表真实测距) */\nconst FOV_FAN_RATIO = 0.55\n/** 网格候选步距(米):挑第一个屏上间距 ≥ 16px 的 */\nconst GRID_STEPS = [2, 5, 10, 25, 50, 100, 200]\nconst GRID_MIN_PX = 16\nconst FONT = '\"Inter\", \"HarmonyOS Sans SC\", system-ui, -apple-system, \"Segoe UI\", sans-serif'\n\nconst CSS = `\n.dcm {\n --dcm-size: 186px;\n --dcm-offset-x: 0px;\n --dcm-offset-y: 0px;\n --dcm-bg: rgba(9, 17, 22, 0.62);\n --dcm-border: rgba(255, 255, 255, 0.16);\n --dcm-grid: rgba(255, 255, 255, 0.06);\n --dcm-ring: rgba(255, 255, 255, 0.17);\n --dcm-obstacle: rgba(186, 188, 183, 0.32);\n --dcm-obstacle-edge: rgba(214, 216, 210, 0.6);\n --dcm-mission: #39d0b0;\n --dcm-mission-done: rgba(57, 208, 176, 0.3);\n --dcm-mission-active: #ffc53d;\n --dcm-trail: rgba(93, 202, 165, 0.5);\n --dcm-home: #35e08a;\n --dcm-drone: #7fe3ff;\n --dcm-fov: rgba(127, 227, 255, 0.13);\n --dcm-text: rgba(226, 240, 246, 0.82);\n position: absolute;\n z-index: 5;\n width: var(--dcm-size);\n height: var(--dcm-size);\n border-radius: 12px;\n border: 0.5px solid var(--dcm-border);\n background: var(--dcm-bg);\n overflow: hidden;\n pointer-events: none;\n user-select: none;\n}\n.dcm[data-position=\"top-left\"] { left: calc(14px + var(--dcm-offset-x)); top: calc(14px + var(--dcm-offset-y)); }\n.dcm[data-position=\"top-right\"] { right: calc(14px + var(--dcm-offset-x)); top: calc(14px + var(--dcm-offset-y)); }\n.dcm[data-position=\"bottom-left\"] { left: calc(14px + var(--dcm-offset-x)); bottom: calc(14px + var(--dcm-offset-y)); }\n.dcm[data-position=\"bottom-right\"] { right: calc(14px + var(--dcm-offset-x)); bottom: calc(14px + var(--dcm-offset-y)); }\n.dcm[data-position=\"bottom-center\"] { left: 50%; transform: translateX(-50%); bottom: calc(14px + var(--dcm-offset-y)); }\n.dcm-canvas { display: block; width: 100%; height: 100%; }\n`\n\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return\n if (document.getElementById(STYLE_ID)) return\n const style = document.createElement('style')\n style.id = STYLE_ID\n style.textContent = CSS\n document.head.appendChild(style)\n}\n\n/** 圆角矩形路径(不依赖 ctx.roundRect,老 WebView 也能用) */\nfunction roundRectPath(\n ctx: CanvasRenderingContext2D,\n x: number,\n y: number,\n width: number,\n height: number,\n radius: number,\n): void {\n const r = Math.min(radius, width / 2, height / 2)\n ctx.beginPath()\n ctx.moveTo(x + r, y)\n ctx.arcTo(x + width, y, x + width, y + height, r)\n ctx.arcTo(x + width, y + height, x, y + height, r)\n ctx.arcTo(x, y + height, x, y, r)\n ctx.arcTo(x, y, x + width, y, r)\n ctx.closePath()\n}\n\nfunction readVar(style: CSSStyleDeclaration, name: string, fallback: string): string {\n const value = style.getPropertyValue(name).trim()\n return value.length > 0 ? value : fallback\n}\n\nexport class DroneMiniMap {\n private readonly fly: DroneFly\n private readonly container: HTMLElement\n private readonly root: HTMLDivElement\n private readonly canvas: HTMLCanvasElement\n private readonly ctx: CanvasRenderingContext2D\n private readonly size: number\n private readonly minRange: number\n private readonly maxRange: number\n private readonly showObstacles: boolean\n private readonly showMission: boolean\n private readonly showTrail: boolean\n private readonly showFov: boolean\n private readonly showGrid: boolean\n\n private colors: MiniMapColors\n private visible: boolean\n private destroyed = false\n\n /** 手动量程(米);null = 自动跟随 */\n private manualRange: number | null = null\n private rangeCurrent = 120\n private rangeTarget = 120\n private rangeReady = false\n\n /** 本帧的投影参数:比例尺(像素/米)、地图中心的世界坐标 */\n private scale = 1\n private originX = 0\n private originZ = 0\n\n private raf: number | null = null\n private lastTime = 0\n private frameDrawn = false\n\n /** 构造鹰眼图:Canvas 2D 俯视 HUD,只读 sim 真值,不碰三维渲染 */\n constructor(fly: DroneFly, options: MiniMapOptions = {}) {\n if (typeof document === 'undefined') {\n throw new Error('[drone-camera] DroneMiniMap 需要浏览器环境(document),Node 侧请勿实例化')\n }\n this.fly = fly\n this.size = Math.max(96, options.size ?? 186)\n this.manualRange = options.range === undefined ? null : options.range\n this.minRange = Math.max(10, options.minRange ?? 40)\n this.maxRange = Math.max(this.minRange, options.maxRange ?? 300)\n this.showObstacles = options.showObstacles !== false\n this.showMission = options.showMission !== false\n this.showTrail = options.showTrail !== false\n this.showFov = options.showFov !== false\n this.showGrid = options.showGrid !== false\n this.visible = options.visible ?? true\n\n if (options.injectStyles !== false) ensureStyles()\n\n this.container = options.container ?? document.body\n const host = options.container ?? null\n // 与 panel 同一套挂载纪律:容器没有定位上下文就补一个,否则绝对定位会飞到页面左上角\n if (host && getComputedStyle(host).position === 'static') host.style.position = 'relative'\n\n this.root = document.createElement('div')\n this.root.className = options.className ? `dcm ${options.className}` : 'dcm'\n this.root.dataset.position = options.position ?? 'bottom-right'\n this.root.style.position = host ? 'absolute' : 'fixed'\n this.root.style.setProperty('--dcm-size', `${this.size}px`)\n if (options.offset?.x)\n this.root.style.setProperty('--dcm-offset-x', `${Math.max(0, options.offset.x)}px`)\n if (options.offset?.y)\n this.root.style.setProperty('--dcm-offset-y', `${Math.max(0, options.offset.y)}px`)\n if (!this.visible) this.root.style.display = 'none'\n\n this.canvas = document.createElement('canvas')\n this.canvas.className = 'dcm-canvas'\n this.root.appendChild(this.canvas)\n this.container.appendChild(this.root)\n\n const context = this.canvas.getContext('2d')\n if (!context) throw new Error('[drone-camera] DroneMiniMap 无法获取 2D 绘图上下文')\n this.ctx = context\n // 高 DPI:逻辑坐标始终按 CSS 像素算,靠 canvas 的 backing store 倍数保证线条清晰\n const dpr = Math.min(2, typeof window === 'undefined' ? 1 : window.devicePixelRatio || 1)\n this.canvas.width = Math.round(this.size * dpr)\n this.canvas.height = Math.round(this.size * dpr)\n this.ctx.scale(dpr, dpr)\n\n this.colors = this.resolveColors()\n\n this.lastTime = typeof performance === 'undefined' ? 0 : performance.now()\n this.render()\n this.start()\n }\n\n // ────────────────────────────────────────────────────────────────────────\n // 公开接口\n // ────────────────────────────────────────────────────────────────────────\n\n /** 立即重绘一帧(宿主想自控节拍时可关掉内置 rAF —— 见 stop()) */\n render(): void {\n if (this.destroyed) return\n this.frameDrawn = true\n this.drawFrame()\n }\n\n /** 显示/隐藏鹰眼图 */\n setVisible(visible: boolean): void {\n this.visible = visible\n this.root.style.display = visible ? '' : 'none'\n }\n\n /** 鹰眼图当前是否可见 */\n isVisible(): boolean {\n return this.visible\n }\n\n /** 量程:传数字 = 固定该半量程(米);传 null = 回到自动跟随 */\n setRange(range: number | null): void {\n this.manualRange = range === null ? null : Math.max(5, range)\n this.rangeReady = false\n }\n\n /** 当前实际半量程(米) */\n getRange(): number {\n return this.rangeCurrent\n }\n\n /** 切主题(读一遍 CSS 变量)后调用;平时不必每帧读 */\n refreshTheme(): void {\n this.colors = this.resolveColors()\n this.drawFrame()\n }\n\n /** 探针读数 */\n getStats(): MiniMapStats {\n const sim = this.fly.sim\n return {\n range: this.rangeCurrent,\n scale: this.scale,\n obstacleCount: this.showObstacles ? sim.obstacles.length : 0,\n waypointCount: this.showMission ? sim.mission.length : 0,\n trailPoints: this.fly.world?.getTrail().count ?? 0,\n fovDeg: this.horizontalFov(),\n centerX: this.originX,\n centerZ: this.originZ,\n }\n }\n\n /** 本帧是否已绘制过(探针用:确认 rAF 真的在跑) */\n hasDrawn(): boolean {\n return this.frameDrawn\n }\n\n /** 停止内置刷新(之后需自行调用 render()) */\n stop(): void {\n if (this.raf !== null) {\n cancelAnimationFrame(this.raf)\n this.raf = null\n }\n }\n\n /** 停止重绘循环并移除画布 */\n destroy(): void {\n if (this.destroyed) return\n this.destroyed = true\n this.stop()\n this.root.parentNode?.removeChild(this.root)\n if (this.container.childElementCount === 0) {\n // 容器是构造函数补的 relative,不还原也无害;这里只清掉可能残留的样式\n this.container.style.removeProperty('position')\n }\n if (typeof document !== 'undefined') {\n document.getElementById(STYLE_ID)?.remove()\n }\n }\n\n // ────────────────────────────────────────────────────────────────────────\n // 内部\n // ────────────────────────────────────────────────────────────────────────\n\n private start(): void {\n if (typeof requestAnimationFrame !== 'function') return\n const tick = (time: number): void => {\n if (this.destroyed) return\n this.raf = requestAnimationFrame(tick)\n const delta = Math.min(0.25, Math.max(0, (time - this.lastTime) / 1000))\n this.lastTime = time\n if (this.visible) this.drawFrame(delta)\n }\n this.raf = requestAnimationFrame(tick)\n }\n\n private resolveColors(): MiniMapColors {\n const style = getComputedStyle(this.root)\n return {\n grid: readVar(style, '--dcm-grid', 'rgba(255,255,255,0.06)'),\n ring: readVar(style, '--dcm-ring', 'rgba(255,255,255,0.17)'),\n obstacle: readVar(style, '--dcm-obstacle', 'rgba(186,188,183,0.32)'),\n obstacleEdge: readVar(style, '--dcm-obstacle-edge', 'rgba(214,216,210,0.6)'),\n mission: readVar(style, '--dcm-mission', '#39d0b0'),\n missionDone: readVar(style, '--dcm-mission-done', 'rgba(57,208,176,0.3)'),\n missionActive: readVar(style, '--dcm-mission-active', '#ffc53d'),\n trail: readVar(style, '--dcm-trail', 'rgba(93,202,165,0.5)'),\n home: readVar(style, '--dcm-home', '#35e08a'),\n drone: readVar(style, '--dcm-drone', '#7fe3ff'),\n fov: readVar(style, '--dcm-fov', 'rgba(127,227,255,0.13)'),\n text: readVar(style, '--dcm-text', 'rgba(226,240,246,0.82)'),\n }\n }\n\n /** 机载取景相机的当前水平视场角(度,已折算 zoom);非机载视角返回 null */\n private horizontalFov(): number | null {\n const camera = this.fly.camera\n if (!camera || this.fly.cameraMode !== 'fpv') return null\n const zoom = Math.max(1e-3, camera.zoom || 1)\n const vertical = camera.fov * DEG\n const effective = 2 * Math.atan(Math.tan(vertical / 2) / zoom)\n const aspect = camera.aspect || 1\n return (2 * Math.atan(Math.tan(effective / 2) * aspect)) / DEG\n }\n\n /**\n * 量程:自动模式下取「机体到返航点 / 到各航点的最远距离 + 余量」,\n * 再向上取整到 RANGE_STEPS 的档位,最后缓动过去(换档不硬切)。\n */\n private updateRange(sim: DroneSim, delta: number): void {\n if (this.manualRange !== null) {\n this.rangeCurrent = this.manualRange\n this.rangeTarget = this.manualRange\n this.rangeReady = true\n return\n }\n const px = sim.position.x\n const pz = sim.position.z\n let need = this.minRange * 0.7\n if (sim.homeReady) {\n const home = sim.homePosition\n need = Math.max(need, Math.hypot(px - home.x, pz - home.z) + 12)\n }\n for (const waypoint of sim.mission) {\n need = Math.max(need, Math.hypot(px - waypoint.x, pz - waypoint.z) + 12)\n }\n this.rangeTarget = Math.min(\n this.maxRange,\n Math.max(this.minRange, RANGE_STEPS.find((step) => step >= need) ?? this.maxRange),\n )\n if (!this.rangeReady) {\n this.rangeCurrent = this.rangeTarget\n this.rangeReady = true\n return\n }\n const ease = Math.min(1, Math.max(0, delta) * RANGE_EASE_RATE)\n if (Math.abs(this.rangeCurrent - this.rangeTarget) < 1e-3) {\n this.rangeCurrent = this.rangeTarget\n return\n }\n this.rangeCurrent += (this.rangeTarget - this.rangeCurrent) * ease\n }\n\n private drawFrame(delta = 1 / 60): void {\n const ctx = this.ctx\n const size = this.size\n const sim = this.fly.sim\n\n ctx.clearRect(0, 0, size, size)\n ctx.save()\n roundRectPath(ctx, 0, 0, size, size, 12)\n ctx.clip()\n\n this.updateRange(sim, delta)\n\n const half = size / 2\n // 投影:机体居中(ox/oz = 机体位置),比例尺按量程折算\n this.scale = (half - MAP_PADDING) / this.rangeCurrent\n this.originX = sim.position.x\n this.originZ = sim.position.z\n\n if (this.showGrid) this.drawGrid()\n this.drawRings()\n if (this.showObstacles) this.drawObstacles(sim.obstacles)\n if (this.showMission) this.drawMission(sim)\n if (this.showTrail) this.drawTrail()\n this.drawHome(sim)\n if (this.showFov) this.drawFov(sim.heading)\n this.drawDrone(sim.heading)\n this.drawCompass()\n this.drawScaleLabel()\n\n ctx.restore()\n }\n\n private toX(worldX: number): number {\n return this.size / 2 + (worldX - this.originX) * this.scale\n }\n\n private toY(worldZ: number): number {\n return this.size / 2 + (worldZ - this.originZ) * this.scale\n }\n\n private gridStep(): number {\n for (const step of GRID_STEPS) {\n if (step * this.scale >= GRID_MIN_PX) return step\n }\n return GRID_STEPS[GRID_STEPS.length - 1] ?? 200\n }\n\n private drawGrid(): void {\n const ctx = this.ctx\n const step = this.gridStep()\n const half = this.rangeCurrent\n const firstX = Math.ceil((this.originX - half) / step)\n const lastX = Math.floor((this.originX + half) / step)\n const firstZ = Math.ceil((this.originZ - half) / step)\n const lastZ = Math.floor((this.originZ + half) / step)\n\n ctx.strokeStyle = this.colors.grid\n ctx.lineWidth = 0.5\n ctx.beginPath()\n for (let i = firstX; i <= lastX; i += 1) {\n const px = this.toX(i * step)\n ctx.moveTo(px, 0)\n ctx.lineTo(px, this.size)\n }\n for (let i = firstZ; i <= lastZ; i += 1) {\n const py = this.toY(i * step)\n ctx.moveTo(0, py)\n ctx.lineTo(this.size, py)\n }\n ctx.stroke()\n }\n\n /** 刻度环:满量程 + 半量程两道虚线同心圆(圆心即机体,因为机体恒居中) */\n private drawRings(): void {\n const ctx = this.ctx\n const center = this.size / 2\n const outer = center - MAP_PADDING\n ctx.strokeStyle = this.colors.ring\n ctx.lineWidth = 0.5\n ctx.setLineDash([3, 4])\n for (const radius of [outer, outer * 0.5]) {\n ctx.beginPath()\n ctx.arc(center, center, radius, 0, Math.PI * 2)\n ctx.stroke()\n }\n ctx.setLineDash([])\n }\n\n private drawObstacles(boxes: readonly ObstacleBox[]): void {\n if (boxes.length === 0) return\n const ctx = this.ctx\n ctx.fillStyle = this.colors.obstacle\n ctx.strokeStyle = this.colors.obstacleEdge\n ctx.lineWidth = 0.5\n for (const box of boxes) {\n const x0 = this.toX(box.minX)\n const y0 = this.toY(box.minZ)\n const width = (box.maxX - box.minX) * this.scale\n const height = (box.maxZ - box.minZ) * this.scale\n // 屏外的整块跳过(大场景里省掉无谓的路径)\n if (x0 + width < -MAP_PADDING || x0 > this.size + MAP_PADDING) continue\n if (y0 + height < -MAP_PADDING || y0 > this.size + MAP_PADDING) continue\n // 极窄的杆件(灯杆)至少画成 2px,否则小地图上直接消失\n ctx.beginPath()\n ctx.rect(x0, y0, Math.max(2, width), Math.max(2, height))\n ctx.fill()\n ctx.stroke()\n }\n }\n\n private drawMission(sim: DroneSim): void {\n const waypoints = sim.mission\n if (waypoints.length === 0) return\n const ctx = this.ctx\n const running = sim.missionStatus !== 'idle'\n const active = running ? sim.missionIndex : -1\n\n // 折线顶点 = [返航点(有则加)] + 各航点;offset 记录航点与顶点的错位\n const nodes: Array<{ x: number; y: number }> = []\n const hasHome = sim.homeReady\n if (hasHome) {\n const home = sim.homePosition\n nodes.push({ x: this.toX(home.x), y: this.toY(home.z) })\n }\n for (const waypoint of waypoints) {\n nodes.push({ x: this.toX(waypoint.x), y: this.toY(waypoint.z) })\n }\n const offset = hasHome ? 1 : 0\n\n ctx.save()\n ctx.globalAlpha = running ? 1 : 0.72\n ctx.lineWidth = 1.5\n ctx.lineJoin = 'round'\n for (let i = 0; i < nodes.length - 1; i += 1) {\n const a = nodes[i]\n const b = nodes[i + 1]\n if (!a || !b) continue\n const target = i + 1 - offset\n const done = active >= 0 && target <= active\n ctx.strokeStyle = done ? this.colors.missionDone : this.colors.mission\n if (done) ctx.setLineDash([3, 3])\n else ctx.setLineDash([])\n ctx.beginPath()\n ctx.moveTo(a.x, a.y)\n ctx.lineTo(b.x, b.y)\n ctx.stroke()\n }\n ctx.setLineDash([])\n\n // 航点圆点:未到 = 主色,当前目标 = 琥珀 + 外圈,已过 = 淡色\n const showIndex = this.rangeCurrent <= 150\n ctx.font = `600 9px ${FONT}`\n ctx.textAlign = 'center'\n ctx.textBaseline = 'middle'\n for (let i = 0; i < waypoints.length; i += 1) {\n const waypoint = waypoints[i]\n if (!waypoint) continue\n const px = this.toX(waypoint.x)\n const py = this.toY(waypoint.z)\n const isActive = i === active\n const isDone = active >= 0 && i < active\n ctx.beginPath()\n ctx.arc(px, py, isActive ? 5.5 : 3.6, 0, Math.PI * 2)\n ctx.fillStyle = isActive\n ? this.colors.missionActive\n : isDone\n ? this.colors.missionDone\n : this.colors.mission\n ctx.fill()\n if (isActive) {\n ctx.strokeStyle = this.colors.missionActive\n ctx.lineWidth = 1\n ctx.beginPath()\n ctx.arc(px, py, 8.5, 0, Math.PI * 2)\n ctx.stroke()\n }\n if (showIndex) {\n ctx.fillStyle = this.colors.text\n ctx.fillText(String(i + 1), px, py - 9)\n }\n }\n ctx.restore()\n }\n\n private drawTrail(): void {\n const trail = this.fly.world?.getTrail()\n if (!trail || trail.count < 2) return\n const ctx = this.ctx\n const positions = trail.positions\n // 每 0.25m 一个点,量程稍大时屏上远小于 1px —— 抽稀到约 600 段即可\n const stride = Math.max(1, Math.floor(trail.count / 600))\n ctx.strokeStyle = this.colors.trail\n ctx.lineWidth = 1.5\n ctx.lineJoin = 'round'\n ctx.beginPath()\n let started = false\n for (let i = 0; i < trail.count; i += stride) {\n const x = positions[i * 3]\n const z = positions[i * 3 + 2]\n if (x === undefined || z === undefined) continue\n const px = this.toX(x)\n const py = this.toY(z)\n if (started) ctx.lineTo(px, py)\n else {\n ctx.moveTo(px, py)\n started = true\n }\n }\n const lastX = positions[(trail.count - 1) * 3]\n const lastZ = positions[(trail.count - 1) * 3 + 2]\n if (started && lastX !== undefined && lastZ !== undefined) {\n ctx.lineTo(this.toX(lastX), this.toY(lastZ))\n }\n ctx.stroke()\n }\n\n private drawHome(sim: DroneSim): void {\n if (!sim.homeReady) return\n const ctx = this.ctx\n const home = sim.homePosition\n const px = this.toX(home.x)\n const py = this.toY(home.z)\n ctx.strokeStyle = this.colors.home\n ctx.lineWidth = 1.5\n ctx.beginPath()\n ctx.arc(px, py, 8, 0, Math.PI * 2)\n ctx.stroke()\n ctx.fillStyle = this.colors.home\n ctx.font = `600 10px ${FONT}`\n ctx.textAlign = 'center'\n ctx.textBaseline = 'middle'\n ctx.fillText('H', px, py + 0.5)\n }\n\n /** FPV 水平视场扇形:从机体出发、机头方向张开,看一眼就知道\"现在镜头能拍到哪一片\" */\n private drawFov(heading: number): void {\n const fov = this.horizontalFov()\n if (fov === null) return\n const ctx = this.ctx\n const radius = this.rangeCurrent * FOV_FAN_RATIO * this.scale\n const half = (fov / 2) * DEG\n ctx.save()\n // canvas 旋转角恰好等于 heading 弧度:俯视图北向上时,机体箭头指向 = 航向角本身\n ctx.translate(this.size / 2, this.size / 2)\n ctx.rotate(heading * DEG)\n ctx.beginPath()\n ctx.moveTo(0, 0)\n ctx.arc(0, 0, radius, -Math.PI / 2 - half, -Math.PI / 2 + half)\n ctx.closePath()\n ctx.fillStyle = this.colors.fov\n ctx.fill()\n ctx.restore()\n }\n\n private drawDrone(heading: number): void {\n const ctx = this.ctx\n ctx.save()\n ctx.translate(this.size / 2, this.size / 2)\n ctx.rotate(heading * DEG)\n ctx.beginPath()\n ctx.moveTo(0, -9)\n ctx.lineTo(6.2, 7)\n ctx.lineTo(0, 3.8)\n ctx.lineTo(-6.2, 7)\n ctx.closePath()\n ctx.fillStyle = this.colors.drone\n ctx.fill()\n ctx.restore()\n }\n\n private drawCompass(): void {\n const ctx = this.ctx\n const x = this.size - 17\n const y = 20\n ctx.fillStyle = this.colors.drone\n ctx.beginPath()\n ctx.moveTo(x, y - 14)\n ctx.lineTo(x - 3.5, y - 7)\n ctx.lineTo(x + 3.5, y - 7)\n ctx.closePath()\n ctx.fill()\n ctx.fillStyle = this.colors.text\n ctx.font = `600 10px ${FONT}`\n ctx.textAlign = 'center'\n ctx.textBaseline = 'middle'\n ctx.fillText('N', x, y)\n }\n\n private drawScaleLabel(): void {\n const ctx = this.ctx\n ctx.fillStyle = this.colors.text\n ctx.font = `500 10px ${FONT}`\n ctx.textAlign = 'left'\n ctx.textBaseline = 'alphabetic'\n ctx.fillText(`${Math.round(this.rangeCurrent)} m`, 10, this.size - 9)\n }\n}\n","/**\n * 零依赖 JPEG EXIF / GPS 解析器。\n *\n * 用途:真实无人机照片导入时,把「拍摄时间 / 机型 / 焦距 / 图像尺寸 / GPS 经纬高」\n * 从文件里读出来,填进统一的 `CapturedImage.metadata`(见 drone-capture)。\n *\n * 为什么自己写而不引三方库:本库除 `three` 外不接受运行时依赖(见 lib/index.ts 的设计约定),\n * 而 V1 只需要 JPEG 的 EXIF 基本项 —— 一个 APP1/TIFF 顺序读就够了,不值得为它挂一个包。\n *\n * 覆盖范围(够用即止,不做全谱):\n * - 只认 JPEG(SOI 0xFFD8 → APP1 0xFFE1 \"Exif\\0\\0\" → TIFF),PNG / HEIC 一律返回 null;\n * - IFD0:Make / Model / Orientation / DateTime / ExifIFD 指针 / GPSIFD 指针;\n * - Exif IFD:曝光时间 / 光圈 / ISO / 原始拍摄时间 / 焦距 / 35mm 等效焦距 / 像素宽高;\n * - GPS IFD:纬度(度分秒 + N/S)/ 经度(度分秒 + E/W)/ 海拔(含海平面下标志);\n * - 字节序 II(小端)/ MM(大端)都支持;值内联(≤4 字节)与偏移两种存放方式都处理。\n *\n * 解析策略是**尽力而为、绝不抛错**:任何一段越界或类型不认识都只记进 `warnings` 并跳过,\n * 已经读到的字段原样保留 —— 因为「元数据缺失」本身就是需要展示给用户的信息,\n * 不该因为某个 tag 坏了就让整张照片导入失败。\n */\n/** EXIF GPS 信息(已转成十进制度) */\nexport interface ExifGps {\n /** 十进制度,北纬为正 */\n latitude?: number\n /** 十进制度,东经为正 */\n longitude?: number\n /** 海拔(米);altitudeRef = 1 表示海平面以下,此处已取负 */\n altitude?: number\n /** 原始海拔标志:0 = 海平面以上,1 = 海平面以下 */\n altitudeRef?: number\n}\n\n/** EXIF 解析结果(字段全部可选 —— 缺失就是缺失,不填默认值) */\nexport interface ExifData {\n make?: string\n model?: string\n lensModel?: string\n /** 原始拍摄时间(相机本地时间,无时区),格式 `YYYY-MM-DDTHH:MM:SS` */\n dateTimeOriginal?: string\n /** 文件修改时间(EXIF DateTime) */\n dateTime?: string\n /** 拍照方向(1~8,见 EXIF 规范;1 = 正常) */\n orientation?: number\n focalLengthMm?: number\n /** 35mm 等效焦距 —— 换算内参的关键字段 */\n focalLength35mm?: number\n exposureTimeSeconds?: number\n fNumber?: number\n isoSpeed?: number\n /** 图像宽高(EXIF 里记录的原始像素尺寸) */\n pixelXDimension?: number\n pixelYDimension?: number\n gps?: ExifGps\n /** 解析过程中的异常/缺失说明(诊断用) */\n warnings: string[]\n}\n\n// ── TIFF tag 常量(只列用得到的) ──\nconst TAG_MAKE = 0x010f\nconst TAG_MODEL = 0x0110\nconst TAG_ORIENTATION = 0x0112\nconst TAG_DATE_TIME = 0x0132\nconst TAG_EXIF_IFD = 0x8769\nconst TAG_GPS_IFD = 0x8825\nconst TAG_EXPOSURE_TIME = 0x829a\nconst TAG_F_NUMBER = 0x829d\nconst TAG_ISO_SPEED = 0x8827\nconst TAG_DATE_TIME_ORIGINAL = 0x9003\nconst TAG_FOCAL_LENGTH = 0x920a\nconst TAG_PIXEL_X = 0xa002\nconst TAG_PIXEL_Y = 0xa003\nconst TAG_FOCAL_LENGTH_35MM = 0xa405\nconst TAG_LENS_MODEL = 0xa434\n\nconst GPS_LAT_REF = 0x0001\nconst GPS_LAT = 0x0002\nconst GPS_LON_REF = 0x0003\nconst GPS_LON = 0x0004\nconst GPS_ALT_REF = 0x0005\nconst GPS_ALT = 0x0006\n\n/** TIFF 字段类型 → 单值字节数(1 BYTE / 2 ASCII / 3 SHORT / 4 LONG / 5 RATIONAL / 7 UNDEFINED / 9 SLONG / 10 SRATIONAL) */\nconst TYPE_SIZE: Record<number, number> = {\n 1: 1,\n 2: 1,\n 3: 2,\n 4: 4,\n 5: 8,\n 6: 1,\n 7: 1,\n 8: 2,\n 9: 4,\n 10: 8,\n 11: 4,\n 12: 8,\n}\n\n/** 一个 IFD 条目在文件里的位置与形状 */\ninterface IfdEntry {\n type: number\n count: number\n /** 条目自身在 DataView 中的偏移(8 字节值域从这里开始) */\n offset: number\n}\n\ntype Ifd = Map<number, IfdEntry>\n\nfunction readU16(view: DataView, offset: number, little: boolean): number {\n return view.getUint16(offset, little)\n}\n\nfunction readU32(view: DataView, offset: number, little: boolean): number {\n return view.getUint32(offset, little)\n}\n\n/** 读一个 IFD(标签 → 条目),越界即止;返回条目表与下一个 IFD 的偏移 */\nfunction readIfd(view: DataView, tiffStart: number, ifdOffset: number, little: boolean): Ifd {\n const entries: Ifd = new Map()\n const countOffset = tiffStart + ifdOffset\n if (countOffset + 2 > view.byteLength) return entries\n const count = readU16(view, countOffset, little)\n for (let i = 0; i < count; i += 1) {\n const offset = countOffset + 2 + i * 12\n if (offset + 12 > view.byteLength) break\n const tag = readU16(view, offset, little)\n const type = readU16(view, offset + 2, little)\n const valueCount = readU32(view, offset + 4, little)\n entries.set(tag, { type, count: valueCount, offset })\n }\n return entries\n}\n\n/** 取条目值域的起始偏移:总长 ≤ 4 字节时值内联在条目里,否则是个指向 TIFF 起点的偏移 */\nfunction valueStart(\n view: DataView,\n tiffStart: number,\n entry: IfdEntry,\n little: boolean,\n): number | null {\n const size = TYPE_SIZE[entry.type]\n if (!size) return null\n if (size * entry.count <= 4) return entry.offset + 8\n const start = tiffStart + readU32(view, entry.offset + 8, little)\n return start < view.byteLength ? start : null\n}\n\n/** 第 index 个分量(用于 RATIONAL / SRATIONAL —— 它们是分子+分母两个 4 字节字) */\nfunction readComponent(\n view: DataView,\n tiffStart: number,\n entry: IfdEntry,\n little: boolean,\n index: number,\n): number | undefined {\n const size = TYPE_SIZE[entry.type]\n const start = valueStart(view, tiffStart, entry, little)\n if (!size || start === null) return undefined\n const offset = start + index * size\n if (offset + size > view.byteLength) return undefined\n switch (entry.type) {\n case 1:\n case 2:\n case 6:\n case 7:\n return view.getUint8(offset)\n case 3:\n return readU16(view, offset, little)\n case 8:\n return view.getInt16(offset, little)\n case 4:\n return readU32(view, offset, little)\n case 9:\n return view.getInt32(offset, little)\n case 11:\n return view.getFloat32(offset, little)\n case 12:\n return view.getFloat64(offset, little)\n default:\n return undefined\n }\n}\n\n/**\n * 实数型字段取第 index 个值。\n *\n * EXIF 里实数有三种写法,都必须认:\n * - `RATIONAL`(5)/`SRATIONAL`(10):分子 + 分母两个 4 字节字 —— 相机固件走这条;\n * - `FLOAT`(11)/`DOUBLE`(12):IEEE 浮点 —— 一些转换工具(含 Pillow 的默认行为)走这条。\n * 只支持前者的话,这类文件的曝光/光圈/焦距会读出一串垃圾数(不是读不到,而是**读错**,\n * 比缺失更危险),所以这里显式分流。\n */\nfunction readRational(\n view: DataView,\n tiffStart: number,\n entry: IfdEntry | undefined,\n little: boolean,\n index = 0,\n): number | undefined {\n if (!entry) return undefined\n if (entry.type === 11 || entry.type === 12) {\n return readComponent(view, tiffStart, entry, little, index)\n }\n const size = TYPE_SIZE[entry.type]\n const start = valueStart(view, tiffStart, entry, little)\n if (start === null) return undefined\n const offset = start + index * (size ?? 8)\n if (offset + 8 > view.byteLength) return undefined\n const numerator =\n entry.type === 10 ? view.getInt32(offset, little) : readU32(view, offset, little)\n const denominator =\n entry.type === 10 ? view.getInt32(offset + 4, little) : readU32(view, offset + 4, little)\n if (denominator === 0) return undefined\n return numerator / denominator\n}\n\n/** ASCII 串(以 NUL 结尾,可能被塞在偏移处) */\nfunction readAscii(\n view: DataView,\n tiffStart: number,\n entry: IfdEntry | undefined,\n little: boolean,\n): string | undefined {\n if (!entry) return undefined\n const start = valueStart(view, tiffStart, entry, little)\n if (start === null) return undefined\n let text = ''\n for (let i = 0; i < entry.count; i += 1) {\n const offset = start + i\n if (offset >= view.byteLength) break\n const code = view.getUint8(offset)\n if (code === 0) break\n text += String.fromCharCode(code)\n }\n const trimmed = text.trim()\n return trimmed.length ? trimmed : undefined\n}\n\n/** 单个数值字段(BYTE / SHORT / LONG / SLONG / RATIONAL / SRATIONAL 统一取第 0 个) */\nfunction readNumber(\n view: DataView,\n tiffStart: number,\n entry: IfdEntry | undefined,\n little: boolean,\n): number | undefined {\n if (!entry) return undefined\n if (entry.type === 5 || entry.type === 10) return readRational(view, tiffStart, entry, little)\n return readComponent(view, tiffStart, entry, little, 0)\n}\n\n/** 度分秒三元组 → 十进制度(GPS 用 RATIONAL[3]) */\nfunction readDegrees(\n view: DataView,\n tiffStart: number,\n entry: IfdEntry | undefined,\n little: boolean,\n): number | undefined {\n if (!entry || entry.count < 3) return undefined\n const degrees = readRational(view, tiffStart, entry, little, 0)\n const minutes = readRational(view, tiffStart, entry, little, 1)\n const seconds = readRational(view, tiffStart, entry, little, 2)\n if (degrees === undefined || minutes === undefined || seconds === undefined) return undefined\n return degrees + minutes / 60 + seconds / 3600\n}\n\n/** EXIF 时间 `YYYY:MM:DD HH:MM:SS` → `YYYY-MM-DDTHH:MM:SS`(保持相机本地时间,不加时区) */\nfunction normalizeExifTime(raw: string | undefined): string | undefined {\n if (!raw) return undefined\n const match = /^(\\d{4}):(\\d{2}):(\\d{2})[ T](\\d{2}):(\\d{2}):(\\d{2})/.exec(raw)\n if (!match) return undefined\n const [, y, mo, d, h, mi, s] = match\n return `${y}-${mo}-${d}T${h}:${mi}:${s}`\n}\n\n/** 读 TIFF(EXIF 段正文,起点即 TIFF header) */\nfunction readTiff(view: DataView, tiffStart: number): ExifData | null {\n if (tiffStart + 8 > view.byteLength) return null\n const byteOrder = readU16(view, tiffStart, false)\n let little: boolean\n if (byteOrder === 0x4949) little = true\n else if (byteOrder === 0x4d4d) little = false\n else return null\n if (readU16(view, tiffStart + 2, little) !== 0x002a) return null\n\n const warnings: string[] = []\n const ifd0Offset = readU32(view, tiffStart + 4, little)\n if (ifd0Offset === 0) return null\n const ifd0 = readIfd(view, tiffStart, ifd0Offset, little)\n if (ifd0.size === 0) return null\n\n const data: ExifData = { warnings }\n\n data.make = readAscii(view, tiffStart, ifd0.get(TAG_MAKE), little)\n data.model = readAscii(view, tiffStart, ifd0.get(TAG_MODEL), little)\n data.orientation = readNumber(view, tiffStart, ifd0.get(TAG_ORIENTATION), little)\n data.dateTime = normalizeExifTime(readAscii(view, tiffStart, ifd0.get(TAG_DATE_TIME), little))\n\n // Exif 子 IFD\n const exifIfdPointer = readNumber(view, tiffStart, ifd0.get(TAG_EXIF_IFD), little)\n const exifIfd =\n exifIfdPointer !== undefined ? readIfd(view, tiffStart, exifIfdPointer, little) : null\n if (exifIfd) {\n data.lensModel = readAscii(view, tiffStart, exifIfd.get(TAG_LENS_MODEL), little)\n data.dateTimeOriginal = normalizeExifTime(\n readAscii(view, tiffStart, exifIfd.get(TAG_DATE_TIME_ORIGINAL), little),\n )\n data.exposureTimeSeconds = readRational(view, tiffStart, exifIfd.get(TAG_EXPOSURE_TIME), little)\n data.fNumber = readRational(view, tiffStart, exifIfd.get(TAG_F_NUMBER), little)\n data.isoSpeed = readNumber(view, tiffStart, exifIfd.get(TAG_ISO_SPEED), little)\n data.focalLengthMm = readRational(view, tiffStart, exifIfd.get(TAG_FOCAL_LENGTH), little)\n data.focalLength35mm = readNumber(view, tiffStart, exifIfd.get(TAG_FOCAL_LENGTH_35MM), little)\n data.pixelXDimension = readNumber(view, tiffStart, exifIfd.get(TAG_PIXEL_X), little)\n data.pixelYDimension = readNumber(view, tiffStart, exifIfd.get(TAG_PIXEL_Y), little)\n } else {\n warnings.push('缺少 Exif 子 IFD(焦距 / 原始拍摄时间等字段不可得)')\n }\n\n // GPS 子 IFD\n const gpsIfdPointer = readNumber(view, tiffStart, ifd0.get(TAG_GPS_IFD), little)\n const gpsIfd =\n gpsIfdPointer !== undefined ? readIfd(view, tiffStart, gpsIfdPointer, little) : null\n if (gpsIfd) {\n const gps: ExifGps = {}\n const lat = readDegrees(view, tiffStart, gpsIfd.get(GPS_LAT), little)\n const latRef = readAscii(view, tiffStart, gpsIfd.get(GPS_LAT_REF), little)\n if (lat !== undefined) gps.latitude = latRef?.toUpperCase() === 'S' ? -lat : lat\n const lon = readDegrees(view, tiffStart, gpsIfd.get(GPS_LON), little)\n const lonRef = readAscii(view, tiffStart, gpsIfd.get(GPS_LON_REF), little)\n if (lon !== undefined) gps.longitude = lonRef?.toUpperCase() === 'W' ? -lon : lon\n const altitude = readRational(view, tiffStart, gpsIfd.get(GPS_ALT), little)\n const altitudeRef = readNumber(view, tiffStart, gpsIfd.get(GPS_ALT_REF), little)\n if (altitudeRef !== undefined) gps.altitudeRef = altitudeRef\n if (altitude !== undefined) gps.altitude = altitudeRef === 1 ? -altitude : altitude\n if (Object.keys(gps).length) data.gps = gps\n } else {\n warnings.push('照片不含 GPS 信息(无法做地理定位,只能当作局部坐标成果)')\n }\n\n return data\n}\n\n/**\n * 解析 JPEG 的 EXIF 段。\n *\n * @returns 解析结果;不是 JPEG / 没有 EXIF 段 / 结构不可识别时返回 `null`。\n */\nexport function parseJpegExif(bytes: Uint8Array): ExifData | null {\n if (bytes.byteLength < 4) return null\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n // SOI\n if (readU16(view, 0, false) !== 0xffd8) return null\n\n let offset = 2\n while (offset + 4 <= view.byteLength) {\n if (view.getUint8(offset) !== 0xff) {\n offset += 1\n continue\n }\n const marker = view.getUint8(offset + 1)\n // 填充 / 无长度段\n if (marker === 0xff || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd9)) {\n offset += 2\n continue\n }\n // SOS:压缩数据开始,后面不会再有元数据\n if (marker === 0xda) break\n const length = readU16(view, offset + 2, false)\n if (length < 2) break\n const segmentStart = offset + 4\n if (marker === 0xe1) {\n // APP1 且有效载荷以 \"Exif\\0\\0\" 开头\n const isExif =\n segmentStart + 6 <= view.byteLength &&\n readU32(view, segmentStart, false) === 0x45786966 &&\n readU16(view, segmentStart + 4, false) === 0x0000\n if (isExif) return readTiff(view, segmentStart + 6)\n }\n offset = segmentStart + length - 2\n }\n return null\n}\n\n/** 从 Blob / File 读前若干字节解析 EXIF(只取头部,不必读整张图) */\nexport async function readJpegExif(file: Blob, maxBytes = 256 * 1024): Promise<ExifData | null> {\n const head = file.slice(0, Math.min(maxBytes, file.size))\n let bytes: Uint8Array\n try {\n bytes = new Uint8Array(await head.arrayBuffer())\n } catch {\n return null\n }\n try {\n return parseJpegExif(bytes)\n } catch {\n return null\n }\n}\n","/**\n * CaptureManifest and quality checks for the virtual photogrammetry PoC.\n *\n * The capture service deliberately keeps its browser facing session model\n * (`CaptureSession`) small. This module is the boundary to an offline\n * reconstruction tool: it turns a session into a stable, JSON serialisable\n * manifest and validates the fields that COLMAP (or another tool) needs.\n */\n\nimport type {\n CameraIntrinsics,\n CaptureSession,\n CapturedImage,\n CoordinateFrame,\n} from './drone-capture'\nimport { createEcefEnuFrame, ecefToEnu } from './drone-photogrammetry'\n\nexport const CAPTURE_MANIFEST_SCHEMA_VERSION = 1 as const\n\nexport type CaptureStatus = 'success' | 'failed' | 'pending'\n\nexport interface CaptureManifestTransform {\n /** Source coordinate frame used by the image poses. */\n source: string\n /** Target frame expected by the reconstruction/export pipeline. */\n target: string\n /** Optional translation in target units: target = rotation * (scale * source) + translation. */\n translation?: [number, number, number]\n /** Optional active rotation, in quaternion [x, y, z, w] order. */\n rotation?: [number, number, number, number]\n /** Optional uniform scale from source units to target units. */\n scale?: number\n /** Origin used when deriving a local ENU frame. */\n origin?: { longitude: number; latitude: number; altitude: number }\n /** Extra transform parameters supplied by an integration. */\n [key: string]: unknown\n}\n\nexport interface CaptureManifestCoordinateFrame {\n world: string\n local: string\n units: 'meters' | string\n transform: CaptureManifestTransform\n}\n\nexport interface CaptureManifestCamera {\n /** COLMAP style camera model, for example PINHOLE or OPENCV. */\n model: string\n width: number\n height: number\n fx?: number\n fy?: number\n cx?: number\n cy?: number\n horizontalFovDeg?: number\n verticalFovDeg?: number\n distortion?: number[]\n /** Projection model as used by the rendering backend. */\n projection?: string\n /** Zoom value if the backend exposes one. */\n zoom?: number\n}\n\nexport interface CaptureManifestImage {\n id: string\n /** Path relative to the capture directory. */\n file: string\n timestamp: string\n position?: [number, number, number]\n rotation?: [number, number, number, number]\n /** Explicitly states quaternion order and camera axis convention. */\n rotationConvention: string\n captureStatus: CaptureStatus\n source?: string\n mimeType?: string\n sizeBytes?: number\n width?: number\n height?: number\n coordinateFrame?: CoordinateFrame | string\n intrinsics?: CaptureManifestCamera\n cameraModel?: string\n engine?: string\n sceneId?: string\n sceneVersion?: string\n /** Optional content hashes or quality probes supplied by a storage layer. */\n contentHash?: string\n quality?: CaptureImageQuality\n validation?: { status: string; messages: string[] }\n}\n\nexport interface CaptureImageQuality {\n /** Set by a decoder when the image was successfully opened. */\n decodable?: boolean\n /** True when all sampled pixels have the same (or almost the same) value. */\n uniform?: boolean\n /** True when the decoded image is effectively black or white. */\n blank?: boolean\n /** Optional luma statistics from an image decoder. */\n meanLuma?: number\n variance?: number\n}\n\nexport interface CaptureManifest {\n schemaVersion: typeof CAPTURE_MANIFEST_SCHEMA_VERSION\n captureId: string\n createdAt: string\n coordinateFrame: CaptureManifestCoordinateFrame\n camera: CaptureManifestCamera\n images: CaptureManifestImage[]\n}\n\nexport interface CaptureManifestOptions {\n captureId?: string\n createdAt?: string\n coordinateFrame?: Partial<CaptureManifestCoordinateFrame> & {\n transform?: Partial<CaptureManifestTransform>\n }\n /** Override the default image path prefix (`images/`). */\n imageDirectory?: string\n /** Explicit camera model when the source does not expose one. */\n cameraModel?: string\n /** Explicit projection model, for example perspective. */\n projection?: string\n}\n\nexport type CaptureValidationSeverity = 'error' | 'warning'\n\nexport interface CaptureValidationIssue {\n code: string\n message: string\n severity: CaptureValidationSeverity\n imageId?: string\n}\n\nexport interface CaptureValidationReport {\n valid: boolean\n status: 'valid' | 'warning' | 'invalid'\n imageCount: number\n checkedImages: number\n errors: CaptureValidationIssue[]\n warnings: CaptureValidationIssue[]\n issues: CaptureValidationIssue[]\n duplicateImageIds: string[][]\n /** Aliases useful to callers that treat the report as a quality gate. */\n passed: boolean\n}\n\nexport interface CaptureValidatorOptions {\n /** Require every image to contain a finite camera pose (default true). */\n requirePose?: boolean\n /** Require a usable global camera model (default true). */\n requireIntrinsics?: boolean\n /** Check optional blank/uniform probes (default true when present). */\n checkBlankImages?: boolean\n /** Treat duplicate content hashes/poses as errors (default true). */\n rejectDuplicates?: boolean\n /** Maximum relative difference allowed for numeric camera parameters. */\n intrinsicsTolerance?: number\n /** Optional additional image quality probes, keyed by id or file. */\n imageQuality?: Record<string, CaptureImageQuality | undefined>\n}\n\nconst ROTATION_CONVENTION =\n 'quaternion [x,y,z,w]; active camera-local-to-world rotation; camera forward=-Z, right=+X, up=+Y'\n\nfunction finite(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value)\n}\n\nfunction finiteTuple<T extends number[]>(value: unknown, length: number): value is T {\n return Array.isArray(value) && value.length === length && value.every(finite)\n}\n\nfunction cloneTuple<T extends number[]>(value: T): T {\n return value.slice() as T\n}\n\nfunction cameraFromIntrinsics(\n intrinsics: CameraIntrinsics | undefined,\n options: CaptureManifestOptions,\n): CaptureManifestCamera {\n const source = intrinsics\n const model = options.cameraModel ?? (source?.fx !== undefined || source?.fy !== undefined ? 'PINHOLE' : 'UNKNOWN')\n return {\n model,\n width: source?.width ?? 0,\n height: source?.height ?? 0,\n ...(source?.fx === undefined ? {} : { fx: source.fx }),\n ...(source?.fy === undefined ? {} : { fy: source.fy }),\n ...(source?.cx === undefined ? {} : { cx: source.cx }),\n ...(source?.cy === undefined ? {} : { cy: source.cy }),\n ...(source?.horizontalFovDeg === undefined ? {} : { horizontalFovDeg: source.horizontalFovDeg }),\n ...(source?.verticalFovDeg === undefined ? {} : { verticalFovDeg: source.verticalFovDeg }),\n ...(source?.distortion === undefined ? {} : { distortion: [...source.distortion] }),\n projection: options.projection ?? 'perspective',\n }\n}\n\nfunction cameraEqual(a: CaptureManifestCamera, b: CaptureManifestCamera, tolerance: number): boolean {\n const fields: Array<keyof CaptureManifestCamera> = [\n 'width',\n 'height',\n 'fx',\n 'fy',\n 'cx',\n 'cy',\n 'horizontalFovDeg',\n 'verticalFovDeg',\n 'zoom',\n ]\n if (a.model !== b.model || a.projection !== b.projection) return false\n if (!fields.every((field) => {\n const av = a[field]\n const bv = b[field]\n if (typeof av !== 'number' || typeof bv !== 'number') return av === bv\n const scale = Math.max(1, Math.abs(av), Math.abs(bv))\n return Math.abs(av - bv) <= tolerance * scale\n })) return false\n const ad = a.distortion ?? []\n const bd = b.distortion ?? []\n return ad.length === bd.length && ad.every((value, index) => {\n const other = bd[index]\n return finite(other) && Math.abs(value - other) <= tolerance * Math.max(1, Math.abs(value), Math.abs(other))\n })\n}\n\nfunction pathForImage(fileName: string, directory: string): string {\n const normalized = fileName.replace(/\\\\/g, '/')\n const prefix = directory.replace(/\\\\/g, '/').replace(/\\/+$/, '')\n if (!prefix || normalized.startsWith(`${prefix}/`)) return normalized\n return `${prefix}/${normalized.replace(/^\\/+/, '')}`\n}\n\nfunction frameName(frame: CoordinateFrame | undefined): string {\n switch (frame) {\n case 'ecef':\n return 'Cesium ECEF'\n case 'local-enu':\n return 'ENU'\n case 'project-local':\n return 'project-local'\n default:\n return 'unknown'\n }\n}\n\nfunction coordinateFrameFromSession(\n session: CaptureSession,\n images: CapturedImage[],\n override?: CaptureManifestOptions['coordinateFrame'],\n): CaptureManifestCoordinateFrame {\n const frames = [...new Set(images.map((image) => image.cameraPose?.coordinateFrame).filter(Boolean))]\n const source = frames.length === 1 ? frameName(frames[0]) : frames.length > 1 ? 'mixed' : 'unknown'\n const origin = session.options.georeference\n const enuFrame = source === 'Cesium ECEF' && origin ? createEcefEnuFrame(origin) : null\n const enuTranslation = enuFrame ? ecefToEnu([0, 0, 0], enuFrame) : null\n const transformParametersAvailable = source !== 'unknown' && source !== 'mixed' && (source !== 'Cesium ECEF' || enuFrame !== null)\n const defaultTransform: CaptureManifestTransform = {\n source,\n target: source === 'Cesium ECEF' ? 'ENU' : source,\n ...(transformParametersAvailable\n ? {\n translation: [0, 0, 0] as [number, number, number],\n rotation: [0, 0, 0, 1] as [number, number, number, number],\n scale: 1,\n }\n : { parametersAvailable: false }),\n ...(origin\n ? {\n origin: {\n longitude: origin.longitude,\n latitude: origin.latitude,\n altitude: origin.altitude ?? 0,\n },\n }\n : {}),\n ...(enuFrame && enuTranslation\n ? {\n parametersAvailable: true,\n translation: enuTranslation,\n rotation: [\n -enuFrame.enuToEcefRotation[0],\n -enuFrame.enuToEcefRotation[1],\n -enuFrame.enuToEcefRotation[2],\n enuFrame.enuToEcefRotation[3],\n ] as [number, number, number, number],\n ecefOrigin: [...enuFrame.ecefOrigin],\n basis: {\n east: [...enuFrame.east],\n north: [...enuFrame.north],\n up: [...enuFrame.up],\n },\n enuToEcefRotation: [...enuFrame.enuToEcefRotation],\n // Row-major affine matrix that maps ECEF metres to local ENU metres.\n matrix: [\n enuFrame.east[0],\n enuFrame.east[1],\n enuFrame.east[2],\n enuTranslation[0],\n enuFrame.north[0],\n enuFrame.north[1],\n enuFrame.north[2],\n enuTranslation[1],\n enuFrame.up[0],\n enuFrame.up[1],\n enuFrame.up[2],\n enuTranslation[2],\n 0,\n 0,\n 0,\n 1,\n ],\n matrixOrder: 'row-major',\n }\n : {}),\n }\n const transform = { ...defaultTransform, ...(override?.transform ?? {}) }\n return {\n world: override?.world ?? (source === 'Cesium ECEF' ? 'Cesium ECEF' : source),\n local: override?.local ?? (source === 'Cesium ECEF' ? 'ENU' : source),\n units: override?.units ?? 'meters',\n transform,\n }\n}\n\nfunction imageStatus(image: CapturedImage): CaptureStatus {\n if (image.validation.status === 'invalid') return 'failed'\n if (image.validation.status === 'pending') return 'pending'\n return 'success'\n}\n\nfunction imageEntry(\n image: CapturedImage,\n camera: CaptureManifestCamera,\n imageDirectory: string,\n): CaptureManifestImage {\n const pose = image.cameraPose\n const intrinsics = image.cameraIntrinsics ? cameraFromIntrinsics(image.cameraIntrinsics, {}) : undefined\n return {\n id: image.id,\n file: pathForImage(image.fileName, imageDirectory),\n timestamp: image.capturedAt,\n ...(pose\n ? {\n position: cloneTuple(pose.position),\n rotation: cloneTuple(pose.rotation),\n coordinateFrame: pose.coordinateFrame,\n }\n : {}),\n rotationConvention: ROTATION_CONVENTION,\n captureStatus: imageStatus(image),\n source: image.source,\n mimeType: image.mimeType,\n sizeBytes: image.sizeBytes,\n width: image.cameraIntrinsics?.width ?? camera.width,\n height: image.cameraIntrinsics?.height ?? camera.height,\n ...(intrinsics ? { intrinsics } : {}),\n ...(image.metadata.cameraModel ? { cameraModel: image.metadata.cameraModel } : {}),\n ...(image.metadata.engine ? { engine: image.metadata.engine } : {}),\n ...(image.metadata.sceneId ? { sceneId: image.metadata.sceneId } : {}),\n ...(image.metadata.sceneVersion ? { sceneVersion: image.metadata.sceneVersion } : {}),\n ...(image.imageUrl ? { contentHash: hashText(image.imageUrl) } : {}),\n validation: { status: image.validation.status, messages: [...image.validation.messages] },\n }\n}\n\n/** Small deterministic fallback hash for browser sessions without Web Crypto. */\nfunction hashText(value: string): string {\n let hash = 2166136261\n for (let index = 0; index < value.length; index += 1) {\n hash ^= value.charCodeAt(index)\n hash = Math.imul(hash, 16777619)\n }\n return `fnv1a32-${(hash >>> 0).toString(16).padStart(8, '0')}`\n}\n\n/** Build a JSON serialisable manifest from an existing capture session. */\nexport function buildCaptureManifest(\n session: CaptureSession,\n options: CaptureManifestOptions = {},\n): CaptureManifest {\n const first = session.images.find((image) => image.cameraIntrinsics)?.cameraIntrinsics\n const camera = cameraFromIntrinsics(first, options)\n return {\n schemaVersion: CAPTURE_MANIFEST_SCHEMA_VERSION,\n captureId: options.captureId ?? session.id,\n createdAt: options.createdAt ?? session.createdAt,\n coordinateFrame: coordinateFrameFromSession(session, session.images, options.coordinateFrame),\n camera,\n images: session.images.map((image) => imageEntry(image, camera, options.imageDirectory ?? 'images')),\n }\n}\n\n/** Compatibility aliases for callers that prefer a verb based name. */\nexport const captureSessionToManifest = buildCaptureManifest\nexport const createCaptureManifest = buildCaptureManifest\n\nexport function serializeCaptureManifest(manifest: CaptureManifest, space = 2): string {\n return JSON.stringify(manifest, null, space)\n}\n\nexport function parseCaptureManifest(value: string | CaptureManifest): CaptureManifest {\n const manifest = typeof value === 'string' ? (JSON.parse(value) as CaptureManifest) : value\n if (!manifest || manifest.schemaVersion !== CAPTURE_MANIFEST_SCHEMA_VERSION) {\n throw new Error(`Unsupported capture manifest schema version: ${String(manifest?.schemaVersion)}`)\n }\n if (!Array.isArray(manifest.images)) throw new Error('Capture manifest images must be an array')\n return manifest\n}\n\nfunction makeIssue(\n severity: CaptureValidationSeverity,\n code: string,\n message: string,\n imageId?: string,\n): CaptureValidationIssue {\n return { severity, code, message, ...(imageId ? { imageId } : {}) }\n}\n\n/** Validate the manifest before handing it to an offline reconstruction task. */\nexport function validateCaptureManifest(\n manifest: CaptureManifest,\n options: CaptureValidatorOptions = {},\n): CaptureValidationReport {\n const errors: CaptureValidationIssue[] = []\n const warnings: CaptureValidationIssue[] = []\n const add = (issue: CaptureValidationIssue): void => {\n if (issue.severity === 'error') errors.push(issue)\n else warnings.push(issue)\n }\n const requirePose = options.requirePose ?? true\n const requireIntrinsics = options.requireIntrinsics ?? true\n const checkBlank = options.checkBlankImages ?? true\n const rejectDuplicates = options.rejectDuplicates ?? true\n const tolerance = options.intrinsicsTolerance ?? 1e-3\n const images = Array.isArray(manifest?.images) ? manifest.images : []\n const duplicateImageIds: string[][] = []\n\n if (!manifest || manifest.schemaVersion !== CAPTURE_MANIFEST_SCHEMA_VERSION) {\n add(makeIssue('error', 'schema-version', 'Unsupported or missing manifest schemaVersion'))\n }\n if (!manifest?.captureId) add(makeIssue('error', 'capture-id', 'captureId is required'))\n if (!manifest?.createdAt || Number.isNaN(Date.parse(manifest.createdAt))) {\n add(makeIssue('error', 'created-at', 'createdAt must be a valid ISO timestamp'))\n }\n if (!manifest?.coordinateFrame?.world || !manifest.coordinateFrame.local) {\n add(makeIssue('error', 'coordinate-frame', 'coordinateFrame.world and local are required'))\n }\n if (!manifest?.coordinateFrame?.transform?.source || !manifest.coordinateFrame.transform.target) {\n add(makeIssue('error', 'coordinate-transform', 'coordinateFrame.transform must declare source and target'))\n }\n if (manifest?.coordinateFrame?.transform?.parametersAvailable === false) {\n add(makeIssue('error', 'coordinate-transform', 'Coordinate transform parameters are unavailable'))\n }\n\n const camera = manifest?.camera\n if (!camera || !camera.model || !finite(camera.width) || !finite(camera.height) || camera.width <= 0 || camera.height <= 0) {\n add(makeIssue('error', 'camera-intrinsics', 'camera model and positive image dimensions are required'))\n }\n if (requireIntrinsics && (!camera || !finite(camera.fx) || !finite(camera.fy) || !finite(camera.cx) || !finite(camera.cy))) {\n add(makeIssue('error', 'camera-intrinsics', 'camera fx/fy/cx/cy must be finite'))\n }\n\n const seenIds = new Set<string>()\n const seenFiles = new Map<string, string>()\n const seenHashes = new Map<string, string>()\n const seenPoses = new Map<string, string>()\n let checkedImages = 0\n for (const image of images) {\n checkedImages += 1\n if (!image?.id) add(makeIssue('error', 'image-id', 'Image id is required'))\n if (image?.id && seenIds.has(image.id)) add(makeIssue('error', 'duplicate-id', `Duplicate image id: ${image.id}`, image.id))\n if (image?.id) seenIds.add(image.id)\n if (image?.captureStatus !== 'success') {\n add(makeIssue('error', 'capture-status', 'Image captureStatus must be success', image?.id))\n }\n if (!image?.file) add(makeIssue('error', 'image-file', 'Image file path is required', image?.id))\n if (!image?.timestamp || Number.isNaN(Date.parse(image.timestamp))) {\n add(makeIssue('error', 'timestamp', 'Image timestamp must be valid', image?.id))\n }\n if (!image?.mimeType?.startsWith('image/')) {\n add(makeIssue('error', 'mime-type', 'Image MIME type must start with image/', image?.id))\n }\n if (!finite(image?.sizeBytes) || (image?.sizeBytes ?? 0) <= 0) {\n add(makeIssue('error', 'empty-file', 'Image file is empty or size is unknown', image?.id))\n }\n const width = image?.width ?? camera?.width\n const height = image?.height ?? camera?.height\n if (!finite(width) || !finite(height) || width <= 0 || height <= 0) {\n add(makeIssue('error', 'image-dimensions', 'Image dimensions must be positive', image?.id))\n } else if (camera && (width !== camera.width || height !== camera.height)) {\n add(makeIssue('error', 'image-dimensions-mismatch', 'Image dimensions do not match the manifest camera', image?.id))\n }\n const quality = options.imageQuality?.[image?.id] ?? options.imageQuality?.[image?.file] ?? image?.quality\n if (quality?.decodable === false) add(makeIssue('error', 'decode', 'Image cannot be decoded', image?.id))\n if (checkBlank && (quality?.blank === true || quality?.uniform === true)) {\n add(makeIssue('error', 'blank-image', 'Image is uniformly black/white or otherwise blank', image?.id))\n }\n if (checkBlank && finite(quality?.variance) && quality.variance <= 1e-6) {\n add(makeIssue('error', 'blank-image', 'Image has no measurable pixel variation', image?.id))\n }\n if (\n checkBlank &&\n finite(quality?.meanLuma) &&\n (quality.meanLuma <= 1 || quality.meanLuma >= 254) &&\n (!finite(quality.variance) || quality.variance <= 1)\n ) {\n add(makeIssue('error', 'blank-image', 'Image is effectively pure black or white', image?.id))\n }\n if (!image?.rotationConvention) add(makeIssue('error', 'rotation-convention', 'rotationConvention is required', image?.id))\n if (requirePose && (!finiteTuple(image?.position, 3) || !finiteTuple(image?.rotation, 4))) {\n add(makeIssue('error', 'pose', 'Image position and quaternion must contain finite values', image?.id))\n }\n if (finiteTuple(image?.rotation, 4)) {\n const norm = Math.hypot(...image.rotation)\n if (norm < 1e-9) add(makeIssue('error', 'pose-quaternion', 'Image rotation quaternion has zero length', image.id))\n }\n\n const file = image?.file\n if (file && seenFiles.has(file)) {\n const firstId = seenFiles.get(file)!\n duplicateImageIds.push([firstId, image.id])\n if (rejectDuplicates) add(makeIssue('error', 'duplicate-file', `Duplicate image file: ${file}`, image.id))\n if (rejectDuplicates) add(makeIssue('error', 'duplicate-content', 'Duplicate image content (same file path)', image.id))\n } else if (file) seenFiles.set(file, image.id)\n const hash = image?.contentHash\n if (hash && seenHashes.has(hash)) {\n const firstId = seenHashes.get(hash)!\n duplicateImageIds.push([firstId, image.id])\n if (rejectDuplicates) add(makeIssue('error', 'duplicate-content', 'Duplicate image content hash', image.id))\n } else if (hash) seenHashes.set(hash, image.id)\n if (finiteTuple(image?.position, 3) && finiteTuple(image?.rotation, 4)) {\n const poseKey = `${image.position.join(',')}|${image.rotation.join(',')}`\n if (seenPoses.has(poseKey)) {\n const firstId = seenPoses.get(poseKey)!\n duplicateImageIds.push([firstId, image.id])\n if (rejectDuplicates) add(makeIssue('error', 'duplicate-pose', 'Duplicate camera pose', image.id))\n } else seenPoses.set(poseKey, image.id)\n }\n\n if (image?.intrinsics && camera && !cameraEqual(camera, image.intrinsics, tolerance)) {\n add(makeIssue('error', 'intrinsics-mismatch', 'Image camera intrinsics do not match the manifest camera', image.id))\n }\n }\n if (images.length === 0) add(makeIssue('error', 'no-images', 'Manifest contains no images'))\n if (images.length === 1) warnings.push(makeIssue('warning', 'single-image', 'A single image cannot provide photogrammetric coverage'))\n\n const issues = [...errors, ...warnings]\n const status = errors.length ? 'invalid' : warnings.length ? 'warning' : 'valid'\n return {\n valid: errors.length === 0,\n passed: errors.length === 0,\n status,\n imageCount: images.length,\n checkedImages,\n errors,\n warnings,\n issues,\n duplicateImageIds,\n }\n}\n\n/** Validate a browser capture session without requiring the caller to build a manifest first. */\nexport function validateCaptureSession(\n session: CaptureSession,\n options: CaptureValidatorOptions = {},\n manifestOptions: CaptureManifestOptions = {},\n): CaptureValidationReport {\n return validateCaptureManifest(buildCaptureManifest(session, manifestOptions), options)\n}\n\n/** Object oriented facade for applications that keep validators as services. */\nexport class CaptureValidator {\n readonly options: CaptureValidatorOptions\n\n constructor(options: CaptureValidatorOptions = {}) {\n this.options = { ...options }\n }\n\n validate(manifest: CaptureManifest): CaptureValidationReport\n validate(session: CaptureSession, manifestOptions?: CaptureManifestOptions): CaptureValidationReport\n validate(input: CaptureManifest | CaptureSession, manifestOptions: CaptureManifestOptions = {}): CaptureValidationReport {\n if ((input as CaptureManifest).schemaVersion === CAPTURE_MANIFEST_SCHEMA_VERSION) {\n return validateCaptureManifest(input as CaptureManifest, this.options)\n }\n return validateCaptureSession(input as CaptureSession, this.options, manifestOptions)\n }\n\n static validate(manifest: CaptureManifest, options?: CaptureValidatorOptions): CaptureValidationReport\n static validate(\n session: CaptureSession,\n options?: CaptureValidatorOptions,\n manifestOptions?: CaptureManifestOptions,\n ): CaptureValidationReport\n static validate(\n input: CaptureManifest | CaptureSession,\n options: CaptureValidatorOptions = {},\n manifestOptions: CaptureManifestOptions = {},\n ): CaptureValidationReport {\n if ((input as CaptureManifest).schemaVersion === CAPTURE_MANIFEST_SCHEMA_VERSION) {\n return validateCaptureManifest(input as CaptureManifest, options)\n }\n return validateCaptureSession(input as CaptureSession, options, manifestOptions)\n }\n}\n\n","/**\n * 影像采集与影像集管理 —— 框架无关的库内实现(方案 V1「阶段 1」)。\n *\n * 覆盖这条链路:**影像与相机参数可靠采集 → 影像集可检查可导出**。\n * 后台重建(COLMAP / 3D Tiles 发布)不在本模块内,它只负责把「输入」做得干净、\n * 完整、可复现 —— 这是整条摄影测量链路里唯一必须在前端做对的部分。\n *\n * 两类来源共用同一套数据模型(见 `CapturedImage`),但**不强行统一相机标定细节**:\n * - `virtual-camera`:场景渲染图。相机位姿与内参直接从渲染后端取(标定来源 `known`),\n * 坐标是场景局部坐标,若宿主给了地理原点则同时落到经纬度;\n * - `real-camera`:真实照片。位姿来自 EXIF/GPS(标定来源 `exif` / `unknown`),\n * GPS 精度天然只够粗定位 —— 这一点由 `metadata.gps` 与 `validation` 明确标出。\n *\n * 设计边界:\n * - 不自己创建 Viewer / 相机:`虚拟采集`通过注入的 `VirtualCaptureProvider` 向**宿主后端**\n * 要取景帧与相机状态(见本文件对 provider 的说明),避免出现第二个场景实例;\n * - 不碰 DOM:`File` / `Blob` 只用于读取与预览,面板与下载由 drone-capture-panel 负责;\n * - 纯数据 + 纯逻辑,可在 Node / Worker 里跑(浏览器 API 全部按需探测)。\n *\n * 与方案文档的两处偏差(有意为之):\n * 1. `ImageSource` 用 `'virtual-camera'` 而不是文档里的 `'cesium-virtual'` —— 本库有 three\n * 与 Cesium 两套后端,用后端名当来源名会把 three 的采集结果错标成 Cesium;\n * 实际产出后端记在 `metadata.engine` 里。\n * 2. 模块落在 `src/lib/drone-camera/` 而不是文档草案的 `src/modules/**` —— 与本库既有\n * 组织约定(扁平、与框架无关、面板用原生 DOM)保持一致。\n */\nimport { readJpegExif } from './drone-exif'\nimport type { ExifData } from './drone-exif'\nimport {\n buildCaptureManifest,\n serializeCaptureManifest,\n validateCaptureManifest,\n} from './drone-capture-manifest'\nimport type {\n CaptureManifest,\n CaptureManifestOptions,\n CaptureValidationReport,\n CaptureValidatorOptions,\n} from './drone-capture-manifest'\n\n/** 影像来源:场景渲染图 / 真实相机照片 */\nexport type ImageSource = 'virtual-camera' | 'real-camera'\n\n/** 相机位姿所在的坐标框架 */\nexport type CoordinateFrame = 'ecef' | 'local-enu' | 'project-local'\n\n/**\n * 相机内参。\n *\n * `calibrationSource` 不是装饰字段:重建时「内参是已知的」和「内参是猜的」要用完全不同的\n * 策略(SfM 可以固定内参、也可以让内参参与优化),混在一起必然出精度问题,所以必须逐张带上。\n */\nexport interface CameraIntrinsics {\n width: number\n height: number\n fx?: number\n fy?: number\n cx?: number\n cy?: number\n horizontalFovDeg?: number\n verticalFovDeg?: number\n distortion?: number[]\n calibrationSource: 'known' | 'exif' | 'estimated' | 'unknown'\n}\n\n/**\n * 相机位姿。\n *\n * ⚠️ 两个隐性契约,换实现前必须一致:\n * - `rotation` 是四元数,**分量顺序固定 `[x, y, z, w]`**(与 three 一致);\n * - `coordinateFrame` 说明这套数字在哪个系里。库内约定:\n * · `local-enu` = 以起飞点(返航点)为原点,**x 东 / y 北 / z 上**,单位米;\n * · `ecef` = 地心地固坐标,单位米(Cesium 后端)。\n * 四元数表示「相机自身的 x 轴(光轴)指向」在该系中的旋转。\n */\nexport interface CameraPose {\n position: [number, number, number]\n /** 四元数 [x, y, z, w] */\n rotation: [number, number, number, number]\n coordinateFrame: CoordinateFrame\n}\n\n/** 影像校验结论 */\nexport interface ImageValidation {\n status: 'pending' | 'valid' | 'warning' | 'invalid'\n messages: string[]\n}\n\n/**\n * 取景**俯仰**覆盖 —— 让取景相机临时偏离\"云台当前俯仰\"的常态。\n *\n * ⚠️ 这里**故意没有水平偏航**:水平视野覆盖不能靠云台转。DJI Mini 4 Pro 的云台 Pan\n * 机械行程只有 ±30°(本库 `DRONE_SPEC.gimbalYawRange` 按 ±5° 建模),\n * 靠云台转不出 180° 全景 —— 真机 180°/360° 是**机体原地自转**补水平、云台只做俯仰分层。\n * 水平网格因此走 `VirtualCaptureProvider.spinBodyTo`(转机身),而不是在这里塞一个偏航角。\n *\n * 与库内\"稳像 = 模型不动、相机侧解耦\"是同一套思路 —— 不动飞控、不动用户视角,\n * 只改这一张的取景俯仰,因此取景帧、拍照成片与记录的位姿天然同源。\n */\nexport interface CaptureViewOverride {\n /**\n * 俯仰(度):**替代**云台俯仰 —— 即该张照片相对水平面的绝对俯仰角。\n * 不给则沿用云台当前俯仰。全景网格给的是绝对值,否则用户把云台压到 -90° 后全景会全朝地面。\n */\n pitch?: number\n}\n\n/** 影像在一次全景扫描里的定位(写进 `metadata.panorama`) */\nexport interface PanoramaShotRef {\n /** 全景组 id:同一次扫描共用一个,拼接时按它聚合 */\n group: string\n /** 全景模式标识(`pano-wide` / `pano-180` / `pano-360`);这里用 string 以免形成循环依赖 */\n mode: string\n /** 组内序号(1 起) */\n index: number\n /** 组内总张数 */\n total: number\n /** 网格行列(0 起):row = 俯仰层, col = 机身水平方位 */\n row: number\n col: number\n /** 该张的**机身航向偏移**(度,相对本组起始航向;正值 = 向右/顺时针) */\n bodyYaw: number\n /** 该张的取景俯仰(度,绝对) */\n pitch: number\n}\n\n/** 虚拟采集的附加参数 */\nexport interface VirtualCaptureOptions {\n /** 本次取景的俯仰覆盖(用完自动复位;见 CaptureViewOverride) */\n view?: CaptureViewOverride | null\n /** 本次取景属于哪个全景组 */\n panorama?: PanoramaShotRef\n}\n\n/** 统一影像条目(虚拟采集与真实导入共用) */\nexport interface CapturedImage {\n id: string\n source: ImageSource\n fileName: string\n mimeType: string\n sizeBytes: number\n /** ISO 8601;虚拟采集是快门时刻,真实照片优先取 EXIF 拍摄时间 */\n capturedAt: string\n /** 预览地址:虚拟 = PNG data URL,真实 = Blob URL(仅浏览器内有效) */\n imageUrl?: string\n /** 只用于浏览器内短期数据流,**不得写入服务端数据库**(见模块头) */\n localBlob?: Blob\n cameraIntrinsics?: CameraIntrinsics\n cameraPose?: CameraPose\n metadata: {\n latitude?: number\n longitude?: number\n altitude?: number\n cameraModel?: string\n sceneId?: string\n sceneVersion?: string\n dronePoseId?: string\n gimbalRotation?: [number, number, number, number]\n gpsAccuracyMeters?: number\n rtkFixed?: boolean\n /** 虚拟影像的产出后端(three / cesium),便于区分同页两套后端的采集结果 */\n engine?: string\n /** 全景扫描定位(属于某次 180°/360° 拍摄时才有) */\n panorama?: PanoramaShotRef\n /** 真实照片的原始 EXIF(保留原始字段,避免二次解析丢信息) */\n exif?: ExifData\n }\n validation: ImageValidation\n}\n\n/** 采集会话参数 */\nexport interface CaptureSessionOptions {\n source: ImageSource\n name: string\n /** 自动采集的时间间隔(毫秒);0 / 缺省 = 不按时间触发 */\n captureIntervalMs?: number\n /** 自动采集的距离间隔(米);0 / 缺省 = 不按距离触发 */\n distanceIntervalMeters?: number\n includeCameraMetadata?: boolean\n /** 虚拟场景标识(便于回溯「这批影像出自哪个版本的场景」) */\n sceneId?: string\n sceneVersion?: string\n /** 地理参考原点:给了它,局部 ENU 坐标同时能换算成经纬度 */\n georeference?: GeoreferenceOrigin\n}\n\n/** 局部坐标 → 经纬高的原点(局部 ENU 的 (0,0,0) 落在这里) */\nexport interface GeoreferenceOrigin {\n longitude: number\n latitude: number\n altitude?: number\n}\n\n/** 采集会话状态 */\nexport type CaptureSessionStatus = 'created' | 'capturing' | 'paused' | 'finished' | 'failed'\n\n/** 采集会话 */\nexport interface CaptureSession {\n id: string\n options: CaptureSessionOptions\n status: CaptureSessionStatus\n images: CapturedImage[]\n createdAt: string\n updatedAt: string\n /** 产出后端(虚拟采集时由 provider 提供) */\n engine?: string\n /** 自动采集是否进行中 */\n autoCapturing: boolean\n}\n\n/**\n * 取景成片的结构契约。\n *\n * 故意只声明结构、不从 drone-fly 引类型:这样 `drone-capture` 不依赖任何渲染模块\n * (drone-fly / drone-cesium 反过来 `import type` 本模块,依赖方向单向,无循环)。\n */\nexport interface VirtualShot {\n dataUrl: string\n width: number\n height: number\n /** 可选的同一渲染帧相机快照。渲染后端应优先提供它。 */\n camera?: CaptureCameraState\n}\n\n/**\n * 虚拟取景相机状态 —— 由渲染后端提供(见 `VirtualCaptureProvider`)。\n * 注意这必须是**取景相机**(云台镜头)而不是机身:两者朝向可能完全不同。\n */\nexport interface CaptureCameraState {\n pose: CameraPose\n intrinsics: CameraIntrinsics\n /** 成片画幅(像素) */\n frame: { width: number; height: number }\n /** 场景标识 */\n scene?: { id?: string; version?: string }\n /** 机体真值(诊断 / 复现用) */\n drone?: {\n position: [number, number, number]\n heading: number\n tiltPitch: number\n tiltRoll: number\n gimbalPitch: number\n gimbalYaw: number\n }\n /** 相机的地理位置(地理坐标系后端提供;纯局部场景没有这个值) */\n geo?: { longitude: number; latitude: number; altitude: number }\n}\n\n/**\n * 宿主渲染后端向采集服务暴露的能力。\n *\n * `DroneFly`(three)与 `CesiumDroneAdapter` 都天然满足:`requestPhoto()` 是既有的拍照入口,\n * `getCaptureCamera()` 是为采集新增的只读快照。**采集服务不创建第二个 Viewer** ——\n * 无论虚拟影像来自哪套后端,取景帧与相机状态都只有这一个来源。\n */\nexport interface VirtualCaptureProvider {\n /** 后端标识,写进影像元数据 */\n readonly engine?: string\n /** 取当前云台取景的一帧(与拍照完全同一条路径) */\n requestPhoto(): Promise<VirtualShot | null>\n /** 取景相机的位姿与内参快照;模型 / 云台未就绪时返回 null */\n getCaptureCamera(): CaptureCameraState | null\n /**\n * 设置取景**俯仰**覆盖(全景分层用);传 `null` 复位。\n *\n * 可选方法:只做单张采集的后端可以不实现 —— 此时全景扫描仍会按预设逐张请求,\n * 只是每张都沿用云台当前俯仰(采不到俯仰网格)。\n */\n setCaptureView?(override: CaptureViewOverride | null): void\n /**\n * 让**机体原地自转**到绝对方位角 `headingDeg`(度,罗盘角);`null` 停止自转。\n *\n * 全景扫描的**水平**覆盖只有这条路可走:云台 Pan 机械行程只有 ±30°,\n * 转不出 180°(见 `CaptureViewOverride` 的说明)。后端只负责下达目标,\n * \"转到位了没\"由调用方用 `getBodyHeading()` 比对 —— 这样各后端不必各自实现等待逻辑。\n */\n spinBodyTo?(headingDeg: number | null): void\n /** 机体当前方位角(度,罗盘角);没实现的后端返回 undefined */\n getBodyHeading?(): number\n}\n\n/** 会话汇总(面板与导出共用) */\nexport interface CaptureSessionSummary {\n total: number\n valid: number\n warning: number\n invalid: number\n pending: number\n /** 真实照片里带 GPS 的张数 */\n withGps: number\n /** 虚拟影像里带完整相机位姿的张数 */\n withPose: number\n}\n\n/** 导出格式 */\nexport type CaptureExportFormat = 'json' | 'csv' | 'manifest'\n\n/** 会话导出文件标识 */\nexport const CAPTURE_EXPORT_FORMAT = 'drone-camera/capture-session'\n/** 导出结构版本 */\nexport const CAPTURE_EXPORT_VERSION = 1\n\n/** 自动采集的判定间隔(毫秒):定时与定距两个触发条件都在这条心跳上评估 */\nconst AUTO_CAPTURE_TICK_MS = 200\n\n/** 逐张影像的校验规则 */\nexport function validateCapturedImage(image: CapturedImage): ImageValidation {\n const messages: string[] = []\n let status: ImageValidation['status'] = 'valid'\n\n const warn = (message: string): void => {\n messages.push(message)\n if (status === 'valid') status = 'warning'\n }\n const note = (message: string): void => {\n messages.push(message)\n }\n const fail = (message: string): void => {\n messages.push(message)\n status = 'invalid'\n }\n\n if (!image.mimeType.startsWith('image/')) fail(`不是图像类型(${image.mimeType || '未知'})`)\n if (image.sizeBytes <= 0) fail('文件为空')\n // 虚拟影像没有位姿 = 相机参数链断了,只能当普通截图看,必须警告;\n // 真实照片没有位姿是常态(相机外参要靠 SfM 解算),不算退化\n if (!image.cameraPose && image.source === 'virtual-camera') {\n warn('缺少相机位姿(无法参与 SfM 相机解算)')\n }\n if (!image.cameraIntrinsics || image.cameraIntrinsics.calibrationSource === 'unknown') {\n warn('相机内参未知(SfM 需自行估计,精度受限)')\n }\n if (image.source === 'real-camera') {\n if (image.metadata.latitude === undefined || image.metadata.longitude === undefined) {\n warn('缺少 GPS 经纬度(无法地理配准)')\n }\n if (!image.metadata.cameraModel) warn('缺少相机型号信息')\n if (!image.metadata.exif) warn('未读到 EXIF 元数据')\n } else if (!image.metadata.sceneId) {\n // 只影响成果与场景版本的可回溯性,不影响重建质量 —— 记为提示而不是警告\n note('未记录场景标识(成果无法与特定场景版本对应)')\n }\n const { width, height } = image.cameraIntrinsics ?? { width: 0, height: 0 }\n if (width <= 0 || height <= 0) fail('图像尺寸无效')\n if (width > 0 && height > 0 && Math.min(width, height) < 200) {\n warn(`图像过小(${width}×${height}),对重建几乎没有贡献`)\n }\n\n return { status, messages }\n}\n\n/** 会话汇总 */\nexport function summarizeSession(session: CaptureSession): CaptureSessionSummary {\n const summary: CaptureSessionSummary = {\n total: session.images.length,\n valid: 0,\n warning: 0,\n invalid: 0,\n pending: 0,\n withGps: 0,\n withPose: 0,\n }\n for (const image of session.images) {\n summary[image.validation.status] += 1\n if (image.metadata.latitude !== undefined && image.metadata.longitude !== undefined) {\n summary.withGps += 1\n }\n if (image.cameraPose) summary.withPose += 1\n }\n return summary\n}\n\n/**\n * 由 EXIF 推相机内参。\n *\n * 换算依据是 **35mm 等效焦距**:全画幅成像面宽 36mm,故水平视场角\n * `fovX = 2·atan(36 / (2·f35))`,再按像素宽度反推 `fx = width / (2·tan(fovX/2))`。\n * 像素为正方形,所以 `fx = fy`;主点按像面中心。\n *\n * 只有实际焦距而没有 35mm 等效值时无法反推传感器尺寸,此时返回 `unknown`\n * —— 宁可让用户看到「内参未知」,也不要编一个假内参把误差带进重建。\n */\nexport function intrinsicsFromExif(\n exif: ExifData | null | undefined,\n width: number,\n height: number,\n): CameraIntrinsics {\n const base: CameraIntrinsics = { width, height, calibrationSource: 'unknown' }\n const focal35 = exif?.focalLength35mm\n if (!focal35 || focal35 <= 0 || width <= 0 || height <= 0) return base\n const fovX = 2 * Math.atan(36 / (2 * focal35))\n const fx = width / (2 * Math.tan(fovX / 2))\n const fy = fx\n const fovY = 2 * Math.atan(height / (2 * fy))\n return {\n width,\n height,\n fx,\n fy,\n cx: width / 2,\n cy: height / 2,\n horizontalFovDeg: (fovX * 180) / Math.PI,\n verticalFovDeg: (fovY * 180) / Math.PI,\n calibrationSource: 'exif',\n }\n}\n\n/** `CaptureService` 构造选项 */\nexport interface CaptureServiceOptions {\n /** 虚拟取景帧来源(渲染后端) */\n provider?: VirtualCaptureProvider | null\n /** 时间源,便于测试注入 */\n now?: () => Date\n /** id 生成器,便于测试注入 */\n createId?: () => string\n}\n\nfunction defaultId(): string {\n return `img-${Math.random().toString(36).slice(2, 10)}`\n}\n\nfunction sessionId(): string {\n return `cap-${Math.random().toString(36).slice(2, 10)}`\n}\n\n/** 简易距离(米) */\nfunction distance3(a: [number, number, number], b: [number, number, number]): number {\n return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2])\n}\n\n/**\n * 影像采集服务:管理采集会话、把两类影像归一成 `CapturedImage`、并支持导出。\n *\n * ```ts\n * const capture = new CaptureService({ provider: fly })\n * const session = capture.createSession({ source: 'virtual-camera', name: '厂区绕飞' })\n * await capture.captureVirtualImage(session.id) // 手动拍一张\n * capture.startAutoCapture(session.id, { intervalMs: 2000 }) // 或按时间自动连拍\n * capture.importRealImages(session.id, files) // 真实照片导入\n * const csv = capture.exportSession(session.id, 'csv')\n * ```\n */\nexport class CaptureService {\n private readonly sessions = new Map<string, CaptureSession>()\n private readonly listeners = new Set<(session: CaptureSession) => void>()\n private readonly now: () => Date\n private readonly createId: () => string\n private provider: VirtualCaptureProvider | null\n /** 自动采集句柄:按会话各存一份 */\n private readonly auto = new Map<\n string,\n {\n timer: ReturnType<typeof setInterval>\n intervalMs: number\n distanceMeters: number\n lastShotAt: number\n lastPosition: [number, number, number] | null\n busy: boolean\n }\n >()\n\n /** 构造采集服务:管理拍摄会话与自动采集(每会话独立相册) */\n constructor(options: CaptureServiceOptions = {}) {\n this.provider = options.provider ?? null\n this.now = options.now ?? (() => new Date())\n this.createId = options.createId ?? defaultId\n }\n\n /** 换绑定后端(机体切换 / 引擎重建后重新挂) */\n setProvider(provider: VirtualCaptureProvider | null): void {\n this.provider = provider\n }\n\n /** 读取当前取景相机内参(未就绪或后端未提供时返回 null)。 */\n getCaptureIntrinsics(): CameraIntrinsics | null {\n const intrinsics = this.provider?.getCaptureCamera()?.intrinsics\n return intrinsics ? { ...intrinsics } : null\n }\n\n /** 将浏览器会话转换为可交给离线重建任务的 CaptureManifest。 */\n buildManifest(sessionId: string, options: CaptureManifestOptions = {}): CaptureManifest | null {\n const session = this.sessions.get(sessionId)\n return session ? buildCaptureManifest(session, options) : null\n }\n\n /** 导出文档约定的 manifest.json 文本。影像二进制仍由宿主按 file/imageUrl 落盘。 */\n exportManifest(sessionId: string, options: CaptureManifestOptions = {}, space = 2): string {\n const manifest = this.buildManifest(sessionId, options)\n return manifest ? serializeCaptureManifest(manifest, space) : ''\n }\n\n /** 在提交离线重建前运行 Manifest 质量门禁。 */\n validateSession(\n sessionId: string,\n options: CaptureManifestOptions & CaptureValidatorOptions = {},\n ): CaptureValidationReport | null {\n const manifest = this.buildManifest(sessionId, options)\n return manifest ? validateCaptureManifest(manifest, options) : null\n }\n\n // ───────────────────────────── 会话 ─────────────────────────────\n\n /** 新建拍摄会话(指定覆盖俯仰/张数等,返回会话) */\n createSession(options: CaptureSessionOptions): CaptureSession {\n const timestamp = this.now().toISOString()\n const session: CaptureSession = {\n id: sessionId(),\n options: { includeCameraMetadata: true, ...options },\n status: 'created',\n images: [],\n createdAt: timestamp,\n updatedAt: timestamp,\n engine: this.provider?.engine,\n autoCapturing: false,\n }\n this.sessions.set(session.id, session)\n this.emit(session)\n return session\n }\n\n /** 按 id 取会话(不存在返回 null) */\n getSession(id: string): CaptureSession | null {\n return this.sessions.get(id) ?? null\n }\n\n /** 列全部会话(按创建时间) */\n listSessions(): CaptureSession[] {\n return [...this.sessions.values()]\n }\n\n /** 当前会话(最近创建 / 最近操作的那个);面板默认展示它 */\n get activeSession(): CaptureSession | null {\n let latest: CaptureSession | null = null\n for (const session of this.sessions.values()) {\n if (!latest || session.updatedAt >= latest.updatedAt) latest = session\n }\n return latest\n }\n\n /** 暂停会话的自动采集 */\n pauseSession(id: string): void {\n const session = this.sessions.get(id)\n if (!session) return\n this.stopAutoCapture(id)\n session.status = 'paused'\n this.touch(session)\n }\n\n /** 续跑被暂停的会话 */\n resumeSession(id: string): void {\n const session = this.sessions.get(id)\n if (!session) return\n session.status = 'capturing'\n this.touch(session)\n }\n\n /** 结束会话(停止采集,照片保留;返回被结束的会话) */\n finishSession(id: string): CaptureSession | null {\n const session = this.sessions.get(id)\n if (!session) return null\n this.stopAutoCapture(id)\n session.status = 'finished'\n this.touch(session)\n return session\n }\n\n /** 删除会话及其全部照片 */\n removeSession(id: string): void {\n this.stopAutoCapture(id)\n this.sessions.delete(id)\n }\n\n /** 移除单张影像(无效照片的常规处置) */\n removeImage(sessionId: string, imageId: string): boolean {\n const session = this.sessions.get(sessionId)\n if (!session) return false\n const index = session.images.findIndex((image) => image.id === imageId)\n if (index < 0) return false\n const [removed] = session.images.splice(index, 1)\n if (removed?.localBlob && removed.imageUrl?.startsWith('blob:')) {\n // 回收 Blob URL,否则整张图会被浏览器缓存一直持有\n try {\n URL.revokeObjectURL(removed.imageUrl)\n } catch {\n /* 非浏览器环境忽略 */\n }\n }\n this.touch(session)\n return true\n }\n\n /** 清空会话内全部照片(会话保留) */\n clearImages(sessionId: string): void {\n const session = this.sessions.get(sessionId)\n if (!session) return\n for (const image of session.images) {\n if (image.imageUrl?.startsWith('blob:')) {\n try {\n URL.revokeObjectURL(image.imageUrl)\n } catch {\n /* ignore */\n }\n }\n }\n session.images = []\n this.touch(session)\n }\n\n /** 订阅会话变更(面板用);返回退订函数 */\n subscribe(listener: (session: CaptureSession) => void): () => void {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n // ─────────────────────── 虚拟影像采集 ───────────────────────\n\n /**\n * 采集一张虚拟影像。\n *\n * 顺序有讲究:先 `requestPhoto()` 拿到成片,再读相机状态 —— 因为渲染后端是**按帧**推进的,\n * 成片用的是「取景那一帧」的机体真值;等 promise 落地时仿真还没走到下一帧,此刻读到的\n * 位姿正好就是那张照片的位姿(反过来先读就会差一帧)。\n *\n * `options.view` 给的是**本次取景**的俯仰覆盖(全景分层用):设置 → 拍照 → 读位姿 →\n * 无条件复位,全程在同一个 try/finally 里,调用方不可能忘记还原。\n */\n async captureVirtualImage(\n sessionId: string,\n options: VirtualCaptureOptions = {},\n ): Promise<CapturedImage | null> {\n const session = this.sessions.get(sessionId)\n if (!session || session.status === 'finished' || session.status === 'failed') return null\n const provider = this.provider\n if (!provider) return null\n\n const view = options.view ?? null\n const setView = provider.setCaptureView?.bind(provider)\n if (view && setView) setView(view)\n try {\n const shot = await provider.requestPhoto()\n if (!shot) return null\n // 成片到手后再读相机状态(那一刻仍是取景帧的仿真状态,含本次的俯仰覆盖)\n // 渲染后端若锁存了同帧状态,优先使用它,避免 await 后读取到下一帧。\n const camera = shot.camera ?? provider.getCaptureCamera()\n const image = this.buildVirtualImage(session, shot, camera, options.panorama)\n session.images.push(image)\n if (session.status === 'created') session.status = 'capturing'\n this.touch(session)\n return image\n } finally {\n if (view && setView) setView(null)\n }\n }\n\n // ─────────────────────── 机体自转(全景水平网格) ───────────────────────\n\n /** 后端是否支持\"机体原地自转\" —— 全景的水平覆盖只有它做得出来 */\n get canSpinBody(): boolean {\n return typeof this.provider?.spinBodyTo === 'function'\n }\n\n /** 机体当前方位角(度,罗盘角);后端不支持时返回 null */\n getBodyHeading(): number | null {\n const heading = this.provider?.getBodyHeading?.()\n return typeof heading === 'number' && Number.isFinite(heading) ? heading : null\n }\n\n /** 让机体原地自转到绝对方位角 `headingDeg`;`null` 停止自转(后端不支持则什么都不做) */\n spinBodyTo(headingDeg: number | null): void {\n this.provider?.spinBodyTo?.(headingDeg)\n }\n\n private buildVirtualImage(\n session: CaptureSession,\n shot: VirtualShot,\n camera: CaptureCameraState | null,\n panorama?: PanoramaShotRef,\n ): CapturedImage {\n const timestamp = this.now().toISOString()\n const { width, height } = { width: shot.width, height: shot.height }\n const intrinsics: CameraIntrinsics | undefined = camera\n ? { ...camera.intrinsics, width, height }\n : { width, height, calibrationSource: 'unknown' }\n // 局部 ENU + 地理原点 → 顺带落一份经纬度,与真实照片的 GPS 字段同形,便于统一查看\n const geo =\n camera?.geo ??\n (camera && camera.pose.coordinateFrame === 'local-enu' && session.options.georeference\n ? enuToGeodetic(camera.pose.position, session.options.georeference)\n : undefined)\n\n const image: CapturedImage = {\n id: this.createId(),\n source: 'virtual-camera',\n fileName: `virtual-${timestamp.replace(/[:.]/g, '-')}.png`,\n mimeType: 'image/png',\n sizeBytes: dataUrlBytes(shot.dataUrl),\n capturedAt: timestamp,\n imageUrl: shot.dataUrl,\n cameraIntrinsics: intrinsics,\n cameraPose: camera?.pose,\n metadata: {\n latitude: geo?.latitude,\n longitude: geo?.longitude,\n altitude: geo?.altitude,\n cameraModel: '虚拟相机(场景渲染)',\n sceneId: camera?.scene?.id ?? session.options.sceneId,\n sceneVersion: camera?.scene?.version ?? session.options.sceneVersion,\n engine: camera ? this.provider?.engine : undefined,\n panorama,\n },\n validation: { status: 'pending', messages: [] },\n }\n image.validation = validateCapturedImage(image)\n return image\n }\n\n /**\n * 开始自动采集:在一条 200ms 的心跳上同时评估**时间**与**距离**两个触发条件,\n * 谁先满足就先拍一张,并同时刷新两个基准 —— 避免时间与距离各自计时导致的重复连拍。\n */\n startAutoCapture(\n sessionId: string,\n options: { intervalMs?: number; distanceMeters?: number } = {},\n ): boolean {\n const session = this.sessions.get(sessionId)\n if (!session) return false\n const intervalMs = Math.max(0, options.intervalMs ?? session.options.captureIntervalMs ?? 0)\n const distanceMeters = Math.max(\n 0,\n options.distanceMeters ?? session.options.distanceIntervalMeters ?? 0,\n )\n if (intervalMs <= 0 && distanceMeters <= 0) return false\n this.stopAutoCapture(sessionId)\n session.options.captureIntervalMs = intervalMs\n session.options.distanceIntervalMeters = distanceMeters\n\n const state = {\n timer: setInterval(() => void this.tickAuto(sessionId), AUTO_CAPTURE_TICK_MS),\n intervalMs,\n distanceMeters,\n lastShotAt: 0,\n lastPosition: null as [number, number, number] | null,\n busy: false,\n }\n this.auto.set(sessionId, state)\n session.autoCapturing = true\n if (session.status === 'created' || session.status === 'paused') session.status = 'capturing'\n this.touch(session)\n // 立刻拍第一张:否则「开始」到第一张之间会空等一个间隔\n void this.tickAuto(sessionId, true)\n return true\n }\n\n private async tickAuto(sessionId: string, force = false): Promise<void> {\n const session = this.sessions.get(sessionId)\n const state = this.auto.get(sessionId)\n if (!session || !state || state.busy) return\n const position = this.provider?.getCaptureCamera()?.pose.position ?? null\n const elapsed = this.now().getTime() - state.lastShotAt\n const moved =\n position && state.lastPosition ? distance3(position, state.lastPosition) : Infinity\n const dueByTime = state.intervalMs > 0 && elapsed >= state.intervalMs\n const dueByDistance = state.distanceMeters > 0 && moved >= state.distanceMeters\n if (!force && !dueByTime && !dueByDistance) return\n\n state.busy = true\n try {\n const image = await this.captureVirtualImage(sessionId)\n if (!image) return\n state.lastShotAt = this.now().getTime()\n const after = this.provider?.getCaptureCamera()?.pose.position ?? position\n state.lastPosition = after ? [...after] : null\n } finally {\n state.busy = false\n }\n }\n\n /** 停止会话的自动采集(会话与照片都保留) */\n stopAutoCapture(sessionId: string): void {\n const state = this.auto.get(sessionId)\n if (!state) return\n clearInterval(state.timer)\n this.auto.delete(sessionId)\n const session = this.sessions.get(sessionId)\n if (session) {\n session.autoCapturing = false\n this.touch(session)\n }\n }\n\n /** 会话是否正在自动采集(定时按间隔拍照) */\n isAutoCapturing(sessionId: string): boolean {\n return this.auto.has(sessionId)\n }\n\n // ─────────────────────── 真实照片导入 ───────────────────────\n\n /**\n * 导入真实照片。**尽力保留原始信息**:EXIF 全读,读不到就明确标记缺失,\n * 但绝不因此拒绝文件 —— 「有没有元数据」本身是需要展示的信息(见 validateCapturedImage)。\n */\n async importRealImages(sessionId: string, files: File[] | FileList): Promise<CapturedImage[]> {\n const session = this.sessions.get(sessionId)\n if (!session) return []\n const list = Array.from(files)\n const imported: CapturedImage[] = []\n for (const file of list) {\n const image = await this.buildRealImage(file)\n session.images.push(image)\n imported.push(image)\n }\n if (session.status === 'created') session.status = 'capturing'\n this.touch(session)\n return imported\n }\n\n private async buildRealImage(file: File): Promise<CapturedImage> {\n const exif = await readJpegExif(file)\n const isJpeg = /jpe?g$/i.test(file.type) || /\\.jpe?g$/i.test(file.name)\n const dimensions = await decodeDimensions(file, exif)\n const intrinsics = intrinsicsFromExif(exif, dimensions.width, dimensions.height)\n const capturedAt = exifTimeToIso(exif?.dateTimeOriginal) ?? this.now().toISOString()\n const gps = exif?.gps\n const hasGps = gps?.latitude !== undefined && gps?.longitude !== undefined\n\n const image: CapturedImage = {\n id: this.createId(),\n source: 'real-camera',\n fileName: file.name,\n mimeType: file.type || (isJpeg ? 'image/jpeg' : ''),\n sizeBytes: file.size,\n capturedAt,\n localBlob: file,\n cameraIntrinsics: intrinsics,\n // 位姿只有「位置」有来源(GPS),**没有朝向** —— 真实无人机照片的相机姿态要靠 SfM 解算,\n // 所以这里只填位置,rotation 留空由后续流程补。用 local-enu? 不:GPS 是地理坐标,\n // 直接记在 metadata 里,pose 留空以免给出一个含义不明的坐标系。\n metadata: {\n latitude: hasGps ? gps?.latitude : undefined,\n longitude: hasGps ? gps?.longitude : undefined,\n altitude: gps?.altitude,\n cameraModel: [exif?.make, exif?.model].filter(Boolean).join(' ') || undefined,\n exif: exif ?? undefined,\n rtkFixed: undefined,\n },\n validation: { status: 'pending', messages: [] },\n }\n // Blob URL 只在浏览器里有意义(预览用)\n if (typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function') {\n image.imageUrl = URL.createObjectURL(file)\n }\n image.validation = validateCapturedImage(image)\n return image\n }\n\n // ───────────────────────── 导出 ─────────────────────────\n\n /** 导出会话清单为 JSON(完整存档)或 CSV(表格式,可直接进 Excel) */\n exportSession(sessionId: string, format: CaptureExportFormat = 'json'): string {\n const session = this.sessions.get(sessionId)\n if (!session) return format === 'csv' ? '' : '{}'\n if (format === 'manifest') return this.exportManifest(sessionId)\n return format === 'csv' ? this.toCsv(session) : this.toJson(session)\n }\n\n private toJson(session: CaptureSession): string {\n return JSON.stringify(\n {\n format: CAPTURE_EXPORT_FORMAT,\n version: CAPTURE_EXPORT_VERSION,\n exportedAt: this.now().toISOString(),\n // 坐标系说明写进文件:换个工具打开也不至于误读那三个数(与航线导出的做法一致)\n frame: {\n virtual: 'local-enu: 原点 = 起飞点(返航点),x 东 / y 北 / z 上,单位米',\n real: 'metadata.longitude / latitude 为 WGS84 十进制度,altitude 为 GPS 海拔(米)',\n quaternion: '[x, y, z, w]',\n },\n session: {\n ...session,\n // 二进制 / 会话内对象不进导出\n images: undefined,\n },\n summary: summarizeSession(session),\n images: session.images.map((image) => this.toExportImage(image)),\n },\n null,\n 2,\n )\n }\n\n /** 导出用的影像视图:剥掉 Blob / data URL(它们只对当前浏览器会话有意义) */\n private toExportImage(image: CapturedImage): Omit<CapturedImage, 'localBlob' | 'imageUrl'> {\n const { localBlob: _blob, imageUrl: _url, ...rest } = image\n return rest\n }\n\n private toCsv(session: CaptureSession): string {\n const escape = (value: string | number | undefined): string => {\n if (value === undefined || value === null) return ''\n const text = String(value)\n return /[\",\\r\\n]/.test(text) ? `\"${text.replace(/\"/g, '\"\"')}\"` : text\n }\n const header = [\n '序号',\n '来源',\n '引擎',\n '文件名',\n '采集时间',\n '图像宽',\n '图像高',\n 'fx',\n 'fy',\n '水平视场角(度)',\n '内参来源',\n '坐标系',\n '相机X',\n '相机Y',\n '相机Z',\n '四元数x',\n '四元数y',\n '四元数z',\n '四元数w',\n '经度',\n '纬度',\n '高度(m)',\n '相机型号',\n '场景',\n '全景组',\n '全景模式',\n '全景序号',\n '机身偏航(度)',\n '取景俯仰(度)',\n '校验',\n '备注',\n ]\n const rows = session.images.map((image, index) => {\n const pose = image.cameraPose\n const intrinsics = image.cameraIntrinsics\n return [\n index + 1,\n image.source,\n image.metadata.engine,\n image.fileName,\n image.capturedAt,\n intrinsics?.width,\n intrinsics?.height,\n intrinsics?.fx === undefined ? undefined : Number(intrinsics.fx.toFixed(3)),\n intrinsics?.fy === undefined ? undefined : Number(intrinsics.fy.toFixed(3)),\n intrinsics?.horizontalFovDeg === undefined\n ? undefined\n : Number(intrinsics.horizontalFovDeg.toFixed(3)),\n intrinsics?.calibrationSource,\n pose?.coordinateFrame,\n pose?.position[0],\n pose?.position[1],\n pose?.position[2],\n pose?.rotation[0],\n pose?.rotation[1],\n pose?.rotation[2],\n pose?.rotation[3],\n image.metadata.longitude,\n image.metadata.latitude,\n image.metadata.altitude,\n image.metadata.cameraModel,\n image.metadata.sceneId,\n image.metadata.panorama?.group,\n image.metadata.panorama?.mode,\n image.metadata.panorama\n ? `${image.metadata.panorama.index}/${image.metadata.panorama.total}`\n : undefined,\n image.metadata.panorama?.bodyYaw,\n image.metadata.panorama?.pitch,\n image.validation.status,\n image.validation.messages.join(' / '),\n ].map(escape)\n })\n return [header.join(','), ...rows.map((row) => row.join(','))].join('\\r\\n')\n }\n\n // ───────────────────────── 内部 ─────────────────────────\n\n private touch(session: CaptureSession): void {\n session.updatedAt = this.now().toISOString()\n this.emit(session)\n }\n\n private emit(session: CaptureSession): void {\n for (const listener of this.listeners) listener(session)\n }\n}\n\n/** data URL 的近似字节数(导出 / 统计用,不做 base64 严格换算) */\nfunction dataUrlBytes(dataUrl: string): number {\n const comma = dataUrl.indexOf(',')\n if (comma < 0) return 0\n const payload = dataUrl.length - comma - 1\n return Math.round((payload * 3) / 4)\n}\n\n/** EXIF 本地时间(无时区)按原样当作本地时间转 ISO,避免凭空造出一个 UTC 偏移 */\nfunction exifTimeToIso(value: string | undefined): string | undefined {\n if (!value) return undefined\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? undefined : date.toISOString()\n}\n\n/** 取图像真实像素尺寸:优先浏览器解码(覆盖 PNG / HEIC 等无 EXIF 的格式),退化到 EXIF 记录值 */\nasync function decodeDimensions(\n file: Blob,\n exif: ExifData | null,\n): Promise<{ width: number; height: number }> {\n const fallback = { width: exif?.pixelXDimension ?? 0, height: exif?.pixelYDimension ?? 0 }\n const createBitmap = (\n globalThis as unknown as { createImageBitmap?: (blob: Blob) => Promise<ImageBitmap> }\n ).createImageBitmap\n if (typeof createBitmap !== 'function') return fallback\n try {\n const bitmap = await createBitmap(file)\n const size = { width: bitmap.width, height: bitmap.height }\n if (typeof (bitmap as unknown as { close?: () => void }).close === 'function') {\n ;(bitmap as unknown as { close: () => void }).close()\n }\n return size.width && size.height ? size : fallback\n } catch {\n return fallback\n }\n}\n\n/**\n * 局部 ENU → 经纬高(小范围近似)。\n *\n * 用等距圆柱近似:1° 纬度 ≈ 111320m,1° 经度 ≈ 111320·cos(lat)。在无人机作业的\n * 几百米~几公里尺度上误差可忽略;真要测绘级精度必须走 ECEF 严格换算 + 椭球参数,\n * 那是后台配准环节的事,不在前端做。\n */\nfunction enuToGeodetic(\n position: [number, number, number],\n origin: GeoreferenceOrigin,\n): { longitude: number; latitude: number; altitude: number } {\n const metersPerDegLat = 111320\n const metersPerDegLon = metersPerDegLat * Math.cos((origin.latitude * Math.PI) / 180)\n return {\n longitude: origin.longitude + (metersPerDegLon > 0 ? position[0] / metersPerDegLon : 0),\n latitude: origin.latitude + position[1] / metersPerDegLat,\n altitude: (origin.altitude ?? 0) + position[2],\n }\n}\n","/**\n * 影像展示工具(采集面板与弹窗查看器共用)。\n *\n * 抽出来的原因很实在:同一张影像的\"元数据文案\"会出现在两处 —— 面板选中后的详情区、\n * 弹窗查看器的信息条。两套实现早晚会漂移(改了一处忘了另一处),所以统一放在这里;\n * 组件文件保持\"只写自己的 DOM 逻辑\"。\n *\n * 这里只放**无状态、无副作用**(除了 triggerDownload 的下载动作)的小函数,不引任何依赖。\n */\nimport type { CapturedImage } from './drone-capture'\n\n/**\n * 极简 HTML 转义。\n *\n * 所有插进 innerHTML 的外部文本(文件名、校验消息、EXIF 字段……)都必须过这一道 ——\n * 真实照片的文件名完全由用户提供。\n */\nexport function escapeHtml(text: string): string {\n const map: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n }\n return text.replace(/[&<>\"']/g, (ch) => map[ch] ?? ch)\n}\n\n/** 人类可读的字节数 */\nexport function formatBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes <= 0) return '–'\n if (bytes < 1024) return `${bytes}B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`\n return `${(bytes / 1024 / 1024).toFixed(1)}MB`\n}\n\n/** 触发浏览器下载(真实照片的 Blob URL 与导出的 data URL 共用) */\nexport function triggerDownload(url: string, filename: string): void {\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = filename\n anchor.rel = 'noopener'\n document.body.appendChild(anchor)\n anchor.click()\n anchor.remove()\n}\n\n/** 将会话里的原始照片打包为不压缩 ZIP,避免重复压缩 JPEG/PNG 并控制额外内存占用。 */\nexport async function createOriginalImagesZip(images: CapturedImage[]): Promise<Blob> {\n if (images.length === 0) throw new Error('当前会话没有可打包的图片')\n if (images.length > 0xffff) throw new RangeError('图片数量超过 ZIP 格式支持的上限')\n\n const encoder = new TextEncoder()\n const parts: BlobPart[] = []\n const centralDirectory: Uint8Array<ArrayBuffer>[] = []\n const usedNames = new Set<string>()\n let offset = 0\n const now = new Date()\n const dosTime =\n (now.getHours() << 11) | (now.getMinutes() << 5) | Math.floor(now.getSeconds() / 2)\n const dosDate =\n ((Math.max(1980, now.getFullYear()) - 1980) << 9) |\n ((now.getMonth() + 1) << 5) |\n now.getDate()\n\n for (const [index, image] of images.entries()) {\n let source: Blob\n if (image.localBlob) {\n source = image.localBlob\n } else if (image.imageUrl) {\n const response = await fetch(image.imageUrl)\n if (!response.ok) throw new Error(`读取图片失败(${image.fileName}):HTTP ${response.status}`)\n source = await response.blob()\n } else {\n throw new Error(`图片缺少原始数据:${image.fileName}`)\n }\n\n const baseName = safeArchiveFileName(image.fileName)\n let fileName = baseName\n if (usedNames.has(fileName)) {\n const dot = baseName.lastIndexOf('.')\n const stem = dot > 0 ? baseName.slice(0, dot) : baseName\n const extension = dot > 0 ? baseName.slice(dot) : ''\n fileName = `${stem}-${String(index + 1).padStart(3, '0')}${extension}`\n while (usedNames.has(fileName)) {\n fileName = `${stem}-${String(index + 1).padStart(3, '0')}-${usedNames.size}${extension}`\n }\n }\n usedNames.add(fileName)\n const nameBytes = encoder.encode(fileName)\n if (nameBytes.length > 0xffff) throw new RangeError(`图片文件名过长:${image.fileName}`)\n if (source.size > 0xffffffff) throw new RangeError(`图片超过 ZIP 单文件大小上限:${image.fileName}`)\n const crc = await blobCrc32(source)\n\n const localHeader = new Uint8Array(new ArrayBuffer(30 + nameBytes.length))\n const localView = new DataView(localHeader.buffer)\n localView.setUint32(0, 0x04034b50, true)\n localView.setUint16(4, 20, true)\n localView.setUint16(6, 0x0800, true)\n localView.setUint16(8, 0, true)\n localView.setUint16(10, dosTime, true)\n localView.setUint16(12, dosDate, true)\n localView.setUint32(14, crc, true)\n localView.setUint32(18, source.size, true)\n localView.setUint32(22, source.size, true)\n localView.setUint16(26, nameBytes.length, true)\n localHeader.set(nameBytes, 30)\n parts.push(localHeader, source)\n\n const centralHeader = new Uint8Array(new ArrayBuffer(46 + nameBytes.length))\n const centralView = new DataView(centralHeader.buffer)\n centralView.setUint32(0, 0x02014b50, true)\n centralView.setUint16(4, 20, true)\n centralView.setUint16(6, 20, true)\n centralView.setUint16(8, 0x0800, true)\n centralView.setUint16(10, 0, true)\n centralView.setUint16(12, dosTime, true)\n centralView.setUint16(14, dosDate, true)\n centralView.setUint32(16, crc, true)\n centralView.setUint32(20, source.size, true)\n centralView.setUint32(24, source.size, true)\n centralView.setUint16(28, nameBytes.length, true)\n centralView.setUint32(42, offset, true)\n centralHeader.set(nameBytes, 46)\n centralDirectory.push(centralHeader)\n\n offset += localHeader.length + source.size\n if (offset > 0xffffffff) throw new RangeError('图片总大小超过 ZIP 格式支持的上限')\n }\n\n const directorySize = centralDirectory.reduce((size, entry) => size + entry.length, 0)\n if (directorySize > 0xffffffff || offset + directorySize > 0xffffffff) {\n throw new RangeError('图片总大小超过 ZIP 格式支持的上限')\n }\n const endRecord = new Uint8Array(new ArrayBuffer(22))\n const endView = new DataView(endRecord.buffer)\n endView.setUint32(0, 0x06054b50, true)\n endView.setUint16(8, images.length, true)\n endView.setUint16(10, images.length, true)\n endView.setUint32(12, directorySize, true)\n endView.setUint32(16, offset, true)\n for (const entry of centralDirectory) parts.push(entry)\n parts.push(endRecord)\n return new Blob(parts, { type: 'application/zip' })\n}\n\nfunction safeArchiveFileName(fileName: string): string {\n const safe = fileName\n .replace(/[\\\\/:*?\"<>|\\u0000-\\u001f]/g, '_')\n .replace(/^\\.+/, '')\n .trim()\n return safe || 'image'\n}\n\nasync function blobCrc32(blob: Blob): Promise<number> {\n const table = new Uint32Array(256)\n for (let index = 0; index < table.length; index += 1) {\n let value = index\n for (let bit = 0; bit < 8; bit += 1) {\n value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1\n }\n table[index] = value >>> 0\n }\n\n let crc = 0xffffffff\n const bytes = new Uint8Array(await blob.arrayBuffer())\n for (const byte of bytes) crc = table[(crc ^ byte) & 0xff]! ^ (crc >>> 8)\n return (crc ^ 0xffffffff) >>> 0\n}\n\n/** 校验状态对应的中文标签 */\nexport const VALIDATION_LABELS: Record<CapturedImage['validation']['status'], string> = {\n valid: '有效',\n warning: '警告',\n invalid: '无效',\n pending: '待检',\n}\n\n/** 缩略图角标字符 */\nexport const VALIDATION_BADGES: Record<CapturedImage['validation']['status'], string> = {\n valid: '✓',\n warning: '!',\n invalid: '×',\n pending: '·',\n}\n\n/** 全景模式的中文名(不引 drone-panorama,免得文案模块反向依赖拍摄模块) */\nexport function panoramaModeLabel(mode: string): string {\n if (mode === 'pano-wide') return '广角'\n if (mode === 'pano-180') return '180° 全景'\n if (mode === 'pano-360') return '360° 全景'\n return mode\n}\n\n/**\n * 一组同源影像(同一次全景扫描,或\"未分组的单张\")。\n *\n * 「组合」在图集里是**各自成块**展示的:广角、180° 与球形 360° 扫描不应混在同一组里\n * 混在一堆缩略图里 —— 既看不出哪几张是一组,也没法对某一组单独合成。\n */\nexport interface ImageGroup {\n /** 分组键:全景组用 `metadata.panorama.group`,单张桶用固定值 */\n key: string\n kind: 'panorama' | 'single'\n /** 展示名,如「180° 全景」/「单张影像」 */\n label: string\n /** 全景模式标识(`pano-wide` / `pano-180` / `pano-360`);单张组为 undefined */\n mode?: string\n /** 组内影像(已按组内序号排序) */\n images: CapturedImage[]\n /** 组内影像在会话里的原始序号(1 起,与 `images` 一一对应) */\n indexes: number[]\n /** 该组是否已拍满(张数达到预设总张数);单张组恒为 false */\n complete: boolean\n /** 预设总张数(全景组才有) */\n total?: number\n}\n\n/** 未分组影像的桶键 */\nexport const UNGROUPED_KEY = '__ungrouped__'\n\n/**\n * 把会话里的影像按「组合」切开 —— 全景组按 `metadata.panorama.group` 聚合(保持首次出现顺序),\n * 其余归入一个「单张影像」桶并排在最后。\n *\n * 顺序刻意保持\"首次出现\"而不是按组名排序:索引用的是 `index`/`session.images` 的下标,\n * 换个顺序就会让面板里显示的 `#n` 与图集里的不一致。\n */\nexport function groupSessionImages(images: CapturedImage[]): ImageGroup[] {\n const order: string[] = []\n const groups = new Map<string, ImageGroup>()\n const singles: ImageGroup = {\n key: UNGROUPED_KEY,\n kind: 'single',\n label: '单张影像',\n images: [],\n indexes: [],\n complete: false,\n }\n\n // 先把 (影像, 原始序号) 配对收进来:组内要按\"组内序号\"重排,而 `indexes` 报的是\n // 会话里的原始序号(与 `session.images` 的下标一致,面板里的 `#n` 才对得上)\n const pairs = new Map<string, Array<{ image: CapturedImage; sequence: number }>>()\n images.forEach((image, index) => {\n const pano = image.metadata.panorama\n const key = pano?.group\n if (!key) {\n singles.images.push(image)\n singles.indexes.push(index + 1)\n return\n }\n let bucket = pairs.get(key)\n if (!bucket) {\n bucket = []\n pairs.set(key, bucket)\n order.push(key)\n }\n bucket.push({ image, sequence: index + 1 })\n })\n\n const result: ImageGroup[] = []\n for (const key of order) {\n const bucket = pairs.get(key)\n if (!bucket?.length) continue\n const first = bucket[0]\n const pano = first?.image.metadata.panorama\n if (!pano) continue\n bucket.sort(\n (a, b) => (a.image.metadata.panorama?.index ?? 0) - (b.image.metadata.panorama?.index ?? 0),\n )\n const total = pano.total ?? bucket.length\n result.push({\n key,\n kind: 'panorama',\n mode: pano.mode,\n label: panoramaModeLabel(pano.mode),\n images: bucket.map((entry) => entry.image),\n indexes: bucket.map((entry) => entry.sequence),\n total,\n complete: total > 0 && bucket.length >= total,\n })\n }\n if (singles.images.length) result.push(singles)\n return result\n}\n\n/** 缩略图右上角的定位标签(全景影像显示\"第几方位 / 第几层\",其余显示序号) */\nexport function imageTileLabel(image: CapturedImage, sequence: number): string {\n const pano = image.metadata.panorama\n if (!pano) return `#${sequence}`\n return `#${sequence} · ${pano.bodyYaw}°/${pano.pitch}°`\n}\n\n/** 单张影像的元数据详情文本(面板详情区 / 弹窗查看器共用;含 `<b>` 等受控标签) */\nexport function describeImage(image: CapturedImage, index: number): string {\n const pose = image.cameraPose\n const intrinsics = image.cameraIntrinsics\n const lines: string[] = []\n lines.push(`<b>#${index + 1} ${escapeHtml(image.fileName)}</b>`)\n lines.push(\n `来源 ${image.source} · ${escapeHtml(image.mimeType || '未知类型')} · ${formatBytes(image.sizeBytes)} · ${escapeHtml(image.capturedAt)}`,\n )\n if (intrinsics) {\n const digits = (value: number | undefined): string =>\n value === undefined ? '–' : value.toFixed(2)\n lines.push(\n `内参 [${intrinsics.calibrationSource}] ${intrinsics.width}×${intrinsics.height} · fx ${digits(intrinsics.fx)} / fy ${digits(intrinsics.fy)}` +\n (intrinsics.horizontalFovDeg === undefined\n ? ''\n : ` · 水平 FOV ${intrinsics.horizontalFovDeg.toFixed(2)}°`),\n )\n }\n if (pose) {\n const round = (value: number): string => value.toFixed(3)\n lines.push(\n `位姿 [${pose.coordinateFrame}] 位置 (${pose.position.map(round).join(', ')}) ` +\n `四元数 (${pose.rotation.map((v) => v.toFixed(4)).join(', ')})`,\n )\n } else {\n lines.push('位姿 – (无相机位置/朝向记录)')\n }\n const meta = image.metadata\n const geo =\n meta.longitude !== undefined && meta.latitude !== undefined\n ? `${meta.longitude.toFixed(7)}, ${meta.latitude.toFixed(7)}` +\n (meta.altitude === undefined ? '' : ` @ ${meta.altitude.toFixed(1)}m`)\n : '无'\n lines.push(`经纬高 ${geo}`)\n if (meta.cameraModel) lines.push(`相机 ${escapeHtml(meta.cameraModel)}`)\n if (meta.sceneId) lines.push(`场景 ${escapeHtml(meta.sceneId)}`)\n if (meta.engine) lines.push(`产出后端 ${escapeHtml(meta.engine)}`)\n const pano = meta.panorama\n if (pano) {\n lines.push(\n `<b>全景</b> ${escapeHtml(pano.mode)} · 组 ${escapeHtml(pano.group)} · 第 ${pano.index}/${pano.total} 张` +\n ` · 网格 第 ${pano.col + 1} 方位 / 第 ${pano.row + 1} 层` +\n ` · 机身偏航 ${pano.bodyYaw}° / 取景俯仰 ${pano.pitch}°`,\n )\n }\n lines.push(\n `<b>校验</b> ${VALIDATION_LABELS[image.validation.status]}` +\n (image.validation.messages.length\n ? `\\n<span class=\"is-warn\">${image.validation.messages.map(escapeHtml).join('\\n')}</span>`\n : ''),\n )\n return lines.join('\\n')\n}\n","/**\n * 影像弹窗查看器(原生 DOM):点缩略图后铺开看大图 —— 大图 / 元数据 / 上一张下一张 / 下载。\n *\n * 与 drone-panel / mission-panel / drone-capture-panel 同一套范式:只用 DOM + CSS,\n * 不引任何 UI 框架,库整体保持与框架无关。\n *\n * 两处刻意的设计:\n *\n * 1. **挂 `document.body`,绝不做面板的后代**。面板带 `backdrop-filter`,`filter` 一类的属性\n * 会为 `position: fixed` 的后代创建新的包含块 —— 弹窗会被裁在面板里、跑不动。\n * (同一坑在面板的悬浮摇杆层上踩过一次:`inset:0` 的悬浮层必须是面板的**兄弟节点**。)\n * 另外挂 body 还有个附带好处:面板为了不打断输入框,采集入库时会重绘缩略图 DOM,\n * 弹窗不在那棵子树里,重绘碰不到它。\n *\n * 2. **键盘在 window 的捕获阶段接管**。控制面板的键盘飞行监听挂在 window 的**冒泡阶段**,\n * 两边真同时监听的话,弹窗里按 ←/→ 翻页会顺带让无人机偏航、Esc 也拦不住任何东西。\n * 所以在捕获阶段就把事件截住(`stopPropagation`),并且**只在弹窗打开时**挂着监听 ——\n * 关闭即摘除,不影响页面其它键盘交互。Ctrl / Cmd / Alt 组合键一律放行,\n * 宿主快捷键与面板的 Ctrl+K 拍照照常工作。\n */\nimport type { CaptureService, CapturedImage, CaptureSession } from './drone-capture'\nimport { describeImage, triggerDownload } from './drone-image-utils'\n\n/** 弹窗查看器构造选项 */\nexport interface DroneImageViewerOptions {\n /** 是否注入自带样式,默认 true */\n injectStyles?: boolean\n /** 弹窗层级,默认 1000(库内面板最高 21,留足余量) */\n zIndex?: number\n /** 是否提供「下载原图」按钮,默认 true */\n downloadable?: boolean\n /** 关闭回调(点 × / 点空白 / Esc 都会触发) */\n onClose?: () => void\n /** 翻页回调 —— 便于宿主把外部状态(如面板选中态)跟着同步 */\n onNavigate?: (image: CapturedImage, index: number) => void\n}\n\nconst STYLE_ID = 'drone-image-viewer-styles'\nconst DEFAULT_Z_INDEX = 1000\n/** 触屏滑动翻页的最小水平位移(px) */\nconst SWIPE_MIN_PX = 48\n\nconst VIEWER_CSS = `\n.dview {\n --dview-text: #e9eff9;\n --dview-muted: rgba(233, 239, 249, 0.58);\n --dview-border: rgba(255, 255, 255, 0.12);\n font: 13px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n user-select: none;\n -webkit-user-select: none;\n -webkit-tap-highlight-color: transparent;\n}\n.dview {\n position: fixed; inset: 0; z-index: ${DEFAULT_Z_INDEX};\n display: flex; align-items: center; justify-content: center;\n padding: max(10px, env(safe-area-inset-top)) 10px max(10px, env(safe-area-inset-bottom));\n color: var(--dview-text);\n outline: none;\n}\n.dview[hidden] { display: none; }\n.dview-mask {\n position: absolute; inset: 0; background: rgba(6, 9, 14, 0.86);\n -webkit-backdrop-filter: blur(7px); backdrop-filter: blur(7px);\n}\n.dview-frame {\n position: relative; z-index: 1;\n display: flex; flex-direction: column;\n width: min(100%, 1280px); height: 100%; overflow: hidden;\n /* height 必须给定:大图是 absolute(不占流内高度),若 frame 高度由内容撑起,\n stage 的 flex:1 会分到 0 高,整块取景区塌掉 */\n border: 1px solid var(--dview-border); border-radius: 12px;\n background: rgba(15, 19, 27, 0.94);\n box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);\n}\n.dview-bar {\n display: flex; align-items: center; gap: 8px; flex: 0 0 auto;\n padding: 8px 10px; border-bottom: 1px solid var(--dview-border);\n}\n.dview-idx { font-weight: 600; font-variant-numeric: tabular-nums; }\n.dview[data-status=\"warning\"] .dview-idx { color: #f5a623; }\n.dview[data-status=\"invalid\"] .dview-idx { color: #ff5f5f; }\n.dview-name {\n flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n color: var(--dview-muted);\n}\n.dview-icon {\n flex: 0 0 auto; width: 26px; height: 26px; padding: 0; cursor: pointer;\n border: 1px solid var(--dview-border); border-radius: 7px;\n background: rgba(255, 255, 255, 0.06); color: var(--dview-text);\n font: inherit; line-height: 1;\n}\n.dview-icon:hover { background: rgba(255, 255, 255, 0.14); }\n.dview-icon[hidden] { display: none; }\n.dview-stage {\n position: relative; flex: 1; min-height: 0;\n display: flex; align-items: center; justify-content: center;\n /* 不裁剪的话,大图会按固有比例溢出 stage、顶栏直接被图盖住 */\n overflow: hidden; background: #05070b; touch-action: pan-y;\n}\n.dview-img {\n /* 绝对定位 + margin auto:最可靠的\"适应窗口\"写法,不依赖 flex 高度的百分比解析 */\n position: absolute; inset: 0; margin: auto;\n max-width: 100%; max-height: 100%; display: block; object-fit: contain;\n}\n.dview-img[hidden] { display: none; }\n.dview-empty { margin: 0; padding: 20px; color: var(--dview-muted); text-align: center; }\n.dview-empty[hidden] { display: none; }\n.dview-nav {\n position: absolute; top: 50%; transform: translateY(-50%);\n width: 38px; height: 58px; padding: 0; cursor: pointer;\n border: 1px solid var(--dview-border); border-radius: 10px;\n background: rgba(10, 14, 20, 0.6); color: var(--dview-text);\n font: 22px/1 inherit;\n}\n.dview-nav:hover { background: rgba(53, 167, 255, 0.28); }\n.dview-nav[hidden] { display: none; }\n.dview-prev { left: 8px; }\n.dview-next { right: 8px; }\n.dview-meta {\n flex: 0 0 auto; max-height: 26vh; overflow-y: auto; overscroll-behavior: contain;\n padding: 8px 10px; border-top: 1px solid var(--dview-border);\n font-size: 11px; line-height: 1.6; color: var(--dview-muted);\n white-space: pre-wrap; word-break: break-all;\n}\n.dview-meta b { color: var(--dview-text); font-weight: 600; }\n.dview-meta .is-warn { color: #f5a623; }\n.dview-hint {\n flex: 0 0 auto; padding: 5px 10px; border-top: 1px solid var(--dview-border);\n color: var(--dview-muted); font-size: 10px;\n}\n@media (max-height: 520px) {\n .dview-meta { max-height: 38vh; font-size: 10px; }\n .dview-nav { height: 44px; }\n}\n@media (pointer: coarse) {\n .dview-icon { width: 32px; height: 32px; }\n .dview-nav { width: 44px; }\n}\n`\n\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return\n if (document.getElementById(STYLE_ID)) return\n const style = document.createElement('style')\n style.id = STYLE_ID\n style.textContent = VIEWER_CSS\n document.head.appendChild(style)\n}\n\ninterface ViewerRefs {\n mask: HTMLElement\n stage: HTMLElement\n img: HTMLImageElement\n empty: HTMLElement\n idx: HTMLElement\n name: HTMLElement\n meta: HTMLElement\n prev: HTMLButtonElement\n next: HTMLButtonElement\n download: HTMLButtonElement\n}\n\n/**\n * 影像弹窗查看器。\n *\n * ```ts\n * const viewer = new DroneImageViewer(service)\n * viewer.open(image.id) // 打开(第二参不给则用当前会话)\n * viewer.step(1) // 下一张(循环)\n * viewer.close() // 关闭;destroy() 彻底卸载\n * ```\n *\n * DOM 是**懒挂载**的:构造只记住 service 与选项,第一次 `open()` 才建节点、注入样式、\n * 订阅服务与键盘;`close()` 则整个拆掉(而不是藏起来)—— 于是\"弹窗关着\"时\n * 它对页面零影响,键盘监听也不在。\n */\nexport class DroneImageViewer {\n private readonly service: CaptureService\n private readonly options: DroneImageViewerOptions\n private root: HTMLDivElement | null = null\n private refs: ViewerRefs | null = null\n private unsubscribe: (() => void) | null = null\n private sessionId: string | null = null\n private imageId: string | null = null\n /** 上一次渲染的图片下标:当前图被移除时用它顶到相邻位置,而不是跳回第一张 */\n private lastIndex = 0\n private bodyOverflow = ''\n private swipeStart: { x: number; y: number; pointerId: number } | null = null\n /** 滑动翻页后浏览器仍会补一个 click,用它在 onClick 里吃掉那一次 */\n private suppressClick = false\n private destroyed = false\n\n /** 构造大图查看弹窗(双击/点遮罩/Esc 关闭) */\n constructor(service: CaptureService, options: DroneImageViewerOptions = {}) {\n this.service = service\n this.options = options\n }\n\n /** 弹窗当前是否打开 */\n get isOpen(): boolean {\n return this.root !== null && !this.root.hidden\n }\n\n /** 当前显示的那张影像(未打开 / 已失效时返回 null) */\n get current(): CapturedImage | null {\n const images = this.resolveSession()?.images ?? []\n if (!this.imageId) return null\n return images.find((image) => image.id === this.imageId) ?? null\n }\n\n /** 打开弹窗;`sessionId` 缺省用采集服务里当前活跃的会话 */\n open(imageId: string, sessionId?: string): void {\n if (this.destroyed) return\n this.sessionId = sessionId ?? this.service.activeSession?.id ?? null\n this.imageId = imageId\n this.mount()\n if (!this.root || !this.refs) return\n this.root.hidden = false\n this.lockScroll()\n this.render()\n this.root.focus()\n }\n\n /** 关闭弹窗(并整个卸载 DOM / 摘掉键盘监听) */\n close(): void {\n if (!this.root) return\n const wasOpen = !this.root.hidden\n this.unmount()\n if (wasOpen) this.options.onClose?.()\n }\n\n /** 翻页:正值向后(下一张),负值向前;越界循环 */\n step(delta: number): void {\n const images = this.resolveSession()?.images ?? []\n if (images.length < 2 || !this.imageId) return\n const index = images.findIndex((image) => image.id === this.imageId)\n if (index < 0) return\n const next = (index + delta + images.length) % images.length\n const target = images[next]\n if (!target) return\n this.imageId = target.id\n this.render()\n }\n\n /** 跳到指定下标(负值 = 最后一张) */\n jump(index: number): void {\n const images = this.resolveSession()?.images ?? []\n if (!images.length) return\n const target = images[index < 0 ? images.length - 1 : Math.min(index, images.length - 1)]\n if (!target) return\n this.imageId = target.id\n this.render()\n }\n\n /** 下载当前显示的原图 */\n download(): void {\n const image = this.current\n if (!image?.imageUrl) return\n triggerDownload(image.imageUrl, image.fileName || `capture-${this.lastIndex + 1}.png`)\n }\n\n /** 关闭并移除弹窗 */\n destroy(): void {\n if (this.destroyed) return\n this.destroyed = true\n this.unmount()\n if (this.options.injectStyles !== false) document.getElementById(STYLE_ID)?.remove()\n }\n\n // ───────────────────────────── 内部 ─────────────────────────────\n\n private resolveSession(): CaptureSession | null {\n if (this.sessionId) {\n const session = this.service.getSession(this.sessionId)\n if (session) return session\n }\n return this.service.activeSession\n }\n\n private mount(): void {\n if (this.root || typeof document === 'undefined') return\n if (this.options.injectStyles !== false) ensureStyles()\n\n const root = document.createElement('div')\n root.className = 'dview'\n root.hidden = true\n root.tabIndex = -1\n root.setAttribute('role', 'dialog')\n root.setAttribute('aria-modal', 'true')\n root.setAttribute('aria-label', '影像查看')\n if (this.options.zIndex && this.options.zIndex !== DEFAULT_Z_INDEX) {\n root.style.zIndex = String(this.options.zIndex)\n }\n root.innerHTML = this.template()\n document.body.appendChild(root)\n\n this.root = root\n this.refs = {\n mask: this.query('[data-role=\"mask\"]'),\n stage: this.query('[data-role=\"stage\"]'),\n img: this.query<HTMLImageElement>('[data-role=\"img\"]'),\n empty: this.query('[data-role=\"empty\"]'),\n idx: this.query('[data-role=\"idx\"]'),\n name: this.query('[data-role=\"name\"]'),\n meta: this.query('[data-role=\"meta\"]'),\n prev: this.query<HTMLButtonElement>('[data-action=\"prev\"]'),\n next: this.query<HTMLButtonElement>('[data-action=\"next\"]'),\n download: this.query<HTMLButtonElement>('[data-action=\"download\"]'),\n }\n\n root.addEventListener('click', this.onClick)\n this.refs.stage.addEventListener('pointerdown', this.onPointerDown)\n this.refs.stage.addEventListener('pointerup', this.onPointerUp)\n this.refs.stage.addEventListener('pointercancel', this.onPointerCancel)\n this.refs.img.addEventListener('error', this.onImageError)\n // 捕获阶段接管键盘,才能压住面板挂在冒泡阶段的键盘飞行(见模块头注释 2)\n window.addEventListener('keydown', this.onKeyDown, { capture: true })\n // 影像被移除 / 清空时同步:当前这张没了就顶到相邻位置,一张不剩就关掉\n this.unsubscribe = this.service.subscribe(() => {\n if (this.root && !this.root.hidden) this.render()\n })\n }\n\n private unmount(): void {\n window.removeEventListener('keydown', this.onKeyDown, { capture: true })\n this.unsubscribe?.()\n this.unsubscribe = null\n this.root?.remove()\n this.root = null\n this.refs = null\n this.swipeStart = null\n this.unlockScroll()\n }\n\n private render(): void {\n const root = this.root\n const refs = this.refs\n if (!root || !refs) return\n\n const images = this.resolveSession()?.images ?? []\n if (!images.length) {\n // 全被删光了:没什么可看,直接收起\n this.close()\n return\n }\n\n let index = this.imageId ? images.findIndex((image) => image.id === this.imageId) : -1\n if (index < 0) index = Math.max(0, Math.min(this.lastIndex, images.length - 1))\n const image = images[index]\n if (!image) {\n this.close()\n return\n }\n this.imageId = image.id\n const moved = index !== this.lastIndex\n this.lastIndex = index\n\n root.dataset.status = image.validation.status\n refs.idx.textContent = `#${index + 1} / ${images.length}`\n refs.name.textContent = image.fileName || '(未命名)'\n refs.name.title = image.fileName\n refs.meta.innerHTML = describeImage(image, index)\n\n const single = images.length < 2\n refs.prev.hidden = single\n refs.next.hidden = single\n refs.download.hidden = this.options.downloadable === false || !image.imageUrl\n\n if (image.imageUrl) {\n refs.img.hidden = false\n refs.empty.hidden = true\n // data URL 可能有几 MB,别在每次入库渲染时重复赋值把解码器打满\n if (refs.img.getAttribute('src') !== image.imageUrl) refs.img.src = image.imageUrl\n } else {\n refs.img.hidden = true\n refs.img.removeAttribute('src')\n refs.empty.hidden = false\n refs.empty.textContent = '这张影像没有可用的预览地址。'\n }\n\n if (moved) this.options.onNavigate?.(image, index)\n }\n\n private readonly onClick = (event: MouseEvent): void => {\n if (this.suppressClick) {\n this.suppressClick = false\n return\n }\n const target = event.target as HTMLElement | null\n const actionEl = target?.closest<HTMLElement>('[data-action]')\n switch (actionEl?.dataset.action) {\n case 'close':\n this.close()\n return\n case 'prev':\n this.step(-1)\n return\n case 'next':\n this.step(1)\n return\n case 'download':\n this.download()\n return\n default:\n break\n }\n // 点空白(遮罩 / 图片以外的大图区)关闭;点图片本身不关,免得想放大人被弹出去\n const refs = this.refs\n if (refs && (target === this.root || target === refs.mask || target === refs.stage)) {\n this.close()\n }\n }\n\n private readonly onPointerDown = (event: PointerEvent): void => {\n const target = event.target as HTMLElement | null\n // 按钮上按下的指针不参与滑动(否则点「下一张」也会被当成滑动起点)\n if (target?.closest('[data-action]')) return\n this.swipeStart = { x: event.clientX, y: event.clientY, pointerId: event.pointerId }\n try {\n this.refs?.stage.setPointerCapture(event.pointerId)\n } catch {\n /* 不支持 pointer capture 的环境忽略 */\n }\n }\n\n private readonly onPointerUp = (event: PointerEvent): void => {\n const start = this.swipeStart\n this.swipeStart = null\n if (!start || start.pointerId !== event.pointerId) return\n const dx = event.clientX - start.x\n const dy = event.clientY - start.y\n if (Math.abs(dx) < SWIPE_MIN_PX || Math.abs(dx) < Math.abs(dy)) return\n this.suppressClick = true\n this.step(dx < 0 ? 1 : -1)\n }\n\n private readonly onPointerCancel = (): void => {\n this.swipeStart = null\n }\n\n private readonly onImageError = (): void => {\n const refs = this.refs\n if (!refs) return\n refs.img.hidden = true\n refs.empty.hidden = false\n refs.empty.textContent = '影像地址已失效(可能已被移除或回收)。'\n }\n\n private readonly onKeyDown = (event: KeyboardEvent): void => {\n if (this.destroyed || !this.root || this.root.hidden) return\n // Ctrl / Cmd / Alt 组合键留给宿主与浏览器:面板的 Ctrl+K / Ctrl+L 拍照录像照常可用\n if (event.ctrlKey || event.metaKey || event.altKey) return\n switch (event.key) {\n case 'Escape':\n event.preventDefault()\n this.close()\n break\n case 'ArrowLeft':\n event.preventDefault()\n this.step(-1)\n break\n case 'ArrowRight':\n event.preventDefault()\n this.step(1)\n break\n case 'Home':\n event.preventDefault()\n this.jump(0)\n break\n case 'End':\n event.preventDefault()\n this.jump(-1)\n break\n default:\n break\n }\n // 独占键盘:面板的键盘飞行挂在 window 冒泡阶段,这里在捕获阶段截住,\n // 于是弹窗开着时 WASD / 方向键不会再顺带把无人机飞出去。\n event.stopPropagation()\n }\n\n private lockScroll(): void {\n if (typeof document === 'undefined') return\n this.bodyOverflow = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n }\n\n private unlockScroll(): void {\n if (typeof document === 'undefined') return\n document.body.style.overflow = this.bodyOverflow\n }\n\n private query<T extends HTMLElement = HTMLElement>(selector: string): T {\n const root = this.root\n if (!root) throw new Error('[DroneImageViewer] 弹窗尚未挂载')\n const el = root.querySelector<T>(selector)\n if (!el) throw new Error(`[DroneImageViewer] 缺少节点:${selector}`)\n return el\n }\n\n private template(): string {\n return `\n <div class=\"dview-mask\" data-role=\"mask\" data-action=\"close\"></div>\n <div class=\"dview-frame\">\n <div class=\"dview-bar\">\n <span class=\"dview-idx\" data-role=\"idx\">–</span>\n <span class=\"dview-name\" data-role=\"name\">–</span>\n <button type=\"button\" class=\"dview-icon\" data-action=\"download\" title=\"下载原图\" aria-label=\"下载原图\">↓</button>\n <button type=\"button\" class=\"dview-icon\" data-action=\"close\" title=\"关闭 (Esc)\" aria-label=\"关闭\">×</button>\n </div>\n <div class=\"dview-stage\" data-role=\"stage\">\n <img class=\"dview-img\" data-role=\"img\" alt=\"\">\n <p class=\"dview-empty\" data-role=\"empty\" hidden></p>\n <button type=\"button\" class=\"dview-nav dview-prev\" data-action=\"prev\" title=\"上一张 (←)\" aria-label=\"上一张\">‹</button>\n <button type=\"button\" class=\"dview-nav dview-next\" data-action=\"next\" title=\"下一张 (→)\" aria-label=\"下一张\">›</button>\n </div>\n <div class=\"dview-meta\" data-role=\"meta\"></div>\n <div class=\"dview-hint\">← / → 翻页 · Esc 关闭 · 触屏可左右滑动</div>\n </div>\n `\n }\n}\n","/**\n * 全景合成 —— 库内自带的默认 `PanoramaStitcher` 实现:**等距柱状(equirectangular)投影拼接**。\n *\n * ── 为什么库内做得了、而且不引任何依赖 ──────────────────────────────────\n *\n * 因为这条件是把「**这一张朝哪儿**」记全了:全景组每张影像都带\n * `metadata.panorama` —— 相对组起始航向的 `bodyYaw`(方位角)与绝对 `pitch`(仰角),\n * 再加上 `cameraIntrinsics` 的视场角,每一张的取景锥体就**完全确定**了。于是可以直接\n * 按已知位姿反投影逐像素采样,不需要特征匹配、不需要 SfM、不需要 OpenCV/WASM。\n * 真机全景拼接最难的一步(估位姿)在这条采集链路上是免费的 —— 这正是\"位姿已知\"的红利。\n *\n * ⚠️ **故意不读 `cameraPose.rotation`**。两个渲染后端的四元数各用各的局部轴约定\n * (three 的相机朝局部 -z,Cesium 的 HPR 帧朝局部 +x),拿它反投影等于把某个后端的\n * 内部约定搬进拼接器 —— 换个后端就错位。而 `bodyYaw` / `pitch` 是**语义化**的\n * (方位角 / 仰角,与后端无关),本模块按定义重建取景锥体,两端通吃。\n *\n * ⚠️ 只解算**旋转**、不解算平移:全景的前提是\"相机绕光心转\"。真机靠定点悬停做到,\n * 本库的扫描也是**机体原地自转**(见 drone-panorama 模块头),所以这一近似与采集方式自洽。\n * 机体重心与云台光心之间那点平移(厘米级)会带来轻微视差,拼接预览可接受;\n * 需要严格无差的重建输入,应该把原始 21/自适应球形全景照片交给宿主自己的光束法平差流程。\n *\n * ── 输出 ────────────────────────────────────────────────────────────\n *\n * `pano-360` 默认输出**整颗球的标准等距柱状图**:360°×180°(宽高比 2:1),\n * 没拍到的天顶 / 天底 / 方位缺口按 `fill` 补上。广角与 180° 默认只输出实际视场带,\n * 并把经纬范围写进结果供环视器正确反投影;180° 的方位范围裁在 [-90°, +90°]。\n * 若想保留\"只输出实际覆盖带\"的旧行为,传 `projection: 'band'`。\n *\n * 重叠区按\"离画面中心越远权重越低\"做羽化加权平均,避免接缝处的硬边;\n * 返回的 `coverage` 是**补边之前**的真实覆盖像素占比,便于宿主判断这组拍全了没有。\n *\n * ```ts\n * const stitcher = createEquirectStitcher({ pixelsPerDegree: 10 })\n * stitcher.onProgress = (done, total) => console.log(`${done}/${total}`)\n * const result = await stitcher.stitch(group) // { url, width, height, coverage }\n * ```\n */\nimport type { CapturedImage } from './drone-capture'\nimport type { PanoramaGroup, PanoramaStitchResult, PanoramaStitcher } from './drone-panorama'\n\nconst DEG = Math.PI / 180\nconst RAD = 180 / Math.PI\n\n/** 输出分辨率(每度多少像素)缺省值:整球 360°×180° → 3600×1800 */\nconst DEFAULT_PIXELS_PER_DEGREE = 10\nconst DEFAULT_MAX_WIDTH = 4096\nconst DEFAULT_MAX_HEIGHT = 2048\n/** 羽化带宽(占半幅比例):0.22 = 从 78% 处开始向边缘衰减 */\nconst DEFAULT_FEATHER = 0.22\n/**\n * 羽化到边缘时留一点权重下限,而不是衰减到 0。\n * 全景最外圈的像素只被一张影像覆盖,权重为 0 会让那一圈直接透明掉(看着像被啃了一口);\n * 加权平均里权重只用于**相对**比较,单张覆盖处颜色与权重无关,所以留个下限是无损的。\n */\nconst MIN_FEATHER_WEIGHT = 0.05\n/** 内参缺失时的视场角兜底:水平 ±30° / 垂直 ±20° */\nconst FALLBACK_TAN_H = Math.tan(30 * DEG)\nconst FALLBACK_TAN_V = Math.tan(20 * DEG)\n\n/**\n * 整颗球的标准等距柱状范围:方位 -180°~+180°(组起始航向 = 0° 落在画面正中)、\n * 仰角 +90°(天顶)~ -90°(天底)。宽高比恒为 2:1 —— DJI 全景的规格。\n */\nconst SPHERE_BAND: CoverageBand = { thetaStart: -180, spanTheta: 360, phiTop: 90, spanPhi: 180 }\n\n/** 输出投影方式 */\nexport type EquirectProjection = 'sphere' | 'band'\n\n/** 球面输出时未覆盖区域的填充方式 */\nexport type EquirectFill = 'edge' | 'none'\n\n/** `EquirectStitcher` 构造选项 */\nexport interface EquirectStitcherOptions {\n /**\n * 输出分辨率(每度像素数),默认 10。整球输出即 3600×1800。\n * 越大越清晰、越慢(耗时与输出像素数 × 影像数成正比)。\n */\n pixelsPerDegree?: number\n /** 输出宽度上限(像素),默认 4096;超出按比例整体缩小 */\n maxWidth?: number\n /** 输出高度上限(像素),默认 2048 */\n maxHeight?: number\n /**\n * 输出投影,默认按全景模式自动选择(`pano-360` 用 sphere,广角/180° 用 band):\n * - `'sphere'`:整颗球的标准等距柱状图(360°×180°、2:1 长条,与 DJI 全景同规格),\n * 没拍到的天顶 / 天底 / 方位缺口按 `fill` 补上,输出是规整矩形;\n * - `'band'`:只输出局部覆盖带(未覆盖像素按 `fill` 策略处理),并将其范围写入结果。\n */\n projection?: EquirectProjection\n /**\n * 球面输出时未覆盖区域的填充方式,默认 `'edge'`:\n * - `'edge'`:**先横向后纵向**补 —— 每行先横向接续(两侧月牙被同行最近的天空/地面\n * 内容接上),再向天顶 / 天底铺该列的边缘颜色,与真机球面全景的观感一致;\n * 填完输出完全不透明。细节见 `fillEdges` 注释;\n * - `'none'`:保持透明(需 PNG)。\n */\n fill?: EquirectFill\n /** 羽化带宽(占半幅比例,0~0.9),默认 0.22 */\n feather?: number\n /** 未覆盖区域的底色;不给 = 保持透明(需要 PNG) */\n background?: string | null\n /** 输出 MIME,默认 `image/png`(透明角落需要 PNG);用 `image/jpeg` 时请同时给 `background` */\n mimeType?: string\n /** JPEG 质量(0~1),默认 0.9 */\n quality?: number\n}\n\n/** 一张影像在\"全景参考系\"里的取景几何 */\ninterface ShotGeometry {\n image: CapturedImage\n /** 取景中心方位角(度,相对本组起始航向;正值 = 向右) */\n theta: number\n /** 取景中心仰角(度,绝对) */\n phi: number\n /** 半视场角的正切(水平 / 垂直) */\n tanH: number\n tanV: number\n /**\n * 相机在全景参考系里的三个基向量:**右 / 上 / 前**。\n * 前 = 该张的取景方向,右 = 前 × 上(右手系,与\"方位角增大向右\"一致)。\n */\n right: [number, number, number]\n up: [number, number, number]\n forward: [number, number, number]\n /** 该张在方位 / 仰角上的覆盖半宽(度,由四角实测,不是 tan 近似) */\n halfTheta: number\n halfPhi: number\n}\n\n/** 输出图对应的方位/仰角范围 */\ninterface CoverageBand {\n /** 最左列的方位角(度,可越出 ±180:组内某张可能在 -131°) */\n thetaStart: number\n /** 方位跨度(度,≤ 360) */\n spanTheta: number\n /** 最上一行的仰角(度) */\n phiTop: number\n /** 仰角跨度(度) */\n spanPhi: number\n}\n\n/** 一次拼接的运行期状态(逐张累加时复用) */\ninterface StitchTarget {\n width: number\n height: number\n thetaStart: number\n spanTheta: number\n /** 带是否首尾相接(整圈):只有整圈才允许输出列 mod 回绕 */\n wrapsTheta: boolean\n phiTop: number\n spanPhi: number\n /** 每列 / 每行的方向分量表(避免逐像素调三角函数) */\n sinTheta: Float32Array\n cosTheta: Float32Array\n sinPhi: Float32Array\n cosPhi: Float32Array\n /** 颜色累加(RGB)与权重累加 */\n acc: Float32Array\n wsum: Float32Array\n /** 每个输出像素被多少张源照片直接覆盖 */\n coverageCount: Uint8Array\n feather: number\n colsPerDeg: number\n rowsPerDeg: number\n}\n\n/**\n * 库内默认的全景拼接器:按已知取景位姿把一组照片反投影成**一张等距柱状全景图**。\n *\n * 与库的其它模块一样是纯浏览器的原生实现(只用 `document.createElement('canvas')`),\n * 不引任何依赖;`stitch()` 里逐张 `await` 让出事件循环,所以能边拼边播进度。\n */\nexport class EquirectStitcher implements PanoramaStitcher {\n /** 逐张回调(已完成张数 / 总张数),供 UI 播进度;不设则不回调 */\n onProgress: ((done: number, total: number) => void) | null = null\n\n private readonly pixelsPerDegree: number\n private readonly maxWidth: number\n private readonly maxHeight: number\n private readonly projection: EquirectProjection | undefined\n private readonly fill: EquirectFill\n private readonly feather: number\n private readonly background: string | null\n private readonly mimeType: string\n private readonly quality: number\n\n /** 构造拼接器:定输出尺寸/投影(fill/band)/补边策略,可复用 */\n constructor(options: EquirectStitcherOptions = {}) {\n this.pixelsPerDegree = clampNumber(options.pixelsPerDegree, 1, 40, DEFAULT_PIXELS_PER_DEGREE)\n this.maxWidth = clampNumber(options.maxWidth, 64, 16384, DEFAULT_MAX_WIDTH)\n this.maxHeight = clampNumber(options.maxHeight, 64, 16384, DEFAULT_MAX_HEIGHT)\n this.projection = options.projection\n this.fill = options.fill === 'none' ? 'none' : 'edge'\n this.feather = clampNumber(options.feather, 0, 0.9, DEFAULT_FEATHER)\n this.background = options.background ?? null\n this.mimeType = options.mimeType ?? 'image/png'\n this.quality = clampNumber(options.quality, 0, 1, 0.9)\n }\n\n /**\n * 把一组全景照片拼成一张等距柱状全景图。\n *\n * 默认按模式输出:球形 360° 为整颗球(360°×180°、2:1),广角/180° 为局部全景带。\n * 可通过 `projection` 显式覆盖,见 `EquirectStitcherOptions`。\n * 返回 `null` 的情况:非浏览器环境、组内可用影像少于 2 张、没有 DOM canvas、\n * 或者一张都没加载成功 —— 这些都是\"没得拼\",而不是\"拼坏了\"。\n */\n async stitch(group: PanoramaGroup): Promise<PanoramaStitchResult | null> {\n if (typeof document === 'undefined') return null\n const shots = buildGeometries(group)\n this.onProgress?.(0, shots.length)\n if (shots.length < 2) return null\n\n // 球面全景输出整球;宽角/180°输出局部带,避免把没拍的背面伪装成球面。\n const projection = this.projection ?? (group.mode === 'pano-360' ? 'sphere' : 'band')\n const coverageBand = computeBand(shots)\n const band =\n projection === 'sphere'\n ? SPHERE_BAND\n : group.mode === 'pano-180' && coverageBand\n ? { ...coverageBand, thetaStart: -90, spanTheta: 180 }\n : coverageBand\n if (!band) return null\n\n // ── 输出画布尺寸:按覆盖范围 × 分辨率,再按上限整体缩放(2:1 等比不变) ──\n let width = Math.max(2, Math.round(band.spanTheta * this.pixelsPerDegree))\n let height = Math.max(2, Math.round(band.spanPhi * this.pixelsPerDegree))\n const shrink = Math.min(1, this.maxWidth / width, this.maxHeight / height)\n if (shrink < 1) {\n width = Math.max(2, Math.round(width * shrink))\n height = Math.max(2, Math.round(height * shrink))\n }\n\n const target = createTarget(band, width, height, this.feather)\n const source = document.createElement('canvas')\n const sourceCtx = source.getContext('2d', { willReadFrequently: true })\n if (!sourceCtx) return null\n\n let loaded = 0\n for (let i = 0; i < shots.length; i += 1) {\n const shot = shots[i]\n if (!shot) continue\n const image = await loadImage(shot.image.imageUrl)\n if (image) {\n const pixels = readPixels(source, sourceCtx, image)\n if (pixels) {\n accumulateShot(target, shot, pixels.data, pixels.width, pixels.height)\n loaded += 1\n }\n }\n this.onProgress?.(i + 1, shots.length)\n // 让出事件循环:进度条要能画出来,长任务别把主线程焊死\n await yieldToEventLoop()\n }\n if (!loaded) return null\n\n const composed = compose(target, this.fill)\n const output = document.createElement('canvas')\n output.width = width\n output.height = height\n const outputCtx = output.getContext('2d')\n if (!outputCtx) return null\n if (this.background) {\n // 先铺底再叠图:ImageData 里未覆盖处 alpha = 0,合成后露出底色(JPEG 需要不透明底)\n outputCtx.fillStyle = this.background\n outputCtx.fillRect(0, 0, width, height)\n const layer = document.createElement('canvas')\n layer.width = width\n layer.height = height\n layer.getContext('2d')?.putImageData(composed.image, 0, 0)\n outputCtx.drawImage(layer, 0, 0)\n } else {\n outputCtx.putImageData(composed.image, 0, 0)\n }\n\n const diagnostic = createCoverageMap(target)\n return {\n url: output.toDataURL(this.mimeType, this.quality),\n diagnosticUrl: diagnostic.url,\n diagnosticCoverage: diagnostic.coverage,\n width,\n height,\n name: `${group.id}.png`,\n coverage: composed.covered / (width * height),\n equirectangular: {\n ...band,\n wrapsTheta: target.wrapsTheta,\n },\n }\n }\n}\n\n/** 输出纯色覆盖热力图,不叠加全景照片,便于直接识别视角空洞与低重叠区。 */\nfunction createCoverageMap(target: StitchTarget): {\n url: string\n coverage: { uncovered: number; single: number; overlap: number }\n} {\n const canvas = document.createElement('canvas')\n canvas.width = target.width\n canvas.height = target.height\n const ctx = canvas.getContext('2d')\n if (!ctx) throw new Error('[EquirectStitcher] 无法创建覆盖诊断画布')\n const image = ctx.createImageData(target.width, target.height)\n let uncovered = 0\n let single = 0\n let overlap = 0\n for (let i = 0; i < target.coverageCount.length; i += 1) {\n const count = target.coverageCount[i] ?? 0\n const offset = i * 4\n if (count === 0) {\n image.data[offset] = 239\n image.data[offset + 1] = 68\n image.data[offset + 2] = 68\n uncovered += 1\n } else if (count === 1) {\n image.data[offset] = 250\n image.data[offset + 1] = 204\n image.data[offset + 2] = 21\n single += 1\n } else {\n image.data[offset] = 34\n image.data[offset + 1] = 197\n image.data[offset + 2] = 94\n overlap += 1\n }\n image.data[offset + 3] = 255\n }\n ctx.putImageData(image, 0, 0)\n const total = target.width * target.height\n return {\n url: canvas.toDataURL('image/png'),\n coverage: {\n uncovered: uncovered / total,\n single: single / total,\n overlap: overlap / total,\n },\n }\n}\n\n/** 建一个默认参数的等距柱状拼接器(库内默认注入给影像集弹窗的\"一键合成\") */\nexport function createEquirectStitcher(options: EquirectStitcherOptions = {}): EquirectStitcher {\n return new EquirectStitcher(options)\n}\n\n// ───────────────────────────── 取景几何 ─────────────────────────────\n\n/** 由全景组的影像元数据重建每张的取景锥体 */\nfunction buildGeometries(group: PanoramaGroup): ShotGeometry[] {\n const geometries: ShotGeometry[] = []\n for (const image of group.images) {\n const pano = image.metadata.panorama\n if (!pano || !image.imageUrl) continue\n const theta = pano.bodyYaw\n const phi = pano.pitch\n const { tanH, tanV } = fieldsOfView(image)\n const forward = direction(theta, phi)\n const up = upVector(theta, phi)\n const right = cross(forward, up)\n const half = angularExtent(theta, phi, right, up, forward, tanH, tanV)\n geometries.push({ image, theta, phi, tanH, tanV, right, up, forward, ...half })\n }\n // 按组内序号排:累加顺序影响不了加权平均,但这让\"覆盖带\"的推演可复现\n geometries.sort(\n (a, b) => (a.image.metadata.panorama?.index ?? 0) - (b.image.metadata.panorama?.index ?? 0),\n )\n return geometries\n}\n\n/** 取景方向单位向量:方位角 θ(0 = 组起始航向,增大向右)、仰角 φ(0 = 水平) */\nfunction direction(theta: number, phi: number): [number, number, number] {\n const t = theta * DEG\n const p = phi * DEG\n const cosP = Math.cos(p)\n return [Math.sin(t) * cosP, Math.sin(p), -Math.cos(t) * cosP]\n}\n\n/** 取景相机的\"上\"方向(∂方向/∂φ,已归一) */\nfunction upVector(theta: number, phi: number): [number, number, number] {\n const t = theta * DEG\n const p = phi * DEG\n const sinP = Math.sin(p)\n return [-Math.sin(t) * sinP, Math.cos(p), Math.cos(t) * sinP]\n}\n\nfunction cross(\n a: [number, number, number],\n b: [number, number, number],\n): [number, number, number] {\n return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]\n}\n\n/** 单张影像的视场角正切(优先用记录的水平/垂直 FOV,退化到焦距,再退化到兜底值) */\nfunction fieldsOfView(image: CapturedImage): { tanH: number; tanV: number } {\n const intrinsics = image.cameraIntrinsics\n let tanH = FALLBACK_TAN_H\n let tanV = FALLBACK_TAN_V\n if (intrinsics) {\n if (typeof intrinsics.horizontalFovDeg === 'number' && intrinsics.horizontalFovDeg > 0) {\n tanH = Math.tan((intrinsics.horizontalFovDeg / 2) * DEG)\n } else if (typeof intrinsics.fx === 'number' && intrinsics.fx > 0 && intrinsics.width > 0) {\n tanH = intrinsics.width / 2 / intrinsics.fx\n }\n if (typeof intrinsics.verticalFovDeg === 'number' && intrinsics.verticalFovDeg > 0) {\n tanV = Math.tan((intrinsics.verticalFovDeg / 2) * DEG)\n } else if (typeof intrinsics.fy === 'number' && intrinsics.fy > 0 && intrinsics.height > 0) {\n tanV = intrinsics.height / 2 / intrinsics.fy\n }\n }\n return { tanH: Math.max(1e-4, tanH), tanV: Math.max(1e-4, tanV) }\n}\n\n/**\n * 实测该张在方位 / 仰角上的覆盖半宽。\n *\n * 不拿 `atan(tanH)` 近似:那个只在仰角为 0 时成立 —— 仰角一大,同样的水平半视场\n * 对应的方位跨度会明显变宽(相机斜着看,画面边缘离中心方位更远)。\n * 逐角点实测才能保证\"覆盖矩形\"真的包住这张影像。\n */\nexport function angularExtent(\n centerTheta: number,\n centerPhi: number,\n right: [number, number, number],\n up: [number, number, number],\n forward: [number, number, number],\n tanH: number,\n tanV: number,\n): { halfTheta: number; halfPhi: number } {\n const samples: Array<[number, number]> = [\n [0, 0],\n [-tanH, -tanV],\n [tanH, -tanV],\n [-tanH, tanV],\n [tanH, tanV],\n [0, -tanV],\n [0, tanV],\n [-tanH, 0],\n [tanH, 0],\n ]\n let maxT = 0\n let halfP = 0\n for (const sample of samples) {\n const a = sample[0]\n const b = sample[1]\n const dx = a * right[0] + b * up[0] + forward[0]\n const dy = a * right[1] + b * up[1] + forward[1]\n const dz = a * right[2] + b * up[2] + forward[2]\n const len = Math.hypot(dx, dy, dz) || 1\n // 方位角相对本张中心**解回绕**再取绝对值:组内某张中心在 ±180 附近时,\n // 直接比 atan2 的原始值会得到 ~360° 的假跨度\n const theta = wrap180(Math.atan2(dx / len, -dz / len) * RAD - centerTheta)\n const phi = Math.asin(Math.max(-1, Math.min(1, dy / len))) * RAD\n if (Math.abs(theta) > maxT) maxT = Math.abs(theta)\n halfP = Math.max(halfP, Math.abs(phi - centerPhi))\n }\n return { halfTheta: Math.max(maxT, 0.5), halfPhi: Math.max(halfP, 0.5) }\n}\n\n/**\n * 求输出图要覆盖的方位 / 仰角范围。\n *\n * 方位是**环上的区间**:把每张的覆盖区间 [θ-半宽, θ+半宽] 归到圈上合并,\n * 最大的那段空隙就是\"没拍到的那一段\",覆盖带 = 空隙之外的那一整段。\n * 于是 180° 组(窄视场)自然得到\"约 240° 的一条带\"(不是整圈),360° 组得到整圈 —— 同一套算法。\n *\n * ⚠️ 端点必须取**区间并集**的边缘,不能拿\"中心相距最远的那两张\"各加半宽凑数:\n * 同一列的三张俯仰不同,角点的方位半宽差得很多(俯仰越大角点甩得越远),\n * 拿哪一张代表这一列是任意的 —— 拿窄的那张,带端点就切掉同列宽张的真实内容;\n * 拿宽的那张,另一端又多出一段谁也没拍到的空带。\n */\nfunction computeBand(shots: ShotGeometry[]): CoverageBand | null {\n if (!shots.length) return null\n let phiTop = -Infinity\n let phiBottom = Infinity\n for (const shot of shots) {\n phiTop = Math.max(phiTop, shot.phi + shot.halfPhi)\n phiBottom = Math.min(phiBottom, shot.phi - shot.halfPhi)\n }\n const spanPhi = Math.max(1, phiTop - phiBottom)\n\n // 每张的覆盖区间归到 [0,360),跨圈的拆成两段;单张就盖满整圈的直接返回整圈\n const intervals: Array<{ lo: number; hi: number }> = []\n for (const shot of shots) {\n if (shot.halfTheta * 2 >= 360) {\n return { thetaStart: 0, spanTheta: 360, phiTop, spanPhi }\n }\n const lo = mod(shot.theta - shot.halfTheta, 360)\n const hi = lo + shot.halfTheta * 2\n if (hi > 360) {\n intervals.push({ lo, hi: 360 })\n intervals.push({ lo: 0, hi: hi - 360 })\n } else {\n intervals.push({ lo, hi })\n }\n }\n intervals.sort((a, b) => a.lo - b.lo)\n\n const merged: Array<{ lo: number; hi: number }> = []\n for (const interval of intervals) {\n const last = merged[merged.length - 1]\n if (last && interval.lo <= last.hi) {\n last.hi = Math.max(last.hi, interval.hi)\n } else {\n merged.push({ lo: interval.lo, hi: interval.hi })\n }\n }\n if (!merged.length) return null\n // 合并后只剩一段且几乎盖满整圈:按整圈处理( accumulateShot 里允许列回绕)\n const only = merged[0]\n if (merged.length === 1 && only && only.hi - only.lo >= 360 - 1e-6) {\n return { thetaStart: only.lo, spanTheta: 360, phiTop, spanPhi }\n }\n\n // 找最大的空隙:空隙之后的合并段是带的首段,空隙之前的是末段\n let gap = -Infinity\n let startIndex = 0\n let endIndex = 0\n for (let i = 0; i < merged.length; i += 1) {\n const current = merged[i]\n const next = merged[(i + 1) % merged.length]\n if (!current || !next) continue\n const delta = i === merged.length - 1 ? next.lo + 360 - current.hi : next.lo - current.hi\n if (delta > gap) {\n gap = delta\n startIndex = (i + 1) % merged.length\n endIndex = i\n }\n }\n const first = merged[startIndex]\n const lastArc = merged[endIndex]\n if (!first || !lastArc) return null\n const thetaStart = first.lo\n const spanTheta = Math.min(\n 360,\n Math.max(1, lastArc.hi + (endIndex < startIndex ? 360 : 0) - thetaStart),\n )\n return { thetaStart, spanTheta, phiTop, spanPhi }\n}\n\n// ───────────────────────────── 逐张累加 ─────────────────────────────\n\nfunction createTarget(\n band: CoverageBand,\n width: number,\n height: number,\n feather: number,\n): StitchTarget {\n const sinTheta = new Float32Array(width)\n const cosTheta = new Float32Array(width)\n const thetaStep = band.spanTheta / width\n for (let c = 0; c < width; c += 1) {\n const t = (band.thetaStart + (c + 0.5) * thetaStep) * DEG\n sinTheta[c] = Math.sin(t)\n cosTheta[c] = Math.cos(t)\n }\n const sinPhi = new Float32Array(height)\n const cosPhi = new Float32Array(height)\n const phiStep = band.spanPhi / height\n for (let r = 0; r < height; r += 1) {\n const p = (band.phiTop - (r + 0.5) * phiStep) * DEG\n sinPhi[r] = Math.sin(p)\n cosPhi[r] = Math.cos(p)\n }\n return {\n width,\n height,\n thetaStart: band.thetaStart,\n spanTheta: band.spanTheta,\n wrapsTheta: band.spanTheta >= 360 - 1e-6,\n phiTop: band.phiTop,\n spanPhi: band.spanPhi,\n sinTheta,\n cosTheta,\n sinPhi,\n cosPhi,\n acc: new Float32Array(width * height * 3),\n wsum: new Float32Array(width * height),\n coverageCount: new Uint8Array(width * height),\n feather,\n colsPerDeg: width / band.spanTheta,\n rowsPerDeg: height / band.spanPhi,\n }\n}\n\n/**\n * 把一张影像按反投影累加进输出缓冲。\n *\n * 循环是\"以这张影像的输出矩形为界\"的 —— 全景各张在输出图上只占一小块,\n * 逐张只扫自己那块,总工作量 ≈ 各张覆盖面积之和,而不是\"输出像素 × 张数\"。\n */\nfunction accumulateShot(\n target: StitchTarget,\n shot: ShotGeometry,\n pixels: Uint8ClampedArray,\n imageWidth: number,\n imageHeight: number,\n): void {\n const base = mod(shot.theta - target.thetaStart, 360)\n if (base >= target.spanTheta) return // 落在未覆盖的空隙里(理论上只在极端不完整的组上出现)\n\n const colFrom = Math.floor((base - shot.halfTheta) * target.colsPerDeg) - 1\n const colTo = Math.ceil((base + shot.halfTheta) * target.colsPerDeg) + 1\n // 非整圈带不允许列回绕:越出带的列是\"带外\"(没人拍到),不是\"带的另一端\"。\n // 整圈带首尾真的相接,mod 回绕才是正确语义 —— 180° 组若也回绕,\n // 最左列那张的左半边会被复制到最右列,两端各叠出一块鬼影。\n const colLo = target.wrapsTheta ? colFrom : Math.max(0, colFrom)\n const colHi = target.wrapsTheta ? colTo : Math.min(target.width - 1, colTo)\n const rowFrom = Math.max(\n 0,\n Math.floor((target.phiTop - (shot.phi + shot.halfPhi)) * target.rowsPerDeg) - 1,\n )\n const rowTo = Math.min(\n target.height - 1,\n Math.ceil((target.phiTop - (shot.phi - shot.halfPhi)) * target.rowsPerDeg) + 1,\n )\n if (rowFrom > rowTo) return\n\n const [rx, ry, rz] = shot.right\n const [ux, uy, uz] = shot.up\n const [fx, fy, fz] = shot.forward\n const tanH = shot.tanH\n const tanV = shot.tanV\n const featherStart = 1 - target.feather\n const maxX = imageWidth - 1\n const maxY = imageHeight - 1\n\n for (let r = rowFrom; r <= rowTo; r += 1) {\n const sinP = target.sinPhi[r] ?? 0\n const cosP = target.cosPhi[r] ?? 1\n const rowBase = r * target.width\n for (let c = colLo; c <= colHi; c += 1) {\n const col = mod(c, target.width)\n const sinT = target.sinTheta[col] ?? 0\n const cosT = target.cosTheta[col] ?? 1\n // 该输出像素在全景参考系里的方向\n const dx = sinT * cosP\n const dy = sinP\n const dz = -cosT * cosP\n // 转到这张的相机系:相机 x = 右、y = 上、深度 = 前\n const a = dx * rx + dy * ry + dz * rz\n const b = dx * ux + dy * uy + dz * uz\n const depth = dx * fx + dy * fy + dz * fz\n if (depth <= 1e-6) continue\n const xn = a / depth\n const yn = b / depth\n if (xn < -tanH || xn > tanH || yn < -tanV || yn > tanV) continue\n\n const nx = xn / tanH // -1 ~ 1\n const ny = yn / tanV\n const weight = taper(Math.abs(nx), featherStart) * taper(Math.abs(ny), featherStart)\n if (weight <= 0) continue\n\n const px = (nx * 0.5 + 0.5) * maxX\n const py = (0.5 - ny * 0.5) * maxY\n const x0 = px | 0\n const y0 = py | 0\n const x1 = x0 < maxX ? x0 + 1 : maxX\n const y1 = y0 < maxY ? y0 + 1 : maxY\n const tx = px - x0\n const ty = py - y0\n const w00 = (1 - tx) * (1 - ty)\n const w10 = tx * (1 - ty)\n const w01 = (1 - tx) * ty\n const w11 = tx * ty\n\n const i00 = (y0 * imageWidth + x0) * 4\n const i10 = (y0 * imageWidth + x1) * 4\n const i01 = (y1 * imageWidth + x0) * 4\n const i11 = (y1 * imageWidth + x1) * 4\n const cell = (rowBase + col) * 3\n for (let ch = 0; ch < 3; ch += 1) {\n const value =\n (pixels[i00 + ch] ?? 0) * w00 +\n (pixels[i10 + ch] ?? 0) * w10 +\n (pixels[i01 + ch] ?? 0) * w01 +\n (pixels[i11 + ch] ?? 0) * w11\n target.acc[cell + ch] = (target.acc[cell + ch] ?? 0) + value * weight\n }\n target.wsum[rowBase + col] = (target.wsum[rowBase + col] ?? 0) + weight\n if ((target.coverageCount[rowBase + col] ?? 0) < 255) {\n target.coverageCount[rowBase + col] = (target.coverageCount[rowBase + col] ?? 0) + 1\n }\n }\n }\n}\n\n/** 羽化衰减:带宽内保持 1,进入边缘带后二次衰减到下限(下限不为 0,见常量说明) */\nfunction taper(normalized: number, featherStart: number): number {\n if (normalized <= featherStart) return 1\n const band = 1 - featherStart\n if (band <= 1e-6) return MIN_FEATHER_WEIGHT\n const k = (normalized - featherStart) / band\n const decay = 1 - k * k\n return decay < MIN_FEATHER_WEIGHT ? MIN_FEATHER_WEIGHT : decay\n}\n\n/**\n * 把累加缓冲归一成像素,并按 `fill` 补上未覆盖区域。\n *\n * `covered` 是**补边之前**真实被影像覆盖的像素数 —— 宿主用 `coverage` 判断这组拍全了没有,\n * 所以不能被填充像素污染。\n */\nfunction compose(target: StitchTarget, fill: EquirectFill): { image: ImageData; covered: number } {\n const width = target.width\n const height = target.height\n const image = new ImageData(width, height)\n const data = image.data\n let covered = 0\n for (let r = 0; r < height; r += 1) {\n const rowBase = r * width\n for (let c = 0; c < width; c += 1) {\n const i = rowBase + c\n const weight = target.wsum[i] ?? 0\n const out = i * 4\n if (weight > 1e-6) {\n const inv = 1 / weight\n data[out] = clampByte((target.acc[i * 3] ?? 0) * inv)\n data[out + 1] = clampByte((target.acc[i * 3 + 1] ?? 0) * inv)\n data[out + 2] = clampByte((target.acc[i * 3 + 2] ?? 0) * inv)\n data[out + 3] = 255\n covered += 1\n } else {\n data[out + 3] = 0\n }\n }\n }\n if (fill === 'edge') fillEdges(width, height, data, target.wrapsTheta)\n if (target.wrapsTheta) closeLongitudeSeam(target, data)\n return { image, covered }\n}\n\n/**\n * 让整球输出的左右边界在环绕时连续。两侧各取约 1° 做渐变修正,\n * 避免只改最外一列后在修正区边缘产生新的细线。\n */\nfunction closeLongitudeSeam(target: StitchTarget, data: Uint8ClampedArray): void {\n const { width, height, colsPerDeg } = target\n const blendColumns = Math.min(Math.floor(width / 2), Math.max(1, Math.round(colsPerDeg)))\n for (let r = 0; r < height; r += 1) {\n const row = r * width * 4\n const leftEdge = row\n const rightEdge = row + (width - 1) * 4\n if ((data[leftEdge + 3] ?? 0) === 0 || (data[rightEdge + 3] ?? 0) === 0) continue\n\n for (let offset = 0; offset < blendColumns; offset += 1) {\n const left = row + offset * 4\n const right = row + (width - 1 - offset) * 4\n const t = offset / blendColumns\n const strength = 1 - t * t * (3 - 2 * t)\n for (let channel = 0; channel < 3; channel += 1) {\n const leftValue = data[left + channel] ?? 0\n const rightValue = data[right + channel] ?? 0\n const midpoint = (leftValue + rightValue) / 2\n data[left + channel] = clampByte(leftValue + (midpoint - leftValue) * strength)\n data[right + channel] = clampByte(rightValue + (midpoint - rightValue) * strength)\n }\n }\n }\n}\n\n/**\n * 把未覆盖像素补上,得到一张不透明的整幅球面图(真机全景的观感)。两步,**先横向后纵向**:\n *\n * ① **行内延展** —— 每一行里没覆盖到的段,用同行最近的已覆盖像素横向接上(整圈才允许绕回)。\n * 这一步专治\"两侧月牙\":顶层照片够不到的高仰角 + 大方位偏移区,和它在**同一行**的\n * 最近覆盖像素是顶层画面的边缘(天空),横向接上就是自然的延续。若先做纵向,\n * 这些列的最高覆盖行在地平线附近(暗),会把地面色一路拖到天顶,拉出一根根暗柱。\n * ② **列向延展** —— 天顶 / 天底方向(整行都没人拍的行)用该列最高 / 最低覆盖行铺:\n * 天空蓝一路铺到天顶、地面黑一路铺到天底,正是球面全景两端的观感。\n *\n * ⚠️ ② 还必须补 colTop~colBot 之间的\"中间整行空隙\":俯仰层间没有重叠时,会出现整圈\n * 空纬度带。用最近一侧整行硬复制会形成明显环状色阶(看起来像一圈墙),所以对空洞按\n * 纬度在上下有效行之间做渐变;这只是减轻未覆盖处的硬边,不能代替拍摄时的真实重叠。\n */\nfunction fillEdges(width: number, height: number, data: Uint8ClampedArray, wraps: boolean): void {\n horizontalFill(width, height, data, wraps)\n verticalFill(width, height, data)\n}\n\n/** 行内延展:每行未覆盖段用同行最近的已覆盖像素补(环形与否按整圈/带状定) */\nfunction horizontalFill(\n width: number,\n height: number,\n data: Uint8ClampedArray,\n wraps: boolean,\n): void {\n const rowBytes = width * 4\n const covered = new Uint8Array(width)\n const nearL = new Int32Array(width)\n const nearR = new Int32Array(width)\n for (let r = 0; r < height; r += 1) {\n const base = r * rowBytes\n let first = -1\n let last = -1\n for (let c = 0; c < width; c += 1) {\n covered[c] = (data[base + c * 4 + 3] ?? 0) > 0 ? 1 : 0\n if ((covered[c] ?? 0) !== 0) {\n if (first < 0) first = c\n last = c\n }\n }\n if (first < 0) continue // 整行都没拍到:交给列向延展\n let p = -1\n for (let c = 0; c < width; c += 1) {\n nearL[c] = covered[c] ? c : p\n if (covered[c]) p = c\n }\n p = -1\n for (let c = width - 1; c >= 0; c -= 1) {\n nearR[c] = covered[c] ? c : p\n if (covered[c]) p = c\n }\n for (let c = 0; c < width; c += 1) {\n if ((covered[c] ?? 0) !== 0) continue\n const fromL = nearL[c] ?? -1\n const fromR = nearR[c] ?? -1\n let pick = -1\n let best = Infinity\n if (fromL >= 0) {\n pick = fromL\n best = c - fromL\n }\n if (fromR >= 0 && fromR - c < best) {\n pick = fromR\n best = fromR - c\n }\n if (wraps) {\n if (last >= 0) {\n const d = c + width - last\n if (d < best) {\n pick = last\n best = d\n }\n }\n if (first >= 0) {\n const d = width - c + first\n if (d < best) {\n pick = first\n best = d\n }\n }\n }\n if (pick >= 0) copyPixel(data, base + pick * 4, base + c * 4)\n }\n }\n}\n\n/** 列向延展:天顶 / 天底方向用该列最高 / 最低覆盖行铺;并补中间整行空隙(见 fillEdges 注释) */\nfunction verticalFill(width: number, height: number, data: Uint8ClampedArray): void {\n const rowBytes = width * 4\n const colTop = new Int32Array(width).fill(-1)\n const colBot = new Int32Array(width).fill(-1)\n for (let r = 0; r < height; r += 1) {\n const base = r * rowBytes\n for (let c = 0; c < width; c += 1) {\n if ((data[base + c * 4 + 3] ?? 0) > 0) {\n if ((colTop[c] ?? -1) < 0) colTop[c] = r\n colBot[c] = r\n }\n }\n }\n // 复用的\"更近一侧覆盖行\"scratch:每列各扫一遍,避免逐列新建数组\n const nearAbove = new Int32Array(height).fill(-1)\n const nearBelow = new Int32Array(height).fill(-1)\n for (let c = 0; c < width; c += 1) {\n const top = colTop[c] ?? -1\n const bot = colBot[c] ?? -1\n if (top < 0 || bot < 0) continue\n const colOff = c * 4\n // 天顶方向:top 以上用 top 行铺\n const srcTop = top * rowBytes + colOff\n for (let r = 0; r < top; r += 1) copyPixel(data, srcTop, r * rowBytes + colOff)\n // 天底方向:bot 以下用 bot 行铺\n const srcBot = bot * rowBytes + colOff\n for (let r = bot + 1; r < height; r += 1) copyPixel(data, srcBot, r * rowBytes + colOff)\n // 中间整行空隙:top..bot 之间没人拍到的行,取更近一侧(上 / 下)的已覆盖行铺\n let last = -1\n for (let r = top; r <= bot; r += 1) {\n if ((data[r * rowBytes + colOff + 3] ?? 0) > 0) last = r\n nearAbove[r] = last\n }\n last = -1\n for (let r = bot; r >= top; r -= 1) {\n if ((data[r * rowBytes + colOff + 3] ?? 0) > 0) last = r\n nearBelow[r] = last\n }\n for (let r = top; r <= bot; r += 1) {\n if ((data[r * rowBytes + colOff + 3] ?? 0) > 0) continue\n const above = nearAbove[r] ?? -1\n const below = nearBelow[r] ?? -1\n const dst = r * rowBytes + colOff\n if (above >= 0 && below >= 0) {\n const upper = above * rowBytes + colOff\n const lower = below * rowBytes + colOff\n const blend = (r - above) / (below - above)\n for (let channel = 0; channel < 3; channel += 1) {\n const upperValue = data[upper + channel] ?? 0\n const lowerValue = data[lower + channel] ?? 0\n data[dst + channel] = clampByte(upperValue + (lowerValue - upperValue) * blend)\n }\n data[dst + 3] = 255\n } else {\n const src = above >= 0 ? above : below\n if (src >= 0) copyPixel(data, src * rowBytes + colOff, dst)\n }\n }\n }\n}\n\nfunction copyPixel(data: Uint8ClampedArray, from: number, to: number): void {\n data[to] = data[from] ?? 0\n data[to + 1] = data[from + 1] ?? 0\n data[to + 2] = data[from + 2] ?? 0\n data[to + 3] = 255\n}\n\n// ───────────────────────────── 小工具 ─────────────────────────────\n\n/** 从图片元素里读出像素(用一块复用的 canvas 当解码中转) */\nfunction readPixels(\n canvas: HTMLCanvasElement,\n ctx: CanvasRenderingContext2D,\n image: HTMLImageElement,\n): { data: Uint8ClampedArray; width: number; height: number } | null {\n const width = image.naturalWidth || image.width\n const height = image.naturalHeight || image.height\n if (!width || !height) return null\n canvas.width = width\n canvas.height = height\n ctx.clearRect(0, 0, width, height)\n ctx.drawImage(image, 0, 0, width, height)\n try {\n const data = ctx.getImageData(0, 0, width, height)\n return { data: data.data, width: data.width, height: data.height }\n } catch {\n // 跨域画布会抛安全错误:数据 URL / Blob URL 不会走到这里,真遇到就当这张读不出来\n return null\n }\n}\n\nfunction loadImage(url: string | undefined): Promise<HTMLImageElement | null> {\n return new Promise((resolve) => {\n if (!url) {\n resolve(null)\n return\n }\n const image = new Image()\n image.decoding = 'async'\n image.onload = () => resolve(image)\n image.onerror = () => resolve(null)\n image.src = url\n })\n}\n\nfunction yieldToEventLoop(): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, 0))\n}\n\n/** 取模到 [0, span) */\nfunction mod(value: number, span: number): number {\n if (!(span > 0)) return 0\n const result = value % span\n return result < 0 ? result + span : result\n}\n\n/** 方位角收敛到 (-180, 180] */\nfunction wrap180(deg: number): number {\n const wrapped = ((deg + 180) % 360) - 180\n return wrapped === -180 ? 180 : wrapped\n}\n\nfunction clampByte(value: number): number {\n if (!(value > 0)) return 0\n return value > 255 ? 255 : value\n}\n\nfunction clampNumber(\n value: number | undefined,\n min: number,\n max: number,\n fallback: number,\n): number {\n if (typeof value !== 'number' || !Number.isFinite(value)) return fallback\n return Math.max(min, Math.min(max, value))\n}\n","/**\n * 全景查看器(原生 DOM + 手写 WebGL):把等距柱状全景图**贴回球面**拖动环视 ——\n * 一张平面的长条图,在这里变成能看四面八方的\"球形照片\"(真机相册里点开全景图的那种体验)。\n *\n * 为什么需要它:等距柱状图(2:1 长条)是全景的**存储格式**,不是**观看格式**。\n * 一张 3600×1800 的条带摊在屏幕上看不出\"四面八方\";只要把它沿球面反投影,同一个文件\n * 立刻变成可自由环视的球面 —— 这正是 DJI 相册打开球面全景时的做法。\n *\n * 三处刻意的设计:\n *\n * 1. **手写 WebGL,而不是引 three**。three 是库的 peerDependency,但影像集/查看器这一层\n * 是\"与渲染后端无关\"的纯 DOM 模块(Cesium 宿主也在用)。为了看一张全景图把整个 three\n * 拖进这条链路并不划算 —— 整件事只需要一个全屏三角形 + 一段反投影的片元着色器,\n * 没有几何体、没有光照、没有材质。\n *\n * 2. **反投影用与 drone-stitch 完全相同的球面基**,两端必须同源才对得上:\n * forward(θ,φ) = [sinθ·cosφ, sinφ, -cosθ·cosφ] θ 向右增、φ 为绝对仰角(天顶 +90°)\n * u = (θ - thetaStart) / spanTheta, v = (phiTop - φ) / spanPhi\n * 合成时是\"球面方向 → 等距柱状像素\",这里是\"屏幕像素 → 球面方向 → 等距柱状像素\"。\n * 相机基 {right, up, forward} 由 θ/φ 直接构造(right = forward × ∂forward/∂φ,推导见\n * `cameraBasis`),于是**拖动只是改两个标量**,没有欧拉角奇点、不用四元数/矩阵库。\n *\n * 3. **挂 `document.body` + 键盘走 window 捕获阶段**(与影像集、大图弹窗同一套约定):\n * 面板的 `backdrop-filter` 会为 `position: fixed` 的后代换包含块;而面板的键盘飞行监听\n * 挂在冒泡阶段,弹窗开着时按方向键必须只环视、绝不顺带把无人机飞出去。\n *\n * ```ts\n * const viewer = new DronePanoramaViewer()\n * viewer.open(result.url, '合成全景图') // 也接受合成结果对象\n * viewer.close() // destroy() 彻底卸载\n * ```\n */\nimport type { PanoramaStitchResult } from './drone-panorama'\nimport { triggerDownload } from './drone-image-utils'\n\n/** 全景查看器构造选项 */\nexport interface DronePanoramaViewerOptions {\n /** 是否注入自带样式,默认 true */\n injectStyles?: boolean\n /** 弹窗层级,默认 1100(排在影像集弹窗之上) */\n zIndex?: number\n /** 初始朝向(方位角,度),默认 0 = 输出图正中(即该组的起始航向) */\n initialYawDeg?: number\n /** 初始仰角(度),默认 0 = 平视 */\n initialPitchDeg?: number\n /** 初始垂直视野角(度),默认 75 */\n initialFovDeg?: number\n /** 垂直视野角上下限,默认 25 ~ 110 */\n minFovDeg?: number\n maxFovDeg?: number\n /** 是否提供「下载原图」按钮,默认 true */\n downloadable?: boolean\n /**\n * 是否提供「导入图片」按钮(从本地选一张等距柱状全景图看球面),默认 false。\n * 影像集弹窗里已有自己的入口,那里不需要;独立打开的查看器(比如 demo 页的按钮)开着才方便。\n */\n importable?: boolean\n /** 关闭回调(点 × / 点遮罩 / Esc) */\n onClose?: () => void\n}\n\ninterface ViewerRefs {\n mask: HTMLElement\n stage: HTMLElement\n canvas: HTMLCanvasElement\n title: HTMLElement\n hud: HTMLElement\n note: HTMLElement\n download: HTMLButtonElement\n file: HTMLInputElement\n}\n\ninterface Uniforms {\n texture: WebGLUniformLocation | null\n tanHalf: WebGLUniformLocation | null\n aspect: WebGLUniformLocation | null\n right: WebGLUniformLocation | null\n up: WebGLUniformLocation | null\n forward: WebGLUniformLocation | null\n thetaStart: WebGLUniformLocation | null\n spanTheta: WebGLUniformLocation | null\n phiTop: WebGLUniformLocation | null\n spanPhi: WebGLUniformLocation | null\n wrap: WebGLUniformLocation | null\n textureWidth: WebGLUniformLocation | null\n}\n\ninterface Program {\n program: WebGLProgram\n buffer: WebGLBuffer\n texture: WebGLTexture\n uniforms: Uniforms\n}\n\nconst STYLE_ID = 'drone-panorama-viewer-styles'\nconst DEFAULT_Z_INDEX = 1100\nconst DEG = Math.PI / 180\n/** 滚轮一格(约 100 deltaY)对应的视野倍率手感系数 */\nconst WHEEL_ZOOM_RATE = 0.0015\n/** 松手后的惯性衰减(每帧乘这个系数)与停下的阈值(度/帧) */\nconst INERTIA_DECAY = 0.9\nconst INERTIA_MIN = 0.03\n/** 仰角上限略小于 90°,免得在极点附近翻滚 */\nconst MAX_PITCH_DEG = 89\nconst DEFAULT_FOV_DEG = 75\nconst DEFAULT_MIN_FOV_DEG = 25\nconst DEFAULT_MAX_FOV_DEG = 110\n/** 全景图默认覆盖整颗球:输出图最左列 = 方位 -180°,最上行 = 仰角 +90° */\nconst SPHERE_THETA_START_DEG = -180\nconst SPHERE_SPAN_THETA_DEG = 360\nconst SPHERE_PHI_TOP_DEG = 90\nconst SPHERE_SPAN_PHI_DEG = 180\nconst DEFAULT_PROJECTION = {\n thetaStart: SPHERE_THETA_START_DEG,\n spanTheta: SPHERE_SPAN_THETA_DEG,\n phiTop: SPHERE_PHI_TOP_DEG,\n spanPhi: SPHERE_SPAN_PHI_DEG,\n wrapsTheta: true,\n}\n\n/**\n * 全屏三角形的顶点着色器:不需要任何几何体 —— 相机射线在片元着色器里按屏幕坐标现算。\n * 输出 `vUv` 覆盖 [0,1]²(左上角 = 屏幕左上角)。\n */\nconst VERTEX_SHADER = `\nattribute vec2 aPos;\nvarying vec2 vUv;\nvoid main() {\n vUv = aPos * 0.5 + 0.5;\n gl_Position = vec4(aPos, 0.0, 1.0);\n}\n`\n\n/**\n * 反投影:屏幕像素 → 相机射线 → 球面方位/仰角 → 等距柱状 UV。\n *\n * 球面基与 drone-stitch 逐字一致(见模块头注释 2),`uWrap` 为 1 时方位在接缝处回绕\n * (整球输出),为 0 时钳制(非整圈的带状输出)。\n */\nconst FRAGMENT_SHADER = `\n#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\nvarying vec2 vUv;\nuniform sampler2D uTex;\nuniform float uTanHalf;\nuniform float uAspect;\nuniform vec3 uRight;\nuniform vec3 uUp;\nuniform vec3 uForward;\nuniform float uThetaStart;\nuniform float uSpanTheta;\nuniform float uPhiTop;\nuniform float uSpanPhi;\nuniform float uWrap;\nuniform float uTextureWidth;\nconst float PI = 3.141592653589793;\n\nvec4 sampleWrappedTexture(float u, float v) {\n float texel = u * uTextureWidth - 0.5;\n float texel0 = floor(texel);\n float blend = fract(texel);\n float x0 = mix(clamp(texel0, 0.0, uTextureWidth - 1.0), mod(texel0, uTextureWidth), uWrap);\n float x1 = mix(clamp(texel0 + 1.0, 0.0, uTextureWidth - 1.0), mod(texel0 + 1.0, uTextureWidth), uWrap);\n float u0 = (x0 + 0.5) / uTextureWidth;\n float u1 = (x1 + 0.5) / uTextureWidth;\n return mix(texture2D(uTex, vec2(u0, v)), texture2D(uTex, vec2(u1, v)), blend);\n}\n\nvoid main() {\n vec2 ndc = vUv * 2.0 - 1.0;\n vec3 dir = normalize(uRight * (ndc.x * uTanHalf * uAspect) + uUp * (ndc.y * uTanHalf) + uForward);\n float phi = asin(clamp(dir.y, -1.0, 1.0)) / PI * 180.0;\n float theta = atan(dir.x, -dir.z) / PI * 180.0;\n float u = (theta - uThetaStart) / uSpanTheta;\n u = mix(clamp(u, 0.0, 1.0), fract(u), uWrap);\n float v = clamp((uPhiTop - phi) / uSpanPhi, 0.0, 1.0);\n gl_FragColor = sampleWrappedTexture(u, v);\n}\n`\n\nconst VIEWER_CSS = `\n.divp {\n --divp-text: #e9eff9;\n --divp-muted: rgba(233, 239, 249, 0.58);\n --divp-border: rgba(255, 255, 255, 0.12);\n font: 13px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n user-select: none;\n -webkit-user-select: none;\n -webkit-tap-highlight-color: transparent;\n}\n.divp {\n position: fixed; inset: 0; z-index: ${DEFAULT_Z_INDEX};\n display: flex; align-items: center; justify-content: center;\n padding: max(8px, env(safe-area-inset-top)) 8px max(8px, env(safe-area-inset-bottom));\n color: var(--divp-text);\n outline: none;\n}\n.divp[hidden] { display: none; }\n.divp-mask {\n position: absolute; inset: 0; background: rgba(3, 5, 8, 0.96);\n -webkit-backdrop-filter: blur(7px); backdrop-filter: blur(7px);\n}\n.divp-frame {\n position: relative; z-index: 1;\n display: flex; flex-direction: column;\n width: min(100%, 1420px); height: 100%; overflow: hidden;\n border: 1px solid var(--divp-border); border-radius: 12px;\n background: rgba(10, 13, 19, 0.96);\n box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);\n}\n.divp-bar {\n flex: 0 0 auto; display: flex; align-items: center; gap: 8px;\n padding: 8px 10px; border-bottom: 1px solid var(--divp-border);\n}\n.divp-title { font-weight: 600; }\n.divp-hud { color: var(--divp-muted); font-variant-numeric: tabular-nums; }\n.divp-spacer { flex: 1; min-width: 4px; }\n.divp-btn {\n flex: 0 0 auto; min-height: 26px; padding: 3px 10px; cursor: pointer; white-space: nowrap;\n border: 1px solid var(--divp-border); border-radius: 7px;\n background: rgba(255, 255, 255, 0.06); color: var(--divp-text); font: inherit; font-size: 12px;\n}\n.divp-btn:hover { background: rgba(255, 255, 255, 0.14); }\n.divp-btn[hidden] { display: none; }\n.divp-icon {\n flex: 0 0 auto; width: 26px; height: 26px; padding: 0; cursor: pointer;\n border: 1px solid var(--divp-border); border-radius: 7px;\n background: rgba(255, 255, 255, 0.06); color: var(--divp-text); font: inherit; line-height: 1;\n}\n.divp-icon:hover { background: rgba(255, 255, 255, 0.14); }\n.divp-stage {\n position: relative; flex: 1; min-height: 0; overflow: hidden;\n background: radial-gradient(circle at 50% 45%, #0a1018, #04060a 70%);\n touch-action: none; cursor: grab;\n}\n.divp-stage[data-dragging=\"true\"] { cursor: grabbing; }\n.divp-canvas { display: block; width: 100%; height: 100%; }\n.divp-canvas[hidden] { display: none; }\n.divp-note {\n position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);\n margin: 0; padding: 14px 18px; max-width: 80%;\n border: 1px solid var(--divp-border); border-radius: 10px;\n background: rgba(10, 14, 20, 0.9); color: var(--divp-muted); text-align: center;\n}\n.divp-note[hidden] { display: none; }\n.divp-foot {\n flex: 0 0 auto; padding: 6px 10px; border-top: 1px solid var(--divp-border);\n color: var(--divp-muted); font-size: 11px;\n}\n@media (pointer: coarse) {\n .divp-icon { width: 32px; height: 32px; }\n .divp-btn { min-height: 32px; }\n}\n`\n\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return\n if (document.getElementById(STYLE_ID)) return\n const style = document.createElement('style')\n style.id = STYLE_ID\n style.textContent = VIEWER_CSS\n document.head.appendChild(style)\n}\n\n/**\n * 由方位角 / 仰角构造相机的正交基。\n *\n * 用与 drone-stitch 同一套球面参数:\n * forward(θ,φ) = [sinθ·cosφ, sinφ, -cosθ·cosφ]\n * up(θ,φ) = ∂forward/∂φ = [-sinθ·sinφ, cosφ, cosθ·sinφ]\n * right = forward × up = [cosθ, 0, sinθ] ← 与仰角无关,始终水平\n * 三者两两正交(right·up = 0、forward·up = 0),所以屏幕射线直接线性组合即可,\n * 不必过一遍旋转矩阵,也不会在极点附近遇到万向节问题。\n */\nfunction cameraBasis(yawDeg: number, pitchDeg: number): {\n forward: [number, number, number]\n up: [number, number, number]\n right: [number, number, number]\n} {\n const theta = yawDeg * DEG\n const phi = pitchDeg * DEG\n const ct = Math.cos(theta)\n const st = Math.sin(theta)\n const cp = Math.cos(phi)\n const sp = Math.sin(phi)\n return {\n forward: [st * cp, sp, -ct * cp],\n up: [-st * sp, cp, ct * sp],\n right: [ct, 0, st],\n }\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\nfunction resolveFov(value: number, min: number, max: number, fallback: number): number {\n if (!Number.isFinite(value)) return fallback\n return clamp(value, min, max)\n}\n\n/** 把角度规整到 [0, 360) —— HUD 上的方位读数 */\nfunction normalizeDeg(value: number): number {\n return ((value % 360) + 360) % 360\n}\n\n/**\n * 全景查看器:一张带有球面投影范围的等距柱状图 + 可拖动环视的球面局部或整球。\n *\n * DOM **懒挂载**:构造只记住选项,首次 `open()` 才建节点、注入样式、建 WebGL 上下文;\n * `close()` 把整棵 DOM 与 GL 资源一并拆掉,于是\"没打开\"时它对页面零影响。\n */\nexport class DronePanoramaViewer {\n private readonly options: DronePanoramaViewerOptions\n private readonly minFov: number\n private readonly maxFov: number\n private root: HTMLDivElement | null = null\n private refs: ViewerRefs | null = null\n private program: Program | null = null\n private gl: WebGLRenderingContext | null = null\n private observer: ResizeObserver | null = null\n private source = ''\n private fileName = 'panorama.png'\n /** 导入的本地文件对应的 blob 地址(换图 / destroy 时 revoke;close 不 revoke,重开要复用) */\n private importUrl: string | null = null\n /** 导入文件的文件名(重开时用作标题) */\n private importName = ''\n private image: HTMLImageElement | null = null\n private projection = { ...DEFAULT_PROJECTION }\n /** 图片加载序号:onload 回调是异步的,换图 / 关闭后旧回调必须作废 */\n private loadSeq = 0\n private ready = false\n /** 视角状态:两个标量就能完全描述朝向 */\n private yaw = 0\n private pitch = 0\n private fov = DEFAULT_FOV_DEG\n private readonly velocity = { yaw: 0, pitch: 0 }\n private readonly pointers = new Map<number, { x: number; y: number }>()\n private lastPointer: { x: number; y: number } | null = null\n private pinchDistance = 0\n private rafId = 0\n private dirty = true\n private bodyOverflow = ''\n private destroyed = false\n\n /** 构造球面全景查看器(手写 WebGL 零依赖;可传初始角与导入开关) */\n constructor(options: DronePanoramaViewerOptions = {}) {\n this.options = options\n this.minFov = resolveFov(options.minFovDeg ?? DEFAULT_MIN_FOV_DEG, 5, 170, DEFAULT_MIN_FOV_DEG)\n this.maxFov = resolveFov(options.maxFovDeg ?? DEFAULT_MAX_FOV_DEG, this.minFov, 170, DEFAULT_MAX_FOV_DEG)\n this.yaw = options.initialYawDeg ?? 0\n this.pitch = clamp(options.initialPitchDeg ?? 0, -MAX_PITCH_DEG, MAX_PITCH_DEG)\n this.fov = clamp(options.initialFovDeg ?? DEFAULT_FOV_DEG, this.minFov, this.maxFov)\n }\n\n /** 弹窗当前是否打开 */\n get isOpen(): boolean {\n return this.root !== null && !this.root.hidden\n }\n\n /** 当前朝向(度),便于宿主做联动 */\n get view(): { yaw: number; pitch: number; fov: number } {\n return { yaw: this.yaw, pitch: this.pitch, fov: this.fov }\n }\n\n /**\n * 打开弹窗;`source` 是等距柱状全景图的地址(data URL / blob / http 均可)。\n * 不传 = 只开一个\"导入\"入口(配 `importable` 用),选了图再进来。\n */\n open(source?: string | PanoramaStitchResult, title?: string): void {\n if (this.destroyed) return\n this.mount()\n const root = this.root\n if (!root || !this.refs) return\n if (!source) {\n // 无图打开:导入过就还看上次那张(关闭不该弄丢用户的图),没导入过才给\"去导入\"空态\n if (this.importUrl) {\n this.source = this.importUrl\n this.projection = { ...DEFAULT_PROJECTION }\n this.refs.title.textContent = this.importName || '360° 全景'\n root.hidden = false\n this.lockScroll()\n this.reset()\n this.loadImage(this.importUrl)\n root.focus()\n return\n }\n this.source = ''\n this.image = null\n this.ready = false\n this.refs.title.textContent = title ?? '360° 全景'\n root.hidden = false\n this.lockScroll()\n this.reset()\n this.invalidate()\n this.setNote('还没有全景图 —— 点右上「导入图片」,选一张等距柱状全景图(2:1 长条)。')\n root.focus()\n return\n }\n const url = typeof source === 'string' ? source : source.url\n if (!url) return\n this.source = url\n this.projection =\n typeof source === 'string'\n ? { ...DEFAULT_PROJECTION }\n : { ...DEFAULT_PROJECTION, ...source.equirectangular }\n const name = typeof source === 'string' ? title : title ?? source.name\n this.fileName = name && /\\.\\w{2,5}$/.test(name) ? name : `${this.slug(name ?? 'panorama')}.png`\n this.refs.title.textContent = name ?? '全景图'\n root.hidden = false\n this.lockScroll()\n this.reset()\n this.loadImage(url)\n root.focus()\n }\n\n /** 关闭弹窗(整个卸载 DOM 并释放 GL 资源) */\n close(): void {\n if (!this.root) return\n const wasOpen = !this.root.hidden\n this.unmount()\n if (wasOpen) this.options.onClose?.()\n }\n\n /** 视角复位到初始状态 */\n reset(): void {\n this.yaw = this.options.initialYawDeg ?? 0\n this.pitch = clamp(this.options.initialPitchDeg ?? 0, -MAX_PITCH_DEG, MAX_PITCH_DEG)\n this.fov = clamp(this.options.initialFovDeg ?? DEFAULT_FOV_DEG, this.minFov, this.maxFov)\n this.velocity.yaw = 0\n this.velocity.pitch = 0\n this.invalidate()\n }\n\n /** 下载当前这张全景原图 */\n download(): void {\n if (!this.source) return\n triggerDownload(this.source, this.fileName)\n }\n\n /** 关闭弹窗并释放 GL 资源与 blob 地址 */\n destroy(): void {\n if (this.destroyed) return\n this.destroyed = true\n this.unmount()\n if (this.importUrl) {\n URL.revokeObjectURL(this.importUrl)\n this.importUrl = null\n }\n if (this.options.injectStyles !== false) document.getElementById(STYLE_ID)?.remove()\n }\n\n // ───────────────────────────── 内部:生命周期 ─────────────────────────────\n\n private mount(): void {\n if (this.root || typeof document === 'undefined') return\n if (this.options.injectStyles !== false) ensureStyles()\n\n const root = document.createElement('div')\n root.className = 'divp'\n root.hidden = true\n root.tabIndex = -1\n root.setAttribute('role', 'dialog')\n root.setAttribute('aria-modal', 'true')\n root.setAttribute('aria-label', '全景查看')\n if (this.options.zIndex && this.options.zIndex !== DEFAULT_Z_INDEX) {\n root.style.zIndex = String(this.options.zIndex)\n }\n root.innerHTML = this.template()\n document.body.appendChild(root)\n this.root = root\n this.refs = {\n mask: this.query('[data-role=\"mask\"]'),\n stage: this.query('[data-role=\"stage\"]'),\n canvas: this.query<HTMLCanvasElement>('[data-role=\"canvas\"]'),\n title: this.query('[data-role=\"title\"]'),\n hud: this.query('[data-role=\"hud\"]'),\n note: this.query('[data-role=\"note\"]'),\n download: this.query<HTMLButtonElement>('[data-action=\"download\"]'),\n file: this.query<HTMLInputElement>('[data-role=\"file\"]'),\n }\n this.refs.file.addEventListener('change', this.onImportFile)\n\n root.addEventListener('click', this.onClick)\n const stage = this.refs.stage\n stage.addEventListener('pointerdown', this.onPointerDown)\n stage.addEventListener('pointermove', this.onPointerMove)\n stage.addEventListener('pointerup', this.onPointerUp)\n stage.addEventListener('pointercancel', this.onPointerUp)\n stage.addEventListener('wheel', this.onWheel, { passive: false })\n stage.addEventListener('dblclick', this.onDoubleClick)\n // 捕获阶段接管键盘,才能压住面板挂在冒泡阶段的键盘飞行(见模块头注释 3)\n window.addEventListener('keydown', this.onKeyDown, { capture: true })\n\n const gl = this.createContext()\n if (!gl) {\n this.refs.canvas.hidden = true\n this.setNote('这个浏览器不支持 WebGL,没法把全景贴到球面上 —— 已回退成平面长图。')\n this.fallbackImage()\n } else if (typeof ResizeObserver !== 'undefined') {\n this.observer = new ResizeObserver(() => this.syncSize())\n this.observer.observe(stage)\n }\n }\n\n private unmount(): void {\n window.removeEventListener('keydown', this.onKeyDown, { capture: true })\n if (this.rafId) {\n window.cancelAnimationFrame(this.rafId)\n this.rafId = 0\n }\n this.observer?.disconnect()\n this.observer = null\n this.image = null\n this.pointers.clear()\n this.lastPointer = null\n this.pinchDistance = 0\n this.disposeGL()\n this.root?.remove()\n this.root = null\n this.refs = null\n this.ready = false\n this.unlockScroll()\n }\n\n // ───────────────────────────── 内部:WebGL ─────────────────────────────\n\n private createContext(): WebGLRenderingContext | null {\n const canvas = this.refs?.canvas\n if (!canvas) return null\n const gl =\n (canvas.getContext('webgl', { alpha: false, antialias: false }) as WebGLRenderingContext | null) ??\n (canvas.getContext('experimental-webgl') as WebGLRenderingContext | null)\n if (!gl) return null\n this.gl = gl\n\n const program = this.buildProgram(gl)\n if (!program) {\n this.gl = null\n return null\n }\n this.program = program\n\n // 图片不是 2 次幂,只能 CLAMP_TO_EDGE;shader 在接缝处显式跨首尾做线性采样。\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false)\n gl.clearColor(0.02, 0.03, 0.05, 1)\n\n canvas.addEventListener('webglcontextlost', this.onContextLost)\n canvas.addEventListener('webglcontextrestored', this.onContextRestored)\n this.syncSize()\n return gl\n }\n\n private buildProgram(gl: WebGLRenderingContext): Program | null {\n const vertex = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER)\n const fragment = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER)\n if (!vertex || !fragment) return null\n const program = gl.createProgram()\n const buffer = gl.createBuffer()\n const texture = gl.createTexture()\n if (!program || !buffer || !texture) return null\n gl.attachShader(program, vertex)\n gl.attachShader(program, fragment)\n gl.linkProgram(program)\n gl.deleteShader(vertex)\n gl.deleteShader(fragment)\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n gl.deleteProgram(program)\n return null\n }\n gl.useProgram(program)\n\n // 两个三角形铺满裁剪空间;它们的 uv 由顶点着色器线性插值成屏幕坐标\n const quad = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1])\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer)\n gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW)\n const aPos = gl.getAttribLocation(program, 'aPos')\n gl.enableVertexAttribArray(aPos)\n gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0)\n\n gl.activeTexture(gl.TEXTURE0)\n gl.bindTexture(gl.TEXTURE_2D, texture)\n gl.uniform1i(gl.getUniformLocation(program, 'uTex'), 0)\n\n return {\n program,\n buffer,\n texture,\n uniforms: {\n texture: gl.getUniformLocation(program, 'uTex'),\n tanHalf: gl.getUniformLocation(program, 'uTanHalf'),\n aspect: gl.getUniformLocation(program, 'uAspect'),\n right: gl.getUniformLocation(program, 'uRight'),\n up: gl.getUniformLocation(program, 'uUp'),\n forward: gl.getUniformLocation(program, 'uForward'),\n thetaStart: gl.getUniformLocation(program, 'uThetaStart'),\n spanTheta: gl.getUniformLocation(program, 'uSpanTheta'),\n phiTop: gl.getUniformLocation(program, 'uPhiTop'),\n spanPhi: gl.getUniformLocation(program, 'uSpanPhi'),\n wrap: gl.getUniformLocation(program, 'uWrap'),\n textureWidth: gl.getUniformLocation(program, 'uTextureWidth'),\n },\n }\n }\n\n private disposeGL(): void {\n const gl = this.gl\n const program = this.program\n this.program = null\n this.gl = null\n if (!gl) return\n this.refs?.canvas.removeEventListener('webglcontextlost', this.onContextLost)\n this.refs?.canvas.removeEventListener('webglcontextrestored', this.onContextRestored)\n if (program) {\n gl.deleteTexture(program.texture)\n gl.deleteBuffer(program.buffer)\n gl.deleteProgram(program.program)\n }\n gl.getExtension('WEBGL_lose_context')?.loseContext()\n }\n\n private readonly onContextLost = (event: Event): void => {\n event.preventDefault()\n this.ready = false\n }\n\n private readonly onContextRestored = (): void => {\n this.ready = false\n this.program = null\n this.gl = null\n const gl = this.createContext()\n if (gl && this.image) this.uploadTexture(this.image)\n }\n\n /** 把图片上传成纹理并开始渲染 */\n private loadImage(url: string): void {\n this.ready = false\n const seq = ++this.loadSeq\n if (this.gl) {\n this.refs?.canvas.removeAttribute('hidden')\n this.setNote('正在把全景图贴上球面…')\n }\n const image = new Image()\n image.decoding = 'async'\n image.onload = () => {\n // 序号不匹配 = 打开期间换过图或已关闭,这次加载作废\n if (this.destroyed || !this.root || seq !== this.loadSeq) return\n this.image = image\n if (this.gl && this.program) {\n this.uploadTexture(image)\n this.refs?.note.setAttribute('hidden', '')\n } else {\n this.fallbackImage()\n }\n }\n image.onerror = () => {\n if (seq !== this.loadSeq) return\n this.setNote('全景图加载失败(地址可能已失效)。')\n }\n image.src = url\n }\n\n private uploadTexture(image: HTMLImageElement): void {\n const gl = this.gl\n const program = this.program\n if (!gl || !program) return\n gl.bindTexture(gl.TEXTURE_2D, program.texture)\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image)\n gl.useProgram(program.program)\n gl.uniform1f(program.uniforms.textureWidth, image.naturalWidth || image.width)\n this.ready = true\n this.invalidate()\n }\n\n /** 没有 WebGL 时的退路:直接把等距柱状长图铺出来(至少能看) */\n private fallbackImage(): void {\n const refs = this.refs\n if (!refs || !this.source) return\n refs.canvas.hidden = true\n const img = document.createElement('img')\n img.src = this.source\n img.alt = '全景图'\n img.style.cssText = 'position:absolute;inset:0;margin:auto;max-width:100%;max-height:100%;object-fit:contain;'\n refs.stage.appendChild(img)\n }\n\n private setNote(text: string): void {\n const refs = this.refs\n if (!refs) return\n refs.note.textContent = text\n refs.note.removeAttribute('hidden')\n }\n\n private syncSize(): void {\n const canvas = this.refs?.canvas\n const stage = this.refs?.stage\n if (!canvas || !stage) return\n const dpr = Math.min(typeof window === 'undefined' ? 1 : window.devicePixelRatio || 1, 2)\n const width = Math.max(1, Math.round(stage.clientWidth * dpr))\n const height = Math.max(1, Math.round(stage.clientHeight * dpr))\n if (canvas.width === width && canvas.height === height) return\n canvas.width = width\n canvas.height = height\n this.invalidate()\n }\n\n private invalidate(): void {\n this.dirty = true\n this.schedule()\n }\n\n private schedule(): void {\n if (this.rafId || this.destroyed) return\n if (this.refs && this.root?.hidden) return\n this.rafId = window.requestAnimationFrame(this.onFrame)\n }\n\n private readonly onFrame = (): void => {\n this.rafId = 0\n const v = this.velocity\n if (Math.abs(v.yaw) > INERTIA_MIN || Math.abs(v.pitch) > INERTIA_MIN) {\n this.yaw += v.yaw\n this.pitch = clamp(this.pitch + v.pitch, -MAX_PITCH_DEG, MAX_PITCH_DEG)\n v.yaw *= INERTIA_DECAY\n v.pitch *= INERTIA_DECAY\n this.dirty = true\n this.schedule()\n } else {\n v.yaw = 0\n v.pitch = 0\n }\n if (!this.dirty) return\n this.dirty = false\n this.draw()\n }\n\n private draw(): void {\n const gl = this.gl\n const program = this.program\n const canvas = this.refs?.canvas\n if (!gl || !program || !canvas) return\n if (!canvas.width || !canvas.height) return\n gl.viewport(0, 0, canvas.width, canvas.height)\n gl.clear(gl.COLOR_BUFFER_BIT)\n if (!this.ready) {\n this.updateHud()\n return\n }\n const basis = cameraBasis(this.yaw, this.pitch)\n const u = program.uniforms\n gl.uniform1f(u.tanHalf, Math.tan((this.fov * DEG) / 2))\n gl.uniform1f(u.aspect, canvas.width / canvas.height)\n gl.uniform3f(u.right, basis.right[0], basis.right[1], basis.right[2])\n gl.uniform3f(u.up, basis.up[0], basis.up[1], basis.up[2])\n gl.uniform3f(u.forward, basis.forward[0], basis.forward[1], basis.forward[2])\n gl.uniform1f(u.thetaStart, this.projection.thetaStart)\n gl.uniform1f(u.spanTheta, this.projection.spanTheta)\n gl.uniform1f(u.phiTop, this.projection.phiTop)\n gl.uniform1f(u.spanPhi, this.projection.spanPhi)\n gl.uniform1f(u.wrap, this.projection.wrapsTheta ? 1 : 0)\n gl.drawArrays(gl.TRIANGLES, 0, 6)\n this.updateHud()\n }\n\n private updateHud(): void {\n const hud = this.refs?.hud\n if (!hud) return\n hud.textContent =\n `方位 ${Math.round(normalizeDeg(this.yaw))}° · 仰角 ${Math.round(this.pitch)}° · ` +\n `视野 ${Math.round(this.fov)}°`\n }\n\n // ───────────────────────────── 内部:交互 ─────────────────────────────\n\n /** 用户从本地选了一张全景图:转 blob 地址喂给查看器(旧导入的地址一并释放) */\n private readonly onImportFile = (event: Event): void => {\n const input = event.target as HTMLInputElement\n const file = input.files?.[0]\n input.value = '' // 允许下次再选同一张(change 不触发的问题)\n if (!file) return\n if (this.importUrl) URL.revokeObjectURL(this.importUrl)\n this.importUrl = URL.createObjectURL(file)\n this.importName = file.name\n this.open(this.importUrl, file.name)\n }\n\n private readonly onClick = (event: MouseEvent): void => {\n const target = event.target as HTMLElement | null\n const actionEl = target?.closest<HTMLElement>('[data-action]')\n switch (actionEl?.dataset.action) {\n case 'close':\n this.close()\n return\n case 'reset':\n this.reset()\n return\n case 'download':\n this.download()\n return\n case 'import':\n this.refs?.file.click()\n return\n default:\n break\n }\n // 只有点遮罩(而不是画面)才关,免得想细看的人一拖就被弹出去\n if (target === this.root || target === this.refs?.mask) this.close()\n }\n\n private readonly onPointerDown = (event: PointerEvent): void => {\n const target = event.target as HTMLElement | null\n if (target?.closest('[data-action]')) return\n this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY })\n try {\n this.refs?.stage.setPointerCapture(event.pointerId)\n } catch {\n /* 不支持 pointer capture 的环境忽略 */\n }\n this.velocity.yaw = 0\n this.velocity.pitch = 0\n if (this.pointers.size === 1) {\n this.lastPointer = { x: event.clientX, y: event.clientY }\n } else {\n this.lastPointer = null\n this.pinchDistance = this.currentPinchDistance()\n }\n this.refs?.stage.setAttribute('data-dragging', 'true')\n }\n\n private readonly onPointerMove = (event: PointerEvent): void => {\n if (!this.pointers.has(event.pointerId)) return\n const previous = this.pointers.get(event.pointerId)\n this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY })\n\n if (this.pointers.size >= 2) {\n // 双指:距离变化 = 缩放视野,中点位移 = 继续平移\n const distance = this.currentPinchDistance()\n if (this.pinchDistance > 0 && distance > 0) {\n this.zoomBy(this.pinchDistance / distance)\n }\n this.pinchDistance = distance\n if (previous) {\n this.rotateBy((event.clientX - previous.x) * 0.5, (event.clientY - previous.y) * 0.5, true)\n }\n return\n }\n\n const last = this.lastPointer\n if (!last) return\n const dx = event.clientX - last.x\n const dy = event.clientY - last.y\n this.lastPointer = { x: event.clientX, y: event.clientY }\n this.rotateBy(dx, dy, false)\n }\n\n private readonly onPointerUp = (event: PointerEvent): void => {\n if (!this.pointers.has(event.pointerId)) return\n this.pointers.delete(event.pointerId)\n if (this.pointers.size === 1) {\n const remaining = this.pointers.values().next().value\n this.lastPointer = remaining ? { x: remaining.x, y: remaining.y } : null\n this.pinchDistance = 0\n } else if (this.pointers.size === 0) {\n this.lastPointer = null\n this.pinchDistance = 0\n this.refs?.stage.removeAttribute('data-dragging')\n }\n }\n\n private readonly onWheel = (event: WheelEvent): void => {\n event.preventDefault()\n this.velocity.yaw = 0\n this.velocity.pitch = 0\n // 向上滚(deltaY < 0)= 视野收窄 = 放大;指数映射让每一格滚轮的\"倍率\"恒定\n this.zoomBy(Math.exp(event.deltaY * WHEEL_ZOOM_RATE))\n }\n\n private readonly onDoubleClick = (event: MouseEvent): void => {\n const target = event.target as HTMLElement | null\n if (target?.closest('[data-action]')) return\n this.reset()\n }\n\n private readonly onKeyDown = (event: KeyboardEvent): void => {\n if (this.destroyed || !this.root || this.root.hidden) return\n if (event.ctrlKey || event.metaKey || event.altKey) return\n const step = event.shiftKey ? 15 : 5\n switch (event.key) {\n case 'Escape':\n event.preventDefault()\n this.close()\n break\n case 'ArrowLeft':\n event.preventDefault()\n this.yaw -= step\n this.invalidate()\n break\n case 'ArrowRight':\n event.preventDefault()\n this.yaw += step\n this.invalidate()\n break\n case 'ArrowUp':\n event.preventDefault()\n this.pitch = clamp(this.pitch + step, -MAX_PITCH_DEG, MAX_PITCH_DEG)\n this.invalidate()\n break\n case 'ArrowDown':\n event.preventDefault()\n this.pitch = clamp(this.pitch - step, -MAX_PITCH_DEG, MAX_PITCH_DEG)\n this.invalidate()\n break\n case '+':\n case '=':\n event.preventDefault()\n this.zoomBy(0.85)\n break\n case '-':\n case '_':\n event.preventDefault()\n this.zoomBy(1 / 0.85)\n break\n case '0':\n case 'Home':\n event.preventDefault()\n this.reset()\n break\n default:\n break\n }\n // 独占键盘:面板的键盘飞行挂在冒泡阶段,这里在捕获阶段截住\n event.stopPropagation()\n }\n\n /**\n * 拖动换算:一屏高恰好对应一个垂直视野,于是\"每像素转多少度\"= fov / 画布高。\n * 视野越小(放得越大)拖得越慢,手感与真机一致。\n * 符号按\"抓住画面拖\"约定:右拖 = 画面右移 = 视线左转,下拖 = 画面下移 = 视线抬高。\n */\n private rotateBy(dx: number, dy: number, continuous: boolean): void {\n const height = this.refs?.stage.clientHeight ?? 0\n if (!height) return\n const perPixel = this.fov / height\n const deltaYaw = -dx * perPixel\n const deltaPitch = dy * perPixel\n this.yaw += deltaYaw\n this.pitch = clamp(this.pitch + deltaPitch, -MAX_PITCH_DEG, MAX_PITCH_DEG)\n if (!continuous) {\n // 惯性:松手后按最后一次的帧增量继续滑一小段(拖动中始终覆盖成最新值)\n this.velocity.yaw = deltaYaw\n this.velocity.pitch = deltaPitch\n }\n this.invalidate()\n }\n\n private zoomBy(factor: number): void {\n if (!Number.isFinite(factor) || factor <= 0) return\n this.fov = clamp(this.fov * factor, this.minFov, this.maxFov)\n this.invalidate()\n }\n\n private currentPinchDistance(): number {\n const points = [...this.pointers.values()]\n if (points.length < 2) return 0\n const [a, b] = points\n if (!a || !b) return 0\n return Math.hypot(a.x - b.x, a.y - b.y)\n }\n\n // ───────────────────────────── 内部:杂项 ─────────────────────────────\n\n private lockScroll(): void {\n if (typeof document === 'undefined') return\n this.bodyOverflow = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n }\n\n private unlockScroll(): void {\n if (typeof document === 'undefined') return\n document.body.style.overflow = this.bodyOverflow\n }\n\n private slug(value: string): string {\n const cleaned = value.replace(/[^\\w.-]+/g, '-').replace(/^-+|-+$/g, '')\n return cleaned || 'panorama'\n }\n\n private query<T extends HTMLElement = HTMLElement>(selector: string): T {\n const root = this.root\n if (!root) throw new Error('[DronePanoramaViewer] 弹窗尚未挂载')\n const el = root.querySelector<T>(selector)\n if (!el) throw new Error(`[DronePanoramaViewer] 缺少节点:${selector}`)\n return el\n }\n\n private template(): string {\n const download = this.options.downloadable === false ? ' hidden' : ''\n const importer = this.options.importable ? '' : ' hidden'\n return `\n <div class=\"divp-mask\" data-role=\"mask\" data-action=\"close\"></div>\n <div class=\"divp-frame\">\n <div class=\"divp-bar\">\n <span class=\"divp-title\" data-role=\"title\">全景图</span>\n <span class=\"divp-hud\" data-role=\"hud\"></span>\n <span class=\"divp-spacer\"></span>\n <button type=\"button\" class=\"divp-btn\" data-action=\"reset\" title=\"视角复位 (0)\">复位</button>\n <button type=\"button\" class=\"divp-btn\" data-action=\"import\" title=\"从本地导入等距柱状全景图\"${importer}>导入图片</button>\n <button type=\"button\" class=\"divp-btn\" data-action=\"download\" title=\"下载原图\"${download}>下载原图</button>\n <button type=\"button\" class=\"divp-icon\" data-action=\"close\" title=\"关闭 (Esc)\" aria-label=\"关闭\">×</button>\n </div>\n <div class=\"divp-stage\" data-role=\"stage\">\n <canvas class=\"divp-canvas\" data-role=\"canvas\"></canvas>\n <p class=\"divp-note\" data-role=\"note\" hidden></p>\n </div>\n <div class=\"divp-foot\">拖动环视球面 · 滚轮 / 双指缩放 · 方向键微调(Shift 加速) · 双击或 0 复位 · Esc 关闭</div>\n </div>\n <input type=\"file\" accept=\"image/*\" data-role=\"file\" hidden />\n `\n }\n}\n\n/** 编译单个着色器;失败时返回 null(调用方回退到平面图),并把编译日志打到控制台便于定位 */\nfunction compileShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | null {\n const shader = gl.createShader(type)\n if (!shader) return null\n gl.shaderSource(shader, source)\n gl.compileShader(shader)\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.warn('[DronePanoramaViewer] 着色器编译失败:', gl.getShaderInfoLog(shader))\n gl.deleteShader(shader)\n return null\n }\n return shader\n}\n","/**\n * 影像集弹窗(原生 DOM):把整个影像集从采集面板里**搬出来单独看** —— 按「组合」分组、\n * 每组一键合成、点缩略图看大图。\n *\n * 为什么要独立成弹窗:采集面板只有 300 多像素宽,缩略图、统计、详情、导出全塞在里面,\n * 既要滚动又看不见全局;而\"翻影像集\"本身就是一件需要大面积的事。\n * 于是面板只留**动作**(拍 / 停 / 导入 / 导出),影像的**浏览与合成**归这里。\n *\n * 三处刻意的设计:\n *\n * 1. **挂 `document.body`,不做面板的后代** —— 面板带 `backdrop-filter`,\n * 会为 `position: fixed` 的后代创建新的包含块(弹窗会被裁在面板里)。\n * 同一坑在悬浮摇杆层与大图查看器上各踩过一次。\n *\n * 2. **不同组合各自成块**。广角、180° 与球形 360° 扫描是同一次会话里\n * 来源完全不同的两批数据:混在一格里既看不出归属,也没法单独合成。\n * 分块后每组有自己的标题、色标、张数、是否拍完与「一键合成」。\n *\n * 3. **键盘在 window 捕获阶段接管,且上层弹窗优先**。控制面板的键盘飞行挂在冒泡阶段,\n * 弹窗开着时 WASD / 方向键必须被截住;而球面全景查看器 / 大图查看器盖在本弹窗之上时\n * (层级更高),本弹窗的按键处理**直接让位**,否则 Esc 会先关掉下面这层、把上面的弹窗留在屏幕上。\n *\n * 4. **合成结果默认落到球面查看器**,不是平面大图。等距柱状图是全景的**存储格式**,\n * 平铺在屏幕上看不出\"四面八方\";点「360° 环视」把同一张图贴回球面拖动环视 ——\n * 与真机相册打开球面全景是同一件事(见 drone-panorama-viewer)。\n *\n * ```ts\n * const gallery = new DroneGallery(service)\n * gallery.open() // 打开(缺省看当前会话)\n * gallery.stitch(groupKey) // 程序化触发某组合成\n * gallery.close() // 关闭;destroy() 彻底卸载\n * ```\n */\nimport type { CaptureService, CaptureSession } from './drone-capture'\nimport type { ImageGroup } from './drone-image-utils'\nimport {\n escapeHtml,\n formatBytes,\n groupSessionImages,\n imageTileLabel,\n createOriginalImagesZip,\n triggerDownload,\n VALIDATION_BADGES,\n VALIDATION_LABELS,\n} from './drone-image-utils'\nimport type { PanoramaStitchResult, PanoramaStitcher } from './drone-panorama'\nimport { createEquirectStitcher } from './drone-stitch'\nimport { DroneImageViewer } from './drone-image-viewer'\nimport type { DroneImageViewerOptions } from './drone-image-viewer'\nimport { DronePanoramaViewer } from './drone-panorama-viewer'\nimport type { DronePanoramaViewerOptions } from './drone-panorama-viewer'\n\n/** 影像集弹窗构造选项 */\nexport interface DroneGalleryOptions {\n /** 是否注入自带样式,默认 true */\n injectStyles?: boolean\n /** 弹窗层级,默认 900(大图查看器会自动排在它之上) */\n zIndex?: number\n /** 标题,默认「影像集」 */\n title?: string\n /**\n * 大图查看器:点缩略图时打开。不传则本弹窗自己建一个(并在销毁时一并卸载);\n * `viewerEnabled: false` 可整块关掉\"点缩略图看大图\"。\n */\n viewer?: DroneImageViewer | null\n /** 是否启用\"点缩略图看大图\",默认 true(显式传了 `viewer` 时以传入的为准) */\n viewerEnabled?: boolean\n /** 建自带查看器时透传的细项 */\n viewerOptions?: DroneImageViewerOptions\n /**\n * 全景查看器:点合成结果时打开,**把等距柱状图贴回球面拖动环视**(真机相册的体验)。\n * 不传则本弹窗自己建一个(并在销毁时一并卸载);`panoramaViewerEnabled: false` 关掉它\n * (那时合成结果仍可下载,只是不再有球面浏览)。\n */\n panoramaViewer?: DronePanoramaViewer | null\n /** 是否启用球面全景查看,默认 true(显式传了 `panoramaViewer` 时以传入的为准) */\n panoramaViewerEnabled?: boolean\n /** 建自带全景查看器时透传的细项 */\n panoramaViewerOptions?: DronePanoramaViewerOptions\n /**\n * 合成器。「一键合成」用它把一组全景照片拼成一张图;\n * 不传 = 库内自带的等距柱状拼接(见 drone-stitch);传 `null` = 关掉合成按钮。\n */\n stitcher?: PanoramaStitcher | null\n /** 关闭回调(点 × / 点遮罩 / Esc) */\n onClose?: () => void\n /** 某组合成结束的回调(失败时 result 为 null) */\n onStitched?: (group: ImageGroup, result: PanoramaStitchResult | null) => void\n}\n\n/** 合成器里可能带的进度回调(库内拼接器有,宿主自建的可选) */\ntype ProgressAwareStitcher = PanoramaStitcher & {\n onProgress?: ((done: number, total: number) => void) | null\n}\n\n/** 单组的合成状态 */\ninterface StitchState {\n status: 'running' | 'done' | 'error'\n done: number\n total: number\n message?: string\n result?: PanoramaStitchResult | null\n}\n\nconst STYLE_ID = 'drone-gallery-styles'\nconst DEFAULT_Z_INDEX = 900\n/** 每组最多渲染这么多张缩略图(再多只报数),避免长全景解码过多原尺寸照片拖垮页面 */\nconst MAX_THUMBS_PER_GROUP = 24\n/** 分组色标的色相起点与步进(不同组合一眼能分开) */\nconst HUE_BASE = 205\nconst HUE_STEP = 47\n\nconst GALLERY_CSS = `\n.dgallery {\n --dg-text: #e9eff9;\n --dg-muted: rgba(233, 239, 249, 0.58);\n --dg-border: rgba(255, 255, 255, 0.12);\n --dg-accent: #35a7ff;\n --dg-ok: #37d67a;\n --dg-warn: #f5a623;\n --dg-bad: #ff5f5f;\n font: 13px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n user-select: none;\n -webkit-user-select: none;\n -webkit-tap-highlight-color: transparent;\n}\n.dgallery {\n position: fixed; inset: 0; z-index: ${DEFAULT_Z_INDEX};\n display: flex; align-items: center; justify-content: center;\n padding: max(10px, env(safe-area-inset-top)) 10px max(10px, env(safe-area-inset-bottom));\n color: var(--dg-text);\n outline: none;\n}\n.dgallery[hidden] { display: none; }\n.dgallery-mask {\n position: absolute; inset: 0; background: rgba(6, 9, 14, 0.86);\n -webkit-backdrop-filter: blur(7px); backdrop-filter: blur(7px);\n}\n.dgallery-frame {\n position: relative; z-index: 1;\n display: flex; flex-direction: column;\n width: min(100%, 1180px); height: 100%; overflow: hidden;\n border: 1px solid var(--dg-border); border-radius: 12px;\n background: rgba(15, 19, 27, 0.95);\n box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);\n}\n.dgallery-bar {\n flex: 0 0 auto; display: flex; align-items: center; gap: 8px;\n padding: 8px 10px; border-bottom: 1px solid var(--dg-border);\n}\n.dgallery-title { font-weight: 600; }\n.dgallery-session {\n max-width: 200px; min-height: 26px; padding: 2px 6px; cursor: pointer;\n border: 1px solid var(--dg-border); border-radius: 7px;\n background: rgba(255, 255, 255, 0.06); color: var(--dg-text); font: inherit;\n}\n.dgallery-stats { color: var(--dg-muted); font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.dgallery-stats .is-ok { color: var(--dg-ok); }\n.dgallery-stats .is-warn { color: var(--dg-warn); }\n.dgallery-stats .is-bad { color: var(--dg-bad); }\n.dgallery-spacer { flex: 1; min-width: 4px; }\n.dgallery-icon {\n flex: 0 0 auto; width: 26px; height: 26px; padding: 0; cursor: pointer;\n border: 1px solid var(--dg-border); border-radius: 7px;\n background: rgba(255, 255, 255, 0.06); color: var(--dg-text); font: inherit; line-height: 1;\n}\n.dgallery-icon:hover { background: rgba(255, 255, 255, 0.14); }\n.dgallery-chips {\n flex: 0 0 auto; display: flex; gap: 6px; align-items: center;\n padding: 6px 10px; overflow-x: auto; border-bottom: 1px solid var(--dg-border);\n}\n.dgallery-chips[hidden] { display: none; }\n.dgallery-chip {\n flex: 0 0 auto; display: inline-flex; align-items: center; gap: 5px;\n min-height: 24px; padding: 2px 8px; cursor: pointer; white-space: nowrap;\n border: 1px solid var(--dg-border); border-radius: 999px;\n background: rgba(255, 255, 255, 0.05); color: var(--dg-text); font: inherit; font-size: 12px;\n}\n.dgallery-chip:hover { background: rgba(255, 255, 255, 0.12); }\n.dgallery-chip i { width: 8px; height: 8px; border-radius: 50%; background: var(--dg-group, var(--dg-accent)); }\n.dgallery-body {\n flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;\n padding: 10px; -webkit-overflow-scrolling: touch;\n}\n.dgallery-group { margin-bottom: 16px; }\n.dgallery-group:last-child { margin-bottom: 4px; }\n.dgallery-group-head {\n display: flex; align-items: center; gap: 8px; flex-wrap: wrap;\n padding: 7px 9px; margin-bottom: 8px; border-radius: 9px;\n border: 1px solid var(--dg-border);\n border-left: 3px solid var(--dg-group, var(--dg-accent));\n background: rgba(255, 255, 255, 0.035);\n}\n.dgallery-group-title { font-weight: 600; }\n.dgallery-group-sub { color: var(--dg-muted); font-size: 11px; font-variant-numeric: tabular-nums; }\n.dgallery-group-sub .is-warn { color: var(--dg-warn); }\n.dgallery-group-sub .is-ok { color: var(--dg-ok); }\n.dgallery-btn {\n min-height: 26px; padding: 3px 10px; cursor: pointer; white-space: nowrap;\n border: 1px solid var(--dg-border); border-radius: 8px;\n background: rgba(255, 255, 255, 0.06); color: var(--dg-text); font: inherit; font-size: 12px;\n}\n.dgallery-btn:hover { background: rgba(255, 255, 255, 0.13); }\n.dgallery-btn[disabled] { opacity: 0.5; cursor: not-allowed; }\n.dgallery-btn.dgallery-primary { background: var(--dg-accent); border-color: transparent; color: #04121f; font-weight: 600; }\n.dgallery-btn.dgallery-on { background: var(--dg-bad); border-color: transparent; color: #fff; font-weight: 600; }\n.dgallery-thumbs {\n display: grid; grid-template-columns: repeat(auto-fill, minmax(94px, 1fr)); gap: 6px;\n}\n.dgallery-thumb {\n position: relative; aspect-ratio: 4 / 3; padding: 0; overflow: hidden; cursor: pointer;\n border: 1px solid var(--dg-border); border-radius: 7px; background: rgba(255, 255, 255, 0.04);\n}\n.dgallery-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }\n.dgallery-thumb[data-selected=\"true\"] { border-color: var(--dg-accent); box-shadow: 0 0 0 1px var(--dg-accent); }\n.dgallery-thumb[data-status=\"warning\"] { border-color: var(--dg-warn); }\n.dgallery-thumb[data-status=\"invalid\"] { border-color: var(--dg-bad); }\n.dgallery-badge {\n position: absolute; left: 3px; top: 3px; min-width: 14px; height: 14px; padding: 0 3px;\n border-radius: 4px; font-size: 10px; line-height: 14px; text-align: center; font-weight: 700;\n background: rgba(0, 0, 0, 0.6); color: #fff;\n}\n.dgallery-thumb[data-status=\"valid\"] .dgallery-badge { background: var(--dg-ok); color: #04140a; }\n.dgallery-thumb[data-status=\"warning\"] .dgallery-badge { background: var(--dg-warn); color: #241500; }\n.dgallery-thumb[data-status=\"invalid\"] .dgallery-badge { background: var(--dg-bad); }\n.dgallery-tag {\n position: absolute; left: 0; right: 0; bottom: 0; padding: 1px 4px;\n font-size: 9px; line-height: 14px; color: #fff; font-variant-numeric: tabular-nums;\n background: linear-gradient(to top, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0));\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n.dgallery-note { margin: 6px 0 0; color: var(--dg-muted); font-size: 11px; }\n.dgallery-note.is-warn { color: var(--dg-warn); }\n.dgallery-note[hidden] { display: none; }\n.dgallery-card {\n display: flex; gap: 10px; align-items: flex-start; margin-top: 8px; padding: 8px;\n border: 1px solid rgba(53, 167, 255, 0.5); border-radius: 9px;\n background: rgba(53, 167, 255, 0.08);\n}\n.dgallery-card[hidden] { display: none; }\n.dgallery-card-img {\n flex: 0 0 auto; max-width: 46%; max-height: 150px; min-width: 80px;\n border-radius: 6px; background: #05070b; object-fit: contain; display: block; cursor: zoom-in;\n}\n.dgallery-card-side { flex: 1; min-width: 0; }\n.dgallery-card-title { font-weight: 600; }\n.dgallery-card-meta { color: var(--dg-muted); font-size: 11px; margin: 2px 0 7px; }\n.dgallery-card-actions { display: flex; gap: 6px; flex-wrap: wrap; }\n.dgallery-diagnostic { margin-top: 8px; border: 1px solid var(--dg-border); border-radius: 8px; }\n.dgallery-diagnostic summary { padding: 7px 9px; cursor: pointer; color: var(--dg-text); }\n.dgallery-diagnostic-content { padding: 0 9px 9px; }\n.dgallery-diagnostic-help { margin: 0 0 8px; color: var(--dg-muted); font-size: 12px; }\n.dgallery-diagnostic-img {\n display: block; width: 100%; max-height: 58vh; object-fit: contain;\n background: #080b10; border-radius: 6px;\n}\n.dgallery-diagnostic-legend { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 8px 0 0; padding: 0; list-style: none; }\n.dgallery-diagnostic-legend li { display: flex; align-items: center; gap: 6px; color: var(--dg-muted); font-size: 11px; }\n.dgallery-diagnostic-swatch { width: 12px; height: 12px; flex: 0 0 auto; border-radius: 3px; background: var(--dg-shot-color); }\n.dgallery-foot {\n flex: 0 0 auto; padding: 6px 10px; border-top: 1px solid var(--dg-border);\n color: var(--dg-muted); font-size: 11px;\n}\n@media (max-height: 560px) {\n .dgallery-thumbs { grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); }\n .dgallery-card-img { max-height: 96px; }\n}\n@media (pointer: coarse) {\n .dgallery-icon { width: 32px; height: 32px; }\n .dgallery-btn { min-height: 32px; }\n}\n`\n\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return\n if (document.getElementById(STYLE_ID)) return\n const style = document.createElement('style')\n style.id = STYLE_ID\n style.textContent = GALLERY_CSS\n document.head.appendChild(style)\n}\n\ninterface GalleryRefs {\n mask: HTMLElement\n sessionSelect: HTMLSelectElement\n stats: HTMLElement\n chips: HTMLElement\n body: HTMLElement\n downloadStatus: HTMLElement\n}\n\n/**\n * 影像集弹窗:按组合分组的影像浏览 + 每组一键合成。\n *\n * DOM **懒挂载**:构造只记住服务与选项,首次 `open()` 才建节点、注入样式、订阅服务与键盘;\n * `close()` 把整棵 DOM 拆掉(不是藏起来),于是\"没打开\"时它对本页零影响。\n */\nexport class DroneGallery {\n private readonly service: CaptureService\n private readonly options: DroneGalleryOptions\n private readonly stitcher: ProgressAwareStitcher | null\n private root: HTMLDivElement | null = null\n private refs: GalleryRefs | null = null\n private unsubscribe: (() => void) | null = null\n private viewer: DroneImageViewer | null = null\n /** viewer 是本弹窗自己建的(销毁时要一并销毁;宿主传进来的不碰) */\n private ownViewer = false\n private panorama: DronePanoramaViewer | null = null\n /** panorama 同上:只有自己建的才由自己销毁 */\n private ownPanorama = false\n private sessionId: string | null = null\n private selectedId: string | null = null\n /** 各组的合成状态(按分组键);合成结果不是会话数据,只活在弹窗生命周期里 */\n private readonly stitches = new Map<string, StitchState>()\n /** 当前正在播报进度的组(拼接器只有一个,逐组串行调用) */\n private progressKey: string | null = null\n private bodyOverflow = ''\n private destroyed = false\n private downloadInProgress = false\n\n /** 构造影像集弹窗:网格相册 + 大图 + 360° 环视入口 */\n constructor(service: CaptureService, options: DroneGalleryOptions = {}) {\n this.service = service\n this.options = options\n this.stitcher =\n options.stitcher === undefined\n ? createEquirectStitcher()\n : (options.stitcher as ProgressAwareStitcher | null)\n if (this.stitcher) {\n this.stitcher.onProgress = (done, total) => {\n const key = this.progressKey\n if (!key) return\n const state = this.stitches.get(key)\n if (!state || state.status !== 'running') return\n state.done = done\n state.total = total\n this.renderStitch(key)\n }\n }\n }\n\n /** 弹窗当前是否打开 */\n get isOpen(): boolean {\n return this.root !== null && !this.root.hidden\n }\n\n /** 打开弹窗;`sessionId` 缺省用采集服务里当前活跃的会话 */\n open(sessionId?: string): void {\n if (this.destroyed) return\n this.sessionId = sessionId ?? this.service.activeSession?.id ?? null\n this.mount()\n const root = this.root\n if (!root) return\n root.hidden = false\n this.lockScroll()\n this.render()\n root.focus()\n }\n\n /** 关闭弹窗(并把整棵 DOM / 键盘监听 / 两个子弹窗一并收走) */\n close(): void {\n if (!this.root) return\n const wasOpen = !this.root.hidden\n this.panorama?.close()\n this.viewer?.close()\n this.unmount()\n if (wasOpen) this.options.onClose?.()\n }\n\n /** 重新渲染当前会话的分组视图(宿主改了服务状态后可主动调用) */\n refresh(): void {\n if (!this.isOpen) return\n this.render()\n }\n\n /** 关闭球面全景查看器(没开时是空操作) */\n closePanorama(): void {\n this.panorama?.close()\n }\n\n /** 程序化合成某一组(与点「一键合成」同一条路径);已在合成中则忽略 */\n async stitch(groupKey: string): Promise<PanoramaStitchResult | null> {\n const session = this.resolveSession()\n const group = session ? groupSessionImages(session.images).find((g) => g.key === groupKey) : null\n if (!group || group.kind !== 'panorama') return null\n const current = this.stitches.get(groupKey)\n if (current?.status === 'running') return null\n if (!this.stitcher) return null\n\n this.stitches.set(groupKey, { status: 'running', done: 0, total: group.images.length })\n this.progressKey = groupKey\n this.renderStitch(groupKey)\n let result: PanoramaStitchResult | null = null\n try {\n result = await this.stitcher.stitch({\n id: group.key,\n mode: (group.mode ?? 'pano-180') as 'pano-wide' | 'pano-180' | 'pano-360',\n sessionId: session?.id ?? '',\n images: group.images,\n })\n } catch (error) {\n this.stitches.set(groupKey, {\n status: 'error',\n done: 0,\n total: group.images.length,\n message: `合成失败:${(error as Error).message}`,\n })\n this.progressKey = null\n this.renderStitch(groupKey)\n this.options.onStitched?.(group, null)\n return null\n }\n this.progressKey = null\n\n if (!result) {\n this.stitches.set(groupKey, {\n status: 'error',\n done: 0,\n total: group.images.length,\n message: '这组没法合成(可用影像不足,或图片没有可读的预览地址)',\n })\n } else {\n this.stitches.set(groupKey, {\n status: 'done',\n done: group.images.length,\n total: group.images.length,\n result,\n })\n }\n this.renderStitch(groupKey)\n this.options.onStitched?.(group, result)\n return result\n }\n\n /** 依次合成当前会话里所有的全景组 */\n async stitchAll(): Promise<void> {\n const session = this.resolveSession()\n if (!session) return\n const groups = groupSessionImages(session.images).filter((group) => group.kind === 'panorama')\n for (const group of groups) await this.stitch(group.key)\n }\n\n /** 关闭并移除弹窗(含嵌套的大图/全景查看器) */\n destroy(): void {\n if (this.destroyed) return\n this.destroyed = true\n // ⚠️ 顺序即正确性:两个子弹窗各自锁过 body 的 overflow,解锁必须**后锁的先解**\n // (全景查看器 / 大图查看器都盖在本弹窗之上,它们保存的\"上一层值\"才是 'hidden'),\n // 先拆它们再拆自己,bodyOverflow 才会一路回退到进场前的空串。反序会残留 overflow:hidden。\n this.panorama?.close()\n this.viewer?.close()\n if (this.ownPanorama) this.panorama?.destroy()\n if (this.ownViewer) this.viewer?.destroy()\n this.panorama = null\n this.viewer = null\n this.unmount()\n if (this.options.injectStyles !== false) document.getElementById(STYLE_ID)?.remove()\n }\n\n // ───────────────────────────── 内部:生命周期 ─────────────────────────────\n\n private resolveSession(): CaptureSession | null {\n if (this.sessionId) {\n const session = this.service.getSession(this.sessionId)\n if (session) return session\n }\n return this.service.activeSession\n }\n\n private mount(): void {\n if (this.root || typeof document === 'undefined') return\n if (this.options.injectStyles !== false) ensureStyles()\n\n const root = document.createElement('div')\n root.className = 'dgallery'\n root.hidden = true\n root.tabIndex = -1\n root.setAttribute('role', 'dialog')\n root.setAttribute('aria-modal', 'true')\n root.setAttribute('aria-label', this.options.title ?? '影像集')\n if (this.options.zIndex && this.options.zIndex !== DEFAULT_Z_INDEX) {\n root.style.zIndex = String(this.options.zIndex)\n }\n root.innerHTML = this.template()\n document.body.appendChild(root)\n this.root = root\n this.refs = {\n mask: this.query('[data-role=\"mask\"]'),\n sessionSelect: this.query<HTMLSelectElement>('[data-role=\"session\"]'),\n stats: this.query('[data-role=\"stats\"]'),\n chips: this.query('[data-role=\"chips\"]'),\n body: this.query('[data-role=\"body\"]'),\n downloadStatus: this.query('[data-role=\"download-status\"]'),\n }\n\n if (this.options.viewer) {\n this.viewer = this.options.viewer\n this.ownViewer = false\n } else if (this.options.viewerEnabled !== false) {\n // 层级排在影像集之上:大图弹窗盖住图集,Esc 先关大图(见模块头注释 3)\n this.viewer = new DroneImageViewer(this.service, {\n ...this.options.viewerOptions,\n zIndex: (this.options.zIndex ?? DEFAULT_Z_INDEX) + 100,\n onNavigate: (image, index) => {\n this.selectedId = image.id\n this.syncSelection()\n this.options.viewerOptions?.onNavigate?.(image, index)\n },\n })\n this.ownViewer = true\n }\n\n if (this.options.panoramaViewer) {\n this.panorama = this.options.panoramaViewer\n this.ownPanorama = false\n } else if (this.options.panoramaViewerEnabled !== false) {\n // 层级排在影像集(与大图弹窗)之上:球面浏览是全屏独占地看一张图,Esc 先关它\n this.panorama = new DronePanoramaViewer({\n ...this.options.panoramaViewerOptions,\n zIndex: (this.options.zIndex ?? DEFAULT_Z_INDEX) + 200,\n })\n this.ownPanorama = true\n }\n\n root.addEventListener('click', this.onClick)\n this.refs.sessionSelect.addEventListener('change', this.onSessionChange)\n window.addEventListener('keydown', this.onKeyDown, { capture: true })\n this.unsubscribe = this.service.subscribe(() => this.render())\n }\n\n private unmount(): void {\n window.removeEventListener('keydown', this.onKeyDown, { capture: true })\n this.unsubscribe?.()\n this.unsubscribe = null\n this.root?.remove()\n this.root = null\n this.refs = null\n this.unlockScroll()\n }\n\n // ───────────────────────────── 内部:渲染 ─────────────────────────────\n\n private render(): void {\n const refs = this.refs\n const root = this.root\n if (!root || !refs || root.hidden) return\n const session = this.resolveSession()\n\n // 会话选择器\n const sessions = this.service.listSessions()\n const signature = sessions.map((item) => `${item.id}:${item.options.name}`).join('|')\n if (refs.sessionSelect.dataset.signature !== signature) {\n refs.sessionSelect.dataset.signature = signature\n refs.sessionSelect.innerHTML = sessions\n .map(\n (item) =>\n `<option value=\"${escapeHtml(item.id)}\">${escapeHtml(item.options.name)} · ${item.images.length}</option>`,\n )\n .join('')\n } else {\n const options = refs.sessionSelect.options\n sessions.forEach((item, i) => {\n const option = options[i]\n if (!option) return\n const text = `${item.options.name} · ${item.images.length}`\n if (option.textContent !== text) option.textContent = text\n })\n }\n if (session) refs.sessionSelect.value = session.id\n\n const images = session?.images ?? []\n const downloadButton = root.querySelector<HTMLButtonElement>(\n '[data-action=\"download-originals\"]',\n )\n if (downloadButton) {\n downloadButton.disabled = images.length === 0 || this.downloadInProgress\n downloadButton.textContent = this.downloadInProgress ? '正在打包…' : '打包原图'\n }\n const groups = groupSessionImages(images)\n const valid = images.filter((image) => image.validation.status === 'valid').length\n const warning = images.filter((image) => image.validation.status === 'warning').length\n const invalid = images.filter((image) => image.validation.status === 'invalid').length\n refs.stats.innerHTML = session\n ? `共 <b>${images.length}</b> 张 · ${groups.length} 组 · ` +\n `<span class=\"is-ok\">有效 ${valid}</span> · <span class=\"is-warn\">警告 ${warning}</span> · ` +\n `<span class=\"is-bad\">无效 ${invalid}</span>`\n : '未创建会话'\n\n refs.chips.hidden = groups.length < 2\n // 「合成全部」只在真的有全景组时才有意义(否则点了什么都不会发生)\n const stitchAll = root.querySelector<HTMLButtonElement>('[data-action=\"stitch-all\"]')\n if (stitchAll) {\n stitchAll.hidden = !groups.some((group) => group.kind === 'panorama' && group.images.length >= 2)\n }\n refs.chips.innerHTML = groups\n .map(\n (group, i) =>\n `<button type=\"button\" class=\"dgallery-chip\" data-action=\"jump\" data-key=\"${escapeHtml(group.key)}\">` +\n `<i style=\"--dg-group:${groupColor(i)}\"></i>${escapeHtml(group.label)} · ${group.images.length}</button>`,\n )\n .join('')\n\n if (!images.length) {\n refs.body.innerHTML = `<p class=\"dgallery-note\">${\n session ? '这个会话里还没有影像。' : '未创建会话。'\n }</p>`\n return\n }\n\n // 重渲染会重建缩略图 DOM,先把滚动位置记下来(否则扫描过程中每入库一张就跳回顶部)\n const scrollTop = refs.body.scrollTop\n refs.body.innerHTML = groups\n .map((group, i) => this.groupMarkup(group, i))\n .join('')\n refs.body.scrollTop = scrollTop\n for (const group of groups) this.renderStitch(group.key)\n this.syncSelection()\n }\n\n private groupMarkup(group: ImageGroup, colorIndex: number): string {\n const shown = group.images.slice(0, MAX_THUMBS_PER_GROUP)\n const hidden = group.images.length - shown.length\n const color = groupColor(colorIndex)\n const parts: string[] = []\n parts.push(\n `<section class=\"dgallery-group\" data-key=\"${escapeHtml(group.key)}\" style=\"--dg-group:${color}\">`,\n )\n parts.push('<div class=\"dgallery-group-head\">')\n parts.push(`<span class=\"dgallery-group-title\">${escapeHtml(group.label)}</span>`)\n let sub = `<span class=\"dgallery-group-sub\">${group.images.length} 张`\n if (group.kind === 'panorama') {\n sub += ` / ${group.total ?? group.images.length} 张`\n sub += group.complete\n ? ` · <span class=\"is-ok\">已拍完</span>`\n : ` · <span class=\"is-warn\">未拍完</span>`\n sub += ` · 组 ${escapeHtml(group.key)}`\n }\n sub += `</span>`\n parts.push(sub)\n parts.push('<span class=\"dgallery-spacer\"></span>')\n if (group.kind === 'panorama' && group.images.length >= 2 && this.stitcher) {\n parts.push(\n `<button type=\"button\" class=\"dgallery-btn dgallery-primary\" data-action=\"stitch\" data-key=\"${escapeHtml(group.key)}\">一键合成</button>`,\n )\n }\n parts.push('</div>')\n\n parts.push('<div class=\"dgallery-thumbs\">')\n shown.forEach((image, i) => {\n const sequence = group.indexes[i] ?? i + 1\n parts.push(\n `<button type=\"button\" class=\"dgallery-thumb\" data-action=\"select-image\" data-id=\"${escapeHtml(image.id)}\" ` +\n `data-status=\"${image.validation.status}\" ` +\n `title=\"#${sequence} ${escapeHtml(image.fileName)} · ${VALIDATION_LABELS[image.validation.status]}\" aria-label=\"查看第 ${sequence} 张\">` +\n (image.imageUrl ? `<img src=\"${image.imageUrl}\" alt=\"\" loading=\"lazy\">` : '') +\n `<span class=\"dgallery-badge\">${VALIDATION_BADGES[image.validation.status]}</span>` +\n `<span class=\"dgallery-tag\">${escapeHtml(imageTileLabel(image, sequence))}</span>` +\n `</button>`,\n )\n })\n parts.push('</div>')\n if (hidden > 0) {\n parts.push(\n `<p class=\"dgallery-note\">还有 ${hidden} 张未在此列出(共 ${group.images.length} 张)。</p>`,\n )\n }\n parts.push(`<div data-role=\"stitch\" data-key=\"${escapeHtml(group.key)}\"></div>`)\n parts.push('</section>')\n return parts.join('')\n }\n\n /** 只刷新某一组的合成区(合成进度高频更新,整表重绘会闪) */\n private renderStitch(groupKey: string): void {\n const refs = this.refs\n if (!refs) return\n const slot = refs.body.querySelector<HTMLElement>(\n `[data-role=\"stitch\"][data-key=\"${cssEscape(groupKey)}\"]`,\n )\n if (!slot) return\n const button = refs.body.querySelector<HTMLButtonElement>(\n `[data-action=\"stitch\"][data-key=\"${cssEscape(groupKey)}\"]`,\n )\n const state = this.stitches.get(groupKey)\n if (!state) {\n slot.innerHTML = ''\n if (button) {\n button.disabled = false\n button.textContent = '一键合成'\n button.classList.remove('dgallery-on')\n }\n return\n }\n if (state.status === 'running') {\n if (button) {\n button.disabled = true\n button.classList.add('dgallery-on')\n button.textContent =\n state.total > 0 ? `合成中 ${state.done}/${state.total}` : `合成中 ${state.done}`\n }\n slot.innerHTML = `<p class=\"dgallery-note\">等距柱状拼接中,逐张反投影…</p>`\n return\n }\n if (state.status === 'error') {\n if (button) {\n button.disabled = false\n button.classList.remove('dgallery-on')\n button.textContent = '重试合成'\n }\n slot.innerHTML = `<p class=\"dgallery-note is-warn\">${escapeHtml(state.message ?? '合成失败')}</p>`\n return\n }\n const result = state.result\n if (button) {\n button.disabled = false\n button.classList.remove('dgallery-on')\n button.textContent = '重新合成'\n }\n if (!result?.url) {\n slot.innerHTML = `<p class=\"dgallery-note is-warn\">合成完成,但没有可用的图片地址。</p>`\n return\n }\n const bits: string[] = []\n if (result.width && result.height) bits.push(`${result.width}×${result.height}`)\n if (typeof result.coverage === 'number') bits.push(`实拍覆盖 ${Math.round(result.coverage * 100)}%`)\n const size = estimateDataUrlBytes(result.url)\n if (size > 0) bits.push(formatBytes(size))\n const diagnostic = result.diagnosticUrl\n ? `<details class=\"dgallery-diagnostic\">` +\n `<summary>拍摄覆盖检查(看颜色即可)</summary>` +\n `<div class=\"dgallery-diagnostic-content\">` +\n `<p class=\"dgallery-diagnostic-help\">这不是照片,是拍摄覆盖地图:红色表示没有任何照片拍到,拼接只能猜着补;黄色表示只有一张照片拍到,容易出现接缝;绿色表示至少两张照片重叠。找墙出现的位置看颜色即可。</p>` +\n `<img class=\"dgallery-diagnostic-img\" src=\"${result.diagnosticUrl}\" alt=\"平面全景覆盖图\">` +\n `<ul class=\"dgallery-diagnostic-legend\">` +\n `<li><i class=\"dgallery-diagnostic-swatch\" style=\"--dg-shot-color:#ef4444\"></i><span>红:没拍到 ${Math.round((result.diagnosticCoverage?.uncovered ?? 0) * 100)}%</span></li>` +\n `<li><i class=\"dgallery-diagnostic-swatch\" style=\"--dg-shot-color:#facc15\"></i><span>黄:只拍到一次 ${Math.round((result.diagnosticCoverage?.single ?? 0) * 100)}%</span></li>` +\n `<li><i class=\"dgallery-diagnostic-swatch\" style=\"--dg-shot-color:#22c55e\"></i><span>绿:有照片重叠 ${Math.round((result.diagnosticCoverage?.overlap ?? 0) * 100)}%</span></li>` +\n `</ul></div></details>`\n : ''\n slot.innerHTML =\n `<div class=\"dgallery-card\">` +\n `<img class=\"dgallery-card-img\" data-action=\"view-panorama\" data-key=\"${escapeHtml(groupKey)}\" src=\"${result.url}\" alt=\"合成全景\">` +\n `<div class=\"dgallery-card-side\">` +\n `<div class=\"dgallery-card-title\">合成全景图</div>` +\n `<div class=\"dgallery-card-meta\">等距柱状 360°×180° · ${bits.map(escapeHtml).join(' · ')}</div>` +\n `<div class=\"dgallery-card-actions\">` +\n `<button type=\"button\" class=\"dgallery-btn dgallery-primary\" data-action=\"view-panorama\" data-key=\"${escapeHtml(groupKey)}\">360° 环视</button>` +\n `<button type=\"button\" class=\"dgallery-btn\" data-action=\"download-stitch\" data-key=\"${escapeHtml(groupKey)}\">下载</button>` +\n `</div></div></div>` +\n diagnostic\n }\n\n /**\n * 只刷新选中态(缩略图高亮 + 结果卡片不重绘)。\n * 大图弹窗里翻页会高频走到这里,整表重绘(几十上百个 `<img>`)纯属浪费。\n */\n private syncSelection(): void {\n const refs = this.refs\n if (!refs) return\n for (const el of refs.body.querySelectorAll<HTMLElement>('.dgallery-thumb')) {\n el.dataset.selected = String(el.dataset.id === this.selectedId)\n }\n }\n\n // ───────────────────────────── 内部:交互 ─────────────────────────────\n\n private readonly onClick = (event: MouseEvent): void => {\n const target = event.target as HTMLElement | null\n const actionEl = target?.closest<HTMLElement>('[data-action]')\n if (!actionEl) {\n // 点遮罩关弹窗;点画面其它地方不关(免得想细看的人被弹出去)\n if (target === this.root || target === this.refs?.mask) this.close()\n return\n }\n const action = actionEl.dataset.action ?? ''\n switch (action) {\n case 'close':\n this.close()\n break\n case 'jump': {\n const key = actionEl.dataset.key ?? ''\n const section = this.refs?.body.querySelector<HTMLElement>(\n `.dgallery-group[data-key=\"${cssEscape(key)}\"]`,\n )\n section?.scrollIntoView({ block: 'start' })\n break\n }\n case 'select-image': {\n const id = actionEl.dataset.id ?? ''\n this.selectedId = id\n this.syncSelection()\n this.viewer?.open(id, this.resolveSession()?.id)\n break\n }\n case 'stitch':\n void this.stitch(actionEl.dataset.key ?? '')\n break\n case 'download-originals':\n void this.downloadOriginals()\n break\n case 'stitch-all':\n void this.stitchAll()\n break\n case 'view-panorama': {\n const key = actionEl.dataset.key ?? ''\n const result = this.stitches.get(key)?.result\n if (result?.url) this.panorama?.open(result, '合成全景图')\n break\n }\n case 'download-stitch': {\n const key = actionEl.dataset.key ?? ''\n const result = this.stitches.get(key)?.result\n if (result?.url) triggerDownload(result.url, result.name ?? `${key}-pano.png`)\n break\n }\n default:\n break\n }\n }\n\n private readonly onSessionChange = (): void => {\n const refs = this.refs\n if (!refs) return\n const session = this.service.getSession(refs.sessionSelect.value)\n if (!session) return\n // 「当前会话」= 服务里 updatedAt 最新的那个(与采集面板同一套语义)\n session.updatedAt = new Date().toISOString()\n this.sessionId = session.id\n this.selectedId = null\n this.viewer?.close()\n this.panorama?.close()\n this.render()\n }\n\n private readonly onKeyDown = (event: KeyboardEvent): void => {\n if (this.destroyed || !this.root || this.root.hidden) return\n // 球面全景查看器 / 大图查看器盖在上面时整体让位:否则 Esc 会先关掉影像集,\n // 把上面那层留在屏幕上(见模块头注释 3)\n if (this.panorama?.isOpen || this.viewer?.isOpen) return\n if (event.ctrlKey || event.metaKey || event.altKey) return\n if (event.key === 'Escape') {\n event.preventDefault()\n this.close()\n }\n // 独占键盘:面板的键盘飞行挂在冒泡阶段,这里在捕获阶段截住,\n // 翻图集时 WASD / 方向键不会顺带把无人机飞出去\n event.stopPropagation()\n }\n\n private async downloadOriginals(): Promise<void> {\n if (this.downloadInProgress) return\n const session = this.resolveSession()\n if (!session?.images.length) {\n this.setDownloadStatus('当前会话没有可下载的图片。')\n return\n }\n this.downloadInProgress = true\n this.setDownloadStatus(`正在打包 ${session.images.length} 张原图…`)\n this.render()\n try {\n const archive = await createOriginalImagesZip(session.images)\n const url = URL.createObjectURL(archive)\n const fileBase =\n session.options.name.replace(/[\\\\/:*?\"<>|\\u0000-\\u001f]/g, '_').trim() || '影像集'\n triggerDownload(url, `${fileBase}-原始图片.zip`)\n window.setTimeout(() => URL.revokeObjectURL(url), 60_000)\n this.setDownloadStatus(`已打包下载 ${session.images.length} 张原图。`)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n this.setDownloadStatus(`打包下载失败:${message}`)\n } finally {\n this.downloadInProgress = false\n this.render()\n }\n }\n\n private setDownloadStatus(message: string): void {\n if (this.refs) this.refs.downloadStatus.textContent = message\n }\n\n private lockScroll(): void {\n if (typeof document === 'undefined') return\n this.bodyOverflow = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n }\n\n private unlockScroll(): void {\n if (typeof document === 'undefined') return\n document.body.style.overflow = this.bodyOverflow\n }\n\n private query<T extends HTMLElement = HTMLElement>(selector: string): T {\n const root = this.root\n if (!root) throw new Error('[DroneGallery] 弹窗尚未挂载')\n const el = root.querySelector<T>(selector)\n if (!el) throw new Error(`[DroneGallery] 缺少节点:${selector}`)\n return el\n }\n\n private template(): string {\n const title = escapeHtml(this.options.title ?? '影像集')\n const canStitchAll = Boolean(this.stitcher)\n return `\n <div class=\"dgallery-mask\" data-role=\"mask\" data-action=\"close\"></div>\n <div class=\"dgallery-frame\">\n <div class=\"dgallery-bar\">\n <span class=\"dgallery-title\">${title}</span>\n <select class=\"dgallery-session\" data-role=\"session\" title=\"切换会话\"></select>\n <span class=\"dgallery-stats\" data-role=\"stats\">–</span>\n <span class=\"dgallery-spacer\"></span>\n ${\n canStitchAll\n ? '<button type=\"button\" class=\"dgallery-btn\" data-action=\"stitch-all\" title=\"把当前会话里所有全景组依次合成\">合成全部</button>'\n : ''\n }\n <button type=\"button\" class=\"dgallery-btn\" data-action=\"download-originals\" title=\"将当前会话的原图打包为 ZIP\">打包原图</button>\n <button type=\"button\" class=\"dgallery-icon\" data-action=\"close\" title=\"关闭 (Esc)\" aria-label=\"关闭\">×</button>\n </div>\n <div class=\"dgallery-chips\" data-role=\"chips\" hidden></div>\n <div class=\"dgallery-body\" data-role=\"body\"></div>\n <div class=\"dgallery-foot\"><span>点缩略图看大图 · 全景组「一键合成」后可「360° 环视」拖动看四周 · Esc 关闭</span><span data-role=\"download-status\" aria-live=\"polite\"></span></div>\n </div>\n `\n }\n}\n\n/** 分组色标(按出现顺序给色相,不引调色板依赖) */\nfunction groupColor(index: number): string {\n const hue = (HUE_BASE + index * HUE_STEP) % 360\n return `hsl(${hue} 78% 62%)`\n}\n\n/** 从 data URL 的头估算字节数(给结果卡片报个体积,不必真解码) */\nfunction estimateDataUrlBytes(url: string): number {\n if (!url.startsWith('data:')) return 0\n const comma = url.indexOf(',')\n if (comma < 0) return 0\n const body = url.length - comma - 1\n const padding = url.endsWith('==') ? 2 : url.endsWith('=') ? 1 : 0\n return Math.max(0, Math.floor((body * 3) / 4) - padding)\n}\n\n/** 选择器里嵌动态字符串时的最小转义(分组键可能带 `-` `.` `:` 等) */\nfunction cssEscape(value: string): string {\n if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') return CSS.escape(value)\n return value.replace(/[\"\\\\]/g, '\\\\$&')\n}\n","/**\r\n * 影像采集面板(原生 DOM):采集会话 / 虚拟采集 / 真实照片导入 / 导出 —— 只放**动作**。\r\n *\r\n * 与 drone-panel / mission-panel / drone-minimap 是同一套范式:只用 DOM + CSS,\r\n * 不引任何 UI 框架,库整体保持与框架无关(Vue / React / 裸页面通用)。\r\n *\r\n * 面板自身不持有采集状态 —— 状态全在 `CaptureService` 里(影像集、会话、自动采集句柄),\r\n * 面板只是它的一个视图:订阅 `service.subscribe()` 后在**局部**刷新计数,\r\n * 不整体重绘骨架(否则会打断正在输入的间隔 / 距离框)。\r\n *\r\n * 三处刻意的设计:\r\n * - 面板按会话的 `source` 分别展示「虚拟采集」与「真实导入」两套入口 —— 两类影像的采集动作\r\n * 本来就不是一回事(一个是快门,一个是文件),混在一起只会让人误操作;\r\n * - 「自动采集」走的是服务里 200ms 心跳上的时间 / 距离双触发,面板只负责把开关与参数递进去;\r\n * - **影像的浏览与合成不在这里**。面板只有 300 多像素宽,缩略图 + 统计 + 详情 + 导出全塞进来\r\n * 既看不清全局又要滚半天。所以「影像集」整体交给 `DroneGallery` 弹窗(按组合分组、\r\n * 每组一键合成、点缩略图看大图),面板只留一个入口按钮。弹窗住在 `document.body` 上、\r\n * 不在面板里 —— 面板带 `backdrop-filter`,会改 `fixed` 后代的定位基准。\r\n */\r\nimport { CaptureService } from './drone-capture'\r\nimport type {\r\n CaptureExportFormat,\r\n CaptureSession,\r\n CaptureSessionOptions,\r\n ImageSource,\r\n VirtualCaptureProvider,\r\n} from './drone-capture'\r\n// 影像文案与无害 DOM 工具在面板 / 弹窗查看器 / 影像集弹窗之间共用(见 drone-image-utils 模块头)\r\nimport { escapeHtml, groupSessionImages, triggerDownload } from './drone-image-utils'\r\nimport { DroneImageViewer } from './drone-image-viewer'\r\nimport type { DroneImageViewerOptions } from './drone-image-viewer'\r\nimport { DroneGallery } from './drone-gallery'\r\nimport type { DroneGalleryOptions } from './drone-gallery'\r\n\r\n/** 面板停靠位置(与 DroneControlPanel 同一组取值) */\r\nexport type DroneCapturePanelPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\r\n\r\n/** DroneCapturePanel 构造选项 */\r\nexport interface DroneCapturePanelOptions {\r\n /** 挂载容器;缺省挂到 document.body(此时用 fixed 定位) */\r\n container?: HTMLElement | null\r\n /** 停靠位置,默认 'bottom-left'(three 页左上/右上/底部中央已被别的面板占用) */\r\n position?: DroneCapturePanelPosition\r\n /**\r\n * 停靠微调(像素)。用 margin 实现,不覆盖 position 的定位规则 ——\r\n * 演示页里底部左右角已经被 HUD 摇杆占住,面板整体上抬一段就能错开。\r\n */\r\n offset?: { x?: number; y?: number }\r\n /** 面板标题,默认「影像采集」 */\r\n title?: string\r\n /** 是否注入自带样式,默认 true */\r\n injectStyles?: boolean\r\n /** 初始是否可见,默认 true */\r\n visible?: boolean\r\n /** 已有采集服务(多面板共享一个会话列表时传入);缺省按 host 新建一个 */\r\n service?: CaptureService\r\n /** 挂载时若一个会话都没有,是否自动建一个虚拟采集会话,默认 true */\r\n autoCreateSession?: boolean\r\n /**\r\n * 新建会话时套用的默认参数(场景标识 / 地理参考原点 / 自动采集间隔)。\r\n * 宿主知道自己跑在哪个场景里,把这些填上,采到的影像才带得上场景版本与经纬度。\r\n */\r\n sessionDefaults?: Partial<CaptureSessionOptions>\r\n /** 导出时是否顺手触发浏览器下载,默认 true */\r\n downloadOnExport?: boolean\r\n /**\r\n * 是否启用大图查看器(影像集弹窗点缩略图时打开),默认 true。\r\n * 弹窗挂在 `document.body` 上(不是面板内部,原因见 drone-image-viewer 模块头),\r\n * 层级 / 下载按钮 / 翻页回调等细项走 `viewerOptions`。\r\n */\r\n viewer?: boolean\r\n /** 弹窗查看器的细项(层级 / 是否提供下载 / 关闭与翻页回调) */\r\n viewerOptions?: DroneImageViewerOptions\r\n /**\r\n * 是否启用影像集弹窗(面板上的「打开影像集」按钮),默认 true。\r\n * 影像的浏览 / 分组 / 一键合成都在它里面(见 drone-gallery);`false` 则面板不提供该入口。\r\n */\r\n gallery?: boolean\r\n /** 影像集弹窗的细项(层级 / 合成器 / 关闭与合成回调) */\r\n galleryOptions?: DroneGalleryOptions\r\n}\r\n\r\nconst STYLE_ID = 'drone-capture-panel-styles'\r\n/** 自动采集默认间隔(秒) */\r\nconst DEFAULT_INTERVAL_SECONDS = 1\r\n\r\nconst PANEL_CSS = `\r\n.dcap {\r\n --dcap-bg: rgba(15, 19, 27, 0.86);\r\n --dcap-border: rgba(255, 255, 255, 0.12);\r\n --dcap-text: #e9eff9;\r\n --dcap-muted: rgba(233, 239, 249, 0.58);\r\n --dcap-accent: #35a7ff;\r\n --dcap-on-accent: #04121f;\r\n --dcap-ok: #37d67a;\r\n --dcap-warn: #f5a623;\r\n --dcap-danger: #ff5f5f;\r\n --dcap-radius: 10px;\r\n font: 12px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\r\n user-select: none;\r\n -webkit-user-select: none;\r\n -webkit-tap-highlight-color: transparent;\r\n}\r\n.dcap {\r\n z-index: 20;\r\n width: 316px;\r\n max-height: min(62vh, 580px);\r\n box-sizing: border-box;\r\n padding: 10px 12px 12px;\r\n display: flex;\r\n flex-direction: column;\r\n border: 1px solid var(--dcap-border);\r\n border-radius: var(--dcap-radius);\r\n background: var(--dcap-bg);\r\n color: var(--dcap-text);\r\n -webkit-backdrop-filter: blur(10px);\r\n backdrop-filter: blur(10px);\r\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);\r\n}\r\n.dcap[data-position=\"top-left\"] { top: 12px; left: 12px; }\r\n.dcap[data-position=\"top-right\"] { top: 12px; right: 12px; }\r\n.dcap[data-position=\"bottom-left\"] { bottom: 12px; left: 12px; }\r\n.dcap[data-position=\"bottom-right\"] { bottom: 12px; right: 12px; }\r\n.dcap-head { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }\r\n.dcap-title { font-size: 12px; font-weight: 600; }\r\n.dcap-count { margin-left: auto; color: var(--dcap-muted); font-variant-numeric: tabular-nums; }\r\n.dcap-icon {\r\n width: 20px; height: 20px; padding: 0; line-height: 1; cursor: pointer;\r\n border: 1px solid var(--dcap-border); border-radius: 6px;\r\n background: rgba(255, 255, 255, 0.06); color: var(--dcap-text);\r\n}\r\n.dcap-body { flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; }\r\n.dcap[data-collapsed=\"true\"] .dcap-body { display: none; }\r\n.dcap-blk { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--dcap-border); }\r\n.dcap-blk:first-child { margin-top: 0; padding-top: 0; border-top: none; }\r\n.dcap-label {\r\n display: flex; justify-content: space-between; align-items: baseline;\r\n color: var(--dcap-muted); margin-bottom: 6px;\r\n}\r\n.dcap-label em { font-style: normal; color: var(--dcap-text); font-variant-numeric: tabular-nums; }\r\n.dcap-row { display: flex; gap: 6px; align-items: center; margin-bottom: 7px; }\r\n.dcap-row > .dcap-field { flex: 1; min-width: 0; }\r\n.dcap-btn {\r\n flex: 1; min-height: 28px; padding: 4px 8px; cursor: pointer; white-space: nowrap;\r\n border: 1px solid var(--dcap-border); border-radius: 8px;\r\n background: rgba(255, 255, 255, 0.06); color: var(--dcap-text); font: inherit;\r\n}\r\n.dcap-btn:hover { background: rgba(255, 255, 255, 0.12); }\r\n.dcap-btn[disabled] { opacity: 0.45; cursor: not-allowed; }\r\n.dcap-btn.dcap-primary { background: var(--dcap-accent); border-color: transparent; color: var(--dcap-on-accent); font-weight: 600; }\r\n.dcap-btn.dcap-on { background: var(--dcap-danger); border-color: transparent; color: #fff; font-weight: 600; }\r\n.dcap-select, .dcap-num, .dcap-text {\r\n box-sizing: border-box; width: 100%; min-height: 26px; padding: 2px 6px;\r\n border: 1px solid var(--dcap-border); border-radius: 7px;\r\n background: rgba(255, 255, 255, 0.06); color: var(--dcap-text); font: inherit;\r\n}\r\n.dcap-num { width: 62px; text-align: right; font-variant-numeric: tabular-nums; }\r\n.dcap-select:focus, .dcap-num:focus, .dcap-text:focus { outline: none; border-color: var(--dcap-accent); }\r\n.dcap-field { display: flex; align-items: center; gap: 5px; color: var(--dcap-muted); white-space: nowrap; }\r\n.dcap-hint { margin: 2px 0 0; color: var(--dcap-muted); font-size: 11px; }\r\n.dcap-note {\r\n margin: 6px 0 0; padding: 5px 7px; border-radius: 7px; font-size: 11px; line-height: 1.5;\r\n background: rgba(255, 255, 255, 0.05); color: var(--dcap-muted);\r\n}\r\n.dcap-note[hidden] { display: none; }\r\n.dcap-note.is-warn { color: var(--dcap-warn); }\r\n@media (pointer: coarse) { .dcap-btn { min-height: 34px; } }\r\n`\r\n\r\nfunction ensureStyles(): void {\r\n if (typeof document === 'undefined') return\r\n if (document.getElementById(STYLE_ID)) return\r\n const style = document.createElement('style')\r\n style.id = STYLE_ID\r\n style.textContent = PANEL_CSS\r\n document.head.appendChild(style)\r\n}\r\n\r\n/** 生成文件名用的时间戳(本地时间,精确到秒) */\r\nfunction stamp(): string {\r\n const now = new Date()\r\n const pad = (value: number): string => String(value).padStart(2, '0')\r\n return (\r\n `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +\r\n `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`\r\n )\r\n}\r\n\r\n/**\r\n * 影像采集面板。\r\n *\r\n * ```ts\r\n * const panel = new DroneCapturePanel(fly, { container: viewportEl, position: 'bottom-left' })\r\n * // 卸载:panel.destroy()\r\n * ```\r\n */\r\nexport class DroneCapturePanel {\r\n /** 采集服务(面板的视图数据源;宿主可直接用它编程式采集 / 导出) */\r\n readonly service: CaptureService\r\n private readonly host: HTMLElement\r\n private readonly root: HTMLDivElement\r\n private readonly refs: {\r\n body: HTMLElement\r\n count: HTMLElement\r\n sessionSelect: HTMLSelectElement\r\n sessionName: HTMLElement\r\n sourceSelect: HTMLSelectElement\r\n nameInput: HTMLInputElement\r\n virtualBlock: HTMLElement\r\n realBlock: HTMLElement\r\n intervalInput: HTMLInputElement\r\n distanceInput: HTMLInputElement\r\n autoBtn: HTMLButtonElement\r\n autoState: HTMLElement\r\n galleryCount: HTMLElement\r\n galleryBtn: HTMLButtonElement\r\n note: HTMLElement\r\n fileInput: HTMLInputElement\r\n dirInput: HTMLInputElement\r\n }\r\n private readonly options: DroneCapturePanelOptions\r\n /** 大图查看器(影像集弹窗点缩略图时打开);`viewer` / `gallery` 关掉时为 null */\r\n private viewer: DroneImageViewer | null = null\r\n /** 影像集弹窗(浏览 / 分组 / 一键合成);`options.gallery === false` 时为 null */\r\n private gallery: DroneGallery | null = null\r\n private unsubscribe: (() => void) | null = null\r\n private sessionSignature = ''\r\n private noteTimer: ReturnType<typeof setTimeout> | null = null\r\n private destroyed = false\r\n\r\n /** 构造采集面板:模式/张数/开始按钮 + 会话相册入口 */\n constructor(host: VirtualCaptureProvider, options: DroneCapturePanelOptions = {}) {\r\n if (typeof document === 'undefined') throw new Error('[DroneCapturePanel] 需要 DOM 环境')\r\n this.options = options\r\n if (options.injectStyles !== false) ensureStyles()\r\n\r\n this.service = options.service ?? new CaptureService({ provider: host })\r\n // 换后端(机体切换)后重新绑定取景帧来源\r\n if (!options.service) this.service.setProvider(host)\r\n\r\n const container = options.container ?? null\r\n this.host = container ?? document.body\r\n // 面板用 absolute 定位到容器;容器没有定位上下文时补一个 relative,否则会跑到页面左上角\r\n if (container && getComputedStyle(container).position === 'static') {\r\n container.style.position = 'relative'\r\n }\r\n\r\n this.root = document.createElement('div')\r\n this.root.className = 'dcap'\r\n this.root.dataset.position = options.position ?? 'bottom-left'\r\n this.root.dataset.collapsed = 'false'\r\n this.root.style.position = container ? 'absolute' : 'fixed'\r\n // 停靠微调用 margin:与 position 类里的 top/left/bottom/right 是两套独立机制,互不覆盖\r\n const position = options.position ?? 'bottom-left'\r\n const offset = options.offset ?? {}\r\n if (offset.y) {\r\n const key = position.startsWith('bottom') ? 'marginBottom' : 'marginTop'\r\n this.root.style[key] = `${offset.y}px`\r\n }\r\n if (offset.x) {\r\n const key = position.endsWith('right') ? 'marginRight' : 'marginLeft'\r\n this.root.style[key] = `${offset.x}px`\r\n }\r\n if (options.visible === false) this.root.style.display = 'none'\r\n this.root.innerHTML = this.template()\r\n this.host.appendChild(this.root)\r\n this.refs = {\r\n body: this.query('[data-role=\"body\"]'),\r\n count: this.query('[data-role=\"count\"]'),\r\n sessionSelect: this.query<HTMLSelectElement>('[data-role=\"session-select\"]'),\r\n sessionName: this.query('[data-role=\"session-name\"]'),\r\n sourceSelect: this.query<HTMLSelectElement>('[data-role=\"source-select\"]'),\r\n nameInput: this.query<HTMLInputElement>('[data-role=\"name-input\"]'),\r\n virtualBlock: this.query('[data-blk=\"virtual\"]'),\r\n realBlock: this.query('[data-blk=\"real\"]'),\r\n intervalInput: this.query<HTMLInputElement>('[data-role=\"interval\"]'),\r\n distanceInput: this.query<HTMLInputElement>('[data-role=\"distance\"]'),\r\n autoBtn: this.query<HTMLButtonElement>('[data-action=\"auto-toggle\"]'),\r\n autoState: this.query('[data-role=\"auto-state\"]'),\r\n galleryCount: this.query('[data-role=\"gallery-count\"]'),\r\n galleryBtn: this.query<HTMLButtonElement>('[data-action=\"open-gallery\"]'),\r\n note: this.query('[data-role=\"note\"]'),\r\n fileInput: this.query<HTMLInputElement>('[data-role=\"file-input\"]'),\r\n dirInput: this.query<HTMLInputElement>('[data-role=\"dir-input\"]'),\r\n }\r\n\r\n this.refs.nameInput.value = `虚拟采集-${stamp()}`\r\n this.refs.intervalInput.value = String(DEFAULT_INTERVAL_SECONDS)\r\n this.refs.distanceInput.value = '0'\r\n\r\n if (options.autoCreateSession !== false && this.service.listSessions().length === 0) {\r\n this.service.createSession(\r\n this.newSessionOptions('virtual-camera', this.refs.nameInput.value),\r\n )\r\n }\r\n\r\n this.root.addEventListener('click', this.onClick)\r\n this.root.addEventListener('change', this.onChange)\r\n this.refs.fileInput.addEventListener('change', () => void this.onFiles(this.refs.fileInput))\r\n this.refs.dirInput.addEventListener('change', () => void this.onFiles(this.refs.dirInput))\r\n this.unsubscribe = this.service.subscribe(() => this.sync())\r\n\r\n // 大图查看器与影像集弹窗都住 document.body,与面板共用同一份 service ——\r\n // 于是虚拟采集 / 真实导入 / 全景照片在影像集里都能看、全景组还能一键合成。\r\n // 查看器实例直接交给影像集弹窗复用(同一个实例,不会出现两套翻页状态)。\r\n if (options.viewer !== false) {\r\n this.viewer = new DroneImageViewer(this.service, { ...options.viewerOptions })\r\n }\r\n if (options.gallery !== false) {\r\n this.gallery = new DroneGallery(this.service, {\r\n ...options.galleryOptions,\r\n ...(this.viewer ? { viewer: this.viewer } : {}),\r\n // 面板关掉了大图查看器时,影像集里也不该冒出一个(两个开关要一致)\r\n viewerEnabled: options.viewer !== false && (options.galleryOptions?.viewerEnabled ?? true),\r\n })\r\n }\r\n\r\n this.sync()\r\n }\r\n\r\n /** 新建会话参数 = 宿主给的默认值(场景标识 / 地理原点)+ 界面上的来源与名称 */\r\n private newSessionOptions(source: ImageSource, name: string): CaptureSessionOptions {\r\n return { ...this.options.sessionDefaults, source, name }\r\n }\r\n\r\n /** 当前展示的会话 */\r\n get session(): CaptureSession | null {\r\n return this.service.activeSession\r\n }\r\n\r\n /** 显示 / 隐藏 */\r\n setVisible(visible: boolean): void {\r\n this.root.style.display = visible ? '' : 'none'\r\n }\r\n\r\n /**\r\n * 打开大图查看器。`imageId` 缺省用当前选中的那张(再缺省 = 最后一张)。\r\n * 宿主若想在自己的界面上挂个「查看」按钮,调这里即可 —— 与点缩略图是同一条路径。\r\n */\r\n openViewer(imageId?: string): void {\r\n const session = this.session\r\n if (!session || !this.viewer) return\r\n const id = imageId ?? session.images[session.images.length - 1]?.id\r\n if (!id) return\r\n this.viewer.open(id, session.id)\r\n }\r\n\r\n /** 关闭大图查看器(没打开时是空操作) */\r\n closeViewer(): void {\r\n this.viewer?.close()\r\n }\r\n\r\n /** 打开影像集弹窗(浏览 / 分组 / 一键合成);`gallery: false` 时是空操作 */\r\n openGallery(sessionId?: string): void {\r\n this.gallery?.open(sessionId ?? this.session?.id)\r\n }\r\n\r\n /** 关闭影像集弹窗(连同它里面的大图查看器) */\r\n closeGallery(): void {\r\n this.gallery?.close()\r\n }\r\n\r\n /** 手动同步一次(宿主改了服务状态后可主动调用) */\r\n sync(): void {\r\n if (this.destroyed) return\r\n const session = this.session\r\n this.refs.count.textContent = session ? `${session.images.length} 张` : '–'\r\n this.refs.sessionName.textContent = session ? `${session.options.name}` : '未创建'\r\n\r\n this.syncSessionOptions()\r\n // 按来源切换入口:虚拟采集与真实导入不该同时暴露\r\n const isVirtual = !session || session.options.source === 'virtual-camera'\r\n this.refs.virtualBlock.style.display = isVirtual ? '' : 'none'\r\n this.refs.realBlock.style.display = isVirtual ? 'none' : ''\r\n\r\n const auto = session ? this.service.isAutoCapturing(session.id) : false\r\n this.refs.autoBtn.textContent = auto ? '停止自动' : '自动采集'\r\n this.refs.autoBtn.classList.toggle('dcap-on', auto)\r\n this.refs.autoState.textContent = auto\r\n ? `自动中 · 每 ${this.refs.intervalInput.value}s / ${this.refs.distanceInput.value}m`\r\n : '空闲'\r\n\r\n // 影像集入口:报\"几组 / 多少张\",比单纯一个总数更能说明这堆影像的构成\r\n if (session) {\r\n const groups = groupSessionImages(session.images)\r\n const panoGroups = groups.filter((group) => group.kind === 'panorama').length\r\n this.refs.galleryCount.textContent = panoGroups\r\n ? `${panoGroups} 组全景 · 共 ${session.images.length} 张`\r\n : `共 ${session.images.length} 张`\r\n } else {\r\n this.refs.galleryCount.textContent = '未创建会话'\r\n }\r\n this.refs.galleryBtn.disabled = !session || session.images.length === 0\r\n this.refs.galleryBtn.title = session?.images.length\r\n ? '按组合浏览影像集,全景组可一键合成'\r\n : '还没有影像'\r\n // 弹窗开着时同步刷新(它自己也订阅了服务,这里只是保证\"刚打开时就是最新的\")\r\n this.gallery?.refresh()\r\n }\r\n\r\n /**\r\n * 面板底部的一行提示(参数不合法、导入进行中……)。\r\n * 影像集搬进弹窗后,面板没有详情区了,这些\"一次性反馈\"需要个落脚处;\r\n * 给个几秒后自动消失,免得过时的话一直挂在那儿。\r\n */\r\n setNote(text: string, warn = false): void {\r\n if (this.destroyed) return\r\n this.refs.note.textContent = text\r\n this.refs.note.classList.toggle('is-warn', warn)\r\n this.refs.note.hidden = !text\r\n if (this.noteTimer !== null) clearTimeout(this.noteTimer)\r\n this.noteTimer = text\r\n ? setTimeout(() => {\r\n if (!this.destroyed) this.refs.note.hidden = true\r\n this.noteTimer = null\r\n }, 5000)\r\n : null\r\n }\r\n\r\n private syncSessionOptions(): void {\r\n const sessions = this.service.listSessions()\r\n const signature = sessions.map((s) => `${s.id}:${s.options.name}:${s.options.source}`).join('|')\r\n if (signature !== this.sessionSignature) {\r\n this.sessionSignature = signature\r\n const active = this.session\r\n this.refs.sessionSelect.innerHTML = sessions\r\n .map(\r\n (session) =>\r\n `<option value=\"${session.id}\">${escapeHtml(session.options.name)} · ${session.images.length}</option>`,\r\n )\r\n .join('')\r\n if (active) this.refs.sessionSelect.value = active.id\r\n } else {\r\n // 张数会变,只刷新选项文案即可(整体重绘会打断正在输入的名称框)\r\n const active = this.session\r\n if (active) this.refs.sessionSelect.value = active.id\r\n const options = this.refs.sessionSelect.options\r\n for (let i = 0; i < options.length; i += 1) {\r\n const session = sessions[i]\r\n const option = options[i]\r\n if (!session || !option) continue\r\n const text = `${session.options.name} · ${session.images.length}`\r\n if (option.textContent !== text) option.textContent = text\r\n }\r\n }\r\n }\r\n\r\n private readonly onClick = (event: MouseEvent): void => {\r\n const target = event.target as HTMLElement | null\r\n // 取「最近的带 data-action 的祖先(含自身)」:按钮里嵌图标时图标会先命中自己\r\n const actionEl = target?.closest<HTMLElement>('[data-action]')\r\n if (!actionEl) return\r\n void this.handleAction(actionEl.dataset.action ?? '', actionEl)\r\n }\r\n\r\n private async handleAction(action: string, element: HTMLElement): Promise<void> {\r\n const session = this.session\r\n switch (action) {\r\n case 'toggle-collapse': {\r\n const collapsed = this.root.dataset.collapsed === 'true'\r\n this.root.dataset.collapsed = collapsed ? 'false' : 'true'\r\n this.query<HTMLButtonElement>('[data-action=\"toggle-collapse\"]').textContent = collapsed\r\n ? '–'\r\n : '+'\r\n break\r\n }\r\n case 'session-new': {\r\n const source = this.refs.sourceSelect.value as ImageSource\r\n const name = this.refs.nameInput.value.trim() || `采集-${stamp()}`\r\n this.service.createSession(this.newSessionOptions(source, name))\r\n this.sync()\r\n break\r\n }\r\n case 'capture-one': {\r\n if (!session) break\r\n element.setAttribute('disabled', 'disabled')\r\n // 手动拍一张:纯粹走服务,与自动采集同一条路径 ⇒ 元数据完全一致\r\n await this.service.captureVirtualImage(session.id)\r\n element.removeAttribute('disabled')\r\n break\r\n }\r\n case 'auto-toggle': {\r\n if (!session) break\r\n if (this.service.isAutoCapturing(session.id)) {\r\n this.service.stopAutoCapture(session.id)\r\n } else {\r\n const seconds = Number(this.refs.intervalInput.value)\r\n const meters = Number(this.refs.distanceInput.value)\r\n const started = this.service.startAutoCapture(session.id, {\r\n intervalMs: Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : 0,\r\n distanceMeters: Number.isFinite(meters) && meters > 0 ? meters : 0,\r\n })\r\n if (!started) this.setNote('自动采集需要时间间隔或距离间隔至少填一个 > 0 的值。')\r\n }\r\n this.sync()\r\n break\r\n }\r\n case 'pick-files':\r\n this.refs.fileInput.click()\r\n break\r\n case 'pick-dir':\r\n this.refs.dirInput.click()\r\n break\r\n case 'open-gallery':\r\n this.openGallery()\r\n break\r\n case 'clear-images': {\r\n if (!session) break\r\n this.service.clearImages(session.id)\r\n this.sync()\r\n break\r\n }\r\n case 'export':\r\n if (session)\r\n this.exportSession(session, (element.dataset.format as CaptureExportFormat) ?? 'json')\r\n break\r\n case 'finish':\r\n if (session) this.service.finishSession(session.id)\r\n break\r\n default:\r\n break\r\n }\r\n }\r\n\r\n private readonly onChange = (event: Event): void => {\r\n const target = event.target as HTMLElement | null\r\n if (!target) return\r\n if (target === this.refs.sessionSelect) {\r\n // 「当前会话」= 服务里 updatedAt 最新的那个;切会话时把它的参数带回输入框\r\n const session = this.service.getSession(this.refs.sessionSelect.value)\r\n if (session) {\r\n session.updatedAt = new Date().toISOString()\r\n // 弹窗锁在旧会话的那张图上,切会话就收起来(否则会出现\"面板已换会话、弹窗还在放旧的\")\r\n this.viewer?.close()\r\n if (session.options.captureIntervalMs) {\r\n this.refs.intervalInput.value = String(session.options.captureIntervalMs / 1000)\r\n }\r\n if (session.options.distanceIntervalMeters) {\r\n this.refs.distanceInput.value = String(session.options.distanceIntervalMeters)\r\n }\r\n this.sync()\r\n }\r\n }\r\n }\r\n\r\n private async onFiles(input: HTMLInputElement): Promise<void> {\r\n const session = this.session\r\n if (!session || !input.files?.length) return\r\n const files = Array.from(input.files)\r\n input.value = ''\r\n this.setNote(`正在导入 ${files.length} 个文件…`)\r\n await this.service.importRealImages(session.id, files)\r\n this.sync()\r\n this.setNote(`已导入 ${files.length} 个文件。`)\r\n }\r\n\r\n /** 导出当前会话清单(JSON 完整存档 / CSV 表格) */\r\n exportSession(session: CaptureSession, format: CaptureExportFormat): string {\r\n const text = this.service.exportSession(session.id, format)\r\n if (this.options.downloadOnExport !== false && typeof document !== 'undefined') {\r\n const mime = format === 'csv' ? 'text/csv' : 'application/json'\r\n const url = URL.createObjectURL(new Blob([text], { type: `${mime};charset=utf-8` }))\r\n const extension = format === 'manifest' ? 'json' : format\r\n triggerDownload(\r\n url,\r\n `capture-${session.options.name}-${stamp()}${format === 'manifest' ? '-manifest' : ''}.${extension}`,\r\n )\r\n // 下载已经开始,URL 可以立刻回收\r\n URL.revokeObjectURL(url)\r\n }\r\n return text\r\n }\r\n\r\n /** 拆除面板 DOM 与轮询 */\n destroy(): void {\r\n if (this.destroyed) return\r\n this.destroyed = true\r\n this.unsubscribe?.()\r\n this.unsubscribe = null\r\n if (this.noteTimer !== null) clearTimeout(this.noteTimer)\r\n this.noteTimer = null\r\n // 先拆大图查看器、再拆影像集弹窗:两者的 body 滚动锁是**嵌套**的\r\n // (查看器开在影像集之上),必须按\"后锁的先解\"的顺序恢复 ——\r\n // 反过来的话,外层会把它进场前保存的值写回去,overflow 就残留成 'hidden'。\r\n this.viewer?.destroy()\r\n this.viewer = null\r\n // 弹窗挂 body,不拆会留在页面上(连同它的键盘监听)\r\n this.gallery?.destroy()\r\n this.gallery = null\r\n for (const session of this.service.listSessions()) this.service.stopAutoCapture(session.id)\r\n this.root.remove()\r\n if (this.options.injectStyles !== false) document.getElementById(STYLE_ID)?.remove()\r\n }\r\n\r\n private query<T extends HTMLElement = HTMLElement>(selector: string): T {\r\n const el = this.root.querySelector<T>(selector)\r\n if (!el) throw new Error(`[DroneCapturePanel] 缺少节点:${selector}`)\r\n return el\r\n }\r\n\r\n private template(): string {\r\n return `\r\n <div class=\"dcap-head\">\r\n <span class=\"dcap-title\">${escapeHtml(this.options.title ?? '影像采集')}</span>\r\n <span class=\"dcap-count\" data-role=\"count\">–</span>\r\n <button type=\"button\" class=\"dcap-icon\" data-action=\"toggle-collapse\" title=\"收起 / 展开\" aria-label=\"收起或展开\">–</button>\r\n </div>\r\n <div class=\"dcap-body\" data-role=\"body\">\r\n <section class=\"dcap-blk\">\r\n <div class=\"dcap-label\">采集会话<em data-role=\"session-name\">–</em></div>\r\n <div class=\"dcap-row\">\r\n <select class=\"dcap-select dcap-field\" data-role=\"session-select\" title=\"切换当前会话\"></select>\r\n <button type=\"button\" class=\"dcap-btn\" style=\"flex:0 0 auto\" data-action=\"finish\" title=\"结束当前会话(保留影像集)\">结束</button>\r\n </div>\r\n <div class=\"dcap-row\">\r\n <input class=\"dcap-text dcap-field\" data-role=\"name-input\" type=\"text\" placeholder=\"会话名称\" title=\"新会话名称\">\r\n <select class=\"dcap-select\" data-role=\"source-select\" style=\"flex:0 0 86px\" title=\"影像来源\">\r\n <option value=\"virtual-camera\">场景虚拟</option>\r\n <option value=\"real-camera\">真实照片</option>\r\n </select>\r\n <button type=\"button\" class=\"dcap-btn dcap-primary\" style=\"flex:0 0 auto\" data-action=\"session-new\">新建</button>\r\n </div>\r\n </section>\r\n\r\n <section class=\"dcap-blk\" data-blk=\"virtual\">\r\n <div class=\"dcap-label\">虚拟采集<em data-role=\"auto-state\">空闲</em></div>\r\n <div class=\"dcap-row\">\r\n <button type=\"button\" class=\"dcap-btn dcap-primary\" data-action=\"capture-one\">拍一张</button>\r\n <button type=\"button\" class=\"dcap-btn\" data-action=\"auto-toggle\">自动采集</button>\r\n </div>\r\n <div class=\"dcap-row\">\r\n <label class=\"dcap-field\">间隔(s)\r\n <input class=\"dcap-num\" data-role=\"interval\" type=\"number\" min=\"0\" step=\"0.5\" value=\"1\">\r\n </label>\r\n <label class=\"dcap-field\">距离(m)\r\n <input class=\"dcap-num\" data-role=\"distance\" type=\"number\" min=\"0\" step=\"1\" value=\"0\">\r\n </label>\r\n </div>\r\n <p class=\"dcap-hint\">与「拍照」同源,每张带取景相机位姿 / 内参 / 坐标系。</p>\r\n </section>\r\n\r\n <section class=\"dcap-blk\" data-blk=\"real\">\r\n <div class=\"dcap-label\">真实照片导入</div>\r\n <div class=\"dcap-row\">\r\n <button type=\"button\" class=\"dcap-btn\" data-action=\"pick-files\">选择照片</button>\r\n <button type=\"button\" class=\"dcap-btn\" data-action=\"pick-dir\">选择文件夹</button>\r\n </div>\r\n <p class=\"dcap-hint\">原图不压缩保存;缺 EXIF / GPS 标记为「警告」。</p>\r\n </section>\r\n\r\n <section class=\"dcap-blk\">\r\n <div class=\"dcap-label\">影像集<em data-role=\"gallery-count\">未创建会话</em></div>\r\n <div class=\"dcap-row\">\r\n <button type=\"button\" class=\"dcap-btn dcap-primary\" data-action=\"open-gallery\" disabled>打开影像集</button>\r\n </div>\r\n <p class=\"dcap-hint\">按组合分组浏览;全景组可「一键合成」成一张等距柱状全景图。</p>\r\n <div class=\"dcap-row\" style=\"margin-top:6px\">\r\n <button type=\"button\" class=\"dcap-btn\" data-action=\"export\" data-format=\"json\">导出 JSON</button>\r\n <button type=\"button\" class=\"dcap-btn\" data-action=\"export\" data-format=\"manifest\">导出 Manifest</button>\r\n <button type=\"button\" class=\"dcap-btn\" data-action=\"export\" data-format=\"csv\">导出 CSV</button>\r\n <button type=\"button\" class=\"dcap-btn\" data-action=\"clear-images\">清空</button>\r\n </div>\r\n </section>\r\n </div>\r\n <p class=\"dcap-note\" data-role=\"note\" hidden></p>\r\n <input type=\"file\" data-role=\"file-input\" accept=\"image/*\" multiple style=\"display:none\">\r\n <input type=\"file\" data-role=\"dir-input\" webkitdirectory directory multiple style=\"display:none\">\r\n `\r\n }\r\n}\r\n","/**\n * Small, renderer agnostic adapter for previewing a reconstructed 3D Tileset.\n *\n * Cesium is deliberately injected by the host. Keeping this class free of a\n * Cesium import means it can be used from a worker or tested with plain\n * objects, while a Cesium application can pass `Cesium3DTileset.fromUrl`\n * (or an object exposing `fromUrl`/`load`) as the loader.\n */\n\nexport type ReconstructionPreviewCompareMode = 'original' | 'reconstructed' | 'both'\n\nexport interface ReconstructionPrimitiveCollection<T> {\n add(primitive: T): T\n remove(primitive: T): boolean\n}\n\nexport interface ReconstructionPreviewViewer<T = unknown> {\n scene?: {\n primitives?: ReconstructionPrimitiveCollection<T>\n }\n}\n\nexport interface ReconstructionTilesetLoader<T> {\n /** A generic loader used by tests and non-Cesium hosts. */\n load?: (url: string) => Promise<T> | T\n /** Cesium3DTileset.fromUrl compatible entry point. */\n fromUrl?: (url: string) => Promise<T> | T\n}\n\nexport type ReconstructionTilesetLoaderInput<T> =\n | ReconstructionTilesetLoader<T>\n | ((url: string) => Promise<T> | T)\n\nexport interface ReconstructionPreviewOptions<TTileset, TOriginal = unknown> {\n viewer: ReconstructionPreviewViewer<TTileset>\n loader: ReconstructionTilesetLoaderInput<TTileset>\n /** Existing scene primitive(s) used for the original/reconstructed comparison. */\n original?: TOriginal | readonly TOriginal[]\n compareMode?: ReconstructionPreviewCompareMode\n visible?: boolean\n}\n\nexport interface ReconstructionPreviewState<TTileset, TOriginal = unknown> {\n url: string | null\n tileset: TTileset | null\n original: TOriginal | readonly TOriginal[] | undefined\n compareMode: ReconstructionPreviewCompareMode\n visible: boolean\n destroyed: boolean\n}\n\ntype Showable = { show?: boolean }\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n\nfunction asShowable(value: unknown): Showable | null {\n return isObject(value) ? (value as Showable) : null\n}\n\nfunction setShow(value: unknown, visible: boolean): void {\n const showable = asShowable(value)\n if (showable) showable.show = visible\n}\n\nfunction forEachOriginal<T>(original: T | readonly T[] | undefined, callback: (value: T) => void): void {\n if (Array.isArray(original)) {\n const values = original as readonly T[]\n values.forEach((value) => callback(value))\n } else if (original !== undefined) {\n callback(original as T)\n }\n}\n\nfunction loaderFunction<T>(loader: ReconstructionTilesetLoaderInput<T>): (url: string) => Promise<T> | T {\n if (typeof loader === 'function') return loader\n if (typeof loader.fromUrl === 'function') return loader.fromUrl.bind(loader)\n if (typeof loader.load === 'function') return loader.load.bind(loader)\n throw new TypeError('A tileset loader function, fromUrl, or load method is required')\n}\n\n/**\n * Manage one reconstructed primitive and an optional original primitive set.\n * The original primitives are owned by the caller and are never removed or\n * destroyed by this class.\n */\nexport class ReconstructionPreview<TTileset, TOriginal = unknown> {\n readonly viewer: ReconstructionPreviewViewer<TTileset>\n readonly loader: (url: string) => Promise<TTileset> | TTileset\n private readonly primitives: ReconstructionPrimitiveCollection<TTileset>\n private original: TOriginal | readonly TOriginal[] | undefined\n private currentTileset: TTileset | null = null\n private currentUrl: string | null = null\n private compareMode: ReconstructionPreviewCompareMode\n private visible: boolean\n private destroyed = false\n private loadVersion = 0\n private readonly originalVisibility = new Map<Showable, { hadShow: boolean; show: boolean | undefined }>()\n\n constructor(options: ReconstructionPreviewOptions<TTileset, TOriginal>) {\n if (!options || !options.viewer) throw new TypeError('A viewer is required')\n const primitives = options.viewer.scene?.primitives\n if (!primitives || typeof primitives.add !== 'function' || typeof primitives.remove !== 'function') {\n throw new TypeError('viewer.scene.primitives with add/remove methods is required')\n }\n this.viewer = options.viewer\n this.primitives = primitives\n this.loader = loaderFunction(options.loader)\n this.original = options.original\n this.compareMode = options.compareMode ?? 'both'\n this.visible = options.visible ?? true\n this.rememberOriginalVisibility()\n this.applyVisibility()\n }\n\n get tileset(): TTileset | null {\n return this.currentTileset\n }\n\n get url(): string | null {\n return this.currentUrl\n }\n\n get mode(): ReconstructionPreviewCompareMode {\n return this.compareMode\n }\n\n get isVisible(): boolean {\n return this.visible\n }\n\n get isDestroyed(): boolean {\n return this.destroyed\n }\n\n get state(): ReconstructionPreviewState<TTileset, TOriginal> {\n return {\n url: this.currentUrl,\n tileset: this.currentTileset,\n original: this.original,\n compareMode: this.compareMode,\n visible: this.visible,\n destroyed: this.destroyed,\n }\n }\n\n /** Load, mount, and return a tileset through the injected adapter. */\n async load(url: string): Promise<TTileset> {\n this.assertAlive()\n if (typeof url !== 'string' || !url.trim()) throw new TypeError('A non-empty tileset URL is required')\n const version = ++this.loadVersion\n\n // Resolve before removing the current result, so a transient network\n // failure does not make an already visible preview disappear.\n const loaded = await this.loader(url)\n if (loaded === null || loaded === undefined) throw new Error('Tileset loader returned no tileset')\n if (this.destroyed || version !== this.loadVersion) {\n if (loaded !== this.currentTileset) this.destroyPrimitive(loaded)\n throw new Error(this.destroyed ? 'ReconstructionPreview has been destroyed' : 'Tileset load was superseded')\n }\n\n const previous = this.currentTileset\n if (loaded !== previous) {\n try {\n this.primitives.add(loaded)\n } catch (error) {\n this.destroyPrimitive(loaded)\n throw error\n }\n this.currentTileset = loaded\n this.currentUrl = url\n if (previous !== null) this.unmount(previous)\n } else {\n this.currentUrl = url\n }\n this.applyVisibility()\n return loaded\n }\n\n /** Change which scene layers are shown. */\n setCompareMode(mode: ReconstructionPreviewCompareMode): void {\n this.assertAlive()\n if (mode !== 'original' && mode !== 'reconstructed' && mode !== 'both') {\n throw new RangeError(`Unknown reconstruction compare mode: ${String(mode)}`)\n }\n this.compareMode = mode\n this.applyVisibility()\n }\n\n /** Toggle the complete preview while preserving the compare mode. */\n setVisible(visible: boolean): void {\n this.assertAlive()\n this.visible = Boolean(visible)\n this.applyVisibility()\n }\n\n /** Replace the caller-owned original primitive used by comparison mode. */\n setOriginal(original: TOriginal | readonly TOriginal[] | undefined): void {\n this.assertAlive()\n this.restoreOriginalVisibility()\n this.original = original\n this.rememberOriginalVisibility()\n this.applyVisibility()\n }\n\n /** Remove and destroy the loaded reconstruction. Safe to call repeatedly. */\n destroy(): void {\n if (this.destroyed) return\n this.destroyed = true\n this.loadVersion += 1\n const loaded = this.currentTileset\n this.currentTileset = null\n this.currentUrl = null\n try {\n if (loaded !== null) this.unmount(loaded)\n } finally {\n this.restoreOriginalVisibility()\n }\n }\n\n private assertAlive(): void {\n if (this.destroyed) throw new Error('ReconstructionPreview has been destroyed')\n }\n\n private rememberOriginalVisibility(): void {\n forEachOriginal(this.original, (primitive) => {\n const showable = asShowable(primitive)\n if (showable && !this.originalVisibility.has(showable)) {\n this.originalVisibility.set(showable, { hadShow: 'show' in showable, show: showable.show })\n }\n })\n }\n\n private restoreOriginalVisibility(): void {\n for (const [primitive, original] of this.originalVisibility) {\n if (original.hadShow) primitive.show = original.show\n else delete primitive.show\n }\n this.originalVisibility.clear()\n }\n\n private applyVisibility(): void {\n const showOriginal = this.visible && (this.compareMode === 'original' || this.compareMode === 'both')\n const showReconstructed =\n this.visible && (this.compareMode === 'reconstructed' || this.compareMode === 'both')\n forEachOriginal(this.original, (primitive) => setShow(primitive, showOriginal))\n if (this.currentTileset !== null) setShow(this.currentTileset, showReconstructed)\n }\n\n private unmount(primitive: TTileset): void {\n setShow(primitive, false)\n try {\n this.primitives.remove(primitive)\n } finally {\n this.destroyPrimitive(primitive)\n }\n }\n\n private destroyPrimitive(primitive: unknown): void {\n if (!isObject(primitive)) return\n const isDestroyed = primitive.isDestroyed\n if (typeof isDestroyed === 'function' && isDestroyed.call(primitive)) return\n const destroy = primitive.destroy\n if (typeof destroy === 'function') destroy.call(primitive)\n }\n}\n\nexport default ReconstructionPreview\n"],"mappings":"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"}