Gaussian viewer SDK

The CW Gaussian viewer supports the tour postMessage protocol. A host can render its own controls while CW supplies only the interactive 3D scene.

Availability: this integration is implemented on CW branch 1.0.1, based on release/1.0.1. It requires deployment of that implementation; it is not yet available on stage1. Existing Matterport tour URLs keep their existing behavior.

Open the scene without CW UI

Use a workspace ID and a space's slug, rather than its Matterport model ID:

https://<cw-origin>/connected-workers/<workspaceId>/locations/<spaceSlug>/noUI

The case-sensitive noUI route omits the CW header, navigation, drawers, viewer buttons and loading UI. Scene annotations, saved media, polygons and placed models remain part of the rendered scene. Tag clicks emit SDK events so the host can open its own record UI.

The route uses the normal signed-in CW session and workspace access checks. It does not provide anonymous access or accept credentials in the URL. A signed-out browser is sent to CW sign-in. For native iOS, establish the authenticated CW session in WebKit before loading the route.

This implementation renders Gaussian scans and model scenes through CW's existing renderer. A Matterport space is rejected instead of opening the older CW tour UI. Standalone, unaligned photo-only E57 scenes are not supported by this route. Aligned capture scenes use the existing shared renderer; photo navigation has the restriction described under Navigate.

Browser integration

Same-origin iframe and popup hosts work without additional configuration. For a cross-origin host, the CW build must list its exact HTTPS origin in the comma-separated VITE_TOUR_SDK_ALLOWED_ORIGINS setting. Wildcards are not supported. The parentOrigin URL parameter selects an origin from that allowlist; it cannot add a trusted origin. Without that parameter, CW uses the embedding document's referrer origin when available.

Register the listener before opening the scene. Check both the sender origin and window:

const cwOrigin = 'https://stage1.cw.treedis.com';
const iframe = document.createElement('iframe');
const url = new URL('/connected-workers/165/locations/<space-slug>/noUI', cwOrigin);
url.searchParams.set('parentOrigin', location.origin);

window.addEventListener('message', (event) => {
  if (event.origin !== cwOrigin || event.source !== iframe.contentWindow) return;
  const message = event.data;
  if (message?.type === 'TourReady' && message.renderer === 'gaussian') {
    send({ type: 'RequestState', requestId: 'initial-state' });
    send({ type: 'RequestTags', requestId: 'initial-tags' });
  }
  if (message?.type === 'TagClicked') openNativeRecord(message.tag);
  if (message?.type === 'SdkError') showViewerError(message);
});

function send(command) {
  iframe.contentWindow?.postMessage(command, cwOrigin);
}

iframe.src = url.href;
iframe.style.cssText = 'width:100%;height:100%;border:0';
document.querySelector('#scene').append(iframe);

The iframe must have access to a valid CW session. Cross-origin embedding does not bypass browser restrictions on third-party cookies or storage. For a popup, use the same checks with the returned popup window instead of iframe.contentWindow.

TourReady is emitted after the engine has loaded and revealed the scene, rather than when the HTML document loads. Send Ping to recover readiness if a listener attached late. Before readiness, Ping has no response and other recognized commands return SdkError with code: "notReady". Bound retries and stop them when the scene closes.

Commands

Every command may include a string requestId of at most 128 characters. Read responses and mutation acknowledgements echo valid IDs. Unknown message types are ignored.

