Skip to content

Latest commit

 

History

235 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TerraEdit

A non-destructive Digital Terrain Model (DTM) editor that applies edits on-the-fly during tile rendering. Built with Electron + React + Rust.

TerraEdit Screenshot

The Problem

Traditional DTM editors require destructive edits or full dataset re-processing. Modify a terrain feature and you either overwrite your source data or wait minutes while the system re-renders megabytes of raster data.

The Solution

TerraEdit stores edits as vector geometry with parameters, then composites them into tiles as they're requested. Your source GeoTIFF stays untouched, and edits render in <150ms per tile. Draw berms, carve channels, raise regions—iterate instantly without re-processing.

Key insight: The tile server receives a tile request, finds which edits intersect that tile's bounds, applies the elevation changes pixel-by-pixel, and returns the modified image. No pre-processing, no destructive writes, instant feedback.

Architecture

TerraEdit uses a three-process model:

┌─────────────────────┐
│   Electron Main     │  Spawns Rust backend, manages lifecycle
│   (Node.js)         │  Handles native dialogs, menus, IPC
└──────────┬──────────┘
           │ IPC bridge
┌──────────▼──────────┐     ┌──────────────────────┐
│   React Renderer    │────▶│  Rust HTTP Server    │
│   (TypeScript)      │ API │  (Axum + GDAL)       │
│   - OpenLayers WebGL│     │  - Tile rendering    │
│   - Zustand store   │     │  - Edit application  │
│   - UI components   │     │  - JSON storage      │
└─────────────────────┘     └──────────────────────┘

Data flow: You draw an edit → OpenLayers stores it as vector geometry → Rust backend persists to JSON → Tile requests trigger on-the-fly compositing → OpenLayers renders modified tiles

Why This Architecture?

Rust backend: GDAL (geospatial raster library) is C-based, and Rust's FFI story is excellent. Axum provides async HTTP that scales to thousands of concurrent tile requests. The backend runs on a dynamic port detected at startup—no hardcoded ports, no conflicts.

Electron IPC bridge: The renderer process is sandboxed (no Node.js access), so all native operations go through a typed IPC layer. Main process spawns the Rust binary, captures its stdout to find the port, and exposes it via get_backend_url().

State split: Zustand stores UI layer state (order, names, visibility) while OpenLayers manages geometry. Undo/redo happens at the OpenLayers interaction level, and a SyncQueue coalesces operations before syncing to the backend.

Non-destructive by design: Edits are stored as edits.geojson alongside your source DTM. The original GeoTIFF is never modified. Load the project later and all edits are there, reversible.

Edit Types

Berm (LineString)

Draw a centerline, specify top width and left/right slopes. The system calculates the full elevation buffer and renders proper slope geometry (not just a flat offset). Adjust properties via the panel and see tiles update immediately.

Technical detail: Berm bounds account for width × slope height to ensure tiles correctly detect intersection.

Region (Polygon)

Draw a polygon and either set an absolute elevation or apply a relative offset. Supports holes (donut shapes) for complex terrain modifications.

In progress: Edge blending via blend_distance parameter is modeled but not yet applied.

Channel (LineString with Cross-Sections)

The most sophisticated edit type: draw a centerline, and the system samples perpendicular cross-sections at start/end points. Edit the elevation profile graphically or in a table, and TerraEdit interpolates between cross-sections along the centerline.

Technical details:

  • Backend samples terrain at cross-section locations via /api/edits/sample_cross_section
  • Supports cut mode (excavation) and fill mode (embankment)
  • Resample cross-sections when adjusting width or after other edits modify terrain
  • Full undo/redo for cross-section modifications

Planned: Intermediate cross-sections along the centerline, asymmetric widths, edge blending

Tile Rendering

The tile server is the core innovation. When OpenLayers requests /tiles/{z}/{x}/{y}:

  1. Load base tile: Read the corresponding region from the COG (Cloud Optimized GeoTIFF)
  2. Find relevant edits: Query spatial index for edits intersecting this tile's bounds
  3. Apply edits: For each intersecting edit, modify pixel elevations
    • Berms: Add elevation buffer around line geometry
    • Regions: Set or offset elevation within polygon
    • Channels: Apply interpolated cross-section profile
  4. Encode and return: Return modified tile as PNG

