MVP checks / mvp (push) Waiting to run
Add shared Rust/WASM physics, worker meshing and diagnostics, 64-chunk full-height streaming, atlas texture support, and baseline world import. Document the current implementation and include the supplied in-game lobby screenshot.
105 lines
4.1 KiB
JavaScript
105 lines
4.1 KiB
JavaScript
import { attachBlockLightSamplers, buildBlockLight } from "./block-light.js";
|
|
import { applyLightProperties, loadLightProperties } from "./light-properties.js";
|
|
|
|
/** Only one build runs at a time. Edits arriving during a build replace its result. */
|
|
export class BlockLightController {
|
|
constructor(publish) {
|
|
this.publish = publish;
|
|
this.generation = 0;
|
|
this.busy = false;
|
|
this.pending = null;
|
|
this.status = "loading";
|
|
this.properties = null;
|
|
this.worker = null;
|
|
if (typeof Worker === "function") {
|
|
try {
|
|
this.worker = new Worker(new URL("./block-light-worker.js", import.meta.url), { type: "module" });
|
|
} catch {
|
|
// Restricted browser contexts can reject workers before any error event.
|
|
// The same solver remains available on the main thread.
|
|
}
|
|
}
|
|
if (this.worker) {
|
|
this.worker.onmessage = ({ data }) => this.receive(data);
|
|
this.worker.onerror = (event) => {
|
|
event.preventDefault();
|
|
this.worker.terminate();
|
|
this.worker = null;
|
|
this.busy = false;
|
|
// Preserve the latest request and recover with the same implementation.
|
|
this.pending ||= this.current;
|
|
this.start();
|
|
};
|
|
}
|
|
loadLightProperties().then(properties => {
|
|
this.properties = properties;
|
|
this.start();
|
|
}).catch(error => {
|
|
this.status = `error: ${error.message}`;
|
|
console.error("Shacraft light metadata", error);
|
|
});
|
|
}
|
|
request(blocks, materials, bounds) {
|
|
this.generation++;
|
|
this.pending = { blocks, materials, bounds, generation: this.generation };
|
|
this.status = "pending";
|
|
if (!this.timer) this.timer = setTimeout(() => { this.timer = null; this.start(); }, 25);
|
|
}
|
|
start() {
|
|
if (this.busy || !this.pending || !this.properties) return;
|
|
const request = this.pending;
|
|
this.pending = null;
|
|
this.busy = true;
|
|
this.current = request;
|
|
this.status = "building";
|
|
const materials = new Map([...request.materials].map(([id, mat]) => [id,
|
|
applyLightProperties({
|
|
minecraft_id: mat.minecraft_id, state: mat.state, light: mat.light,
|
|
opacity: mat.opacity, render: mat.render, transparent: mat.transparent,
|
|
light_dampening: mat.light_dampening, light_occlusion: mat.light_occlusion,
|
|
}, this.properties)]));
|
|
const packet = { generation: request.generation, bounds: request.bounds,
|
|
blocks: [...request.blocks], materials: [...materials] };
|
|
this.current = { ...request, blocks: new Map(packet.blocks), materials };
|
|
if (this.worker) this.worker.postMessage(packet);
|
|
else setTimeout(() => {
|
|
const start = performance.now();
|
|
try {
|
|
this.receive({ generation: request.generation,
|
|
field: buildBlockLight(this.current.blocks, materials, request.bounds),
|
|
milliseconds: performance.now() - start });
|
|
} catch (error) { this.receive({ generation: request.generation, error: error.message }); }
|
|
}, 0);
|
|
}
|
|
receive(message) {
|
|
const current = this.current;
|
|
if (!this.busy || message.generation !== current?.generation) return;
|
|
this.busy = false;
|
|
if (message.generation === this.generation) {
|
|
if (message.error) {
|
|
this.status = `error: ${message.error}`;
|
|
console.error("Shacraft block lighting", message.error);
|
|
} else {
|
|
this.status = "ready";
|
|
this.milliseconds = message.milliseconds;
|
|
this.publish(attachBlockLightSamplers(message.field, current.blocks, current.materials));
|
|
}
|
|
}
|
|
this.start();
|
|
}
|
|
}
|
|
|
|
/** In a standalone scene, keep a full light-radius margin around its geometry. */
|
|
export function inferLightBounds(blocks) {
|
|
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
for (const key of blocks.keys()) {
|
|
const pos = key.split(",").map(Number);
|
|
for (let axis = 0; axis < 3; axis++) {
|
|
min[axis] = Math.min(min[axis], pos[axis]);
|
|
max[axis] = Math.max(max[axis], pos[axis]);
|
|
}
|
|
}
|
|
if (!Number.isFinite(min[0])) return { min: [-16, -16, -16], max: [16, 16, 16] };
|
|
return { min: min.map(v => v - 15), max: max.map(v => v + 15) };
|
|
}
|