Command Fields Response / behavior
Ping — TourReady, once ready
RequestSweeps — SweepsChanged with currently available viewpoints
RequestTags — TagsChanged with tag metadata
RequestState — StateChanged with current mode, available modes and floors
CaptureScreenshot Optional width, height, crop; mattertags: false, sweeps: false ScreenshotCaptured with a bounded JPEG of the currently rendered scene
FocusTag tagId: string Teleports to the authored approach of a current scene tag; does not require a sweep
RequestNavigation — NavigationChanged with the active route state
StartNavigation tagId: string Requests a walkable route from the current camera to that tag
SetNavigationWalking walking: boolean Plays or pauses the active route through collision walking
StopNavigation — Clears the route, destination and walking state
Navigate sweepId, or position plus target/rotation; optional transitionTime Moves the existing engine camera; see below
SetViewMode mode Chooses an available orbit, fly, walk or dollhouse mode
ResetView — Restores the starting view in Orbit
RequestModelBrowser — ModelBrowserChanged with the current published twin tree and browser state
ModelBrowser action, plus the action fields below Controls existing twin selection, grouping, visibility, framing and linked-record opening
RequestMeasurementTool — MeasurementToolChanged snapshot of the transient drawing tool
MeasurementTool action, plus the action fields below Controls the existing drawing tool; host coordinates are never accepted as picks
RequestMeasurements — MeasurementsChanged with saved count, current pin and overlay visibility
FocusMeasurement sid: string Pins and frames a saved measurement from this scene's authenticated measurement rows
ClearMeasurementFocus — Unpins the selected measurement without changing saved data
SetDollhouseFloor index: number or null Selects a detected floor, or all floors, in the active dollhouse
SetDollhouseSection mode: "open", "full" or "section" Chooses the active dollhouse's cut presentation
SetDollhouseHeight height: number Sets the section height within the returned dollhouse range
SetAppearance appearance: string Chooses a returned scan source or twin appearance
SetFloor index Moves to a detected, reachable floor
SetTagFilter ids: string[] or null Restricts visible scene pins for a native module lens; composes with flow visibility
SetTagProjection ids: string[] or null, required requestId Subscribes to real canvas positions for native overlays; null unsubscribes
SetTagsVisible visible: boolean Shows or hides scene tag markers
SetMeasurementsVisible visible: boolean Shows or hides saved measurement overlays; does not create a measurement
SetElementsVisible kind, ids: string[], visible: boolean Shows or hides selected scene tags, viewpoints, polygons, media or objects
SetScenePresentation scene: object or null, optional tagId Applies an authored flow step's twin view, parts, camera and clip; null restores the plain scan

Modes and floor indices must come from RequestState; availability depends on the scene's configuration and geometry. A model without walking collision does not advertise Walk. An unavailable mode, unknown viewpoint, invalid floor or malformed command returns SdkError.

Mutation success emits CommandResult:

{ "type": "CommandResult", "command": "SetViewMode", "requestId": "mode-1", "ok": true }

Mode/visibility acknowledgements confirm the engine control was applied. Navigate acknowledges after the camera movement finishes. Another navigation, mode change, floor change or reset can cancel an in-progress camera flight. Observe PoseChanged and request state when the host needs the resulting view.

CaptureScreenshot

Capture the current Gaussian/model/photo frame through the existing renderer:

send({
  type: 'CaptureScreenshot',
  width: 512,
  height: 512,
  mattertags: false,
  sweeps: false,
  requestId: 'context-image',
});

The matching response contains image bytes only, with no CW session or credentials:

{
  "type": "ScreenshotCaptured", "requestId": "context-image",
  "dataUrl": "data:image/jpeg;base64,...", "width": 235, "height": 512
}
  • width and height are maximum output dimensions, each an integer from 1 to 2048. Defaults are 1920 and 1080. Aspect ratio is preserved and the source canvas is never upscaled; use the returned dimensions.
  • Optional crop: { x, y, width, height } selects a rectangle within the current canvas. Values are normalized fractions from 0 to 1; width and height must be positive and the rectangle must fit entirely inside the canvas. The crop is applied before output size limits.
  • Output is a JPEG at quality 0.85, read immediately after the existing engine draws a frame. It includes the current scan/model/photo appearance and rendered 3D content. It excludes CW/native controls and DOM overlays, including tags, station rings and measurement labels. mattertags and sweeps may be omitted or false; true is rejected because overlay inclusion is unsupported.
  • Tag/ring visibility is restored after capture, including on failure or cancellation. Concurrent captures run in order. The capture does not move the camera or change its mode/floor. Teardown cancels pending captures; an undrawn frame times out after five seconds.
  • Success emits ScreenshotCaptured rather than CommandResult. Invalid dimensions/flags/crop, unavailable encoding or a capture timeout return a correlated SdkError. Keep image payloads out of logs and pass them directly to a native image decoder or image upload flow.

