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) }; }