The level editor saves a diff, not a level
Placing blocks by hand usually means abandoning the generator that made the level. This keeps both, by never writing a level file.
The Mountain is generated: five octagonal terraces and a set of ramps falling
out of a height field in MountainLevel.swift, so “make the terraces
wider” is a constant you edit rather than a level you rebuild.
A level editor usually eats that property. Once a tool writes out a level file,
the generator becomes the thing that produced the starting point once, before a
human took over. So MarbleEditor does not write a level.
What it writes
mountain.edits.json records only what you added and what you removed. At
launch, GameBoot applies that diff over MountainLevel.make(). The generator
stays in charge of the shape; hand edits survive it changing.
Removals are keyed by cell and block type rather than by index. That is the
detail that makes it hold: reorder the generator, add a terrace, change the
routes, and the overlay still knows which block you meant to delete. The applier
is LevelOverlay — pure data, in MarbleCore, where tests reach it
without a simulator.

BlockType,
rendered by marble-ci-renderer --block-gallery from the same
MeshData used for collision. Wireframe on, so the triangles
the physics actually sees are visible.The tool
A palette of every block type and bundled scenery prop, with the selection following the cursor as a ghost preview. Q/E rotate, R resets, F cycles a prop’s footprint, ⌘-click or X deletes, ⌘S saves.
Clicking places against whichever face the pointer is on — on top, beside a wall, under an overhang — so placement follows the surface normal rather than grid coordinates. The camera is orthographic at the game’s own isometric pitch: a tool showing a different projection than the game lies about what you built.
The refactor inside the commit
SurfaceGeometry.swift pulls the face-orientation split and the
MeshData → SCNGeometry conversion out of SceneKitRenderer
— about 90 lines leaving the renderer to become 112 shared ones.
Editor and game now bake pixels through the same code, which is why the ghost
preview is worth trusting: what you see under the cursor is the block, shaded by
the renderer’s own path. MarbleEditor is a third target over
MarbleCore, alongside the iOS and tvOS apps, and the game never links it.
The trade
Edits reach the game at the next build of MarbleApp, not live. Save, rebuild, roll. The alternative was a file watcher and a live level-swap path in the running game — real complexity in the part of the codebase least able to absorb it, for a few seconds per iteration.