Commit 9c9efc33 by James Yuan

feat: 场景 Marker 附近视频弹窗与坐标换算

点击摄像头 Marker 在附近打开可拖动预览窗,换算推流坐标并避免关闭穿透与标题显示为 id。

Co-authored-by: Cursor <cursoragent@cursor.com>
parent 0903597b
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from 'vue'
const props = withDefaults(
defineProps<{
open: boolean
title: string
/** 对话框左上角(视口坐标);无效时由父级传屏幕中心 */
left: number
top: number
src?: string
}>(),
{
src: '/cams/lab-seq.mp4',
},
)
const emit = defineEmits<{ close: [] }>()
const BOX_W = 400
const BOX_H = 240
const TITLE_H = 36
const posLeft = ref(0)
const posTop = ref(0)
const dragging = ref(false)
let dragOx = 0
let dragOy = 0
function clampPos(left: number, top: number) {
const vw = window.innerWidth
const vh = window.innerHeight
return {
left: Math.max(0, Math.min(Math.round(left), Math.max(0, vw - BOX_W))),
top: Math.max(0, Math.min(Math.round(top), Math.max(0, vh - BOX_H))),
}
}
function syncFromProps() {
const c = clampPos(props.left, props.top)
posLeft.value = c.left
posTop.value = c.top
}
watch(
() => [props.open, props.left, props.top] as const,
([open]) => {
if (open) syncFromProps()
},
{ immediate: true },
)
const style = computed(() => ({
left: `${posLeft.value}px`,
top: `${posTop.value}px`,
width: `${BOX_W}px`,
height: `${BOX_H}px`,
}))
const videoH = BOX_H - TITLE_H
function onKey(e: KeyboardEvent) {
if (e.key === 'Escape') requestClose()
}
function onTitleDown(e: PointerEvent) {
if (e.button !== 0) return
// 关闭钮不进入拖拽,避免 pointer capture 吞掉 click
if ((e.target as HTMLElement | null)?.closest?.('.near-close')) return
const t = e.currentTarget as HTMLElement
t.setPointerCapture(e.pointerId)
dragging.value = true
dragOx = e.clientX - posLeft.value
dragOy = e.clientY - posTop.value
}
function onTitleMove(e: PointerEvent) {
if (!dragging.value) return
const c = clampPos(e.clientX - dragOx, e.clientY - dragOy)
posLeft.value = c.left
posTop.value = c.top
}
function onTitleUp(e: PointerEvent) {
if (!dragging.value) return
dragging.value = false
try {
;(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId)
} catch {
/* ignore */
}
}
function requestClose(e?: Event) {
e?.preventDefault()
e?.stopPropagation()
if (dragging.value) {
dragging.value = false
}
emit('close')
}
watch(
() => props.open,
(v) => {
if (v) window.addEventListener('keydown', onKey)
else window.removeEventListener('keydown', onKey)
},
)
onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
</script>
<template>
<Teleport to="body">
<div v-if="open" class="near-layer">
<!-- pointerdown 关闭,避免 click 卸层后穿透到 iframe 再次打开 -->
<div
class="near-backdrop"
@pointerdown="requestClose"
@click.prevent.stop
/>
<div
class="near-dialog"
:style="style"
role="dialog"
:aria-label="title"
@pointerdown.stop
@click.stop
>
<header
class="near-head"
@pointerdown="onTitleDown"
@pointermove="onTitleMove"
@pointerup="onTitleUp"
@pointercancel="onTitleUp"
>
<h3 :title="title">{{ title }}</h3>
<button
class="near-close"
type="button"
aria-label="关闭"
@pointerdown.stop="requestClose"
@click.stop.prevent="requestClose"
>
×
</button>
</header>
<div class="near-body" :style="{ height: `${videoH}px` }">
<video
class="near-video"
:src="src"
autoplay
muted
loop
playsinline
controls
/>
</div>
</div>
</div>
</Teleport>
</template>
<style scoped>
.near-layer {
position: fixed;
inset: 0;
z-index: 340;
pointer-events: none;
}
.near-backdrop {
position: absolute;
inset: 0;
pointer-events: auto;
background: transparent;
}
.near-dialog {
position: absolute;
pointer-events: auto;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 6px;
border: 1px solid rgba(0, 247, 255, 0.4);
background: #071426;
box-shadow:
0 12px 36px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(0, 0, 0, 0.35);
}
.near-head {
flex: 0 0 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 0 8px 0 12px;
border-bottom: 1px solid rgba(0, 247, 255, 0.18);
background: linear-gradient(180deg, rgba(12, 42, 72, 0.98), rgba(6, 22, 42, 0.98));
cursor: move;
user-select: none;
touch-action: none;
}
.near-head h3 {
margin: 0;
min-width: 0;
flex: 1;
font-size: 13px;
font-weight: 600;
color: #e8fbff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
}
.near-close {
flex: 0 0 auto;
width: 28px;
height: 28px;
border: none;
border-radius: 4px;
background: transparent;
color: #9ec8dc;
font-size: 18px;
line-height: 1;
cursor: pointer;
}
.near-close:hover {
color: #fff;
background: rgba(255, 255, 255, 0.08);
}
.near-body {
flex: 1;
min-height: 0;
background: #000;
}
.near-video {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
background: #000;
}
</style>
...@@ -3,11 +3,13 @@ import { computed, ref, onMounted, onBeforeUnmount } from 'vue' ...@@ -3,11 +3,13 @@ import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
import { useRealtimeStore } from '@/stores/realtime' import { useRealtimeStore } from '@/stores/realtime'
import { SCREEN_AREAS } from '@/data/rooms' import { SCREEN_AREAS } from '@/data/rooms'
import AppIcon from '@/components/icons/AppIcon.vue' import AppIcon from '@/components/icons/AppIcon.vue'
import CameraPlayDialog from '@/components/video/CameraPlayDialog.vue' import MarkerNearDialog from '@/components/scene/MarkerNearDialog.vue'
import { api } from '@/api/http' import { api } from '@/api/http'
import { import {
preloadRtengineIcons, preloadRtengineIcons,
useRtengineBridge, useRtengineBridge,
type SceneMarkerHit,
type ScenePoint,
} from '@/composables/useRtengineBridge' } from '@/composables/useRtengineBridge'
import type { VideoDevice } from '@/types/video' import type { VideoDevice } from '@/types/video'
...@@ -44,14 +46,236 @@ const cloudFrameRef = ref<HTMLIFrameElement | null>(null) ...@@ -44,14 +46,236 @@ const cloudFrameRef = ref<HTMLIFrameElement | null>(null)
const activeAreaId = ref<string>(SCREEN_AREAS[0]?.id || 'seq') const activeAreaId = ref<string>(SCREEN_AREAS[0]?.id || 'seq')
const cameras = ref<VideoDevice[]>([]) const cameras = ref<VideoDevice[]>([])
const playOpen = ref(false) /** Marker 附近弹窗(400×240) */
const playCam = ref<VideoDevice | null>(null) const nearOpen = ref(false)
const nearTitle = ref('监控预览')
const nearLeft = ref(120)
const nearTop = ref(120)
/** 关闭后短时忽略 Marker 点击,防止卸层点击穿透再次打开 */
let nearCloseGuardUntil = 0
/** iframe 内最近一次指针(用于贴 Marker 定位) */
const lastIframePointer = ref<{ x: number; y: number } | null>(null)
/** fill 模式:等 #screen-float-overlays 就绪再 Teleport,避免视点条丢失 */ /** fill 模式:等 #screen-float-overlays 就绪再 Teleport,避免视点条丢失 */
const floatTargetReady = ref(false) const floatTargetReady = ref(false)
let markersSynced = false let markersSynced = false
let engineBootstrapping = false let engineBootstrapping = false
/** 就绪管线只跑一次,避免重复 addMarkers 导致光照/场景闪变 */ /** 就绪管线只跑一次,避免重复 addMarkers 导致光照/场景闪变 */
let engineReadyDone = false let engineReadyDone = false
let iframePointerBound: Document | null = null
/** 最近一次命中:页面视口坐标(已换算) */
const lastHitPagePos = ref<{ left: number; top: number } | null>(null)
function getStreamVideo(): HTMLVideoElement | null {
try {
return cloudFrameRef.value?.contentDocument?.querySelector('video') ?? null
} catch {
return null
}
}
/**
* 引擎常按推流分辨率(如 1920×1080)回报屏幕坐标,
* 需按 video 的 CSS 盒与 object-fit 映射到 iframe CSS 像素。
*/
function streamPxToIframeCss(sx: number, sy: number): { x: number; y: number } | null {
const video = getStreamVideo()
const fr = cloudFrameRef.value?.getBoundingClientRect()
if (!fr) return null
const cssW = video?.clientWidth || fr.width
const cssH = video?.clientHeight || fr.height
const nw = video?.videoWidth || 0
const nh = video?.videoHeight || 0
if (!nw || !nh || cssW <= 0 || cssH <= 0) {
return { x: sx, y: sy }
}
const fit = video ? getComputedStyle(video).objectFit : 'fill'
if (fit === 'contain' || fit === 'scale-down') {
const scale = Math.min(cssW / nw, cssH / nh)
const dispW = nw * scale
const dispH = nh * scale
const ox = (cssW - dispW) / 2
const oy = (cssH - dispH) / 2
return { x: ox + sx * scale, y: oy + sy * scale }
}
// fill / cover / none:按各轴独立拉伸(PixelStreaming 默认 fill)
if (fit === 'cover') {
const scale = Math.max(cssW / nw, cssH / nh)
const dispW = nw * scale
const dispH = nh * scale
const ox = (cssW - dispW) / 2
const oy = (cssH - dispH) / 2
return { x: ox + sx * scale, y: oy + sy * scale }
}
return { x: (sx / nw) * cssW, y: (sy / nh) * cssH }
}
/** iframe 内 CSS 坐标 → 父页面视口坐标 */
function iframeCssToPage(local: { x: number; y: number }): { left: number; top: number } | null {
const frame = cloudFrameRef.value
if (!frame) return null
const fr = frame.getBoundingClientRect()
const style = getComputedStyle(frame)
const bl = parseFloat(style.borderLeftWidth) || 0
const bt = parseFloat(style.borderTopWidth) || 0
return {
left: fr.left + bl + local.x,
top: fr.top + bt + local.y,
}
}
function onIframePointer(ev: MouseEvent) {
// clientX/Y 相对 iframe 视口 = CSS 像素
const page = iframeCssToPage({ x: ev.clientX, y: ev.clientY })
if (page) lastHitPagePos.value = page
lastIframePointer.value = { x: ev.clientX, y: ev.clientY }
}
function bindIframePointer() {
unbindIframePointer()
const doc = cloudFrameRef.value?.contentDocument
if (!doc) return
doc.addEventListener('pointerdown', onIframePointer, true)
doc.addEventListener('click', onIframePointer, true)
iframePointerBound = doc
}
function unbindIframePointer() {
if (!iframePointerBound) return
iframePointerBound.removeEventListener('pointerdown', onIframePointer, true)
iframePointerBound.removeEventListener('click', onIframePointer, true)
iframePointerBound = null
}
function screenCenter(): { left: number; top: number } {
return {
left: Math.max(0, Math.round(window.innerWidth / 2 - 200)),
top: Math.max(0, Math.round(window.innerHeight / 2 - 120)),
}
}
function parseScreenFromRaw(raw: unknown): { x: number; y: number } | null {
if (!raw || typeof raw !== 'object') return null
const bags: Record<string, unknown>[] = [raw as Record<string, unknown>]
const root = raw as Record<string, unknown>
if (root.data && typeof root.data === 'object') bags.push(root.data as Record<string, unknown>)
if (root.metaData && typeof root.metaData === 'object') {
bags.push(root.metaData as Record<string, unknown>)
}
for (const o of bags) {
const x = Number(
o.screenX ?? o.clientX ?? o.mouseX ?? o.px ?? o.screen_x ?? o.ScreenX ?? o.viewportX,
)
const y = Number(
o.screenY ?? o.clientY ?? o.mouseY ?? o.py ?? o.screen_y ?? o.ScreenY ?? o.viewportY,
)
if (Number.isFinite(x) && Number.isFinite(y) && (x !== 0 || y !== 0)) return { x, y }
}
return null
}
/**
* 把引擎回报的屏幕坐标换算为页面视口坐标。
* 推流分辨率(如 1920×1080)大于 iframe CSS 时需按 video 盒缩放。
*/
function engineScreenToPage(pt: { x: number; y: number }): { left: number; top: number } | null {
const fr = cloudFrameRef.value?.getBoundingClientRect()
if (!fr) return null
const video = getStreamVideo()
const nw = video?.videoWidth || 0
const nh = video?.videoHeight || 0
// 超出 iframe CSS 盒 → 视为推流像素,做分辨率换算
const isStreamPx =
nw > fr.width + 8 &&
nh > fr.height + 8 &&
(pt.x > fr.width + 1 || pt.y > fr.height + 1) &&
pt.x >= 0 &&
pt.y >= 0 &&
pt.x <= nw + 2 &&
pt.y <= nh + 2
if (isStreamPx) {
const css = streamPxToIframeCss(pt.x, pt.y)
return css ? iframeCssToPage(css) : null
}
// iframe CSS 像素
if (pt.x >= 0 && pt.y >= 0 && pt.x <= fr.width + 2 && pt.y <= fr.height + 2) {
return iframeCssToPage(pt)
}
// 页面视口坐标
if (pt.x >= 0 && pt.y >= 0 && pt.x <= window.innerWidth && pt.y <= window.innerHeight) {
return { left: pt.x, top: pt.y }
}
return null
}
/**
* Marker 屏幕位置 → 对话框左上角:
* 1) 点击命中时已换算的页面坐标(最准)
* 2) 引擎回调屏幕坐标(推流分辨率需换算)
* 3) getMarker 屏幕字段
* 失败 → null(屏幕中心)
*/
function resolveMarkerScreenPos(
markerId: string | null,
raw: unknown,
): { left: number; top: number } | null {
if (lastHitPagePos.value) {
const p = lastHitPagePos.value
lastHitPagePos.value = null
return p
}
const ptr = lastIframePointer.value
if (ptr) {
const page = iframeCssToPage(ptr)
if (page) return page
}
const fromMsg = parseScreenFromRaw(raw)
if (fromMsg) {
const page = engineScreenToPage(fromMsg)
if (page) return page
}
if (markerId) {
try {
const w = cloudFrameRef.value?.contentWindow as
| (Window & { $gisObj?: Record<string, unknown> })
| null
const getMarker = w?.$gisObj?.getMarker
if (typeof getMarker === 'function') {
const m = (getMarker as (id: string) => unknown).call(w?.$gisObj, markerId)
const scr = parseScreenFromRaw(m)
if (scr) return engineScreenToPage(scr)
}
} catch {
/* ignore */
}
}
return null
}
function openNearDialog(title: string, markerId: string | null, raw: unknown) {
if (Date.now() < nearCloseGuardUntil) return
const anchor = resolveMarkerScreenPos(markerId, raw) || screenCenter()
nearTitle.value = title
nearLeft.value = Math.round(anchor.left)
nearTop.value = Math.round(anchor.top)
nearOpen.value = true
}
function closeNearDialog() {
nearOpen.value = false
nearCloseGuardUntil = Date.now() + 400
lastHitPagePos.value = null
}
function cameraMarkerList() { function cameraMarkerList() {
return cameras.value return cameras.value
...@@ -100,21 +324,84 @@ function syncCameraMarkers(force = false) { ...@@ -100,21 +324,84 @@ function syncCameraMarkers(force = false) {
markersSynced = true markersSynced = true
} }
function normalizeCamKey(v: unknown): string {
return String(v || '')
.trim()
.replace(/^cam:/i, '')
}
/** 引擎有时把 UUID/内部 id 填进 name,不能当标题 */
function looksLikeRawId(s: string): boolean {
const t = s.trim()
if (!t) return true
if (/^cam:/i.test(t)) return true
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(t)) return true
if (/^[0-9a-f]{16,}$/i.test(t)) return true
return false
}
function findCamera(marker: SceneMarkerHit, markerId: string | null): VideoDevice | null {
// 引擎回调里 id/name/tag1 都可能是设备 id
const keys = [markerId, marker.id, marker.tag1, marker.tag2, marker.name]
.map(normalizeCamKey)
.filter(Boolean)
for (const id of keys) {
const hit = cameras.value.find((d) => d.id === id)
if (hit) return hit
}
const label = (marker.name || '').trim()
if (label && !looksLikeRawId(label)) {
const byName = cameras.value.find((d) => d.name === label || d.location === label)
if (byName) return byName
}
return null
}
function resolveNearTitle(
cam: VideoDevice | null,
marker: SceneMarkerHit,
markerId: string | null,
): string {
const fromCam = (cam?.name || cam?.location || '').trim()
if (fromCam) return fromCam
const label = (marker.name || '').trim()
const idKeys = new Set(
[markerId, marker.id, marker.tag1, marker.tag0, marker.tag2]
.map(normalizeCamKey)
.filter(Boolean),
)
if (label && !looksLikeRawId(label) && !idKeys.has(normalizeCamKey(label))) {
return label
}
return '监控预览'
}
function onMarkerClick(payload: {
point: ScenePoint | null
markerId: string | null
marker: SceneMarkerHit
raw: unknown
}) {
if (Date.now() < nearCloseGuardUntil) return
// 已打开时忽略,避免关闭穿透或连点反复打断
if (nearOpen.value) return
const { markerId, marker, raw } = payload
// 空白地面点击:无对象 id 则忽略
if (!markerId && !marker.id && !marker.tag0 && !marker.name) return
const cam = findCamera(marker, markerId)
openNearDialog(resolveNearTitle(cam, marker, markerId), markerId || marker.id, raw)
}
const bridge = useRtengineBridge({ const bridge = useRtengineBridge({
iframeRef: cloudFrameRef, iframeRef: cloudFrameRef,
cloudUrl, cloudUrl,
onReady: () => { onReady: () => {
void onEngineReady() void onEngineReady()
bindIframePointer()
}, },
onMouseClick: ({ markerId }) => { onMouseClick: onMarkerClick,
if (!markerId) return
const cam =
cameras.value.find((d) => d.id === markerId) ||
cameras.value.find((d) => d.id === markerId.replace(/^cam:/, ''))
if (!cam) return
playCam.value = cam
playOpen.value = true
},
}) })
async function loadCameras() { async function loadCameras() {
...@@ -128,6 +415,7 @@ async function loadCameras() { ...@@ -128,6 +415,7 @@ async function loadCameras() {
} }
function onCloudFrameLoad() { function onCloudFrameLoad() {
bindIframePointer()
// iframe 文档就绪后稍等信令/场景,再触发一次就绪管线 // iframe 文档就绪后稍等信令/场景,再触发一次就绪管线
window.setTimeout(() => bridge.markReady(), 2500) window.setTimeout(() => bridge.markReady(), 2500)
} }
...@@ -279,6 +567,7 @@ onMounted(() => { ...@@ -279,6 +567,7 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
viewerRef.value?.removeEventListener('load', onViewerLoad) viewerRef.value?.removeEventListener('load', onViewerLoad)
unbindIframePointer()
}) })
</script> </script>
...@@ -380,11 +669,13 @@ onBeforeUnmount(() => { ...@@ -380,11 +669,13 @@ onBeforeUnmount(() => {
</span> </span>
</div> </div>
<CameraPlayDialog <MarkerNearDialog
:open="playOpen" :open="nearOpen"
:device-id="playCam?.id || ''" :title="nearTitle"
:title="playCam?.name" :left="nearLeft"
@close="playOpen = false" :top="nearTop"
src="/cams/lab-seq.mp4"
@close="closeNearDialog"
/> />
</div> </div>
</template> </template>
......
...@@ -11,24 +11,27 @@ const emit = defineEmits<{ close: [] }>() ...@@ -11,24 +11,27 @@ const emit = defineEmits<{ close: [] }>()
</script> </script>
<template> <template>
<div v-if="open" class="mask" @click.self="emit('close')"> <Teleport to="body">
<div class="dialog"> <div v-if="open" class="mask" @click.self="emit('close')">
<div class="head"> <div class="dialog" @click.stop>
<h3>{{ title || '视频预览' }}</h3> <div class="head">
<button class="btn ghost" type="button" @click="emit('close')">关闭</button> <h3>{{ title || '视频预览' }}</h3>
</div> <button class="btn ghost" type="button" @click="emit('close')">关闭</button>
<div class="body"> </div>
<WebRtcPlayer v-if="deviceId" :device-id="deviceId" /> <div class="body">
<WebRtcPlayer v-if="deviceId" :device-id="deviceId" />
<div v-else class="empty">未绑定视频设备</div>
</div>
</div> </div>
</div> </div>
</div> </Teleport>
</template> </template>
<style scoped> <style scoped>
.mask { .mask {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 70; z-index: 330;
background: rgba(0, 8, 20, 0.65); background: rgba(0, 8, 20, 0.65);
display: grid; display: grid;
place-items: center; place-items: center;
...@@ -60,4 +63,23 @@ const emit = defineEmits<{ close: [] }>() ...@@ -60,4 +63,23 @@ const emit = defineEmits<{ close: [] }>()
min-height: 360px; min-height: 360px;
background: #000; background: #000;
} }
.empty {
min-height: 320px;
display: grid;
place-items: center;
color: rgba(185, 214, 240, 0.55);
font-size: 13px;
}
.btn {
border: 1px solid rgba(0, 247, 255, 0.28);
background: transparent;
color: #d7f7ff;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
font-size: 12px;
}
.btn:hover {
border-color: rgba(0, 247, 255, 0.55);
}
</style> </style>
...@@ -143,8 +143,8 @@ export function parseScenePointFromMessage(data: unknown): ScenePoint | null { ...@@ -143,8 +143,8 @@ export function parseScenePointFromMessage(data: unknown): ScenePoint | null {
return null return null
} }
export function parseMarkerIdFromMessage(data: unknown): string | null { function messageBags(data: unknown): Record<string, unknown>[] {
if (!data || typeof data !== 'object') return null if (!data || typeof data !== 'object') return []
const root = data as Record<string, unknown> const root = data as Record<string, unknown>
const bags: Record<string, unknown>[] = [root] const bags: Record<string, unknown>[] = [root]
if (root.data && typeof root.data === 'object') bags.push(root.data as Record<string, unknown>) if (root.data && typeof root.data === 'object') bags.push(root.data as Record<string, unknown>)
...@@ -158,11 +158,69 @@ export function parseMarkerIdFromMessage(data: unknown): string | null { ...@@ -158,11 +158,69 @@ export function parseMarkerIdFromMessage(data: unknown): string | null {
/* ignore */ /* ignore */
} }
} }
for (const o of bags) { if (root.params && typeof root.params === 'object') {
const id = o.id ?? o.markerId ?? o.objectId ?? o.tag0 ?? o.tag1 bags.push(root.params as Record<string, unknown>)
if (id != null && String(id).trim()) return String(id).trim()
} }
return null return bags
}
export type SceneMarkerHit = {
id: string | null
name: string | null
type: string | null
tag0: string | null
tag1: string | null
tag2: string | null
icon: string | null
eventId: string | null
}
export function parseMarkerInfoFromMessage(data: unknown): SceneMarkerHit {
const empty: SceneMarkerHit = {
id: null,
name: null,
type: null,
tag0: null,
tag1: null,
tag2: null,
icon: null,
eventId: null,
}
const bags = messageBags(data)
if (!bags.length) return empty
const pick = (...keys: string[]) => {
for (const o of bags) {
for (const k of keys) {
const v = o[k]
if (v != null && String(v).trim()) return String(v).trim()
}
}
return null
}
const id = pick('id', 'markerId', 'objectId')
// 优先可读文案;若 name 与 id 相同(引擎常把 id 填进 name),改用 text/title/label
const rawName = pick('name', 'text', 'title', 'label')
const altLabel = pick('text', 'title', 'label', 'name')
const name =
rawName && id && rawName === id && altLabel && altLabel !== id ? altLabel : rawName
return {
id,
name,
type: pick('type', 'objectType'),
tag0: pick('tag0', 'Tag0'),
tag1: pick('tag1', 'Tag1'),
tag2: pick('tag2', 'Tag2'),
icon: pick('icon', 'url', 'Icon'),
eventId: pick('eventId'),
}
}
export function parseMarkerIdFromMessage(data: unknown): string | null {
const info = parseMarkerInfoFromMessage(data)
return info.id || info.tag1 || info.tag0 || null
} }
type GisObj = { type GisObj = {
...@@ -177,7 +235,12 @@ type GisObj = { ...@@ -177,7 +235,12 @@ type GisObj = {
export function useRtengineBridge(opts: { export function useRtengineBridge(opts: {
iframeRef: Ref<HTMLIFrameElement | null> iframeRef: Ref<HTMLIFrameElement | null>
cloudUrl: string cloudUrl: string
onMouseClick?: (payload: { point: ScenePoint | null; markerId: string | null; raw: unknown }) => void onMouseClick?: (payload: {
point: ScenePoint | null
markerId: string | null
marker: SceneMarkerHit
raw: unknown
}) => void
onReady?: () => void onReady?: () => void
}) { }) {
const ready = ref(false) const ready = ref(false)
...@@ -420,9 +483,11 @@ export function useRtengineBridge(opts: { ...@@ -420,9 +483,11 @@ export function useRtengineBridge(opts: {
type === 'selectMarker' || type === 'selectMarker' ||
(data && typeof data === 'object' && (data as { eventId?: string }).eventId === 'mouseClick') (data && typeof data === 'object' && (data as { eventId?: string }).eventId === 'mouseClick')
) { ) {
const marker = parseMarkerInfoFromMessage(data)
opts.onMouseClick?.({ opts.onMouseClick?.({
point: parseScenePointFromMessage(data), point: parseScenePointFromMessage(data),
markerId: parseMarkerIdFromMessage(data), markerId: marker.id || marker.tag1 || marker.tag0,
marker,
raw: data, raw: data,
}) })
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment