<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: TheXper</title>
    <description>The latest articles on DEV Community by TheXper (@thexper_f46a597a4e23988d2).</description>
    <link>https://dev.to/thexper_f46a597a4e23988d2</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3938134%2Fe4f05dc7-428e-4b23-a806-e7df62f77f01.png</url>
      <title>DEV Community: TheXper</title>
      <link>https://dev.to/thexper_f46a597a4e23988d2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thexper_f46a597a4e23988d2"/>
    <language>en</language>
    <item>
      <title>Building a Browser-Based RPG Map Editor with Rust, WebAssembly, WebGL2, and React</title>
      <dc:creator>TheXper</dc:creator>
      <pubDate>Mon, 18 May 2026 13:08:33 +0000</pubDate>
      <link>https://dev.to/thexper_f46a597a4e23988d2/building-a-browser-based-rpg-map-editor-with-rust-webassembly-webgl2-and-react-1iof</link>
      <guid>https://dev.to/thexper_f46a597a4e23988d2/building-a-browser-based-rpg-map-editor-with-rust-webassembly-webgl2-and-react-1iof</guid>
      <description>&lt;p&gt;I've been building &lt;a href="https://www.rpgmapeditor.com" rel="noopener noreferrer"&gt;RPGMapEditor.com&lt;/a&gt; — a browser-based fantasy map editor for dungeon masters, worldbuilders, and tabletop RPG players.&lt;/p&gt;

&lt;p&gt;The stack is: &lt;strong&gt;Rust + WebAssembly&lt;/strong&gt; for the editor core, &lt;strong&gt;WebGL2&lt;/strong&gt; for rendering, &lt;strong&gt;React + TypeScript&lt;/strong&gt; for UI, &lt;strong&gt;Rocket&lt;/strong&gt; for the backend, and &lt;strong&gt;SQLite&lt;/strong&gt; for storage.&lt;/p&gt;

&lt;p&gt;This post is not a product pitch. It's about the architecture decisions I made, what broke, what I'd change, and why a map editor is a surprisingly brutal problem domain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick answers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is RPGMapEditor.com?&lt;/strong&gt;&lt;br&gt;
A browser-based fantasy map editor for tabletop RPG creators, dungeon masters, worldbuilders, and virtual tabletop users. No install required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Rust and WebAssembly in a browser app?&lt;/strong&gt;&lt;br&gt;
Rust/WASM keeps editor state, geometry, layer data, and commands outside of React. React owns the UI. Rust owns the map. They don't fight over the source of truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it an Inkarnate alternative?&lt;/strong&gt;&lt;br&gt;
It is in the same category as Inkarnate and Dungeondraft — fantasy maps, battle maps, dungeon maps — but built with a Rust/WASM/WebGL2 browser-native architecture instead of Unity or a pure JS canvas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who is it for?&lt;/strong&gt;&lt;br&gt;
Dungeon masters, tabletop RPG players, worldbuilders, indie game developers, and creators who need fantasy maps and VTT-ready exports.&lt;/p&gt;


&lt;h2&gt;
  
  
  The hard part
&lt;/h2&gt;

&lt;p&gt;A map editor is not a normal web app.&lt;/p&gt;

&lt;p&gt;The login page and dashboard were easy. The hard parts were:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1621yho7mw18h6qnm79e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1621yho7mw18h6qnm79e.png" alt=" " width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keeping editor state &lt;strong&gt;deterministic&lt;/strong&gt; across undo/redo&lt;/li&gt;
&lt;li&gt;Rendering hundreds of stamps without &lt;strong&gt;one draw call per object&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Preventing React state from becoming the source of truth for the map&lt;/li&gt;
&lt;li&gt;Keeping saves &lt;strong&gt;structured&lt;/strong&gt; rather than flattening everything into a PNG&lt;/li&gt;
&lt;li&gt;Supporting layers, procedural brushes, terrain, fog, and export without turning the codebase into spaghetti&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every time I tried to prototype a map editor in pure React with a canvas, it collapsed. State was in three places. Undo was wrong. Re-renders were killing rendering performance. The map and the UI were constantly fighting over who owned what.&lt;/p&gt;