For example, capture the center half of the rendered view with crop: { x: 0.25, y: 0.25, width: 0.5, height: 0.5 }. Crops use canvas coordinates, independent of the device pixel ratio.

Native guided-flow scene controls

First inspect TourReady.commands: the viewer advertises SetElementsVisible and SetScenePresentation only when its host implements those scene adapters. A native host must gate a flow that needs a missing command and explain that its 3D effects are unavailable; a basic flow without those effects can still play. An unsupported command returns a correlated SdkError rather than pretending it applied.

A native host owns the procedure panel and reads the published flow graph from CW. It sends only the step's scene changes to the viewer; the noUI page never mounts another flow panel or runs a second procedure engine.

send({ type: 'SetElementsVisible', kind: 'tags', ids: ['44'], visible: false, requestId: 'hide-pin' });
send({
  type: 'SetScenePresentation',
  scene: {
    v: 1,
    view: 'twin',
    parts: ['cabinet#door'],
    focus: ['cabinet#door'],
    hide: ['cabinet#cover'],
    seeThrough: ['cabinet#glass'],
    camera: { yaw: 45, pitch: 10, distance: 2 },
    clip: { name: 'Open', from: 0, to: 1 },
  },
  requestId: 'flow-step-2',
});
// On procedure close, restore the scan and release its part/clip presentation.
send({ type: 'SetScenePresentation', scene: null, requestId: 'flow-closed' });

SetElementsVisible.kind is tags, sweeps, polygons, media or objects. Supply scene IDs as strings: tag/viewpoint IDs returned by the SDK, polygon row IDs (or polygon-<id>), media IDs such as media-image-12 and media-video-3, and staging object IDs. Each message accepts at most 2000 nonempty IDs, each at most 2048 characters. These are temporary view filters; they do not edit published content or save visibility changes.

SetScenePresentation.scene uses the published step's presentation fields: optional v: 1; view: "reality" | "twin" | "model"; part-key arrays parts, focus, show, hide, seeThrough; only: boolean; camera: { yaw, pitch, distance } in degrees/metres; and clip: { name, from?, to?, show? }. Part keys are the model's full paths, including the placed-object prefix when applicable. Arrays have the same 2000-entry bound. Camera values must be finite, pitch is between −90 and 90, and distance is positive and at most 100000 metres. Clip times are finite, nonnegative seconds; to cannot precede from.

Optional tagId is a nonempty string of at most 256 characters. It supplies the same approach side as the source flow's target pin. A pin anchored to a moving model part frames that part when the step has no explicit focus; when scene is null, its anchor supplies the step's default part presentation. Omit tagId with scene: null to restore the plain scan.

The existing twin controls apply the view and filters, frame the selected parts, and play the named clip. The intent is retained while the model finishes loading. CommandResult acknowledges accepted presentation data rather than clip completion. A clip that cannot be played emits SdkError with code: "commandFailed"; this asynchronous model error has no request ID. Send scene: null when leaving the procedure so the twin stops playing and restores its normal view. No HTML, JavaScript or arbitrary URLs are evaluated from a scene command.

Native scene controls and saved measurements

