|
@@ -252,7 +252,7 @@ function buildNeighborhood(plan) {
|
|
|
* Mount a bounded local viewer. The optional rendererFactory is a system-boundary seam
|
|
* Mount a bounded local viewer. The optional rendererFactory is a system-boundary seam
|
|
|
* for offline tests; production uses WebGLRenderer.
|
|
* for offline tests; production uses WebGLRenderer.
|
|
|
*/
|
|
*/
|
|
|
-export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRendererFactory, pixelRatio = 1, onDevicePick = () => {}, onObjectPick = () => {}, iconResolver = defaultDeviceIcon, loadTexture = null } = {}) {
|
|
|
|
|
|
|
+export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRendererFactory, pixelRatio = 1, onDevicePick = () => {}, onObjectPick = () => {}, iconResolver = defaultDeviceIcon, loadTexture = null, scheduleFrame = null, now = null } = {}) {
|
|
|
if (!host || !plan || plan.visible !== true || !plan.object) {
|
|
if (!host || !plan || plan.visible !== true || !plan.object) {
|
|
|
return { status: 'unavailable', sync: () => {}, destroy: () => {}, getState: () => ({ status: 'unavailable' }) }
|
|
return { status: 'unavailable', sync: () => {}, destroy: () => {}, getState: () => ({ status: 'unavailable' }) }
|
|
|
}
|
|
}
|
|
@@ -302,6 +302,48 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
|
|
|
renderer.setSize?.(width, height, false)
|
|
renderer.setSize?.(width, height, false)
|
|
|
render()
|
|
render()
|
|
|
}
|
|
}
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 镜头定位飞行(维护者 2026-10-22:「点击点位后的镜头定位运动效果也需要实现。」)
|
|
|
|
|
+ *
|
|
|
|
|
+ * 点击设备时不再瞬移:从当前 `target`/`distance` 在 `FLIGHT_MS` 内缓动到目标,
|
|
|
|
|
+ * 每帧重绘。缓动用 ease-in-out cubic(两端慢、中间快),与主图相机飞行的观感一致。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 两个可注入接缝(`scheduleFrame` / `now`)让脱浏览器回归能逐帧断言「起步不是终点」
|
|
|
|
|
+ * 与「到期精确到位」,而不是只能验证终态。
|
|
|
|
|
+ */
|
|
|
|
|
+ const FLIGHT_MS = 620
|
|
|
|
|
+ const nowImpl = typeof now === 'function'
|
|
|
|
|
+ ? now
|
|
|
|
|
+ : () => (typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now())
|
|
|
|
|
+ const scheduleFrameImpl = typeof scheduleFrame === 'function'
|
|
|
|
|
+ ? scheduleFrame
|
|
|
|
|
+ : (fn) => (typeof requestAnimationFrame === 'function' ? requestAnimationFrame(fn) : setTimeout(fn, 16))
|
|
|
|
|
+ let flight = null
|
|
|
|
|
+ const easeInOutCubic = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2)
|
|
|
|
|
+ const cancelFlight = () => { flight = null }
|
|
|
|
|
+ const stepFlight = () => {
|
|
|
|
|
+ if (!flight || destroyed) return
|
|
|
|
|
+ const elapsed = nowImpl() - flight.startedAt
|
|
|
|
|
+ const progress = Math.min(1, Math.max(0, elapsed / flight.duration))
|
|
|
|
|
+ const eased = easeInOutCubic(progress)
|
|
|
|
|
+ target.lerpVectors(flight.fromTarget, flight.toTarget, eased)
|
|
|
|
|
+ distance = flight.fromDistance + (flight.toDistance - flight.fromDistance) * eased
|
|
|
|
|
+ render()
|
|
|
|
|
+ if (progress >= 1) { flight = null; return }
|
|
|
|
|
+ scheduleFrameImpl(stepFlight)
|
|
|
|
|
+ }
|
|
|
|
|
+ const startFlight = (toTarget, toDistance, duration = FLIGHT_MS) => {
|
|
|
|
|
+ flight = {
|
|
|
|
|
+ fromTarget: target.clone(),
|
|
|
|
|
+ fromDistance: distance,
|
|
|
|
|
+ toTarget: toTarget.clone(),
|
|
|
|
|
+ toDistance,
|
|
|
|
|
+ startedAt: nowImpl(),
|
|
|
|
|
+ duration: Math.max(1, duration),
|
|
|
|
|
+ }
|
|
|
|
|
+ scheduleFrameImpl(stepFlight)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const render = () => {
|
|
const render = () => {
|
|
|
const horizontal = Math.cos(pitch) * distance
|
|
const horizontal = Math.cos(pitch) * distance
|
|
|
camera.position.set(
|
|
camera.position.set(
|
|
@@ -440,6 +482,7 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
|
|
|
})
|
|
})
|
|
|
const onDown = (event) => {
|
|
const onDown = (event) => {
|
|
|
if (drag || (event.button !== 0 && event.button !== 2)) return
|
|
if (drag || (event.button !== 0 && event.button !== 2)) return
|
|
|
|
|
+ cancelFlight()
|
|
|
const panDistance = Math.max(0.02, Math.min(distance, hitAt(event.clientX, event.clientY)?.distance ?? distance))
|
|
const panDistance = Math.max(0.02, Math.min(distance, hitAt(event.clientX, event.clientY)?.distance ?? distance))
|
|
|
drag = { button: event.button, id: event.pointerId, x: event.clientX, y: event.clientY, panDistance }
|
|
drag = { button: event.button, id: event.pointerId, x: event.clientX, y: event.clientY, panDistance }
|
|
|
event.preventDefault?.()
|
|
event.preventDefault?.()
|
|
@@ -474,6 +517,7 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
|
|
|
}
|
|
}
|
|
|
const onWheel = (event) => {
|
|
const onWheel = (event) => {
|
|
|
event.preventDefault?.()
|
|
event.preventDefault?.()
|
|
|
|
|
+ cancelFlight()
|
|
|
const hit = hitAt(event.clientX, event.clientY)
|
|
const hit = hitAt(event.clientX, event.clientY)
|
|
|
const factor = Math.exp(Math.max(-1000, Math.min(1000, event.deltaY)) * 0.001)
|
|
const factor = Math.exp(Math.max(-1000, Math.min(1000, event.deltaY)) * 0.001)
|
|
|
const limit = Math.max(sceneRadius * 50, 10)
|
|
const limit = Math.max(sceneRadius * 50, 10)
|
|
@@ -507,16 +551,20 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
|
|
|
if (destroyed) return false
|
|
if (destroyed) return false
|
|
|
const device = root?.getObjectByName(`device:${code}`)
|
|
const device = root?.getObjectByName(`device:${code}`)
|
|
|
if (!device) return false
|
|
if (!device) return false
|
|
|
- const bounds = new THREE.Box3().setFromObject(device).getBoundingSphere(new THREE.Sphere())
|
|
|
|
|
- target.copy(bounds.center)
|
|
|
|
|
- distance = Math.max(0.5, bounds.radius / Math.sin(THREE.MathUtils.degToRad(camera.fov / 2)) * 1.3)
|
|
|
|
|
- render()
|
|
|
|
|
|
|
+ // 定位目标取**稳定几何**(设备主体/支杆),不能用含图标的整体包围盒:
|
|
|
|
|
+ // 图标按屏幕像素恒定尺寸缩放,飞行途中会变大变小,用整体盒算中心会让镜头「飘」。
|
|
|
|
|
+ const anchor = device.getObjectByName('device-body') || device
|
|
|
|
|
+ const bounds = new THREE.Box3().setFromObject(anchor).getBoundingSphere(new THREE.Sphere())
|
|
|
|
|
+ const toDistance = Math.max(0.5, bounds.radius / Math.sin(THREE.MathUtils.degToRad(camera.fov / 2)) * 1.3)
|
|
|
|
|
+ // 平滑飞行定位(不再瞬移);返回 true 表示已受理,飞行在后续帧推进。
|
|
|
|
|
+ startFlight(bounds.center, toDistance)
|
|
|
return true
|
|
return true
|
|
|
},
|
|
},
|
|
|
getState: () => ({ status: destroyed ? 'destroyed' : 'ready', code: current.object.code, title: enhancementLabel(current.object.sourceKind) }),
|
|
getState: () => ({ status: destroyed ? 'destroyed' : 'ready', code: current.object.code, title: enhancementLabel(current.object.sourceKind) }),
|
|
|
destroy() {
|
|
destroy() {
|
|
|
if (destroyed) return
|
|
if (destroyed) return
|
|
|
destroyed = true
|
|
destroyed = true
|
|
|
|
|
+ cancelFlight()
|
|
|
onUp()
|
|
onUp()
|
|
|
observer?.disconnect()
|
|
observer?.disconnect()
|
|
|
detachFocus()
|
|
detachFocus()
|