npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.0

Installation

npm install rdigital-twin

Quick 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 width and height — the viewer fills its container via ResizeObserver.


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:

  1. The backend stores a registry of clients, each with a list of allowedMapIds they can access
  2. The viewer calls GET /api/v1/client/maps (with Authorization: Bearer <clientId>) to retrieve licensed map IDs
  3. The host app presents the available maps in a selector (see ViewerPage.vue)
  4. Switching mapId triggers 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 URLhttps://cdn.example.com/icons/my-custom-marker.png
  • Data URIdata: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 detection

DRM Flow

  1. HandshakeGET /api/v1/maps/:mapId/handshake with Authorization: Bearer <clientId>
  2. Session key — Imported in-browser as a non-extractable CryptoKey
  3. StreamGET /api/v1/maps/:mapId/stream with Bearer + X-Session-ID
  4. Decryptcrypto.subtle.decrypt('AES-GCM', ...) — all in-memory, no disk cache
  5. RenderGLTFLoader.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.