The native UI should render the scene's returned choices rather than infer capabilities from its file format. These optional StateChanged fields describe the same controls as the web viewer:

  • modeOptions: [{ mode, enabled }] preserves the configured button order and disabled modes. availableModes continues to list only modes a command may select. controlsReady is false while an appearance transition is moving or busy.
  • selectedFloor is a zero-based detected floor index, or null. floorPresentation is "compact" for the floor-travel selector and "stacked" for the active dollhouse's floor list. Each floor may include displayName, the localized/custom compact-selector name; name keeps its existing metadata meaning.
  • dollhouseSection, present for an active scan dollhouse, contains mode: "open" | "full" | "section", world-height range: [minimum, maximum], height: number | null, and automatic: boolean. The section height command rejects nonfinite or out-of-range heights. Floor selection, section mode and height call the existing renderer's dollhouse controls; they do not save floor settings.
  • appearance, when this scene offers an appearance switch, contains kind: "capture" | "twin", selected, source, and arrays sources and partners. An option has { id, icon, labelKey }; IDs are opaque choices for SetAppearance, icons are source Lucide names and label keys are CW translation keys. Capture sources can include photos, splats, geometry and points; twin partners can include combined and model. A scan with only a twin uses the reality/twin/model icon bar. Missing appearances are omitted, not fabricated.

Inspect TourReady.commands for the installed optional control adapters. SetDollhouseFloor accepts only an index returned by this scene or null, and only while floorPresentation is "stacked". SetFloor retains compact floor travel, and now also selects a floor correctly if a scan dollhouse is active. SetAppearance accepts only an ID in the current appearance options. Its acknowledgement follows the existing appearance adapter's completion. Keep the native controls disabled while controlsReady is false.

Saved measurement controls use the same authenticated workspace/tour query and overlay as the source Gaussian viewer. RequestMeasurements returns a snapshot:

{ "type": "MeasurementsChanged", "requestId": "saved", "count": 2, "selectedSid": "pump-clearance", "visible": false }

The event also arrives when saved count, pin or show-all visibility changes. FocusMeasurement.sid is an opaque nonempty string of at most 256 characters that must match an already loaded saved row. The adapter highlights that row and computes its framing camera from its saved segments. Acknowledgement confirms accepted selection; observe PoseChanged for camera arrival. It does not edit the record, create a measurement or change the show-all toggle.

With visible: false, only the pinned measurement is drawn; with true, all saved visible measurements are drawn. ClearMeasurementFocus releases the pin; an enabled show-all toggle continues to show the other saved rows. The source Gaussian drawer stays open when a row is selected and keeps its pinned overlay when explicitly closed. Clear the pin when opening another drawer or starting a new measurement drawer context. MeasurementsRequested asks the native host to open its drawer when the scene handles a ?measurementSid= deep link, after the first rendered frame and matching saved row are available.

The Gaussian bridge exposes saved measurement selection only. It does not manufacture Matterport measurement-draft subscription data or persist a new measurement.

Native projected overlays and pin lenses

A native scheduling panel can draw its own markers over the existing canvas. Inspect TourReady.commands for SetTagProjection; the adapter is optional and older deployed viewers do not advertise it.

send({ type: 'SetTagProjection', ids: ['44', '501'], requestId: 'schedule-window-3' });
// On panel close, release the subscription.
send({ type: 'SetTagProjection', ids: null, requestId: 'schedule-closed' });

Active subscriptions require a nonempty requestId (maximum 128 characters), which is echoed as subscriptionId on every TagProjectionChanged event. Keep only the current token so frames from a previous panel cannot reappear after closing or changing its selection. A new subscription replaces the previous one. ids: [] requests an empty overlay; ids: null stops projection work and emits an empty event with viewport: null.

{
  "type": "TagProjectionChanged", "subscriptionId": "schedule-window-3",
  "viewport": { "width": 402, "height": 874 },
  "tags": [{ "id": "44", "x": 201.3, "y": 361.8 }]
}

