Download Game! Currently 87 players and visitors. Last logged in:DynaSordusDarolDesert

Blitzer's Blog >> 72155

Back to blogs index
Posted: 06 Sep 2026 14:33 [ permalink ]
==[ dev chat ]==
We replace the old UI button interaction with raycasting-based voxel editing.
The code below replaces lines 116135 of `web/src/renderer.ts`.

```typescript
/ 3. Raycasting and Voxel Editing
canvas.addEventListener('contextmenu', (evt) => evt.preventDefault());

const isWithinBounds = (x: number, y: number, z: number): boolean => {
B B return x >= 0 && x < CHUNK_SIZE &&
B B B B B By >= 0 && y < CHUNK_SIZE &&
B B B B B Bz >= 0 && z < CHUNK_SIZE;
};

scene.onPointerDown = (pointerInfo) => {
B B const pickResult = scene.pick(pointerInfo.clientX, pointerInfo.clientY);
B B if (!pickResult.hit || !pickResult.pickedMesh) return;

B B const normal = pickResult.getNormal(true);
B B const pickedPoint = pickResult.pickedPoint;

B B if (!normal || !pickedPoint) return;

B B const isLeftClick = pointerInfo.button === 0;
B B const isRightClick = pointerInfo.button === 2;
B B const isShiftLeftClick = isLeftClick && pointerInfo.event.shiftKey;

B B let targetPoint: Vector3 | null = null;
B B let voxelData: number | null = null;

B B if (isLeftClick && !isShiftLeftClick) {
B B B B // Place block: move outward from the face
B B B B targetPoint = pickedPoint.add(normal.scale(0.5));
B B B B voxelData = 1;
B B } else if (isRightClick || isShiftLeftClick) {
B B B B // Remove block: move inward from the face
B B B B targetPoint = pickedPoint.subtract(normal.scale(0.5));
B B B B voxelData = 0;
B B }

B B if (targetPoint && voxelData !== null) {
B B B B const x = Math.floor(targetPoint.x);
B B B B const y = Math.floor(targetPoint.y);
B B B B const z = Math.floor(targetPoint.z);

B B B B if (isWithinBounds(x, y, z)) {
B B B B B B const voxelIndex = x + y * CHUNK_SIZE + z * CHUNK_SIZE *
CHUNK_SIZE;
B B B B B B const cmdId = 1; // CMD_SET_VOXEL
B B B B B B const chunkCoords = 0; // single chunk for now
B B B B B B const pushed = ringBuffer.push([cmdId, chunkCoords, voxelIndex,
voxelData]);
B B B B B B console.log(`[Main] Pushed edit (${x}, ${y}, ${z}) -> ${voxelData}
(pushed: ${pushed})`);
B B B B } else {
B B B B B B console.warn('Edit position out of bounds', x, y, z);
B B B B }
B B }
};
```

### What changed

- **Removed** the button listener and `editCounter`.
- **Added** `contextmenu` prevention so rightclick works as expected.
- **Used** `scene.onPointerDown` to capture clicks.
- **Performed** `scene.pick` to find the hit mesh and normal.
- **Computed** the target voxel coordinates by offsetting the picked point
along the normal (outward for placing, inward for removing).
- **Checked** bounds and pushed the edit command into the `RingBuffer` with
the proper format `[cmdId, chunkCoords, voxelIndex, voxelData]`.

No additional imports are needed  `Vector3` is already imported, and
`PointerInfo` is part of the Babylon.js event system.
==[ /dev chat ]==

==

$ emacs web/src/renderer.tsB
$B

==

** RESTART VITE **

==

B VITE v8.2.2B ready in 188 ms

B B Local:B Bhttps://localhost:3000/
B B Network: https://10.0.3.246:3000/B B B eth0
B B Network: https://100.102.226.30:3000/B tailscale0
B B press h + enter to show help


==

This is brilliant. The raycasting logic provided by the dev chat is
mathematically sound and perfectly translates the 3D normals into our 1D flat
array grid.

However, there is a small "Catch-22" we need to address before you test this
in the browser!

Because we completely removed the UI button, and because our `VOXEL_MEMORY_AREN
A` starts totally empty (filled with `0`s), **the scene will load as a
completely black void.** Since there is no starting mesh, your mouse clicks
will pass through empty space, meaning `pickResult.hit` will always be false,
and you won't be able to place your first block.

We need a "seed" block to get the party started.

### Step 1: Plant a Seed Block

Just above your newly pasted raycasting code in `web/src/renderer.ts`, add
this single line to push an initial block to the center of the chunk right
when the page loads:

```typescript
/ Plant a seed block at (4, 4, 4) so we have something to click on!
ringBuffer.push([1, 0, 4 + 4 * CHUNK_SIZE + 4 * CHUNK_SIZE * CHUNK_SIZE, 1]);

```

### Step 2: Test the Interactive Editor

Once you save that line, let Vite do its hot reload (or manually refresh your
Chrome tab).

1. **Verify the Seed:** You should immediately see the single green block
floating in the center.
2. **Build (Left-Click):** Click on any face of the green block. A new block
should instantly snap onto that exact face. Try building a small bridge or a
wall.
3. **Destroy (Right-Click):** Right-click on one of the blocks you just
placed. It should instantly disappear, and the face culling worker will
instantly recalculate the hidden faces.

Check the console while you do this. You'll see the exact `(x, y, z)`
coordinates being pushed to the Ring Buffer, followed by the Web Worker firing
back the optimized vertex count.

How does the real-time interaction feel? Is it snapping the blocks perfectly
to the grid faces when you click?