惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
U
Unit 42
MyScale Blog
MyScale Blog
B
Blog
阮一峰的网络日志
阮一峰的网络日志
量子位
IT之家
IT之家
The GitHub Blog
The GitHub Blog
F
Fortinet All Blogs
Recent Announcements
Recent Announcements
V
Visual Studio Blog
G
Google Developers Blog
Last Week in AI
Last Week in AI
雷峰网
雷峰网
博客园 - 聂微东
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
J
Java Code Geeks
博客园 - 司徒正美
Y
Y Combinator Blog
T
The Blog of Author Tim Ferriss
月光博客
月光博客
aimingoo的专栏
aimingoo的专栏

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How I Rebuilt an RPG Map Editor with Rust, React, and WASM
TheXper · 2026-05-26 · via DEV Community

Replacing a map editor sounds like a frontend task.

It is not.

A browser-based RPG map editor lives inside a product loop:

  1. Create a map.
  2. Open it in the editor.
  3. Paint, place assets, rename it, and change layers.
  4. Save automatically.
  5. Reopen it later and trust that nothing disappeared.

If that loop is fragile, the editor can have great brushes, nice UI, and a fast canvas, but the product still feels broken.

This post walks through how I replaced a legacy map editor with a Rust/WebAssembly editor, rebuilt the maps dashboard around the new flow, and made cloud autosave safer.

The stack:

  • Rust + Rocket for authenticated routes and map persistence
  • SQLite for map records and saved project snapshots
  • Tera templates for the product shell
  • React + Vite + WebAssembly for the editor UI
  • Rust/WASM + WebGL2 for the map editing engine

The boring parts mattered most: routing, boot state, create flow, revisions, and autosave correctness.

The actual problem

The original request was simple:

Replace the current map editor with a WASM editor.
screenshot here;

the old rpg map editor
But replacing the editor route was only the first layer.

A real D&D map maker or RPG map editor needs more than a canvas. It needs a stable workflow around the canvas. Users do not care that the editor is written in Rust or that the renderer uses WebGL2 if the New map button fails or their work does not save reliably.

So the real goal became:

Make map creation, editor boot, cloud saving, and reopening feel boring and trustworthy.

That meant fixing three areas at the same time:

  • the editor route
  • the maps dashboard
  • the save model

1. Replacing the editor route with the WASM editor

The first step was changing the authenticated editor route so that opening a map loaded the new WebAssembly editor instead of the old editor experience.

The important part was not just serving a new index.html file.

The editor needed account-aware boot data from the server:

{
  "map_id": "map_uuid",
  "title": "Dungeon Entrance",
  "revision": 12,
  "updated_at": "2026-05-25T12:00:00Z",
  "read_only": false,
  "csrf_token": "...",
  "api": {
    "load": "/api/maps/map_uuid",
    "save": "/api/maps/map_uuid"
  }
}

That boot payload became the bridge between the product shell and the editor.

The React/Vite/WASM editor should not guess who the user is, which map is open, whether the map is editable, or where it should save. The server already knows that. The editor should receive it explicitly.

That keeps the editor focused on editing, while the product shell owns authentication, permissions, persistence, and routing.

2. The dashboard had to become part of the editor experience

Once the WASM editor route worked, the maps dashboard became the next bottleneck.

The old dashboard still behaved like a marketing page connected to an older creation wizard. But the user expectation was much simpler:

  1. Click New map.
  2. Create a map record.
  3. Redirect into the editor.

That is the core loop of a browser-based battle map maker. If this flow is not reliable, the product feels unfinished before the editor even loads.

So the dashboard was rebuilt around the editing workflow:

  • map summary cards
  • search
  • sorting
  • recent map cards
  • empty state
  • one primary create action

The dashboard stopped being a passive page and became the starting surface for map work.

3. Why the first New map fix was not enough

The first implementation used JavaScript:

await fetch('/api/maps', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken
  },
  body: JSON.stringify({ title: 'Untitled Map' })
});

window.location.href = `/editor/${createdMap.id}`;

That worked in a smoke test.

Then the user reported that clicking New map did nothing.

That exposed the design flaw: a core product action depended entirely on a client-side event listener.

For something as important as creating a map, that is too fragile. JavaScript can fail because of stale assets, a bad bundle, a browser extension, a hydration issue, or a simple selector mismatch.

The fix was progressive enhancement.

The create action became a real HTML form:

<form method="post" action="/maps/new">
  <input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
  <button type="submit">New map</button>
</form>

The server route does the critical work:

POST /maps/new
→ create map record
→ redirect to /editor/<id>

JavaScript can still intercept the submit and use the faster API path when everything is healthy. But the fallback path is now server-owned.

That is the right tradeoff:

  • HTML handles the critical action.
  • JavaScript improves the experience.
  • The server remains the source of truth.

This is the kind of boring engineering that makes a SaaS product feel reliable.

4. Keeping the API contract small

The editor did not need a huge API surface.

The useful contract was small:

GET /api/maps/<id>
PUT /api/maps/<id>

GET /api/maps/<id> loads the current map snapshot.

PUT /api/maps/<id> saves the new title, project JSON, and concurrency metadata.

The client sends something like:

{
  "title": "Dungeon Entrance",
  "snapshot": {
    "version": 1,
    "layers": [],
    "camera": {},
    "objects": []
  },
  "expected_revision": 12,
  "expected_updated_at": "2026-05-25T12:00:00Z"
}

The server can then reject stale writes instead of silently overwriting newer data.

