Skip to content

@diginestai/editor - Spatial Drawing Editor ​

The editor package is a fully featured, in-browser GIS editor that allows facilities managers and developers to draw, modify, copy, and georeference indoor assets directly. It extends Terra Draw and implements a highly memory-efficient, transactional undo/redo system.


πŸ–ŒοΈ Custom Drawing Modes ​

<SpatialEditor /> wraps the Terra Draw MapLibre adapter and extends it with three custom architectural drawing modes alongside standard shapes:

typescript
export type EditorMode =
  | 'select'     // Drag, rotate, and scale features. Draggable handles enabled.
  | 'polygon'    // Draw rooms/zones. Enforces ValidateNotSelfIntersecting.
  | 'rectangle'  // Draw rectangular units.
  | 'circle'     // Draw circular zones.
  | 'freehand'   // Draw arbitrary paths.
  | 'linestring' // Draw path vectors.
  | 'point'      // Drop point markers.
  | 'wall'       // Custom LineString mode: auto-generates Opening lines at intersections.
  | 'door'       // Custom Point mode: snaps to the nearest wall LineString.
  | 'fixture'    // Custom Polygon mode: draws and auto-categorizes furniture.
  | 'image-snap' // Georeferencing control points selection tool.

πŸ—ΊοΈ WebGL Homography Warping & Georeferencing ​

Photographed or scanned floor plan blueprints frequently suffer from perspective distortions. Simple 2D affine scaling (6 Degrees of Freedom) is insufficient. DigiNest uses a full 3D Homography Matrix (8 effective Degrees of Freedom):

1. Control-Points Alignment Wizard ​

The user clicks $\ge 4$ control points on the raw image (pixel $u,v$ coordinates) and maps them to the corresponding $\ge 4$ points on the georeferenced MapLibre map (latitude/longitude coordinates).

2. DLT Least-Squares Solver ​

The editor passes these control point pairs to a Direct Linear Transformation (DLT) solver running inside a Web Worker. It solves for a $3 \times 3$ Homography matrix $H$:

$$H = \begin{bmatrix} h_0 & h_1 & h_2 \ h_3 & h_4 & h_5 \ h_6 & h_7 & 1 \end{bmatrix}$$

3. High-Performance WebGL Custom Shader ​

To avoid massive CPU-bound image recalculations during map pans or zooms:

  • The raw blueprint is loaded as a WebGL texture.
  • The editor injects a MapLibre CustomLayerInterface that binds the WebGL context.
  • WebGL Fragment Shader: Takes the inverse homography matrix $H^{-1}$ and transforms map-space coordinates ($x,y$) directly to texture UV coordinates ($u,v$) in real time.
  • Z-Fighting Mitigation: floorplan.elevationInCm introduces a customizable z-axis height offset (default 2 cm) to prevent z-fighting overlays between building floors and basemaps in 3D views. floorplan.alpha controls opacity (0–1).

πŸ“„ Lazy-Loaded PDF Rasterization ​

Facilities blueprints frequently arrive as multi-megabyte vector PDFs.

To prevent this from bloating your application's initial bundle, pdfjs-dist is lazy-loaded dynamically via an async import ONLY when the first PDF file is dropped onto the canvas:

typescript
// Lazy-loads ~1.5 MB gzipped dependency at runtime only when needed
const { getDocument } = await import('pdfjs-dist');

PDFs are parsed and rasterized locally in the browser at high-fidelity 150 DPI for coordinate snapping.

You must install pdfjs-dist yourself for PDF drop support β€” it is never a hard dependency of @diginestai/editor:

sh
npm install pdfjs-dist   # or: pnpm add pdfjs-dist

Worker configuration ​

pdfjs-dist requires its worker script (pdf.worker.min.mjs) to be reachable at runtime via GlobalWorkerOptions.workerSrc. usePDFDrop configures this automatically the first time a PDF is dropped, using the Vite/Rollup-friendly pattern:

typescript
GlobalWorkerOptions.workerSrc = new URL(
  'pdfjs-dist/build/pdf.worker.min.mjs',
  import.meta.url,
).toString();

Vite (and other bundlers that special-case new URL(bareSpecifier, import.meta.url)) rewrite this into a resolvable, fingerprinted asset URL automatically β€” no extra setup required. It is only applied if workerSrc isn't already set, so it never clobbers a value your app configured itself.

If your bundler doesn't support that pattern (e.g. a plain webpack config without an asset/resource rule for .mjs workers, or you're serving the worker from a CDN), pass pdfWorkerSrc explicitly:

tsx
<SpatialEditor pdfWorkerSrc="https://cdn.example.com/pdf.worker.min.mjs" />

or, when using the usePDFDrop hook directly:

typescript
usePDFDrop({ onImageReady, containerRef, pdfWorkerSrc: '/assets/pdf.worker.min.mjs' });