&lt;p&gt;That's why I moved the editor core to Rust/WASM.&lt;/p&gt;


&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User input (mouse, keyboard, touch)
        ↓
React UI — toolbar, panels, modals
        ↓
TypeScript ↔ WASM bindings (wasm-bindgen)
        ↓
Rust editor engine
        ↓
Command system + immutable map state
        ↓
Renderer pipeline
        ↓
WebGL2 — batched draw calls, atlas, framebuffers
        ↓
&amp;lt;canvas&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;React handles &lt;strong&gt;what the user sees and clicks&lt;/strong&gt;. Rust handles &lt;strong&gt;what the map actually is&lt;/strong&gt;. They communicate through typed WASM bindings. The Rust side is the single source of truth. React is display.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Rust instead of TypeScript for the core
&lt;/h2&gt;

&lt;p&gt;I want to be honest: Rust has a steep learning curve, and I'm not a graphics programming expert. I picked it anyway because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ownership model forces you to think about state clearly.&lt;/strong&gt; You can't have two mutable references to the same map state. That prevents a whole class of bugs that destroyed my earlier Canvas/JS prototypes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WASM performance is real.&lt;/strong&gt; Geometry operations, tessellation with &lt;code&gt;lyon_tessellation&lt;/code&gt;, and texture atlas packing with &lt;code&gt;guillotiere&lt;/code&gt; are fast and deterministic. No GC pauses, no hidden re-allocation surprises.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The command system is cleaner in Rust.&lt;/strong&gt; Every editor action — stamp placement, eraser stroke, layer reorder, terrain paint — is a typed &lt;code&gt;Command&lt;/code&gt; enum. Undo/redo is just a stack of commands. This is theoretically possible in TypeScript but Rust's type system makes it much harder to cheat.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  The rendering pipeline
&lt;/h2&gt;

&lt;p&gt;WebGL2 was the right call for this kind of editor. The key constraint: &lt;strong&gt;minimizing draw calls&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A naive implementation draws one quad per stamp. Drop 200 stamps on a map and you have 200 draw calls. That doesn't scale.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All stamp textures are packed into a &lt;strong&gt;texture atlas&lt;/strong&gt; at load time using &lt;code&gt;guillotiere&lt;/code&gt; on the Rust side.&lt;/li&gt;
&lt;li&gt;The renderer batches sprites that share the same atlas texture into a &lt;strong&gt;single draw call&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Each layer has its own &lt;strong&gt;framebuffer&lt;/strong&gt;. Compositing layers means blending a small number of textures rather than re-drawing every object.&lt;/li&gt;
&lt;li&gt;The atlas UV coordinates are pre-calculated in Rust and passed to WebGL2 as typed arrays.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: a map with a few hundred stamps and four layers renders comfortably at 60fps on mid-range hardware.&lt;/p&gt;