Coordinates are CSS pixels from the actual renderer canvas's top-left, independent of device pixel ratio and the page's outer size. Scale x / viewport.width and y / viewport.height to the native overlay bounds that cover that canvas. The adapter uses the same current-camera projector and published tag positions as the source scheduling discs. Positions behind the camera or more than 40 CSS pixels outside an edge are omitted; the 40-pixel allowance lets a clipped native marker slide off smoothly. Projection does not test scan occlusion, matching the source scheduling layer.

Changed positions/viewport are emitted at most 30 times per second while subscribed, separately from PoseChanged. A static camera does not generate duplicate position events. Each request accepts at most 2000 nonempty tag IDs, each at most 2048 characters; projection IDs must belong to the current scene. No arbitrary world points, JavaScript or URLs are accepted. The noUI page draws no scheduling HTML markers.

SetTagFilter supplies the source work-order lens independently of projected overlays:

send({ type: 'SetTagFilter', ids: [], requestId: 'orders-loading' }); // No pins while loading.
send({ type: 'SetTagFilter', ids: ['44'], requestId: 'orders-ready' });
send({ type: 'SetTagFilter', ids: null, requestId: 'orders-closed' }); // Release the lens.

The same ID bounds apply. Filter IDs may include rows from other tours; only matching current-scene pins are shown. null releases the module restriction and [] hides all pins. The filter intersects SetTagsVisible and authored SetElementsVisible masks; releasing a lens does not reveal a pin hidden by a flow. Both commands receive normal CommandResult acknowledgements. They change only temporary view state and do not write CW records.

Native model browser

Inspect TourReady.commands for RequestModelBrowser and ModelBrowser. These optional adapters expose the Gaussian viewer's existing published twin tree and model-browser state. available: false hides the native entry; do not infer availability from a BIM feature count or fabricate an unloaded model.

send({ type: 'RequestModelBrowser', requestId: 'models' });
// IDs and group keys are opaque values returned by this scene.
send({ type: 'ModelBrowser', action: 'select', id: element.externalId, requestId: 'part' });
send({ type: 'ModelBrowser', action: 'focus', id: element.externalId, requestId: 'frame' });
send({ type: 'ModelBrowser', action: 'openRecord', requestId: 'record' });

ModelBrowserChanged arrives for requested snapshots and subsequent tree, filter or selection changes:

{
  "type": "ModelBrowserChanged", "requestId": "models",
  "available": true, "loading": false, "loadFailed": false,
  "groups": [{ "key": "opaque-group", "label": "Rack twin", "color": "currentColor", "elements": [
    { "externalId": "RACK/MODULE", "name": "MODULE", "model": "Rack twin", "ancestors": ["RACK"] }
  ] }],
  "enabledGroups": ["opaque-group"], "hiddenElements": [],
  "selectedId": "RACK/MODULE", "isolatedId": null,
  "profiles": [], "canOpenRecord": true
}

Elements may also contain category, mark, level, sourceId, system, ifcClass, revision and reviewStatus. Missing metadata is omitted. Search and drawer presentation belong to the native host. The web source matches all search words across name, external ID, source, model, category, mark, level, system and IFC class. A nonempty query expands matching groups.

action Additional fields Behavior
open — Opens the existing twin appearance when currently viewing reality
select id: string Selects a returned element and highlights its parts
focus id: string Selects and frames the element with the existing renderer camera
isolate id: string Shows only the selected element's parts
toggleElement id: string Toggles that element's visibility
toggleGroup key: string Toggles a returned model/layer group
restore — Restores browser filters and group visibility
setProfile profile: "engineering" or "equipment" Chooses a profile listed in profiles; the current value is profile
openRecord — Emits TagClicked with by: "part" for the selected part's actual linked tag

IDs/keys must be nonempty strings no longer than 2048 characters and refer to the current published tree. Unsupported profiles, unknown elements/groups and openRecord without canOpenRecord return a correlated SdkError before applying changes. Actions receive normal CommandResult acknowledgements. Selection, isolation, visibility and framing reuse the same twin renderer as the web model browser; they do not edit published data, write records or create another WebView. Linked records must use the emitted tag's link metadata, never a guessed asset row derived from its tag ID.