Error surfacing ​

Rasterization failures (corrupt/encrypted PDFs, a missing pdfjs-dist install, or a worker that fails to load) are reported via the onError callback rather than being swallowed silently. <SpatialEditor> wires this to a visible role="alert" banner (data-testid="pdf-import-error"), the same pattern used for MVF import failures (issue #409).


πŸ”„ Transactional Patch Undo/Redo (zundo) ​

Standard undo/redo stacks store full state snapshots, consuming hundreds of megabytes of memory on large floor plans. DigiNest uses patch-based transaction histories:

  • Diff-Patching: Using Zustand middleware powered by zundo, the editor captures only JSON Patch arrays (forward and backward changes) using immer.
  • Sub-Megabyte Footprint: A massive copy operation across multiple floors containing 200+ rooms takes up less than 1 MB of transaction history (vs. 50 MB for raw snapshots).
  • Transaction Cap: Default queue size is 50 steps.
  • Group Collapsing: Multi-select operations or bulk actions are automatically grouped and collapsed into a single undo transaction step.

⌨️ Custom Keyboard Shortcuts ​

ShortcutMode/ActionDescription
VSelect ModeToggle selection handles, drag, and scale shapes.
RRectangle ModeDraw rectangular shapes.
PPolygon ModeDraw custom multi-point room shapes.
WWall ModeDraw partition lines with opening snap hooks.
DDoor ModeSnaps doors to the nearest wall.
FFixture ModeDraws furniture polygon boundaries.
IImage-Snap ModeDrop georeferencing wizard points.
Delete / BackspaceDelete SelectionRemoves selected features.
Cmd/Ctrl + CCopy AssetsCopy selected shapes to clipboard.
Cmd/Ctrl + VPaste AssetsPaste clipboard shapes at mouse pointer.
Cmd/Ctrl + Shift + CCopy Across FloorsOpens the cross-floor translation modal.
Cmd/Ctrl + ZUndoStep backward in the patch stack.
Cmd/Ctrl + Shift + ZRedoStep forward in the patch stack.
EscExit ModeClear current drawing shape and exit to Select mode.
↑ / ↓Floor SwitchMove active floor rendering up or down.
EExplode PivotExplode floors out in 3D view.

πŸ“‹ Cross-Floor & Cross-Building Copy-Paste ​

typescript
interface CopyOptions {
  /** carry level details. geometry-only copies coordinates; geometry+metadata includes attributes. */
  carry: 'geometry' | 'geometry+metadata' | 'geometry+metadata+sensorBindings';
  /** conflict resolutions */
  conflict: 'merge' | 'replace' | 'skip' | 'offset';
  /** offset distance applied when duplicating to avoid exact geometry stacking overlaps. Default is 0.5m. */
  offsetMeters?: number;
}
  • Cross-Building Re-Projection: When copying features between different buildings, the editor takes coordinates from Building A, converts them to local meters, translates them using Building B's homography matrix, and projects them accurately to the new WGS-84 coordinates.
  • Visual Pasting Diff: Pasted features render with a dashed blue outline (#2393d4) at 60% opacity until the user confirms the action, letting them adjust coordinates visually before committing the transaction.

πŸšͺ Door Swing Layer (v2.16) ​

The editor renders a geometric arc overlay on each door feature to communicate swing direction and clearance radius. Managed via three imperative helpers:

typescript
import {
  addDoorSwingLayer,
  updateDoorSwingLayer,
  removeDoorSwingLayer,
  buildDoorSwingPolygon,
  buildDoorSwingFC,
  DOOR_SWING_SOURCE_ID,
  DOOR_SWING_LAYER_ID,
} from '@diginestai/editor';

// Add the layer when the map loads
addDoorSwingLayer(map, doorFeatureCollection);

// Rebuild arcs when door features change
updateDoorSwingLayer(map, updatedDoorFeatureCollection);

// Remove the layer on unmount
removeDoorSwingLayer(map);

Utility functions:

typescript
/** Build a single arc polygon for one door feature. */
buildDoorSwingPolygon(door: DoorFeature, opts?: { segments?: number }): GeoJSON.Polygon;

/** Build a FeatureCollection of all door swing polygons. */
buildDoorSwingFC(doors: DoorFeature[]): GeoJSON.FeatureCollection;
ConstantValueDescription
DOOR_SWING_SOURCE_ID'bs-door-swing-source'MapLibre source ID for door swing GeoJSON.
DOOR_SWING_LAYER_ID'bs-door-swing-layer'MapLibre fill layer ID for the arc overlays.

The arc radius is derived from the door feature's width property (IMDF opening.properties.width). Arcs are rendered at z-index 400, interleaved with the door-* and opening-* layers.

Released under commercial licensing.