Files
shacraft-core/client/section-voxel-map.js
Emil c7e86663d8
MVP checks / mvp (push) Waiting to run
Expand voxel gameplay, lighting, full-height streaming and world imports
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.
2026-09-17 02:10:53 +03:00

76 lines
4.8 KiB
JavaScript

/** 16³ sections; uniform air/solid sections retain a single uint32 value.
* Compatibility iterators visit non-air blocks; hot paths use numeric access.
*/
export class SectionVoxelMap {
#sections = new Map();
#bytes = 0;
size = 0;
static sectionKey(x,y,z) { return `${Math.floor(x/16)},${Math.floor(y/16)},${Math.floor(z/16)}`; }
static index(x,y,z) { return ((x%16+16)%16)+16*((z%16+16)%16)+256*((y%16+16)%16); }
getAt(x,y,z) { const cells=this.#sections.get(SectionVoxelMap.sectionKey(x,y,z))?.cells; return cells ? cells[cells.length===1 ? 0 : SectionVoxelMap.index(x,y,z)] : 0; }
get(key) { const p=key.split(',').map(Number); return this.getAt(...p) || undefined; }
has(key) { return this.get(key)!==undefined; }
hasSection(id) { return this.#sections.has(id); }
set(key,value) {
const p=key.split(',').map(Number), id=SectionVoxelMap.sectionKey(...p);
let section=this.#sections.get(id);
if(!section) {section={cells:new Uint32Array(4096),count:0,ids:new Map()};this.#sections.set(id,section);this.#bytes+=16384;}
const index=SectionVoxelMap.index(...p), old=section.cells[section.cells.length===1 ? 0 : index];
if(old===value) return this;
if(section.cells.length===1) {section.cells=new Uint32Array(4096).fill(old);this.#bytes+=16380;}
if(old) {this.size--;section.count--;const count=section.ids.get(old)-1;if(count) section.ids.set(old,count);else section.ids.delete(old);}
section.cells[index]=value;
if(value) {this.size++;section.count++;section.ids.set(value,(section.ids.get(value)||0)+1);}
return this;
}
setInSection(key,value) {return this.set(key,value);}
delete(key) {if(!this.has(key)) return false;this.set(key,0);return true;}
clear() {this.#sections.clear();this.size=0;this.#bytes=0;}
deleteSection(id) {const previous=this.#sections.get(id);if(!previous)return 0;this.size-=previous.count;this.#bytes-=previous.cells.byteLength;this.#sections.delete(id);return previous.count;}
setSection(id,cells) {
if(!(cells instanceof Uint32Array)||![1,4096].includes(cells.length)) throw Error('A section must contain 4096 uint32 cells or one uniform value');
const ids=new Map();let count=0;
for(const value of cells) if(value) {const n=cells.length===1 ? 4096 : 1;count+=n;ids.set(value,(ids.get(value)||0)+n);}
this.size+=count-(this.#sections.get(id)?.count||0);
this.#bytes+=cells.byteLength-(this.#sections.get(id)?.cells.byteLength||0);
this.#sections.set(id,{cells,count,ids});
}
/** Borrowed dense cells or a single uniform value; copy before transferring. */
getSectionCells(id) { return this.#sections.get(id)?.cells; }
get sectionCount() {return this.#sections.size;}
get byteLength() {return this.#bytes;}
sectionIsEmpty(id) {return this.#sections.get(id)?.count===0;}
loadedSectionKeys() {return this.#sections.keys();}
*sectionEntries() {for(const [id,section] of this.#sections) yield [id,section.cells];}
*materialIds() {const seen=new Set();for(const section of this.#sections.values())for(const id of section.ids.keys())if(!seen.has(id)){seen.add(id);yield id;}}
forEachBlock(callback,bounds=null) {
const selected = function* (sections) {
if (!bounds) {yield* sections;return;}
for(let x=Math.floor(bounds.min[0]/16);x<=Math.floor(bounds.max[0]/16);x++)
for(let y=Math.floor(bounds.min[1]/16);y<=Math.floor(bounds.max[1]/16);y++)
for(let z=Math.floor(bounds.min[2]/16);z<=Math.floor(bounds.max[2]/16);z++) {
const id=`${x},${y},${z}`,section=sections.get(id);
if(section)yield [id,section];
}
};
for(const [id,section] of selected(this.#sections)) {
if(!section.count)continue;
const [sx,sy,sz]=id.split(',').map(v=>Number(v)*16), cells=section.cells;
if(bounds&&(sx>bounds.max[0]||sx+15<bounds.min[0]||sy>bounds.max[1]||sy+15<bounds.min[1]||sz>bounds.max[2]||sz+15<bounds.min[2]))continue;
for(let i=0;i<4096;i++){const value=cells[cells.length===1?0:i];if(value){
const x=sx+(i&15),y=sy+(i>>8),z=sz+((i>>4)&15);
if(!bounds||(x>=bounds.min[0]&&x<=bounds.max[0]&&y>=bounds.min[1]&&y<=bounds.max[1]&&z>=bounds.min[2]&&z<=bounds.max[2]))callback(x,y,z,value);
}}
}
}
*keysInSection(id) {
const section=this.#sections.get(id);if(!section?.count)return;
const [sx,sy,sz]=id.split(',').map(v=>Number(v)*16);
for(let i=0;i<4096;i++)if(section.cells[section.cells.length===1?0:i])yield `${sx+(i&15)},${sy+(i>>8)},${sz+((i>>4)&15)}`;
}
*entries() {for(const id of this.#sections.keys())for(const key of this.keysInSection(id))yield [key,this.get(key)];}
*keys() {for(const id of this.#sections.keys())yield* this.keysInSection(id);}
*values() {for(const section of this.#sections.values())if(section.count)for(let i=0;i<4096;i++){const value=section.cells[section.cells.length===1?0:i];if(value)yield value;}}
[Symbol.iterator]() {return this.entries();}
}