Optimizations:

  • Spatial filtering: Only edits intersecting tile bounds are considered
  • Double-buffered rendering: Crossfade swap prevents tile flashing during edits
  • Edit versioning: editVersion parameter busts tile cache when edits change
  • Synthetic zoom levels: Extends native COG resolutions to max zoom 26 for deep zoom

Visualization

Elevation coloring: WebGL tile style variables apply dynamic color ramps based on viewport statistics. Sample min/max from current view, lock the ramp, pan somewhere else—colors stay consistent for comparison.

Hillshade: Toggle shaded relief with adjustable sun position (azimuth/elevation), vertical exaggeration, and ambient light. Computed on-the-fly in the tile server.

Color ramp lock: Lock the elevation range to maintain consistent colors across different viewport locations, or unlock to auto-sample from current view.

Technical Stack

Frontend:

  • React 19 with TypeScript
  • OpenLayers 10 for WebGL tile rendering
  • Zustand for global state (UI layer state, not geometry)
  • Tailwind CSS 4 for styling
  • Radix UI primitives for components

Backend:

  • Rust 1.70+ with Axum 0.7 HTTP framework
  • GDAL (via georust/gdal) for raster processing and COG conversion
  • Tokio async runtime
  • Filesystem-based JSON storage

Build:

  • Electron + electron-vite for development and packaging
  • Playwright for E2E tests
  • Vitest for unit tests
  • Supports macOS (universal arm64 + x64), Windows, Linux

Development

# Install dependencies
npm install

# Development (with hot reload)
npm run dev           # Runs Rust cargo-watch + Electron + Vite HMR

# Run tests
npm test              # Vitest unit tests (watch mode)
npm run test:e2e      # Playwright E2E tests

# Build for distribution
npm run build:mac     # macOS universal binary

Prerequisites:

  • GDAL development libraries (brew install gdal on macOS)
  • cargo-watch for Rust hot-reload (cargo install cargo-watch)

Hot reload workflow:

  1. cargo-watch monitors .rs files and triggers rebuilds
  2. electron-vite restarts Electron when the Rust binary changes
  3. React/TypeScript changes use Vite's HMR (no restart needed)

Save any file and see changes automatically.

Testing

Frontend unit tests: Vitest + React Testing Library for components, hooks, and state management logic.

E2E tests: Playwright Electron mode tests the full application flow with real raster fixtures. The app runs in --test-mode to register test IPC handlers that bypass native dialogs.

Test fixtures: Complete sample projects with source GeoTIFF, converted COG, metadata, and edits stored in tests/fixtures/ for reproducible tests.

Performance

  • Tile load time: <50ms (cached), <150ms (uncached)
  • Startup time: <3 seconds (includes Rust backend spawn)
  • Memory usage: ~200 MB idle, ~500 MB with large DTM
  • Edit application: On-the-fly, no re-processing of source

Project Structure

src/
├── main/           # Electron main process (backend lifecycle, IPC)
├── preload/        # Context bridge (typed API exposure)
└── renderer/       # React frontend
    ├── components/ # UI components
    ├── hooks/     # Custom React hooks (map interactions, edits, properties)
    ├── lib/       # API clients, utilities
    └── stores/    # Zustand state management

rust-backend/
├── src/
│   ├── api/           # Axum route handlers
│   ├── editing/       # Edit compilation and tile patching
│   ├── tile_server/   # On-the-fly tile rendering
│   ├── processing/    # COG conversion, hillshade
│   ├── storage/       # JSON persistence
│   └── main.rs        # HTTP server entry point
└── Cargo.toml

What's Next

See docs/feature-map.md for implementation status and planned features.

Planned: Region edge blending, intermediate channel cross-sections, asymmetric channel widths, per-project edit defaults.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages