PanoTagPlugin
Overview
The Panorama Hotspot Tag plugin lets you annotate hotspot information at different positions of a listing while in panorama mode.
Detailed feature set:
- Hotspot tags come in several kinds: "Audio tag (Audio)", "Text tag (Text)", "Image-Text tag (ImageText)", "VR jump tag (Link)", "Marketing tag (Marketing)", "Image/Video plane (MediaPlane)", "Custom tag (Custom)", and so on.
contentTypeselects the tag's content and corresponding data shape.stickTypeselects how the tag attaches to the scene:2DPoint,3DPoint,Plane,Model,3DBox,Polygon, orMask.- You can freely combine the classification attributes above and, based on your own business needs, craft panorama hotspot tags that fit your scenario.
External Demo
Open the Panorama Hotspot Tag demo to try the plugin outside this documentation page.
Installation
Choose yarn or npm as needed:
npm install @realsee/dnalogelImport via ES modules:
import { PanoTagPlugin } from '@realsee/dnalogel'Development Guide
Initialization
When initializing the Five instance, just include PanoTagPlugin in the plugin initialization parameters.
import { Five } from '@realsee/five'
import { PanoTagPlugin } from '@realsee/dnalogel'
const five = new Five({
plugins: [
[
PanoTagPlugin,
'panoTagPlugin', // custom plugin name
{
// parameter configuration
},
],
],
})React Initialization
When creating the FiveProvider, just include PanoTagPlugin in the plugin initialization parameters.
import { PanoTagPlugin } from '@realsee/dnalogel'
import { createFiveProvider, FiveCanvas } from '@realsee/five/react'
const FiveProvider = createFiveProvider({
plugins: [
[
PanoTagPlugin,
'panoTagPlugin', // custom plugin name
{
// parameter configuration
},
],
],
})Vue Initialization
When using FiveProvider, just include PanoTagPlugin in the plugin initialization parameters.
<template>
<FiveProvider :fiveInitArgs="fiveInitArgs"> </FiveProvider>
</template>
<script setup>
import PanoTagPlugin from '@realsee/dnalogel/libs/PanoTagPlugin'
import { FiveProvider, FiveCanvas } from '@realsee/five/vue'
const fiveInitArgs = {
plugins: [
[
PanoTagPlugin,
'panoTagPlugin', // custom plugin name
{
// parameter configuration
},
],
],
}
</script>Loading Data
// Call the `load` method to load panorama tag data
plugin.load(data)Core Methods
Loading Plugin Data
Full load (overwrites previously loaded data):
plugin.load(data)Incrementally add data:
plugin.addTag(tag)Clear tag data:
plugin.clearTags()Changing Tag Configuration
plugin.changeConfig({ globalConfig, contentTypeConfig })
plugin.changeGlobalConfig(globalConfig)
plugin.changeContentTypeConfig(contentTypeKey, config)Enable / Disable / Show / Hide / Dispose
// enable the plugin
plugin.enable()
// disable the plugin
plugin.disable()
// show the plugin
plugin.show()
// hide the plugin
plugin.hide()
// dispose the plugin
plugin.dispose()Changing Tag Data
Change tag.data:
plugin.changeDataById(1, data)
// or
const tag = plugin.getTagById(1)
tag?.changeData(data)destroyTagById: (id: TagId | TagId[]) => void— destroy a tagpauseCurrentMedia: () => void— pause all media currently playing inside tags
Hotspot Tag Configuration
Hotspot tags can achieve a wide range of display effects through rich configuration:
Adding config
- Add it at load time:
import type { TagConfig } from '@realsee/dnalogel'
const globalConfig: TagConfig = {}
const TextTagConfig: TagConfig = {}
pluginInstance.load({
tagList: [],
globalConfig: globalConfig,
contentTypeConfig: {
'Text': TextTagConfig
}
})- Switch it at runtime:
import type { TagConfig } from '@realsee/dnalogel'
const globalConfig: TagConfig = {}
const contentTypeConfig = {
'Text': {}
}
pluginInstance.changeConfig({
globalConfig,
contentTypeConfig
})Config Categories
Global Config
import type { TagConfig } from '@realsee/dnalogel'
const globalConfig: TagConfig = {}
pluginInstance.load({
tagList: [],
globalConfig: globalConfig
})Per-content-type config, applies only to the specified tag type
With this approach the global config also takes effect at the same time; when the two conflict, the per-type config wins.
import type { TagConfig } from '@realsee/dnalogel'
const TextTagConfig: TagConfig = {}
pluginInstance.load({
tagList: [],
contentTypeConfig: {
'Text': TextTagConfig
}
})Per-tag config, applies only to a single tag
With this approach the global config and the per-content-type config also take effect at the same time; when they conflict, the per-tag config wins.
import type { TagConfig } from '@realsee/dnalogel'
const TagConfig1: TagConfig = {}
pluginInstance.load({
tagList: [
{
...tagData, // tag data
config: TagConfig1 // applies only to this tag
}
],
})Config interface
import type { TagConfig } from '@realsee/dnalogel'
const config: TagConfig = {
visibleConfig: {
keep: null,
visibleFiveMode: ['Panorama'],
visibleDistance: { min: 0, max: 10 },
visiblePanoIndex: undefined,
// In Mapview/Floorplan, follow the visibility of the current floor
followModelVisibility: false,
// When entering the panorama by clicking an entry point from model mode, whether to allow looking toward the tag point (panorama tags only)
entryFromModel: false,
// Intersection-detection config: controls occlusion judgment
intersectRaycaster: {
enabled: true,
distanceAccuracy: 0.04,
checkPoints: 'corner',
needPassed: 1,
},
// For 3D tags in panorama, the visible range based on the angle between the camera and the normal
angleRange: undefined,
},
unfoldedConfig: {
keep: null,
autoFoldWhenHide: false,
autoUnfold: {
enable: true,
strategy: 'MinimumDistance',
maxNumber: 1,
distance: { min: 0, max: 10 }
},
// Unfold when entering the visible range, fold when leaving
unfoldDistance: undefined,
// Other strategies: 'ScreenPostion' | 'FoldWhenMove' | 'ScreenCenter'
// ScreenPostion: unfold based on the projected x position range
// FoldWhenMove: auto-fold when the screen moves
// ScreenCenter: prioritize unfolding at the screen center
},
// initial state
initialState: {
visible: undefined,
unfolded: undefined,
},
// default data merged into tag.data on initialization (supports `important` override)
initialData: {
important: false,
},
// 3D tag container config
tag3DConfig: {
// scaling parameters such as ratio can also be set in the global defaults
}
}Changing Config
See Changing Tag Configuration.
Common Configuration Options
visibleConfig (visibility)
keep: force visible/invisible in the specified mode. One of'visible' | 'hidden' | null. Once set, the other items in the same group no longer take effect.visibleFiveMode: specify the Five SDK modes in which the tag is visible. AcceptsFive.Mode | Five.Mode[] | 'PanoramaLike' | 'ModelLike' | 'all' | ((tag: TagInstance) => TagVisibleMode).followModelVisibility: in Mapview/Floorplan, whether to be visible only on the current floor.entryFromModel: when clicking an entry point in model mode, automatically look toward the tag after entering the panorama (panorama tags only).visibleDistance: control the visible range by distance from the camera,MinMaxor'unLimited'.visiblePanoIndex: control at which panorama points the tag is visible:'all' | 'current' | number[].intersectRaycaster: tag occlusion detection (when enabled, visibility is judged according to model occlusion).enabled: whether it is enableddistanceAccuracy: distance tolerancecheckPoints:'center' | 'corner' | Vector3[]needPassed: how many check points must pass for the tag to count as visible
angleRange: in panorama (3D tags), be visible based on the angle range between the camera and the normal.
unfoldedConfig (unfold/fold)
keep:'unfolded' | 'folded' | null(highest priority).autoFoldWhenHide: auto-fold when hidden (some tags inherently cannot be folded).unfoldDistance: unfold when entering the range, fold when leaving it.autoUnfoldstrategies:'MinimumDistance': the nearest tag auto-unfolds; supportsmaxNumberanddistance.'ScreenPostion': unfold based on the tag's projected X coordinate range on screen (autoUnfoldProjectX, valued in the range [-1, 1]; the closer to 0, the closer to the screen center).'FoldWhenMove': fold when the screen moves.'ScreenCenter': prioritize unfolding at the screen center (supportsdistance,maxNumber).
Initial Behavior and Data
initialState: the initialvisible/unfolded.initialData: default data merged intotag.data, with support for theimportanthard override.
Rendering and Interaction
tag3DConfig: 3D container parameters (parameters such as ratio are also commonly set in the global defaults).modelConfig.autoLookAtEnabled: whether a model tag automatically faces the camera after entering the panorama.renderType:'Mesh' | 'Dom'(marketing planes and the like can use Mesh under certain conditions).simulate3D: whether to simulate the near-large/far-small effect.clickable: whether the tag is clickable.
popoverConfig (popover; when enabled, unfoldedConfig no longer applies)
enabled: whether to enable the popover (defaults tofalse).trigger:'hover' | 'click'(defaults to'hover').triggerDelay: hover trigger delay (in ms; the plugin default is 500, which can be overridden).placement: popover position (top/bottom/left/right/.../auto).autoPlacementBaseSpace: the reference partition for auto-positioning{ top, right, bottom }.transitionDuration: animation duration (ms).theme:'dark' | 'light'.toolbar:{ showMore?: boolean; showShare?: boolean }.zIndex: popover layer zIndex (defaults to2000000).imageURLTransform:(url, { width, height }) => stringimage URL transformation.viewMoreText: the "View More" text.beforeOpen(tag): returningfalseintercepts the hover open.
Example: enable the popover globally via load and trigger it on hover
pluginInstance.load({
tagList: [],
globalConfig: {
popoverConfig: {
enabled: true,
trigger: 'hover',
triggerDelay: 500,
placement: 'auto',
},
},
})Example: enable the popover for a single tag with a custom theme/toolbar
pluginInstance.load({
tagList: [
{
// ... other tag fields
config: {
popoverConfig: {
enabled: true,
theme: 'light',
toolbar: { showMore: true, showShare: true },
},
},
},
],
})Example: update the global config at runtime
pluginInstance.changeGlobalConfig({
popoverConfig: {
enabled: true,
trigger: 'click',
},
})Adding a Custom Hotspot Tag
Among the hotspot tag types there is one called the "Custom hotspot tag". With this tag type, developers can add any tag style that follows the spec, according to their own business needs.
Refer to the example below:
import type { Tag } from '@realsee/dnalogel'
// Add a custom hotspot
const addCustomerTag = () => {
// custom Element
const ele = document.createElement('div')
ele.innerText = 'This is a custom hotspot tag'
ele.style.color = 'red'
ele.style.width = '200px'
ele.style.border = '1px solid #000'
const tagData: Tag = {
id: '03338b76-b64a-4e90-37fb-44e3c0ffeb88',
stickType: '2DPoint',
position: [-1.7882169929208833, 1.022040232156752, -2.339700937271118],
data: {
text: 'Custom hotspot tag',
},
element: ele,
// set ContentType to Custom
contentType: 'Custom',
}
pluginInstance.addTag(tagData)
}Custom Tag Renderers
Specify the rendering method for a custom tag type
Scenario:
I produced a batch of tag data with the contentType super_tag by some means. I now want to use PanoTagPlugin to render this batch of data, with the result displayed as: <div>I'm super tag, my name is {data.name}</div>.
Code example:
import { createRoot } from 'react-dom/client'
import type { TagInstance } from '@realsee/dnalogel'
const plugin = five.plugins.panoTagPlugin
/** custom tag component */
function SuperTag(props: { tag: TagInstance<'Custom'> }) {
return <div>I'm a super tag, my name is {props.tag.data.name}</div>
}
plugin.registerRenderer('super_tag', (container: HTMLElement, tagInstance: TagInstance<'Custom'>) => {
// use <SuperTag> to render tags whose contentType is "super_tag"
const root = createRoot(container)
root.render(<SuperTag tag={tagInstance} />)
// a destroy function must be returned
return () => root.unmount()
})
// custom tag data
plugin.load({
tagList: [
{
contentType: 'super_tag',
position: [0, 1, 2],
data: {
name: 'super tag 1',
},
},
],
})Tips:
plugin.registerRendereronly needs to be called once; there is no need to call it multiple times. If you are unsure whether it has already been called, you can useif (plugin.rendererMap.has('My_Custom_Tag_Type')) {}to check whether the renderer has been registered.
Replace the rendering method of an existing tag
Scenario:
I think the text tag (contentType: 'Text') in PanoTagPlugin is too ugly, and I want to apply my own style without changing the data.
Code example:
import { createRoot } from 'react-dom/client'
import type { TagInstance } from '@realsee/dnalogel'
const plugin = five.plugins.panoTagPlugin
/** custom text tag component */
function BeautifulTextTag(props: { tag: TagInstance<'Text'> }) {
return <div>I'm a beautiful text tag, my title is {props.tag.data.title}</div>
}
plugin.registerRenderer('Text', (container: HTMLElement, tagInstance: TagInstance<'Text'>) => {
// use <BeautifulTextTag> to render tags whose contentType is "Text", replacing the default rendering
const root = createRoot(container)
root.render(<BeautifulTextTag tag={tagInstance} />)
// a destroy function must be returned
return () => root.unmount()
})
// existing tag data
plugin.load({
tagList: [
{
contentType: 'Text',
position: [0, 1, 2],
data: {
title: 'text tag',
},
},
],
})Use a built-in renderer to render a custom tag
Scenario:
I have a set of tag data with the contentType My_Text. I think the text tag style in PanoTagPlugin fits my needs exactly, and I want to use the plugin's built-in text tag to render my data, but I cannot change the original data type.
Code example:
const plugin = five.plugins.panoTagPlugin
/** Use the built-in `Text` tag style to render `My_Text` tags */
plugin.bindRenderer('My_Text', `Text`)
// existing tag data
plugin.load({
tagList: [
{
contentType: 'My_Text',
position: [0, 1, 2],
data: {
title: 'text tag',
},
},
],
})Keep the component data in sync when the tag data changes
Scenario: I have an input field, and what I type should render onto my custom text tag in real time.
Code example:
import { useEffect, useState } from 'react'
import type { TagInstance } from '@realsee/dnalogel'
// the custom tag component BeautifulTextTag from example 2
function BeautifulTextTag(props: { tag: TagInstance<'Text'> }) {
const [data, setData] = useState(props.tag.data)
useEffect(() => {
// listen for dataChanged and update state in real time
props.tag.hooks.on('dataChanged', setData)
return () => props.tag.hooks.off('dataChanged', setData)
}, [props.tag])
return <div>inputting text is: {data.title}</div>
}
// input field
function Input() {
const [text, setTextState] = useState('')
const setText = (inputValue: string) => {
setTextState(inputValue)
plugin.changeDataById('id', { title: inputValue })
}
return <input value={text} onChange={(e) => setText(e.target.value)} />
}Listening for Tag Events
Supported events:
click— single-tag click eventplayStateChange— change in the playback state of media inside a tagexposure— tag exposure eventshow— tag plugin show eventhide— tag plugin hide eventenable— tag plugin enable eventdisable— tag plugin disable eventhover— tag-level hover trigger event (fires when popover is enabled and trigger is 'hover' or 'click')showPopover— tag popover show eventhidePopover— tag popover hide event
Refer to the following code to listen for events:
import type { TagClickParams, TagId, TagInstance } from '@realsee/dnalogel'
// listen for tag click events
pluginInstance.hooks.on("click", (params: TagClickParams) => {
console.log("click", params);
});
// listen for changes in the playback state of media inside a tag
pluginInstance.hooks.on("playStateChange", (params: { event: Event; state: 'playing' | 'paused'; tag: TagInstance; mediaInstance: HTMLMediaElement }) => {
console.log("playStateChange", params);
});
// listen for tag exposure events
pluginInstance.hooks.on("exposure", (params: { id: TagId; type: 'start' | 'end' }) => {
console.log("exposure", params);
});
// listen for the tag plugin show event
pluginInstance.hooks.on("show", (options: { userAction: boolean }) => {
console.log("show", options);
});
// listen for the tag plugin hide event
pluginInstance.hooks.on("hide", (options: { userAction: boolean }) => {
console.log("hide", options);
});
// listen for the tag plugin enable event
pluginInstance.hooks.on("enable", (options: { userAction: boolean }) => {
console.log("enable", options);
});
// listen for the tag plugin disable event
pluginInstance.hooks.on("disable", (options: { userAction: boolean }) => {
console.log("disable", options);
});Hover Trigger Support
Enable the popover and use hover/click to trigger its display:
// enable the popover globally and configure the trigger method/delay
pluginInstance.changeGlobalConfig({
popoverConfig: {
enabled: true,
trigger: 'hover', // or 'click'
triggerDelay: 500, // only takes effect when trigger='hover'
},
})
// can also be configured at load time, or overridden in a tag-level config.popoverConfigListen for hover and popover events:
const tagInstance = pluginInstance.getTagById('tag-id')
// listen for tag hover (a unified event fired before display is triggered by hover or click)
tagInstance?.hooks.on('hover', ({ event, tag }) => {
console.log('hover', { event, id: tag.id, contentType: tag.contentType })
})
// listen for popover show/hide
tagInstance?.hooks.on('showPopover', () => console.log('popover show'))
tagInstance?.hooks.on('hidePopover', () => console.log('popover hide'))Temporarily disable/enable hover at runtime:
// globally turn hover off/on (does not affect the config, only a temporary mask)
pluginInstance.setGlobalHoverEnabled(false)
pluginInstance.setGlobalHoverEnabled(true)
// disable/enable hover by tag ID (temporary control only)
pluginInstance.setTagHoverEnabled('tag-id', false)
pluginInstance.setTagHoverEnabled('tag-id', true)Remove event listeners:
// remove event listeners
pluginInstance.hooks.off('click', clickHandler)
pluginInstance.hooks.off('playStateChange', playStateChangeHandler)
pluginInstance.hooks.off('exposure', exposureHandler)
pluginInstance.hooks.off('show', showHandler)
pluginInstance.hooks.off('hide', hideHandler)
pluginInstance.hooks.off('enable', enableHandler)
pluginInstance.hooks.off('disable', disableHandler)Data Structures
The most important structure in the plugin is Tag. Operations such as adding a hotspot tag or modifying tag information all require it. In @realsee/dnalogel@3.81.0, its two generic parameters are the content type and the attachment type:
import type { Tag } from '@realsee/dnalogel'
const tag: Tag<'Text', '2DPoint'> = {
id: 'living-room',
contentType: 'Text',
stickType: '2DPoint',
position: [0, 1.4, -2],
data: {
title: 'Living room',
},
}The current public shape uses these fields:
| Field | Contract |
|---|---|
contentType | Required content key such as Text, ImageText, Audio, Link, Marketing, MediaPlane, Model, or Custom. It controls the corresponding data shape. |
stickType | Optional attachment type. Supported values are 2DPoint, 3DPoint, Plane, Model, 3DBox, Polygon, and Mask; the default is 2DPoint. |
position | Required coordinates whose shape depends on stickType. A plane uses four positions; point and model tags use one position. |
data | Required content payload selected by contentType. |
config | Optional TagConfig<C> for visibility, unfolding, interaction, and popover behavior. |
id, enabled, normal, element, className, fiveState, style, hoverEnabled | Optional identity, state, rendering, and interaction fields. A 3DPoint tag requires normal. |
The older pointType and dimensionType properties remain only as deprecated compatibility fields. New integrations should use stickType and the two-parameter Tag<C, S> contract.
