In this chapter you will learn
- How to pick a point in 3D space, map it to the screen, and render a DOM tag.
- How to refresh tag positions with
project2das the camera moves.
Vue example: screen tags
<template>
<div style="width: 100vw; height: 100vh">
<div ref="container" style="width: 100%; height: 100%; overflow: hidden; position: relative" />
<div v-for="t in validTags" :key="t.id" :style="t.style">
<div style="background: #333; color: #fff; padding: 6px 10px; border-radius: 4px; font-size: 12px">Tag</div>
</div>
<div style="position: fixed; top: 16px; left: 16px"><button class="btn btn-primary" @click="enabled = !enabled">{{
enabled ? 'Disable picking' : 'Enable picking' }}</button></div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, computed } from 'vue'
import { Five, parseWork } from '@realsee/five'
const container = ref(null)
let five = null
const enabled = ref(false)
const tags = ref([]) // { id, position: THREE.Vector3 }
const tick = ref(0)
const workURL = 'https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json'
// Computed property: safely filter and project the tags
const validTags = computed(() => {
// Depend on tick to trigger recomputation
tick.value
return tags.value
.filter(tag => tag && tag.position) // Make sure both tag and position exist
.map(tag => {
const projected = project(tag.position)
return projected ? { ...tag, projected, style: toStyle(projected) } : null
})
.filter(Boolean) // Drop invalid projection results
})
onMounted(() => {
five = new Five()
if (container.value) five.appendTo(container.value)
fetch(workURL).then(r => r.json()).then(json => five.load(parseWork(json)))
const onResize = () => five.refresh()
window.addEventListener('resize', onResize, false)
const onCameraUpdate = () => { tick.value++ }
five.on('cameraUpdate', onCameraUpdate)
const onWantsTap = (raycaster) => {
if (!enabled.value) return true
const hit = five.model.intersectRaycaster(raycaster)
if (hit && hit[0] && hit[0].point) {
tags.value.push({ id: Date.now(), position: hit[0].point })
}
return false
}
five.on('wantsTapGesture', onWantsTap)
onUnmounted(() => {
window.removeEventListener('resize', onResize, false)
five.off('cameraUpdate', onCameraUpdate)
five.off('wantsTapGesture', onWantsTap)
five.dispose()
})
})
function project(pos) {
if (!five || !pos) return null
return five.project2d(pos, true)
}
function toStyle(v2) {
if (!v2 || typeof v2.x !== 'number' || typeof v2.y !== 'number') {
return { display: 'none' }
}
return { position: 'absolute', transform: 'translate(-50%, 0)', left: `${v2.x}px`, top: `${v2.y}px` }
}
</script><template>
<div style="width: 100vw; height: 100vh">
<div ref="container" style="width: 100%; height: 100%; overflow: hidden; position: relative" />
<div v-for="t in validTags" :key="t.id" :style="t.style">
<div style="background: #333; color: #fff; padding: 6px 10px; border-radius: 4px; font-size: 12px">Tag</div>
</div>
<div style="position: fixed; top: 16px; left: 16px"><button class="btn btn-primary" @click="enabled = !enabled">{{
enabled ? 'Disable picking' : 'Enable picking' }}</button></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, computed } from 'vue'
import { Five, parseWork } from '@realsee/five'
import type { Vector2, Vector3 } from 'three'
const container = ref<HTMLDivElement | null>(null)
let five: Five | null = null
const enabled = ref(false)
type Tag = { id: number; position: Vector3 }
const tags = ref<Tag[]>([])
const tick = ref(0)
const workURL = 'https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json'
// Computed property: safely filter and project the tags
const validTags = computed(() => {
// Depend on tick to trigger recomputation
tick.value
return tags.value
.filter(tag => tag && tag.position) // Make sure both tag and position exist
.map(tag => {
const projected = project(tag.position)
return projected ? { ...tag, projected, style: toStyle(projected) } : null
})
.filter(Boolean) // Drop invalid projection results
})
onMounted(() => {
five = new Five()
if (container.value) five.appendTo(container.value)
fetch(workURL).then(r => r.json()).then(json => five!.load(parseWork(json)))
const onResize = () => five!.refresh()
window.addEventListener('resize', onResize, false)
const onCameraUpdate = () => { tick.value++ }
five.on('cameraUpdate', onCameraUpdate)
const onWantsTap = (raycaster: any) => {
if (!enabled.value) return true
const hit = five!.model.intersectRaycaster(raycaster)
if (hit && hit[0] && hit[0].point) {
tags.value.push({ id: Date.now(), position: hit[0].point })
}
return false
}
five.on('wantsTapGesture', onWantsTap)
onUnmounted(() => {
window.removeEventListener('resize', onResize, false)
five!.off('cameraUpdate', onCameraUpdate)
five!.off('wantsTapGesture', onWantsTap)
five!.dispose()
})
})
function project(pos: Vector3): Vector2 | null {
if (!five || !pos) return null
return five.project2d(pos, true)
}
function toStyle(v2: Vector2) {
if (!v2 || typeof v2.x !== 'number' || typeof v2.y !== 'number') {
return { display: 'none' }
}
return { position: 'absolute', transform: 'translate(-50%, 0)', left: `${v2.x}px`, top: `${v2.y}px` }
}
</script>Key points:
- Use
wantsTapGestureto suppress the default move-to-point behavior. - Pick a 3D point with
five.model.intersectRaycaster. - Use
cameraUpdateto trigger re-rendering, andproject2dto map the 3D point to screen coordinates.