For a single-user editor, optimistic concurrency may look unnecessary. It is not. Multiple tabs, reloads, slow requests, and old local drafts can all create write conflicts.

The editor should not pretend those cases do not exist.

5. Cloud autosave has to handle real editing

The editor already had local autosave and a cloud save function.

The naive loop looked reasonable:

  1. Mark the editor dirty after a change.
  2. Wait a few seconds.
  3. Export project JSON.
  4. Send it to the server.
  5. Mark the editor saved.

The problem is that users keep editing while saves are in flight.

If an old save finishes after newer edits were made, the UI must not say everything is saved.

That is how users lose trust.

The safer model is a dirty generation counter.

Every edit increments a number:

let dirtyGeneration = 0;
let savedGeneration = 0;
let saveInFlight = false;

function markDirty() {
  dirtyGeneration += 1;
  scheduleAutosave();
}

When a save starts, the editor captures the current generation:

async function saveToCloud() {
  if (saveInFlight) return;
  if (dirtyGeneration === savedGeneration) return;

  saveInFlight = true;
  const generationAtStart = dirtyGeneration;
  const snapshot = exportProjectJson();

  try {
    const result = await putMapSnapshot(snapshot);

    if (dirtyGeneration === generationAtStart) {
      savedGeneration = generationAtStart;
      updateRevision(result.revision, result.updated_at);
      markCloudSaved();
    } else {
      scheduleAutosaveSoon();
    }
  } catch (error) {
    keepLocalDraft();
    scheduleRetry();
  } finally {
    saveInFlight = false;
  }
}

This protects against a common editor bug:

Save request A starts. The user makes edit B. Save request A finishes. The UI incorrectly says B is saved.

With generation tracking, the editor only clears the dirty state if no newer edits happened during the request.

6. Local drafts are a safety net, not the source of truth

Local autosave still matters.

But local storage should not become the final persistence model for an authenticated cloud editor.

The better model is:

  • local draft protects the user from crashes, reloads, and network failures
  • cloud save becomes the source of truth after a successful server write
  • server revision prevents stale overwrites
  • conflict handling decides what happens when the client is behind

That gives the user two layers of protection:

  1. Immediate local safety while editing.
  2. Durable cloud persistence once the network catches up.

For a creative tool, this matters more than it sounds. Losing a map is not a small bug. It is a trust-breaking event.

7. What changed in the product loop

Before this pass, the product had editor functionality, but the loop around it was fragile.

After the pass, the workflow became much tighter:

  1. The dashboard opens as the user's map workspace.
  2. New map always has a server-backed path.
  3. The server creates a real map record.
  4. The user lands in the Rust/WASM editor.
  5. The editor boots with map ID, API URLs, title, revision, permissions, and CSRF token.
  6. Changes save locally first.
  7. Cloud autosave persists the project snapshot with optimistic concurrency.
  8. Newer edits made during an in-flight save are not accidentally marked as saved.

That is the product loop a browser RPG map editor needs before deeper features matter.

What I would not overbuild yet

It is tempting to jump into advanced editor features immediately:

  • multiplayer editing
  • complex asset marketplaces
  • AI-generated maps
  • procedural dungeons
  • advanced sharing permissions
  • plugin systems

But those are not the first bottleneck.

The first bottleneck is trust.

For an RPG map editor, users need to believe that:

  • creating a map works
  • opening a map works
  • saving works
  • reopening works
  • their work will not disappear

Until that is true, advanced features are just decoration on top of a weak product loop.

Practical lessons

The useful lessons from this rebuild were simple:

  • Make critical actions work without JavaScript.
  • Pass explicit boot state into the editor.
  • Keep the editor API small.
  • Save title and project snapshot together.
  • Use server revisions for optimistic concurrency.
  • Track dirty generations on the client.
  • Treat local drafts as a fallback, not the final persistence layer.
  • Make saves boring.

The best editor infrastructure disappears when it works.

A user should not think about revisions, snapshots, CSRF tokens, local drafts, or save generations. They should create a dungeon map, close the tab, reopen it later, and see their work still there.

That is the real feature.

FAQ

Why use Rust and WebAssembly for a browser map editor?

Rust and WebAssembly make sense when the editor has engine-like requirements: canvas rendering, map state management, undo/redo, geometry, asset placement, and performance-sensitive operations. React can own the UI shell, while Rust/WASM owns the editing core.

Why not make the whole editor a React app?

React is good for panels, buttons, modals, inspectors, and dashboard UI. It is not the best place to put the entire editing engine. For a map editor, the canvas state, rendering model, command history, and project serialization benefit from a stricter engine layer.

Why use progressive enhancement for the New Map button?

Because creating a map is a critical action. If JavaScript fails, users should still be able to create a map and enter the editor. The enhanced JavaScript path can improve speed, but the server form path should remain reliable.

Why does autosave need a dirty generation counter?

Because users can continue editing while a save request is still running. A generation counter prevents an older save response from incorrectly marking newer edits as saved.

What is the most important part of cloud autosave?

The most important part is not the timer. It is correctness. The editor must know which edits were included in a save, which edits happened later, and whether the server accepted the write.

Final thought

Replacing the editor was the visible task.

Fixing the product loop was the real task.

For a browser-based D&D map maker, the canvas is only one part of the experience. The user also needs reliable creation, clear routing, safe autosave, and boring persistence.

That is what makes the editor feel like a product instead of a demo.

And the Result is here:

RPG Map Editor v2

RPG Map Editor v2