The noUI route composes browser visibility with authored flow-step visibility. Opening the browser does not discard the native procedure runtime. An Autodesk/APS side-by-side viewer is a separate renderer integration and is not supplied by these commands.

Transient measuring tool

The drawing ruler is separate from saved-measurement visibility. Inspect TourReady.commands for RequestMeasurementTool and MeasurementTool. These commands reuse the renderer's existing measurement session, GPU depth picks, model/surface rays, snapping and drawing layer; no new renderer or measurement backend is introduced.

send({ type: 'MeasurementTool', action: 'setActive', active: true, requestId: 'ruler-open' });
// The worker now taps actual surfaces in the 3D canvas to place points.
send({ type: 'MeasurementTool', action: 'finish', requestId: 'ruler-done' });
send({ type: 'MeasurementTool', action: 'setUnit', unit: 'imperial', requestId: 'ruler-unit' });
send({ type: 'MeasurementTool', action: 'setActive', active: false, requestId: 'ruler-close' });

Supported actions are:

action Additional fields Behavior
setActive active: boolean Opens or closes the ruler; opening an empty session starts placement
add — Starts a new line/chain while retaining existing lines
finish, cancel — Ends point placement and retains the lines already drawn
remove id: positive safe integer Removes that transient line
clear — Clears transient lines and ends placement
setUnit unit: "metric" or "imperial" Changes display units; fixed "scene" contexts reject a unit change
setSnapping, setContinuous enabled: boolean Changes the same snapping/continuous setting as the source toolbar

RequestMeasurementTool returns MeasurementToolChanged, also emitted when tool state changes:

{
  "type": "MeasurementToolChanged", "requestId": "ruler-state",
  "active": true, "drawing": true, "hasStart": true, "missed": false,
  "canAdd": true, "unit": "metric", "snapping": true, "continuous": true,
  "items": [{ "id": 1, "metres": 0.5, "label": "0.50 m" }]
}

Line IDs are opaque transient session IDs, distinct from saved measurement SIDs. metres is the actual world-space line length and label is the existing engine's formatted value. The source session allows at most 50 lines. Reopening the ruler retains its session's lines; changing scene identity resets them. Camera or appearance-context changes cancel an in-progress placement. Clear/remove/unit/settings do not write CW records.

The host owns the ruler button, wrapping toolbar, settings, unit selector and textual readings. The noUI scene draws only the measurement lines, scope/cursor and projected length labels beside the measured surface. MeasurementTool intentionally has no place, coordinates or arbitrary-point action: only real canvas interaction can create a measurement. Malformed tool actions return a correlated SdkError; a renderer without the adapter omits the capability and rejects it.

Native walking routes and tag teleport

Inspect TourReady.commands before enabling route controls. FocusTag and the four navigation commands are advertised only by a host that supplies their adapters. A deployed older viewer can still serve its existing commands without these additions.

send({ type: 'StartNavigation', tagId: tag.id, requestId: 'route-to-pump' });
// Enable the native play button after StartNavigation succeeds.
send({ type: 'SetNavigationWalking', walking: true, requestId: 'route-play' });
send({ type: 'SetNavigationWalking', walking: false, requestId: 'route-pause' });
// Teleport uses the tag's authored approach, independently of generated viewpoints.
send({ type: 'FocusTag', tagId: tag.id, requestId: 'route-teleport' });
send({ type: 'StopNavigation', requestId: 'route-clear' });

tagId is an opaque, nonempty string of at most 256 characters from RequestTags. The noUI host resolves it against this scene's authenticated tag data. FocusTag uses the same teleport adapter as the source viewer's pin, including the tag's authored camera and collision approach. Its acknowledgement confirms acceptance of the intent, rather than camera arrival; observe PoseChanged for the resulting camera.

