Skip to content

Potree Sync Reference

Complete reference for the bidirectional synchronization between a Potree 3D viewer and MapPrism2D.


Overview

mapprism-2d supports two integration depths with Potree:

ModeWhen to use
Transition-time handoffSimple toggle UX — users switch between 3D and 2D. Use getView() / setView() manually at the transition boundary.
Continuous runtime syncBoth panels visible simultaneously. Pass sync.potree.viewer and set sync.enabled: true. The library keeps them linked automatically.

The library never imports Potree as a package. Potree is referenced exclusively through the object you pass at runtime.


Quick start

Transition-time handoff (no config needed)

ts
// 3D → 2D: call setView() when the user switches panels
map.setView({
  center: [-79.4346, 41.1922], // WGS84 [lng, lat]
  bearing: 45,
  zoom: 17,
}, { animate: true })

// 2D → 3D: read the current view
const view = map.getView()
// view.center  → [lng, lat] WGS84
// view.bearing → degrees, clockwise from north
// view.zoom    → MapLibre zoom level

Continuous runtime sync

ts
const map = new MapPrism2D(container, {
  map2d: {
    sync: {
      enabled: true,
      potree: {
        viewer: window.viewer, // Potree viewer instance
      },
    },
  },
})

Initialization behavior

Setting sync.potree.viewer has two distinct effects:

viewer setenabledEffect
Yesfalse (default)Potree-first init + loading overlay. No continuous sync.
YestruePotree-first init + loading overlay plus continuous bidirectional sync.
NoanyinitialView is used normally. No Potree integration.

What Potree-first init means: map2d.initialView is ignored entirely. Instead:

  1. The map starts at a placeholder world view.
  2. A loading overlay is shown on the container.
  3. The library listens for the first valid Potree update event.
  4. On the first valid update, the map jumps to the position derived from Potree's camera and the overlay is dismissed.

This ensures the 2D map opens framed on the same geographic area as the 3D scene, regardless of what initialView would have said.

If no valid position can be derived (point cloud not loaded yet, non-finite coordinates), the listener keeps waiting until one arrives.


Config reference

All sync fields live under map2d.sync in the config object passed to the constructor.

ts
{
  map2d: {
    sync: {
      enabled?: boolean             // default: false
      master?: 'auto' | 'map2d' | 'potree'  // default: 'auto'
      mapPriorityMs?: number        // default: 1200
      potree?: {
        viewer?: PotreeViewerLike   // Potree viewer instance (runtime only)
        throttleMs?: number         // default: 50
        srs?: string                // default: 'EPSG:3857'
      }
    }
  }
}

enabled

Activates the continuous runtime sync loop. When false (default), the library only uses viewer to derive the initial position — no ongoing sync occurs.

master

Controls which viewer wins when both move simultaneously.

ValueBehaviour
'auto'Map wins while the pointer is inside the map container or within mapPriorityMs of the last map interaction. Potree wins otherwise.
'map2d'Map always drives. Potree camera is updated to match the map.
'potree'Potree always drives. Map is updated to match Potree.

mapPriorityMs

In auto mode, the map retains priority for this many milliseconds after the user's last interaction with it (drag, zoom, rotate, pitch). Default 1200.

potree.viewer

The Potree viewer instance. This is a runtime-only field — do not persist it in JSON. The library reads and writes viewer.scene.view and listens on viewer.addEventListener('update', ...).

potree.throttleMs

Potree fires its update event every render frame (~16 ms at 60 fps). throttleMs (default 50) limits how often a Potree update is allowed to move the map. Set to 0 to disable throttling.

potree.srs

The EPSG code of the Potree scene's coordinate system. Omit (or set to 'EPSG:3857') when the scene is in Web Mercator — the most common case.

For scenes in a different CRS (e.g. a US State Plane):

ts
potree: {
  viewer: window.viewer,
  srs: 'EPSG:6565', // Pennsylvania South, survey feet
}

See Non-EPSG:3857 scenes below.


Sync mechanics

Loop guard

Writing to the map triggers a move event, which would normally write back to Potree, which would write to the map again — an infinite loop. The library prevents this with a 120 ms mute window:

  • When Potree updates the map → map→Potree sync is muted for 120 ms.
  • When the map updates Potree → Potree→map sync is muted for 120 ms.

This means you should expect a ~120 ms dead zone after each sync direction change, which is imperceptible to users.

Camera math

Potree's camera is defined by a pivot point model:

pivot = position + direction * radius

The library computes the pivot point, converts it to WGS84, and uses radius to calculate the equivalent MapLibre zoom level.

Bearing / yaw:

bearing (degrees, CW from north) = -yaw (radians, CW from south) × 180/π

Pitch:

MapLibre pitch (degrees from vertical) = (Potree pitch (rad from horizontal) + π/2) × 180/π

Radius ↔ zoom (latitude-corrected, viewport-height-aware):

zoom = log₂( viewportHeight × earthCircumference × cos(lat) / (tileSize × radius) )

Non-EPSG:3857 scenes

By default the library assumes scene coordinates are in Web Mercator (EPSG:3857) and uses a lightweight inline conversion. For any other CRS, set srs to the EPSG code.

How the CRS definition is resolved

The library reads the proj4 definition from the first loaded point cloud at runtime (no proj4 import needed in your code):

  1. pcoGeometry.projection — a proj4 string, present when Potree was able to convert the EPT SRS.
  2. pcoGeometry.ept.srs.wkt — the OGC WKT string from the EPT metadata, always present for EPT datasets.

The unit scale (e.g. US survey feet → metres) is read from proj4's parsed to_meter value and applied to radius conversions automatically.

Fallback

If neither source yields a definition, the library logs a warning and falls back to EPSG:3857:

[MapPrism2D] Could not read proj4 definition for EPSG:6565 from Potree point clouds.
Falling back to EPSG:3857 — map position may be incorrect.

This usually means the point cloud hasn't finished loading yet. Once loaded, the next update event will resolve correctly (the projection is resolved lazily and cached).


TypeScript types

The library exports these interfaces for the Potree viewer contract:

ts
interface PotreeViewerLike {
  scene: {
    view: {
      position: { x: number; y: number; z: number; set?: (x, y, z) => void }
      direction: { x: number; y: number; z: number }
      yaw: number      // radians
      pitch: number    // radians
      radius: number   // scene units
    }
    pointclouds?: Array<{
      pcoGeometry?: {
        projection?: string | null  // proj4 string
        ept?: { srs?: { wkt?: string } }
      }
    }>
  }
  addEventListener?:    (event: string, cb: () => void) => void
  removeEventListener?: (event: string, cb: () => void) => void
}

Any object that satisfies this shape will work — you do not need to import Potree types.


Cleanup

Always call map.destroy() when unmounting. This removes all Potree event listeners and stops the sync loop:

ts
map.destroy()

Troubleshooting

Map stays on the loading overlay after init The library is waiting for a valid Potree update event with finite, non-zero coordinates. This means the point cloud hasn't produced a valid camera position yet. Once Potree loads the first point cloud and the camera settles, the overlay will dismiss automatically.

Map and Potree drift apart at high zoom Check that srs matches the actual scene CRS. If the scene is in survey feet and srs is not set, all radius-to-zoom conversions will be off by a factor of ~3.28.

Only one viewer responds to the other Verify sync.enabled: true. Providing viewer without enabled only resolves the initial position — it does not activate the ongoing sync loop.

Continuous fighting / jitter between viewers This usually means master: 'auto' and the pointer focus is ambiguous. Pin the master explicitly (master: 'potree' or master: 'map2d') to eliminate the uncertainty.