Tour postMessage SDK

The Treedis tour exposes a postMessage-based SDK that lets any parent page — whether an iframe or a popup window — communicate bi-directionally with the running tour. No relay server or custom bridge script is required.

Overview

The SDK works by passing structured messages through the browser's native window.postMessage API:

  • Tour → Parent: window.opener.postMessage(event, '*') (popup) or window.parent.postMessage(event, '*') (iframe)
  • Parent → Tour: tourWindow.postMessage(command, '*') (popup) or iframe.contentWindow.postMessage(command, '*') (iframe)

The tour auto-detects whether it is running inside an iframe or was opened via window.open() and routes messages to the correct target automatically.

Integration Patterns

Open the tour in a separate browser window. The viewer and tour run side by side.

// Open the tour — must be triggered by a user gesture (click)
const tourWindow = window.open('https://app.treedis.com/tour/<model-id>', 'treedis-tour');

// Listen for events from the tour
window.addEventListener('message', (event) => {
  const { type, ...data } = event.data;
  switch (type) {
    case 'TourReady':     handleTourReady();           break;
    case 'PoseChanged':   handlePose(data);             break;
    case 'SweepsChanged': handleSweeps(data.sweeps);    break;
    case 'TagClicked':    handleTagClicked(data.tag);   break;
    case 'TagHovered':    handleTagHovered(data.tag);   break;
    case 'TagFocused':    handleTagFocused(data.tag);   break;
    case 'TagDocked':     handleTagDocked(data.tag);    break;
  }
});

// Send a command to the tour
function navigateToSweep(sweepId) {
  tourWindow.postMessage({ type: 'Navigate', sweepId, transitionTime: 1500 }, '*');
}

Iframe Embed

Embed the tour directly in your page using an <iframe>.

<iframe
  id="tour-frame"
  src="https://app.treedis.com/tour/<model-id>"
  allow="fullscreen"
  style="width:100%;height:600px;border:none;"
/>
const frame = document.getElementById('tour-frame');

// Listen for events from the tour
window.addEventListener('message', (event) => {
  const { type, ...data } = event.data;
  // same handler as popup pattern above
});

// Send a command to the tour
function navigateToSweep(sweepId) {
  frame.contentWindow.postMessage({ type: 'Navigate', sweepId, transitionTime: 1500 }, '*');
}

Note: Both patterns share the same event/command protocol. Only the method of opening the tour and sending commands differs.


Handshake / Connection Flow

Parent                                      Tour
  │                                           │
  │── window.open() / <iframe src="..."> ────►│
  │                                           │ (loading...)
  │◄─── { type: 'TourReady' } ───────────────│ app state = 'playing'
  │                                           │
  │◄─── { type: 'SweepsChanged', sweeps: []}─│ sweeps loaded
  │                                           │
  │◄─── { type: 'PoseChanged', ... } ────────│ camera moved (~10Hz)
  │                                           │
  │──── { type: 'Navigate', sweepId } ───────►│
  │                                           │

Tip: Send a Ping command after opening the tour — if the tour is already loaded, it responds immediately with TourReady. This handles the case where the tour window was already open before your page loaded.

// Ping the tour to check if it's already ready
tourWindow.postMessage({ type: 'Ping' }, '*');

Inbound Events (Tour → Parent)

These events are emitted by the tour and received by your message event listener.

TourReady

Fired when the tour has fully loaded and is ready to receive commands.

{ "type": "TourReady" }

Also fired in response to a Ping command.


PoseChanged

Fired continuously as the camera moves, approximately every 100ms.

{
  "type": "PoseChanged",
  "x": 1.23,
  "y": 0.0,
  "z": -4.56,
  "rotationY": 137.4,
  "sweep": "abc12345-..."
}
Field Type Description
x number Camera world position X (meters)
y number Camera world position Y (meters)
z number Camera world position Z (meters)
rotationY number Camera horizontal rotation (degrees, 0–360)
sweep string ID of the currently active Matterport sweep

SweepsChanged

Fired once after the tour loads all sweep data. Re-emitted in response to a RequestSweeps command.

{
  "type": "SweepsChanged",
  "sweeps": [
    { "id": "abc12345-...", "x": 1.23, "y": 0.0, "z": -4.56 },
    { "id": "def67890-...", "x": 3.10, "y": 0.0, "z": -1.20 }
  ]
}
Field Type Description
sweeps[].id string Matterport sweep ID
sweeps[].x number Sweep world position X (meters)
sweeps[].y number Sweep world position Y (meters)
sweeps[].z number Sweep world position Z (meters)

Only enabled sweeps are included.


TagClicked

Fired when a user clicks on a tag in the tour.

{
  "type": "TagClicked",
  "tag": {
    "sid": "abc123",
    "defaultSid": "abc123",
    "id": "tag-uuid",
    "title": "Room Label",
    "url": "https://...",
    "type": "mattertag",
    "description": "...",
    "position": { "x": 1.2, "y": 1.5, "z": -3.4 }
  }
}

TagHovered

Fired when the cursor hovers over a tag.

{ "type": "TagHovered", "tag": { ... } }

Same tag shape as TagClicked.


TagFocused