StartNavigation uses CW's existing authenticated public/getPath request with the current camera's world position, the selected tag's world position and the current tour ID. The scan must have a collision map and the response must contain a walkable path. Its acknowledgement occurs after that path is accepted. Unknown tags, unavailable collision/path data and malformed commands return a correlated SdkError with code: "invalidCommand"; they never manufacture a straight-line route. Leaving the scan's showcase layer clears the route. Teardown drops late acknowledgements.

The existing renderer draws path dots and performs collision walking; the host owns the destination label, distance, play/pause, teleport and close buttons. The source route adapter also reroutes when the camera moves off its path, keeps its old path if rerouting fails, and clears the route on arrival. SetNavigationWalking requires an active route. StopNavigation is safe when no route is active.

NavigationChanged is emitted when destination, distance or walking changes, and in reply to RequestNavigation:

{
  "type": "NavigationChanged", "requestId": "route-state",
  "active": true, "walking": false, "distance": 3.2,
  "destination": { "id": "44", "sid": "44", "title": "Pump A" }
}

The full destination uses the same tag shape as TagsChanged. distance is the remaining world-space distance in metres, rounded to a tenth, or null before the renderer reports progress. An inactive route has walking: false, distance: null, and destination: null. Convert distance to the workspace's display unit only in the native UI. A request ID appears only on a requested snapshot; unsolicited progress has no request ID.

Navigate to a viewpoint obtained from RequestSweeps:

send({
  type: 'Navigate',
  sweepId: viewpoint.id,
  rotation: { x: 0, y: 90 },
  transitionTime: 1500,
  requestId: 'viewpoint-1',
});

Or navigate to world coordinates:

send({
  type: 'Navigate',
  position: { x: 1, y: 1.5, z: 4 },
  target: { x: 0, y: 1.5, z: 0 },
  transitionTime: 0,
  requestId: 'camera-1',
});
  • Use either sweepId or position, never both. A position requires a distinct target, or a rotation.
  • rotation.x is pitch in degrees, from −90 to 90. rotation.y is yaw in degrees: zero faces −Z and 90 faces +X. At a viewpoint, omitting rotation preserves the viewing direction.
  • Coordinates are in the rendered scene's world frame. Use the returned tag/viewpoint positions; do not substitute coordinates from an unrelated scan or AR map.
  • For scan/model camera movement, transitionTime is milliseconds, from 0 to 10000, and defaults to 1500. Zero moves on the next animation frame. Navigation enters Orbit.
  • In an active aligned capture's photo mode, navigation requires a sweepId. The photo engine controls its transition; specifying transitionTime returns an error. Optional rotation is applied after arrival.
  • Viewpoint IDs identify capture stations or collision-derived standing positions (vp-...). Treat them as opaque and request them again after loading another scene. A scene without either source returns an empty array.

Events

Event Payload
TourReady renderer: "gaussian", protocolVersion: 1, commands: string[]
PoseChanged x, y, z, rotationY in degrees, mode; camera updates throttled to about 10 Hz
SweepsChanged sweeps: [{ id, x, y, z }]; emitted on readiness and on request
MeasurementToolChanged transient active, drawing, hasStart, missed, canAdd, unit, snapping, continuous, items; emitted on changes and on request
MeasurementsChanged count, selectedSid: string or null, visible; emitted when saved selection/visibility changes and on request
MeasurementsRequested —; host should open its native saved-measurements drawer
NavigationChanged active, walking, distance in metres or null, destination: Tag or null; emitted on route changes and on request
StateChanged mode, availableModes, floors: [{ index, name, level }], optional native-control metadata described above; emitted on readiness, mode changes and on request
TagsChanged tags: Tag[]; emitted on request
TagClicked tag: Tag, optional by: 'pin' | 'part'
TagHovered tag: Tag or null when the pointer leaves
PolygonClicked id of the polygon
CommandResult command, requestId when supplied, ok: true
ScreenshotCaptured dataUrl (JPEG), width, height, requestId when supplied
SdkError code, optional message, command and requestId

