|
|
@@ -22,6 +22,7 @@ function createFakeEngine({ viewport = { width: 1680, height: 900 }, fovY = Math
|
|
|
const applied = []
|
|
|
let clock = 1000
|
|
|
let pendingFrame = null
|
|
|
+ let groundAt = { ...ground }
|
|
|
|
|
|
return {
|
|
|
applied,
|
|
|
@@ -33,7 +34,7 @@ function createFakeEngine({ viewport = { width: 1680, height: 900 }, fovY = Math
|
|
|
return { heading: 0, pitch: -60, range: 10000 }
|
|
|
},
|
|
|
pickGroundAtCenter() {
|
|
|
- return { ...ground }
|
|
|
+ return { ...groundAt }
|
|
|
},
|
|
|
projectPivot() {
|
|
|
return { x: viewport.width / 2, y: viewport.height / 2 }
|
|
|
@@ -54,6 +55,10 @@ function createFakeEngine({ viewport = { width: 1680, height: 900 }, fovY = Math
|
|
|
pendingFrame = callback
|
|
|
},
|
|
|
},
|
|
|
+ /** 换掉「屏幕中心拾取到哪」——用来验证松手时的重锚规则。 */
|
|
|
+ setGround(next) {
|
|
|
+ groundAt = { lon: next.lon, lat: next.lat, height: next.height || 0 }
|
|
|
+ },
|
|
|
get pendingFrame() {
|
|
|
return pendingFrame
|
|
|
},
|
|
|
@@ -269,8 +274,13 @@ const degreesLonFor = (meters, lat) => meters / (111320 * Math.cos((lat * Math.P
|
|
|
|
|
|
control.beginDrag({ button: 0, x: 0, y: 0 })
|
|
|
control.moveDrag({ x: 10, y: 0 })
|
|
|
+ // 真实引擎里「屏幕中心拾取」与平移推算值应当几乎重合(lookAt 保证中心射线落在中心点上),
|
|
|
+ // 所以这里让拾取点跟上推算值,验证「平移把中心交回地面拾取点」这条语义仍然成立。
|
|
|
+ const panned = control.getState().pivot
|
|
|
+ fake.setGround(panned)
|
|
|
control.endDrag()
|
|
|
- assert.deepEqual(control.getState().pivot, GROUND, '左键平移后中心回到地面拾取点')
|
|
|
+ assert.deepEqual(control.getState().pivot, panned, '左键平移后中心应是地面拾取点(不再是选中对象)')
|
|
|
+ assert.notDeepEqual(control.getState().pivot, XIANGLING, '平移后中心不再跟随选中对象')
|
|
|
console.log('PASS 7 中心规则:选中优先 → 取消不回退 → 平移才更新为地面拾取点')
|
|
|
}
|
|
|
|
|
|
@@ -362,6 +372,97 @@ const degreesLonFor = (meters, lat) => meters / (111320 * Math.cos((lat * Math.P
|
|
|
console.log(`PASS 9b 阻尼与帧率无关(60fps ${(at60 * 100).toFixed(1)}% vs 120fps ${(at120 * 100).toFixed(1)}%)`)
|
|
|
}
|
|
|
|
|
|
+// ---------- 9c. 松手有惯性:平移与滚轮/右键手感一致(维护者 2026-09-17 复验反馈) ----------
|
|
|
+{
|
|
|
+ // 平移:拖拽中一直有滞后(实际态追不上目标态),松手后必须**继续滑行**到目标。
|
|
|
+ const { fake, control } = newControl()
|
|
|
+ control.beginDrag({ button: 0, x: 0, y: 0 })
|
|
|
+ for (let i = 1; i <= 6; i += 1) {
|
|
|
+ control.moveDrag({ x: i * 80, y: 0 })
|
|
|
+ fake.runFrame(FRAME_MS)
|
|
|
+ }
|
|
|
+ const atRelease = control.getState()
|
|
|
+ const gapAtRelease = Math.abs(atRelease.pivot.lon - atRelease.shownPivot.lon)
|
|
|
+ assert.ok(gapAtRelease > 0, '快速拖动时实际态应落后于目标态(这正是惯性来源)')
|
|
|
+
|
|
|
+ control.endDrag()
|
|
|
+ fake.runFrame(FRAME_MS)
|
|
|
+ const justAfter = control.getState()
|
|
|
+ const gapAfterOneFrame = Math.abs(justAfter.pivot.lon - justAfter.shownPivot.lon)
|
|
|
+ assert.ok(
|
|
|
+ gapAfterOneFrame > gapAtRelease * 0.5,
|
|
|
+ `松手后目标不许被改写(否则滞后被抹平、惯性消失):松手前缺口 ${gapAtRelease},一帧后 ${gapAfterOneFrame}`,
|
|
|
+ )
|
|
|
+
|
|
|
+ // 继续滑行:实际态继续朝目标推进,直到吸附。
|
|
|
+ const before = justAfter.shownPivot.lon
|
|
|
+ let frames = 1
|
|
|
+ while (!control.getState().settled && frames < 400) {
|
|
|
+ fake.runFrame(FRAME_MS)
|
|
|
+ frames += 1
|
|
|
+ }
|
|
|
+ const glided = Math.abs(control.getState().shownPivot.lon - before)
|
|
|
+ assert.ok(glided > 0, `松手后应继续滑行一段(实际滑了 ${glided})`)
|
|
|
+ assert.deepEqual(control.getState().shownPivot, control.getState().pivot, '滑行结束后吸附到目标')
|
|
|
+ console.log(
|
|
|
+ `PASS 9c 平移松手有惯性(松手时滞后 ${gapAtRelease.toFixed(6)}°,之后又滑行 ${glided.toFixed(6)}°、${frames} 帧停稳)`,
|
|
|
+ )
|
|
|
+
|
|
|
+ // 滚轮与右键:滚完/松手后同样继续收敛 —— 三种交互手感一致(按各自动的维度量)。
|
|
|
+ const gliding = (act) => {
|
|
|
+ const { fake: f, control: c } = newControl()
|
|
|
+ const read = () => c.getState()
|
|
|
+ const lagOf = (s) => (act === 'wheel' ? Math.abs(s.hpr.range - s.shownHpr.range) : Math.abs(s.hpr.heading - s.shownHpr.heading))
|
|
|
+
|
|
|
+ if (act === 'wheel') {
|
|
|
+ c.wheel({ deltaY: -600 })
|
|
|
+ } else {
|
|
|
+ c.beginDrag({ button: 2, x: 0, y: 0 })
|
|
|
+ c.moveDrag({ x: 200, y: 0 })
|
|
|
+ c.endDrag()
|
|
|
+ }
|
|
|
+ f.runFrame(FRAME_MS)
|
|
|
+ const lag = lagOf(read())
|
|
|
+ let frames = 1
|
|
|
+ while (!c.getState().settled && frames < 400) {
|
|
|
+ f.runFrame(FRAME_MS)
|
|
|
+ frames += 1
|
|
|
+ }
|
|
|
+ return { lag, frames }
|
|
|
+ }
|
|
|
+ const wheel = gliding('wheel')
|
|
|
+ const orbit = gliding('orbit')
|
|
|
+ assert.ok(wheel.lag > 0, `滚轮滚完应有余韵(range 滞后 ${wheel.lag})`)
|
|
|
+ assert.ok(orbit.lag > 0, `右键松手应有余韵(heading 滞后 ${orbit.lag})`)
|
|
|
+ console.log(
|
|
|
+ `PASS 9c2 滚轮与右键同样有余韵(一帧后 range 滞后 ${wheel.lag.toFixed(1)}m / heading 滞后 ${orbit.lag.toFixed(3)}°)`,
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+// ---------- 9d. 松手重锚只清微小误差,绝不把画面拽回去 ----------
|
|
|
+{
|
|
|
+ // 偏差可忽略(比 range 的 1% 小得多):允许重锚,中心落到真实地表拾取点。
|
|
|
+ const { fake, control } = newControl()
|
|
|
+ control.beginDrag({ button: 0, x: 0, y: 0 })
|
|
|
+ control.moveDrag({ x: 200, y: 0 })
|
|
|
+ const target = control.getState().pivot
|
|
|
+ fake.setGround({ lon: target.lon + 1e-6, lat: target.lat }) // 偏差 ≈ 0.1m,远小于阈值
|
|
|
+ const shownBefore = control.getState().shownPivot.lon
|
|
|
+ control.endDrag()
|
|
|
+ near(control.getState().pivot.lon, target.lon + 1e-6, 1e-12, '偏差可忽略时应重锚到拾取点')
|
|
|
+ assert.equal(control.getState().shownPivot.lon, shownBefore, '重锚不得动实际态(不许把画面拽回去)')
|
|
|
+
|
|
|
+ // 偏差过大(等距近似在该尺度已失效):宁可不重锚,也不让画面跳。
|
|
|
+ const { fake: f2, control: c2 } = newControl()
|
|
|
+ c2.beginDrag({ button: 0, x: 0, y: 0 })
|
|
|
+ c2.moveDrag({ x: 200, y: 0 })
|
|
|
+ const before2 = c2.getState().pivot
|
|
|
+ f2.setGround({ lon: before2.lon + 5, lat: before2.lat }) // 偏差几百公里 ≫ range*1%
|
|
|
+ c2.endDrag()
|
|
|
+ assert.deepEqual(c2.getState().pivot, before2, '偏差过大时不得重锚(避免画面瞬移)')
|
|
|
+ console.log('PASS 9d 松手重锚只在偏差可忽略时发生(大偏差保持推算值,不跳画面)')
|
|
|
+}
|
|
|
+
|
|
|
// ---------- 10. 用户操作打断换中心飞行 ----------
|
|
|
{
|
|
|
const { fake, control } = newControl()
|