Skip to main content
Documentation

Panorama Hotspot Tags

Add interactive text, image, and custom hotspot tags to panorama mode.

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.
  • contentType selects the tag's content and corresponding data shape.
  • stickType selects how the tag attaches to the scene: 2DPoint, 3DPoint, Plane, Model, 3DBox, Polygon, or Mask.
  • 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/dnalogel

Import 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 tag
  • pauseCurrentMedia: () => 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

  1. 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
  }
})
  1. 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. Accepts Five.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, MinMax or '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 enabled
    • distanceAccuracy: distance tolerance
    • checkPoints: '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.
  • autoUnfold strategies:
    • 'MinimumDistance': the nearest tag auto-unfolds; supports maxNumber and distance.
    • '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 (supports distance, maxNumber).

Initial Behavior and Data

  • initialState: the initial visible/unfolded.
  • initialData: default data merged into tag.data, with support for the important hard 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 to false).
  • 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 to 2000000).
  • imageURLTransform: (url, { width, height }) => string image URL transformation.
  • viewMoreText: the "View More" text.
  • beforeOpen(tag): returning false intercepts 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.registerRenderer only 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 use if (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 event
  • playStateChange — change in the playback state of media inside a tag
  • exposure — tag exposure event
  • show — tag plugin show event
  • hide — tag plugin hide event
  • enable — tag plugin enable event
  • disable — tag plugin disable event
  • hover — tag-level hover trigger event (fires when popover is enabled and trigger is 'hover' or 'click')
  • showPopover — tag popover show event
  • hidePopover — 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.popoverConfig

Listen 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:

FieldContract
contentTypeRequired content key such as Text, ImageText, Audio, Link, Marketing, MediaPlane, Model, or Custom. It controls the corresponding data shape.
stickTypeOptional attachment type. Supported values are 2DPoint, 3DPoint, Plane, Model, 3DBox, Polygon, and Mask; the default is 2DPoint.
positionRequired coordinates whose shape depends on stickType. A plane uses four positions; point and model tags use one position.
dataRequired content payload selected by contentType.
configOptional TagConfig<C> for visibility, unfolding, interaction, and popover behavior.
id, enabled, normal, element, className, fiveState, style, hoverEnabledOptional 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.

Source Reference

PanoTagPlugin example source