TagClicked.by identifies the selected surface. A pin selection may move the camera to the asset's approach point. A part selection on a tag with a model anchor.objectId opens the native record at the worker's current camera position. Older viewers may omit by; hosts should retain their previous pin-selection behavior. The tag.link table and row IDs identify the CW record; tag.id identifies the scene pin and is not the equipment row ID.

A Gaussian tag retains the legacy aliases sid and defaultSid. A model pin also returns its optional anchor: { partKey, offset: [x,y,z], objectId? }, so native controls can frame the pin's actual moving model part through SetScenePresentation rather than substituting an unrelated standing viewpoint:

{
  "id": "44", "sid": "44", "defaultSid": "44",
  "title": "Pump A", "description": "Inspect the inlet",
  "type": "gaussian", "url": "",
  "position": { "x": 1, "y": 1.5, "z": 2 },
  "link": { "entity": "asset", "cwAssetId": 39, "cwAssetItemId": 91 }
}

link is optional and identifies a linked CW asset/location record. The host owns its detail UI. Gaussian does not emit Matterport-only TagFocused or TagDocked events.

Error codes are notReady, invalidCommand, unavailable (space resolution failed or the space uses another renderer), and loadFailed (scene loading failed). A command error does not require discarding a healthy scene. A loading failure must not be treated as readiness.

Native iOS bridge

Use a WKWebView for the 3D scene only. CW's native header, surface switcher, record screens and viewer controls live outside it. AR uses the native Treedis VPS SDK directly; it does not load this route.

Before loading, copy the authenticated session's accepted cookies into the WebView's cookie store, preserving domain, path, Secure and HttpOnly attributes. Install the CW access/public token and user bootstrap at document start in the main frame, restricted to the configured CW origin. Never put credentials in the route or inject them into child frames. Keep native and WebKit session refresh/sign-out synchronized, and clear viewer storage on teardown.

The no-parent WebKit page uses itself as the SDK peer. A document-start user script can forward validated events:

const expectedOrigin = 'https://stage1.cw.treedis.com';
const bridgeNonce = '<fresh per-view nonce injected by Swift>';
if (window.top === window && location.origin === expectedOrigin) {
  addEventListener('message', (event) => {
    if (event.source !== window || event.origin !== expectedOrigin) return;
    const types = ['TourReady', 'PoseChanged', 'StateChanged', 'TagsChanged',
      'SweepsChanged', 'NavigationChanged', 'MeasurementsChanged', 'MeasurementsRequested', 'MeasurementToolChanged', 'TagClicked', 'TagHovered', 'PolygonClicked', 'CommandResult', 'ScreenshotCaptured', 'SdkError'];
    if (!types.includes(event.data?.type)) return;
    window.webkit.messageHandlers.cwViewerSdk.postMessage({ nonce: bridgeNonce, event: event.data });
  });
}

Generate the nonce in Swift for each WebView and inject the configuration as encoded JSON. The Swift message handler must also validate the main frame, exact security origin and per-view bridge nonce. CW's native implementation includes that nonce in the script and envelope. Do not accept an arbitrary frame's messages merely because it knows the handler name.

Send a command using structured arguments:

webView.callAsyncJavaScript(
    "window.postMessage(command, origin)",
    arguments: [
        "origin": cwOrigin,
        "command": ["type": "ResetView", "requestId": UUID().uuidString]
    ],
    in: nil,
    in: .page
) { result in
    // Handle transport errors; engine results arrive through cwViewerSdk.
}

Remove message handlers, user scripts and readiness timers when closing the scene. Reopening must create a fresh viewer connection. A WebKit transport error, successful authentication or a loaded HTML page alone does not prove that the 3D renderer is ready.