Browse Source

fix(cockpit): keep device icon fully visible and widen focus composition

ChenJZ 4 hours ago
parent
commit
a8a2d46081

+ 21 - 0
.scratch/cockpit-shell-panel-layout/verify/t52-camera-flight.mjs

@@ -62,6 +62,27 @@ const fitDistance = bodyBounds.radius / Math.sin(THREE.MathUtils.degToRad(camera
 const settleDistance = camera.position.distanceTo(deviceBody.getWorldPosition(new THREE.Vector3()))
 assert.ok(settleDistance > fitDistance * 1.5, `终点距设备 ${settleDistance.toFixed(2)} 必须明显大于贴合距离 ${fitDistance.toFixed(2)},不能贴脸`)
 
+// 3c) 设备与点位图标必须**同时完整可见**(维护者 2026-10-23:「需要能展示设备及点位图标的全貌」)。
+// 图标是屏幕恒定像素尺寸、挂在设备上方,只按设备包围球算距离会让图标顶出画面上沿。
+const nIcon = scene.getObjectByName('device:D').getObjectByName('device-icon')
+const nBody = scene.getObjectByName('device:D').getObjectByName('device-body')
+const ndcOf = (node) => {
+    const b = new THREE.Box3().setFromObject(node)
+    const out = []
+    for (const x of [b.min.x, b.max.x]) for (const y of [b.min.y, b.max.y]) for (const z of [b.min.z, b.max.z]) {
+        out.push(new THREE.Vector3(x, y, z).project(camera))
+    }
+    return out
+}
+for (const [name, node] of [['设备主体', nBody], ['点位图标', nIcon]]) {
+    for (const p of ndcOf(node)) {
+        assert.ok(Math.abs(p.x) <= 0.95 && Math.abs(p.y) <= 0.95, `${name} 必须完整在画面内,实测 NDC (${p.x.toFixed(2)}, ${p.y.toFixed(2)})`)
+    }
+}
+// 图标顶端还要留出上方空间,不能顶到上沿。
+const iconTop = Math.max(...ndcOf(nIcon).map((p) => p.y))
+assert.ok(iconTop <= 0.6, `点位图标顶端 ${iconTop.toFixed(2)} 应留出上方空间(≤0.6)`)
+
 // 4) 用户接管镜头(拖拽)必须取消未完成的飞行,不能和用户抢相机。
 clock = 0
 frames.length = 0

+ 36 - 8
src/modules/cockpit/services/threePipeEnhancement.js

@@ -16,6 +16,9 @@ import { renderMarker } from './markerAssets.js'
  * 色值真源仍是 tokens.css;本表只在读不到计算样式时兜底(脱浏览器回归、样式未挂载)。
  * 刻意不与 mapService 共享常量:那两个文件带有其它在制品改动,本票不触碰它们。
  */
+/** 设备图标相对设备主体的世界高度偏移(不受 group 缩放影响)。 */
+const ICON_OFFSET_ABOVE_DEVICE = 0.62
+
 const TONE_FALLBACK = Object.freeze({
     lv1: '#ff3b52',
     lv2: '#ffb020',
@@ -226,7 +229,10 @@ function buildNeighborhood(plan) {
         const icon = new THREE.Sprite(new THREE.SpriteMaterial({ transparent: true, depthTest: false, depthWrite: false, sizeAttenuation: true }))
         icon.name = 'device-icon'
         icon.center.set(0.5, 0)
-        icon.position.set(0, 0.72, 0)
+        // 图标挂在设备正上方一个**稳定的世界距离**,必须除掉 group.scale:
+        // 否则 group 的 3.5~7 倍缩放会把它顶到设备上方 3~5 个世界单位处
+        // (实测 NDC y 达 1.63~1.80,整个图标落在画面上沿之外,拉多远都看不全)。
+        icon.position.set(0, ICON_OFFSET_ABOVE_DEVICE / deviceScale, 0)
         icon.renderOrder = 30
         icon.userData.device = device
         icon.visible = false
@@ -312,8 +318,12 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
      * 与「到期精确到位」,而不是只能验证终态。
      */
     const FLIGHT_MS = 620
-    /** 定位终点距离倍率:1.3 太贴脸,1.8 留出邻域上下文(维护者 2026-10-23 反馈)。 */
-    const FOCUS_DISTANCE_FACTOR = 1.8
+    /** 定位构图:图标顶端不超过这个 NDC 高度(1=画面上沿),留出上方空间。 */
+    const FOCUS_ICON_TOP_NDC = 0.4
+    /** 设备底端不越过这个 NDC 高度(负值=画面下沿),保证设备完整。 */
+    const FOCUS_BODY_BOTTOM_NDC = 0.9
+    /** 解析距离的安全系数:再多留一点上下文,避免刚好贴边。 */
+    const FOCUS_SAFETY = 1.18
     const nowImpl = typeof now === 'function'
         ? now
         : () => (typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now())
@@ -557,11 +567,29 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
             // 图标按屏幕像素恒定尺寸缩放,飞行途中会变大变小,用整体盒算中心会让镜头「飘」。
             const anchor = device.getObjectByName('device-body') || device
             const bounds = new THREE.Box3().setFromObject(anchor).getBoundingSphere(new THREE.Sphere())
-            // 终点距离:把设备包围球刚好塞满视野再往后退一截(维护者 2026-10-23:
-            // 「最终移动到的终点离设备有点太近了,可以稍微拉远一点点」)。
-            // 1.3 会把设备顶到画面边缘,改为 1.8(约 +38%)留出周围管网上下文。
-            const fitDistance = bounds.radius / Math.sin(THREE.MathUtils.degToRad(camera.fov / 2))
-            const toDistance = Math.max(0.8, fitDistance * FOCUS_DISTANCE_FACTOR)
+            /**
+             * 终点距离按「**设备与图标都要完整可见**」解析求出,不再用固定倍率猜。
+             *
+             * 维护者 2026-10-23:「还需要拉远一点,需要能展示设备及点位图标的全貌。」
+             * 图标是**屏幕恒定像素**的(约 33px 高),头顶在设备上方 `ICON_OFFSET_ABOVE_DEVICE`;
+             * 只按设备包围球算距离会让图标顶出画面上沿(实测 NDC y 曾达 1.63~1.80)。
+             *
+             * 设 tanHalf=tan(fov/2)、H=视口高、iconPx=图标屏显高、o=图标偏移、r=设备半径:
+             *   图标顶端 NDC ≈ (o + 2·d·tanHalf·iconPx/H) / (d·tanHalf) ≤ ICON_TOP_NDC
+             *   →  d ≥ o / (tanHalf · (ICON_TOP_NDC − 2·iconPx/H))
+             *   设备底端 NDC ≈ r / (d·tanHalf) ≤ BODY_BOTTOM_NDC → d ≥ r / (BODY_BOTTOM_NDC · tanHalf)
+             * 取两者较大值再乘安全系数。这样任意视口尺寸都能适配,不是写死的倍数。
+             */
+            const tanHalf = Math.tan(THREE.MathUtils.degToRad(camera.fov / 2))
+            const viewHeight = host.clientHeight || 220
+            const iconNode = device.getObjectByName('device-icon')
+            const iconWidthPx = iconNode?.userData?.iconWidth || 26.7
+            const iconAspect = iconNode?.userData?.iconAspect || 1.2566
+            const iconPx = iconWidthPx * iconAspect
+            const iconSpan = 2 * iconPx / viewHeight
+            const distanceForIcon = ICON_OFFSET_ABOVE_DEVICE / Math.max(0.01, tanHalf * (FOCUS_ICON_TOP_NDC - iconSpan))
+            const distanceForBody = bounds.radius / (FOCUS_BODY_BOTTOM_NDC * tanHalf)
+            const toDistance = Math.max(0.8, Math.max(distanceForIcon, distanceForBody) * FOCUS_SAFETY)
             // 平滑飞行定位(不再瞬移);返回 true 表示已受理,飞行在后续帧推进。
             startFlight(bounds.center, toDistance)
             return true