Fired when a tag panel comes into focus (e.g., navigating to a tag's sweep).

{ "type": "TagFocused", "tag": { ... } }

TagDocked

Fired when a tag is docked into the sidebar or info panel.

{ "type": "TagDocked", "tag": { ... } }

Tag Object Shape

All tag events share the same tag object:

Field Type Description
sid string Matterport scene ID
defaultSid string Default scene ID
id string Treedis tag UUID
title string Tag display name
url string Associated URL (if any)
type string Tag type (e.g., "mattertag")
description string Tag description text
position.x/y/z number Tag 3D world position (meters)

Outbound Commands (Parent → Tour)

Send commands by calling postMessage on the tour window or iframe.

Moves the tour camera to the specified sweep.

tourWindow.postMessage({
  type: 'Navigate',
  sweepId: 'abc12345-...',         // required
  rotation: { x: 0, y: 180 },     // optional, degrees (x = pitch, y = yaw)
  transitionTime: 1500,            // optional, ms (default: 1500)
}, '*');
Field Type Required Description
sweepId string Yes Target sweep ID
rotation { x: number, y: number } No Camera rotation at destination
transitionTime number No Transition duration in milliseconds

RequestSweeps

Asks the tour to re-emit a SweepsChanged event with the current sweep list.

tourWindow.postMessage({ type: 'RequestSweeps' }, '*');

Ping

Checks whether the tour is ready. The tour responds with a TourReady event.

tourWindow.postMessage({ type: 'Ping' }, '*');

Useful for detecting if the tour was already loaded before your page connected to it.


Complete Integration Example

class TourSDK {
  constructor(tourUrl) {
    this.tourUrl = tourUrl;
    this.tourWindow = null;
    this.ready = false;
    this._pingInterval = null;

    window.addEventListener('message', this._handleMessage.bind(this));
  }

  // Open the tour — must be called from a user gesture (click handler)
  open() {
    this.tourWindow = window.open(this.tourUrl, 'treedis-tour');
    this.ready = false;

    // Ping until TourReady is received
    this._pingInterval = setInterval(() => {
      if (this.tourWindow?.closed) {
        clearInterval(this._pingInterval);
        this.onDisconnect?.();
        return;
      }
      if (!this.ready) {
        this.tourWindow?.postMessage({ type: 'Ping' }, '*');
      }
    }, 2000);
  }

  // Send navigate command
  navigateTo(sweepId, transitionTime = 1500) {
    this.tourWindow?.postMessage({ type: 'Navigate', sweepId, transitionTime }, '*');
  }

  // Request fresh sweep list
  requestSweeps() {
    this.tourWindow?.postMessage({ type: 'RequestSweeps' }, '*');
  }

  _handleMessage(event) {
    const { type, ...data } = event.data ?? {};

    switch (type) {
      case 'TourReady':
        this.ready = true;
        clearInterval(this._pingInterval);
        this.onReady?.();
        break;

      case 'PoseChanged':
        this.onPose?.(data);
        break;

      case 'SweepsChanged':
        this.onSweeps?.(data.sweeps);
        break;

      case 'TagClicked':
        this.onTagClicked?.(data.tag);
        break;

      case 'TagHovered':
        this.onTagHovered?.(data.tag);
        break;

      case 'TagFocused':
        this.onTagFocused?.(data.tag);
        break;

      case 'TagDocked':
        this.onTagDocked?.(data.tag);
        break;
    }
  }
}

// Usage
const sdk = new TourSDK('https://app.treedis.com/tour/<model-id>');

sdk.onReady    = ()        => console.log('Tour is ready');
sdk.onPose     = (pose)    => updatePositionDot(pose.x, pose.z, pose.rotationY);
sdk.onSweeps   = (sweeps)  => buildMinimap(sweeps);
sdk.onTagClicked = (tag)   => showTagInfo(tag);

// Triggered by a button click
document.getElementById('open-tour').addEventListener('click', () => sdk.open());

TypeScript Types

// Inbound event types
type TourEvent =
  | { type: 'TourReady' }
  | { type: 'PoseChanged'; x: number; y: number; z: number; rotationY: number; sweep: string }
  | { type: 'SweepsChanged'; sweeps: Array<{ id: string; x: number; y: number; z: number }> }
  | { type: 'TagClicked' | 'TagHovered' | 'TagFocused' | 'TagDocked'; tag: TourTag };

// Outbound command types
type TourCommand =
  | { type: 'Navigate'; sweepId: string; rotation?: { x: number; y: number }; transitionTime?: number }
  | { type: 'RequestSweeps' }
  | { type: 'Ping' };

interface TourTag {
  sid: string;
  defaultSid: string;
  id: string;
  title: string;
  url: string;
  type: string;
  description: string;
  position: { x: number; y: number; z: number };
}

Security Considerations

  • Always validate event.origin in production if you know the exact tour domain:
    window.addEventListener('message', (event) => {
      if (event.origin !== 'https://app.treedis.com') return;
      // handle event
    });
    
  • The '*' targetOrigin used in postMessage is acceptable when the tour URL is controlled by Treedis, but origin validation on the receiving end is recommended for sensitive integrations.

Troubleshooting

Tour window is null after window.open()

  • window.open() returns null if blocked by the browser's popup blocker. Always call it from inside a user gesture (a click event handler).

TourReady never fires

  • Use the Ping interval pattern to retry until the tour acknowledges. The tour may still be loading.

Events stop arriving after closing and re-opening the tour window

  • Re-add your message listener after re-opening, or keep it on window permanently (it persists across popup sessions).

Navigate command has no effect

  • Ensure TourReady has been received before sending commands. The tour SDK is not active until the app state reaches 'playing'.