rdigital-twin
v1.0.19
Published
Self-contained DigitalTwinViewer Vue component — drag, rotate, inspect POIs on 2D/3D maps
Readme
rdigital-twin
Self-contained DigitalTwinViewer Vue 3 component — a 3D map viewer with DRM-protected model loading, interactive POI markers, drag-to-move, orbit controls, and blueprint top-down view.
Powered by Three.js (OrbitControls, CSS2DRenderer) and the Web Crypto API (AES-256-GCM in-browser decryption).
Requirements
| Dependency | Version |
|-----------|---------|
| vue | ^3.5.0 |
| three | ^0.173.0 |
| dxf-parser | ^1.1.2 |
Install peer dependencies in your host project:
npm install --save vue@^3.5.0 three@^0.173.0Installation
npm install rdigital-twinQuick Start
<script setup lang="ts">
import { DigitalTwinViewer } from 'rdigital-twin';
import 'rdigital-twin/styles.css';
import type { PointOfInterest } from 'rdigital-twin';
import { ref } from 'vue';
const pois = ref<PointOfInterest[]>([]);
function onPoiClick(id: string) {
console.log('POI clicked:', id);
}
</script>
<template>
<div style="width: 100%; height: 600px;">
<DigitalTwinViewer
api-base-url="https://api.your-domain.com/api"
client-id="your-client-license-id"
map-id="floor-plan"
:pois="pois"
@poi-click="onPoiClick"
/>
</div>
</template>Note: The parent container must have explicit
widthandheight— the viewer fills its container viaResizeObserver.
Map ID & Licensed Assets
The mapId prop corresponds to a map asset registered on the backend. Each 3D model (GLB, DXF, PNG floor plan) stored in backend/storage/maps/ is registered with a unique id (e.g. floor-plan, warehouse-wing).
Licensing flow:
- The backend stores a registry of clients, each with a list of
allowedMapIdsthey can access - The viewer calls
GET /api/v1/client/maps(withAuthorization: Bearer <clientId>) to retrieve licensed map IDs - The host app presents the available maps in a selector (see ViewerPage.vue)
- Switching
mapIdtriggers a viewer teardown + re-initialize with the new map
The backend also exposes a config endpoint (GET /api/config) that returns the public apiBaseUrl, allowing the viewer to discover the correct API endpoint dynamically.
Props
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| apiBaseUrl | string | Yes | — | Absolute backend API URL, e.g. https://api.example.com/api |
| clientId | string | Yes | — | DRM client license identifier (sent as Authorization: Bearer) |
| mapId | string | Yes | — | Encrypted map asset id |
| pois | PointOfInterest[] | No | [] | Live POI / sensor data array |
| areasOfInterest | AreaOfInterest[] | No | [] | Semi-translucent floor polygons |
| draggable | boolean | No | true | Allow dragging POI markers and AOI vertices |
| showPoiInfoOnClick | boolean | No | true | Show the in-viewer POI detail panel on marker click |
| backgroundColor | string | No | '#111827' | Scene & container background (any CSS color) |
| modelSource | ModelSource | No | { type: 'encrypted-gltf', mapId } | Override model loading strategy |
| initialCameraPosition | Coordinates | No | — | Override the initial camera focus position. Defaults to auto-framing the map centre. |
| hideOverlays | boolean | No | false | Hide all HUD overlays (POI count, hints, detail panel). Toolbar buttons are not affected. |
| hideRecenter | boolean | No | false | Hide the "Recenter" button. |
| hideZoom | boolean | No | false | Hide the Zoom (− / +) buttons. |
| hideFreeRoamToggle | boolean | No | false | Hide the "Free 3D roam / Blueprint view" toggle. |
| lockCameraAboveFloor | boolean | No | true | Prevent the camera from rotating below the floor plane. Set false to allow full 3D orbit. |
ModelSource variants
// Default — DRM-protected GLB via backend handshake + stream
{ type: 'encrypted-gltf', mapId: 'floor-plan' }
// Plain GLB from a public URL
{ type: 'gltf', url: 'https://cdn.example.com/model.glb' }
// DXF floor plan (parsed and extruded client-side)
{ type: 'dxf', url: '/models/floor-plan.dxf' }
// Extruded 2.5D floor plan from a JSON definition
{ type: 'extruded', url: '/models/floor-plan.json' }Events
| Event | Payload | Description |
|-------|---------|-------------|
| poiClick | id: string | A POI marker was clicked/tapped |
| poiDragMove | { id: string, coordinates: Coordinates } | POI is being dragged (continuous) |
| poiDragEnd | { id: string, coordinates: Coordinates } | POI drag finished |
| aoiDragMove | { id: string, points: Coordinates[] } | AOI vertex is being dragged (all polygon points) |
| aoiDragEnd | { id: string, points: Coordinates[] } | AOI vertex drag finished (all polygon points) |
| ready | — | Viewer initialized and scene is rendering |
| error | message: string | A non-DRM error occurred (network, parse, etc.) |
| accessDenied | error: MapAccessDeniedError | DRM handshake/stream rejected (revoked, expired, etc.) |
Area of Interest (AOI) Data Shape
interface AreaOfInterest {
id: string;
points: Coordinates[]; // closed ring on the floor plane (x/z); y = ground height
color: string; // CSS color, e.g. '#3b82f6'
}Polygons render as semi-translucent fills with an outline. When draggable is true, each vertex shows a handle; dragging one emits aoiDragMove / aoiDragEnd with the full updated points array.
POI Data Shape
interface PointOfInterest {
id: string;
name: string;
coordinates: Coordinates;
iconType: PoiIconType;
status: PoiStatus;
color: string;
/** Optional custom icon URL (PNG or SVG). Overrides iconType when set. */
customIcon?: string;
/** Marker scale (1–10, default 5). 1 = 20%, 5 = 100%, 10 = 200%. */
scale?: number;
}
interface Coordinates {
x: number;
y: number;
z: number;
}Icon types
sensor, camera, hvac, door, fire, default, fireExtinguisherMissing, fireExtinguisherPressureLost, fireExtinguisherExpireDate, fireExtinguisherExtremeTemp, fireHoseReelsCabinetForcedOpened, fireHoseWaterPumpLowPressure, breakGlassHousingForcedOpened, fireHoseObstruction, breakGlassMissing, breakGlassObstruction, fireAlarmPanelTrigger, sosPanicAlarm, intrusionDetected, lineCrossingDetected
Custom icon (customIcon)
When the optional customIcon property is provided, the viewer renders that URL directly as the POI marker image, bypassing the built-in iconType → SVG mapping.
Accepts any valid image URL:
- Absolute URL —
https://cdn.example.com/icons/my-custom-marker.png - Data URI —
data:image/svg+xml;base64,... - Relative path —
/assets/icons/custom-marker.svg
Important: The host application is responsible for ensuring the URL is resolvable (e.g. by hosting the file, embedding as a data URI, or providing a publicly accessible endpoint).
Scale (scale)
Each POI can have its own size control via the optional scale property (1–10).
| Value | Render size |
|-------|-------------|
| 1 | 20 % of default |
| 5 (default) | 100 % (normal) |
| 10 | 200 % (2× larger) |
{ "id": "big-poi", "scale": 8, ... }When unset or null, the POI renders at the default scale (5).
Status colors
| Status | Color |
|--------|-------|
| NORMAL | #22c55e (green) |
| ALERT | #ef4444 (red) |
| WARNING | #f59e0b (amber) |
| OFFLINE | #94a3b8 (gray) |
Toolbar Controls
The viewer includes a built-in toolbar (bottom-left corner) with:
| Control | Description | |---------|-------------| | Recenter | Reset camera view to map center | | Zoom − / + | Step zoom in/out | | Free 3D roam / Blueprint view | Toggle between perspective orbit and top-down orthographic |
Free 3D roam mode allows full orbit, pan, and zoom. Blueprint view locks to a top-down orthographic projection (ideal for floor plans).
Exposed Methods (via template ref)
<script setup lang="ts">
import { ref } from 'vue';
import { DigitalTwinViewer } from 'rdigital-twin';
const viewerRef = ref<InstanceType<typeof DigitalTwinViewer>>();
</script>
<template>
<DigitalTwinViewer ref="viewerRef" ... />
</template>| Method | Return | Description |
|--------|--------|-------------|
| getSelectedPoi() | PointOfInterest \| null | Currently selected POI |
| getMarkers() | PointOfInterest[] | All active POI markers on the scene |
| recenterCamera() | void | Reset view to map center |
| toggleFreeCamera() | void | Switch between perspective (free 3D) and orthographic (blueprint) |
| zoomInCamera() | void | Step zoom in |
| zoomOutCamera() | void | Step zoom out |
| freeCameraEnabled | Readonly<Ref<boolean>> | Reactive ref — true when in free 3D roam mode |
Styles
The component ships with fully self-contained CSS — no Tailwind or other framework required.
Import separately:
import 'rdigital-twin/styles.css';All class names are prefixed with dtv- to avoid collisions.
Architecture
rdigital-twin/
├── index.ts # Entry point — re-exports component + types
├── package.json # Package manifest (deps: dxf-parser; peer: vue, three)
├── styles.css # Self-contained viewer styles
├── assets/
│ ├── poiIcons.ts # Icon URL resolver
│ └── icons/*.svg # 20 POI icon SVGs
├── components/
│ └── DigitalTwinViewer.vue # Main Vue SFC component
├── composables/
│ ├── useThreeScene.ts # Three.js scene, controls, markers, GLTF/DXF loading
│ ├── useMapDecryption.ts # AES-GCM decryption via Web Crypto API
│ └── useMapDrmApi.ts # DRM handshake + stream API client
├── types/
│ ├── poi.ts # POI, Coordinates, ModelSource types
│ ├── viewer.ts # DigitalTwinViewerProps + constants
│ ├── drm.ts # DRM types + MapAccessDeniedError class
│ ├── dxf.ts # DXF entity/block type definitions
│ ├── mapLoad.ts # MapLoadResult interface
│ └── aoi.ts # AreaOfInterest interface + AoiDragPayload
└── utils/
├── apiBase.ts # URL normalisation helpers
├── poiLabels.ts # Label formatting utilities
├── poiWorldCoords.ts # POI + AOI ↔ scene coordinate transforms
├── renderDxfJson.ts # DXF rendering to Three.js linework
├── mapFormat.ts # Map file/mime-type detection
├── extrudeDxfJson.ts # DXF closed polyline extrusion to 3D meshes
├── loadImageFloorPlan.ts # PNG/JPEG blueprint loading
├── sceneVisuals.ts # Lighting, grid, shadow, atmosphere, zoom scaling
├── viewerCamera.ts # Camera rig (orthographic/perspective), framing, zoom
└── aoiPolygons.ts # AOI polygon rendering, vertex handles, hit detectionDRM Flow
- Handshake —
GET /api/v1/maps/:mapId/handshakewithAuthorization: Bearer <clientId> - Session key — Imported in-browser as a non-extractable
CryptoKey - Stream —
GET /api/v1/maps/:mapId/streamwith Bearer +X-Session-ID - Decrypt —
crypto.subtle.decrypt('AES-GCM', ...)— all in-memory, no disk cache - Render —
GLTFLoader.parse(arrayBuffer)— decrypted model rendered on screen
On accessDenied the scene is cleared, the decryption session is purged, and the viewer shows a locked state.
License
Proprietary — see license agreement with your provider.