&lt;h2&gt;
  
  
  What broke (and what I'd redesign)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Biggest mistake: underestimating wasm-bindgen boilerplate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The boundary between Rust and TypeScript takes serious maintenance. Every time I changed a Rust type that crossed the boundary, I had to update bindings, types, and sometimes the React component that consumed them. I should have designed a stable, narrow API surface between the two sides much earlier. Instead, the boundary grew organically and became a source of bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second mistake: starting the atlas too simple.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first texture atlas was a fixed 2048×2048 texture. That ran out quickly once I added procedural terrain brushes. Moving to a dynamic atlas system (&lt;code&gt;guillotiere&lt;/code&gt;) mid-project was painful. I should have planned for this from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third mistake: saving too late.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I spent months on the rendering pipeline before I built the save format. When I finally sat down to serialize a map, I realized my state structure wasn't as clean as I thought. Parts of the render state had leaked into the map state. Redesigning the save format forced a partial refactor of the state model.&lt;/p&gt;

&lt;p&gt;If I started over: design the save format on day one. It forces you to define what the map actually &lt;em&gt;is&lt;/em&gt;, separate from how it &lt;em&gt;renders&lt;/em&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  A real code snippet: the command system
&lt;/h2&gt;

&lt;p&gt;Here's a simplified version of how editor commands work in Rust. Every user action that modifies the map becomes a &lt;code&gt;Command&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;Command&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;PlaceStamp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stamp_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;StampId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Vec2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LayerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;EraseArea&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LayerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;MoveStamp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stamp_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;StampId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Vec2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Vec2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;ReorderLayer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;layer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LayerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;new_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;EditorState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MapState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;undo_stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;redo_stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;EditorState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.map&lt;/span&gt;&lt;span class="nf"&gt;.execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.undo_stack&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.redo_stack&lt;/span&gt;&lt;span class="nf"&gt;.clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;undo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.undo_stack&lt;/span&gt;&lt;span class="nf"&gt;.pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.map&lt;/span&gt;&lt;span class="nf"&gt;.revert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.redo_stack&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key: &lt;code&gt;revert&lt;/code&gt; is the inverse of &lt;code&gt;execute&lt;/code&gt;. Every command knows how to undo itself. No magic. No diff-based snapshots. No full state cloning on every action.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current status
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdqpft42kgmrfvjgoybas.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdqpft42kgmrfvjgoybas.png" alt=" " width="800" height="377"&gt;&lt;/a&gt;&lt;br&gt;
The editor is in active development. Working right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stamp placement, eraser, selection tools&lt;/li&gt;
&lt;li&gt;Layer system with blend modes&lt;/li&gt;
&lt;li&gt;Procedural terrain brushes (noise-based)&lt;/li&gt;
&lt;li&gt;Texture atlas and batched rendering&lt;/li&gt;
&lt;li&gt;Fog of War&lt;/li&gt;
&lt;li&gt;JSON-based save format&lt;/li&gt;
&lt;li&gt;PNG export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In progress:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VTT export format (Foundry, Roll20 compatibility)&lt;/li&gt;
&lt;li&gt;Lighting effects with framebuffer compositing&lt;/li&gt;
&lt;li&gt;Collaborative editing (long-term goal)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Is this approach overkill for a side project?
&lt;/h2&gt;

&lt;p&gt;Probably, yes.&lt;/p&gt;

&lt;p&gt;A simpler implementation with Fabric.js or Konva would have gotten me a working editor faster. If the goal was just shipping a demo, I over-engineered this. hah&lt;/p&gt;

&lt;p&gt;But I've used those approaches before. They hit walls. When you want reliable undo/redo, proper layer compositing, atlas batching, and a structured save format, the simple canvas library starts fighting you. You end up bolting architecture onto a foundation that wasn't designed for it.&lt;/p&gt;

&lt;p&gt;Building it as an architecture problem from the start has made it easier to add features correctly, even if it made the first six months slower.&lt;/p&gt;




&lt;h2&gt;
  
  
  If you're building something similar
&lt;/h2&gt;

&lt;p&gt;A few things I'd tell myself earlier:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Design your save format first.&lt;/strong&gt; It forces clarity on what your data model actually is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick one source of truth and protect it.&lt;/strong&gt; If Rust owns the map, don't let React sneak in and mutate it directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design a narrow, stable WASM API surface.&lt;/strong&gt; Fewer crossing points = fewer headaches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't optimize the renderer until you have a correct renderer.&lt;/strong&gt; Get batching right logically before profiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log your draw call count during development.&lt;/strong&gt; It's the fastest feedback loop for renderer health.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;If you're working on browser-based creative tools, game-adjacent editors, or WebAssembly/WebGL projects, I'd genuinely enjoy discussing the architecture. Drop a comment or find me at &lt;a href="https://www.rpgmapeditor.com" rel="noopener noreferrer"&gt;RPGMapEditor.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>webgl</category>
      <category>webassembly</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
