Complete local voxel MVP with server, client, MCP, packages and world interchange

This commit is contained in:
Emil
2026-09-14 17:35:17 +03:00
parent cf8d1aae2e
commit b6ba064339
98 changed files with 58116 additions and 194 deletions
+72
View File
@@ -0,0 +1,72 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
direction,
raycast,
rayBox,
multiply,
perspective,
viewMatrix,
project,
unitBox,
} from "../math.js";
test("camera axes and world projection match the server coordinate convention", () => {
assert.deepEqual(direction(0, 0), [0, 0, -1]);
const east = direction(Math.PI / 2, 0);
assert.ok(Math.abs(east[0] - 1) < 1e-8);
const vp = multiply(
perspective(Math.PI / 2, 1, 0.1, 100),
viewMatrix([0, 2, 8], 0, 0),
);
const center = project([0, 2, 0], vp, 100, 100);
assert.ok(Math.abs(center[0] - 50) < 1e-5 && Math.abs(center[1] - 50) < 1e-5);
assert.equal(project([0, 2, 20], vp, 100, 100), null);
});
test("DDA resolves negative world coordinates and nearest entry face", () => {
const blocks = new Map([
["-1,0,-2", 1],
["-1,0,-4", 1],
]),
materials = new Map([[1, { render: [unitBox] }]]);
const hit = raycast([-0.5, 0.5, 0], [0, 0, -1], blocks, materials);
assert.deepEqual(hit.pos, [-1, 0, -2]);
assert.deepEqual(hit.normal, [0, 0, 1]);
assert.equal(hit.distance, 1);
});
test("ray selection respects slab geometry and reach instead of selecting empty half", () => {
const blocks = new Map([["0,0,-2", 1]]),
materials = new Map([
[1, { render: [{ min: [0, 0, 0], max: [1, 0.5, 1] }] }],
]);
assert.equal(raycast([0.5, 0.75, 0], [0, 0, -1], blocks, materials), null);
assert.ok(raycast([0.5, 0.25, 0], [0, 0, -1], blocks, materials));
assert.equal(
raycast([0.5, 0.25, 10], [0, 0, -1], blocks, materials, 6),
null,
);
});
test("axis aligned ray slabs reject parallel paths outside a box", () => {
assert.equal(rayBox([2, 0.5, 3], [0, 0, -1], [0, 0, 0], [1, 1, 1], 6), null);
assert.equal(
rayBox([0.5, 0.5, 3], [0, 0, -1], [0, 0, 0], [1, 1, 1], 6).distance,
2,
);
});
test("selection intersects a fence extension owned by the cell below", () => {
const blocks = new Map([["0,0,-2", 1]]),
materials = new Map([
[1, { render: [{ min: [0.375, 0, 0.375], max: [0.625, 1.5, 0.625] }] }],
]);
const hit = raycast([0.5, 1.25, 0], [0, 0, -1], blocks, materials);
assert.deepEqual(hit.pos, [0, 0, -2]);
assert.deepEqual(hit.normal, [0, 0, 1]);
assert.equal(hit.distance, 1.375);
});
test("package geometry is selectable across its documented three-block extent", () => {
const blocks = new Map([["0,0,0", 1]]);
const materials = new Map([[1, { render: [{min:[0,0,0],max:[1,3,1]}] }]]);
const hit = raycast([0.5,2.5,4], [0,0,-1], blocks, materials, 6);
assert.deepEqual(hit.pos,[0,0,0]);
assert.deepEqual(hit.normal,[0,0,1]);
});
+64
View File
@@ -0,0 +1,64 @@
<!doctype html><meta charset="utf-8" /><title>Shacraft renderer fixture</title
><style>
body {
margin: 0;
}
canvas {
width: 100vw;
height: 100vh;
display: block;
}</style
><canvas id="c"></canvas>
<script type="module">
import { Renderer } from "../renderer.js";
const canvas = document.querySelector("canvas");
try {
const r = new Renderer(canvas),
blocks = new Map(),
mats = new Map([
[
1,
{
state: "minecraft:grass_block",
color: [107, 151, 65],
opacity: 1,
render: [{ min: [0, 0, 0], max: [1, 1, 1] }],
},
],
[
2,
{
state: "minecraft:bricks",
color: [169, 88, 68],
opacity: 1,
render: [{ min: [0, 0, 0], max: [1, 1, 1] }],
},
],
[
3,
{
state: "minecraft:glass",
color: [169, 220, 219],
opacity: 0.35,
render: [{ min: [0, 0, 0], max: [1, 1, 1] }],
},
],
]);
for (let x = -16; x <= 16; x++)
for (let z = -16; z <= 16; z++) blocks.set(`${x},0,${z}`, 1);
for (let x = -3; x < 3; x++)
for (let y = 1; y < 5; y++)
blocks.set(
`${x},${y},-7`,
(x === 0 || x === 1) && y > 1 && y < 4 ? 3 : 2,
);
r.replace(blocks, mats);
for (let n = 0; n < 24; n++) {
r.draw({ eye: [7, 5, 12], yaw: -0.38, pitch: -0.2 }, [], [], new Map());
}
document.body.dataset.status = `ready:${r.triangles}`;
} catch (error) {
document.body.dataset.status = "error:" + error.message;
document.body.textContent = error.stack;
}
</script>