minerender 2.1.0-alpha.86 → 2.1.0-alpha.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/index.d.mts +194 -3
- package/dist/browser/index.d.ts +194 -3
- package/dist/browser/index.js +806 -184
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/index.mjs +806 -185
- package/dist/browser/index.mjs.map +1 -1
- package/dist/browser/section.worker.js +119 -0
- package/dist/browser/section.worker.js.map +1 -0
- package/dist/browser/section.worker.mjs +119 -0
- package/dist/browser/section.worker.mjs.map +1 -0
- package/dist/bundle.js +826 -187
- package/dist/bundle.js.map +1 -1
- package/dist/node/index.d.mts +193 -3
- package/dist/node/index.d.ts +193 -3
- package/dist/node/index.js +804 -184
- package/dist/node/index.js.map +1 -1
- package/dist/node/index.mjs +798 -184
- package/dist/node/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/browser/index.js
CHANGED
|
@@ -191,6 +191,23 @@ async function sleep(timeout) {
|
|
|
191
191
|
setTimeout(() => resolve(), timeout);
|
|
192
192
|
});
|
|
193
193
|
}
|
|
194
|
+
function yieldToEventLoop() {
|
|
195
|
+
const scheduler = globalThis.scheduler;
|
|
196
|
+
if (scheduler?.yield) return scheduler.yield();
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
if (typeof MessageChannel !== "undefined") {
|
|
199
|
+
const channel = new MessageChannel();
|
|
200
|
+
channel.port1.onmessage = () => {
|
|
201
|
+
channel.port1.close();
|
|
202
|
+
channel.port2.close();
|
|
203
|
+
resolve();
|
|
204
|
+
};
|
|
205
|
+
channel.port2.postMessage(void 0);
|
|
206
|
+
} else {
|
|
207
|
+
setTimeout(resolve, 0);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
194
211
|
function clampRotationDegrees(deg) {
|
|
195
212
|
return deg % 360;
|
|
196
213
|
}
|
|
@@ -18688,6 +18705,7 @@ var index_browser_exports = {};
|
|
|
18688
18705
|
__export(index_browser_exports, {
|
|
18689
18706
|
AXES: () => AXES,
|
|
18690
18707
|
AnvilParser: () => AnvilParser,
|
|
18708
|
+
AnvilWorldSource: () => AnvilWorldSource,
|
|
18691
18709
|
ArchiveAssetSource: () => ArchiveAssetSource,
|
|
18692
18710
|
AssetKey: () => AssetKey,
|
|
18693
18711
|
AssetLoadError: () => AssetLoadError,
|
|
@@ -18777,6 +18795,7 @@ __export(index_browser_exports, {
|
|
|
18777
18795
|
SchematicParser: () => SchematicParser,
|
|
18778
18796
|
SectionMesh: () => SectionMesh,
|
|
18779
18797
|
SectionModels: () => SectionModels,
|
|
18798
|
+
SectionWorker: () => SectionWorker,
|
|
18780
18799
|
SkinImage: () => SkinImage,
|
|
18781
18800
|
SkinObject: () => SkinObject,
|
|
18782
18801
|
SkinPart: () => SkinPart,
|
|
@@ -18791,6 +18810,7 @@ __export(index_browser_exports, {
|
|
|
18791
18810
|
TintColors: () => TintColors,
|
|
18792
18811
|
UVMapper: () => UVMapper,
|
|
18793
18812
|
VideoExporter: () => VideoExporter,
|
|
18813
|
+
WorldStreamer: () => WorldStreamer,
|
|
18794
18814
|
WrappedImage: () => WrappedImage,
|
|
18795
18815
|
addBox3WireframeToObject: () => addBox3WireframeToObject,
|
|
18796
18816
|
addWireframeToMesh: () => addWireframeToMesh,
|
|
@@ -18800,6 +18820,7 @@ __export(index_browser_exports, {
|
|
|
18800
18820
|
axisToVec3: () => axisToVec3,
|
|
18801
18821
|
base64decode: () => base64decode,
|
|
18802
18822
|
base64encode: () => base64encode,
|
|
18823
|
+
buildSectionGeometry: () => buildSectionGeometry,
|
|
18803
18824
|
capeTextureSizes: () => capeTextureSizes,
|
|
18804
18825
|
captureException: () => captureException,
|
|
18805
18826
|
changeEvent: () => changeEvent,
|
|
@@ -18852,6 +18873,7 @@ __export(index_browser_exports, {
|
|
|
18852
18873
|
sampleEntityAnimation: () => sampleEntityAnimation,
|
|
18853
18874
|
sampleEntityKeyframes: () => sampleEntityKeyframes,
|
|
18854
18875
|
sampleFluid: () => sampleFluid,
|
|
18876
|
+
sectionGeometryTransferables: () => sectionGeometryTransferables,
|
|
18855
18877
|
serializeBoxGeometryKey: () => serializeBoxGeometryKey,
|
|
18856
18878
|
serializeImageKey: () => serializeImageKey,
|
|
18857
18879
|
serializeMaterialKey: () => serializeMaterialKey,
|
|
@@ -18869,7 +18891,8 @@ __export(index_browser_exports, {
|
|
|
18869
18891
|
toDegrees: () => toDegrees,
|
|
18870
18892
|
toRadians: () => toRadians,
|
|
18871
18893
|
toTrianglesDrawMode: () => toTrianglesDrawMode,
|
|
18872
|
-
trimCanvas: () => trimCanvas
|
|
18894
|
+
trimCanvas: () => trimCanvas,
|
|
18895
|
+
yieldToEventLoop: () => yieldToEventLoop
|
|
18873
18896
|
});
|
|
18874
18897
|
module.exports = __toCommonJS(index_browser_exports);
|
|
18875
18898
|
|
|
@@ -18954,6 +18977,7 @@ function probeImageSize(data) {
|
|
|
18954
18977
|
}
|
|
18955
18978
|
|
|
18956
18979
|
// src/env/browser/BrowserEnv.ts
|
|
18980
|
+
var import_meta = {};
|
|
18957
18981
|
var BrowserEnv = class {
|
|
18958
18982
|
constructor() {
|
|
18959
18983
|
this.name = "browser";
|
|
@@ -18981,6 +19005,14 @@ var BrowserEnv = class {
|
|
|
18981
19005
|
version: version2
|
|
18982
19006
|
}));
|
|
18983
19007
|
}
|
|
19008
|
+
createWorker(name) {
|
|
19009
|
+
if (name !== "section" || typeof Worker === "undefined") return void 0;
|
|
19010
|
+
try {
|
|
19011
|
+
return new Worker(new URL("./section.worker.mjs", import_meta.url), { type: "module" });
|
|
19012
|
+
} catch {
|
|
19013
|
+
return void 0;
|
|
19014
|
+
}
|
|
19015
|
+
}
|
|
18984
19016
|
};
|
|
18985
19017
|
function registerBrowserEnv() {
|
|
18986
19018
|
const env = new BrowserEnv();
|
|
@@ -28634,6 +28666,16 @@ init_NBTHelper();
|
|
|
28634
28666
|
|
|
28635
28667
|
// src/world/ChunkData.ts
|
|
28636
28668
|
init_AssetKey();
|
|
28669
|
+
var blockKeys = /* @__PURE__ */ new Map();
|
|
28670
|
+
function blockKey(type) {
|
|
28671
|
+
let key = blockKeys.get(type);
|
|
28672
|
+
if (!key) {
|
|
28673
|
+
key = AssetKey.parse("blockstates", type);
|
|
28674
|
+
if (blockKeys.size >= 4096) blockKeys.clear();
|
|
28675
|
+
blockKeys.set(type, key);
|
|
28676
|
+
}
|
|
28677
|
+
return key;
|
|
28678
|
+
}
|
|
28637
28679
|
var ChunkData = class _ChunkData {
|
|
28638
28680
|
constructor() {
|
|
28639
28681
|
this.indices = new Uint16Array(4096);
|
|
@@ -28661,10 +28703,10 @@ var ChunkData = class _ChunkData {
|
|
|
28661
28703
|
let key;
|
|
28662
28704
|
let nbt;
|
|
28663
28705
|
if (block && !_ChunkData.isAir(block)) {
|
|
28664
|
-
const type =
|
|
28706
|
+
const type = blockKey(block.type).toNamespacedString();
|
|
28665
28707
|
const properties = Object.fromEntries(Object.keys(block.properties ?? {}).sort().map((name) => [name, block.properties[name]]));
|
|
28666
28708
|
state = Object.freeze(Object.keys(properties).length ? { type, properties: Object.freeze(properties) } : { type });
|
|
28667
|
-
key =
|
|
28709
|
+
key = _ChunkData.paletteKey(block);
|
|
28668
28710
|
if (block.nbt !== void 0) nbt = structuredClone(block.nbt);
|
|
28669
28711
|
}
|
|
28670
28712
|
const previousId = this.indices[index];
|
|
@@ -28701,9 +28743,14 @@ var ChunkData = class _ChunkData {
|
|
|
28701
28743
|
}
|
|
28702
28744
|
static isAir(block) {
|
|
28703
28745
|
if (typeof block === "undefined" || typeof block.type === "undefined") return true;
|
|
28704
|
-
const key =
|
|
28746
|
+
const key = blockKey(block.type);
|
|
28705
28747
|
return key.namespace === "minecraft" && !key.type && ["air", "cave_air", "void_air"].includes(key.path);
|
|
28706
28748
|
}
|
|
28749
|
+
/** Identifies a normalized block type and its sorted properties, excluding per-cell NBT. */
|
|
28750
|
+
static paletteKey(block) {
|
|
28751
|
+
const properties = Object.fromEntries(Object.keys(block.properties ?? {}).sort().map((name) => [name, block.properties[name]]));
|
|
28752
|
+
return JSON.stringify([blockKey(block.type).toNamespacedString(), properties]);
|
|
28753
|
+
}
|
|
28707
28754
|
static copyBlock(state, nbt) {
|
|
28708
28755
|
const block = { type: state.type };
|
|
28709
28756
|
if (state.properties) block.properties = { ...state.properties };
|
|
@@ -28883,19 +28930,150 @@ var AnvilParser = class {
|
|
|
28883
28930
|
const perLong = Math.floor(64 / bits);
|
|
28884
28931
|
const expected = padded ? Math.ceil(4096 / perLong) : Math.ceil(4096 * bits / 64);
|
|
28885
28932
|
if (tag.value.length !== expected) throw new MineRenderError(`Invalid Anvil block-state array length: expected ${expected}`);
|
|
28886
|
-
const words = tag.value.map(([high, low]) =>
|
|
28887
|
-
const mask = (
|
|
28933
|
+
const words = tag.value.map(([high, low]) => [high >>> 0, low >>> 0]);
|
|
28934
|
+
const mask = (1 << bits) - 1;
|
|
28888
28935
|
for (let index = 0; index < 4096; index++) {
|
|
28889
|
-
const
|
|
28890
|
-
const
|
|
28891
|
-
|
|
28892
|
-
|
|
28893
|
-
|
|
28936
|
+
const bitPos = index * bits;
|
|
28937
|
+
const word = padded ? Math.floor(index / perLong) : bitPos >>> 6;
|
|
28938
|
+
const shift = padded ? index % perLong * bits : bitPos & 63;
|
|
28939
|
+
const [high, low] = words[word];
|
|
28940
|
+
let value = shift < 32 ? low >>> shift : high >>> shift - 32;
|
|
28941
|
+
if (shift < 32 && shift + bits > 32) value += high * 2 ** (32 - shift);
|
|
28942
|
+
if (!padded && shift + bits > 64) value += words[word + 1][1] * 2 ** (64 - shift);
|
|
28943
|
+
indices[index] = value & mask;
|
|
28894
28944
|
}
|
|
28895
28945
|
return indices;
|
|
28896
28946
|
}
|
|
28897
28947
|
};
|
|
28898
28948
|
|
|
28949
|
+
// src/world/AnvilWorldSource.ts
|
|
28950
|
+
init_MineRenderError();
|
|
28951
|
+
async function abortable2(promise, signal) {
|
|
28952
|
+
if (!signal) return promise;
|
|
28953
|
+
let abort;
|
|
28954
|
+
const cancelled = new Promise((_, reject) => {
|
|
28955
|
+
abort = () => reject(signal.reason);
|
|
28956
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
28957
|
+
if (signal.aborted) abort();
|
|
28958
|
+
});
|
|
28959
|
+
try {
|
|
28960
|
+
return await Promise.race([promise, cancelled]);
|
|
28961
|
+
} finally {
|
|
28962
|
+
signal.removeEventListener("abort", abort);
|
|
28963
|
+
}
|
|
28964
|
+
}
|
|
28965
|
+
var AnvilWorldSource = class {
|
|
28966
|
+
constructor(readRegion, options = {}) {
|
|
28967
|
+
this.readRegion = readRegion;
|
|
28968
|
+
this.regions = /* @__PURE__ */ new Map();
|
|
28969
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
28970
|
+
this.cachedBytes = 0;
|
|
28971
|
+
this.maxCachedRegions = options.maxCachedRegions ?? 4;
|
|
28972
|
+
this.maxCachedBytes = options.maxCachedBytes ?? 64 * 1024 * 1024;
|
|
28973
|
+
for (const [name, value] of Object.entries({ maxCachedRegions: this.maxCachedRegions, maxCachedBytes: this.maxCachedBytes })) {
|
|
28974
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${name} must be a nonnegative safe integer`);
|
|
28975
|
+
}
|
|
28976
|
+
}
|
|
28977
|
+
/** Decodes one column at absolute chunk coordinates. Aborting a caller leaves other shared readers active. */
|
|
28978
|
+
async getChunk(x, z, signal) {
|
|
28979
|
+
if (!Number.isSafeInteger(x) || !Number.isSafeInteger(z)) {
|
|
28980
|
+
throw new RangeError("Chunk column coordinates must be safe integers");
|
|
28981
|
+
}
|
|
28982
|
+
const regionX = Math.floor(x / 32), regionZ = Math.floor(z / 32);
|
|
28983
|
+
const data = await this.getRegion(regionX, regionZ, signal);
|
|
28984
|
+
signal?.throwIfAborted();
|
|
28985
|
+
if (!data) return void 0;
|
|
28986
|
+
const chunk = await abortable2(AnvilParser.parseChunk(data, x - regionX * 32, z - regionZ * 32), signal);
|
|
28987
|
+
signal?.throwIfAborted();
|
|
28988
|
+
if (chunk && (chunk.x !== x || chunk.z !== z)) {
|
|
28989
|
+
throw new MineRenderError(`Anvil chunk coordinates ${chunk.x},${chunk.z} do not match requested column ${x},${z}`);
|
|
28990
|
+
}
|
|
28991
|
+
return chunk;
|
|
28992
|
+
}
|
|
28993
|
+
/** Releases cached regions. Reads already in progress finish without repopulating this cache. */
|
|
28994
|
+
clearCache() {
|
|
28995
|
+
this.regions.clear();
|
|
28996
|
+
this.pending.clear();
|
|
28997
|
+
this.cachedBytes = 0;
|
|
28998
|
+
}
|
|
28999
|
+
getRegion(x, z, signal) {
|
|
29000
|
+
signal?.throwIfAborted();
|
|
29001
|
+
const key = `${x},${z}`;
|
|
29002
|
+
const cached2 = this.regions.get(key);
|
|
29003
|
+
if (cached2) {
|
|
29004
|
+
this.regions.delete(key);
|
|
29005
|
+
this.regions.set(key, cached2);
|
|
29006
|
+
return Promise.resolve(cached2.data);
|
|
29007
|
+
}
|
|
29008
|
+
let loading = this.pending.get(key);
|
|
29009
|
+
if (!loading) {
|
|
29010
|
+
const controller = new AbortController();
|
|
29011
|
+
loading = {
|
|
29012
|
+
controller,
|
|
29013
|
+
waiters: 0,
|
|
29014
|
+
settled: false,
|
|
29015
|
+
promise: Promise.resolve().then(() => {
|
|
29016
|
+
controller.signal.throwIfAborted();
|
|
29017
|
+
return this.readRegion(x, z, controller.signal);
|
|
29018
|
+
}).then((data) => {
|
|
29019
|
+
controller.signal.throwIfAborted();
|
|
29020
|
+
let bytes = data instanceof Uint8Array ? data : data === void 0 ? void 0 : new Uint8Array(data);
|
|
29021
|
+
if (bytes && bytes.byteLength !== bytes.buffer.byteLength) bytes = new Uint8Array(bytes);
|
|
29022
|
+
if (bytes) AnvilParser.getChunkList(bytes);
|
|
29023
|
+
loading.settled = true;
|
|
29024
|
+
if (this.pending.get(key) === loading) this.cacheRegion(key, bytes);
|
|
29025
|
+
return bytes;
|
|
29026
|
+
}).finally(() => {
|
|
29027
|
+
loading.settled = true;
|
|
29028
|
+
if (this.pending.get(key) === loading) this.pending.delete(key);
|
|
29029
|
+
})
|
|
29030
|
+
};
|
|
29031
|
+
this.pending.set(key, loading);
|
|
29032
|
+
}
|
|
29033
|
+
const read = loading;
|
|
29034
|
+
read.waiters++;
|
|
29035
|
+
return new Promise((resolve, reject) => {
|
|
29036
|
+
let finished = false;
|
|
29037
|
+
const release = () => {
|
|
29038
|
+
if (finished) return false;
|
|
29039
|
+
finished = true;
|
|
29040
|
+
signal?.removeEventListener("abort", abort);
|
|
29041
|
+
read.waiters--;
|
|
29042
|
+
return true;
|
|
29043
|
+
};
|
|
29044
|
+
const abort = () => {
|
|
29045
|
+
if (!release()) return;
|
|
29046
|
+
if (!read.waiters && !read.settled) {
|
|
29047
|
+
if (this.pending.get(key) === read) this.pending.delete(key);
|
|
29048
|
+
read.controller.abort(signal.reason);
|
|
29049
|
+
}
|
|
29050
|
+
reject(signal.reason);
|
|
29051
|
+
};
|
|
29052
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
29053
|
+
read.promise.then(
|
|
29054
|
+
(data) => {
|
|
29055
|
+
if (release()) resolve(data);
|
|
29056
|
+
},
|
|
29057
|
+
(error) => {
|
|
29058
|
+
if (release()) reject(error);
|
|
29059
|
+
}
|
|
29060
|
+
);
|
|
29061
|
+
if (signal?.aborted) abort();
|
|
29062
|
+
});
|
|
29063
|
+
}
|
|
29064
|
+
cacheRegion(key, data) {
|
|
29065
|
+
const size = data?.byteLength ?? 0;
|
|
29066
|
+
if (!this.maxCachedRegions || size > this.maxCachedBytes) return;
|
|
29067
|
+
this.regions.set(key, { data, size });
|
|
29068
|
+
this.cachedBytes += size;
|
|
29069
|
+
while (this.regions.size > this.maxCachedRegions || this.cachedBytes > this.maxCachedBytes) {
|
|
29070
|
+
const oldest = this.regions.keys().next().value;
|
|
29071
|
+
this.cachedBytes -= this.regions.get(oldest).size;
|
|
29072
|
+
this.regions.delete(oldest);
|
|
29073
|
+
}
|
|
29074
|
+
}
|
|
29075
|
+
};
|
|
29076
|
+
|
|
28899
29077
|
// src/world/Chunk.ts
|
|
28900
29078
|
var import_three42 = require("three");
|
|
28901
29079
|
init_BlockStates();
|
|
@@ -28904,7 +29082,7 @@ init_AssetKey();
|
|
|
28904
29082
|
// src/world/MineRenderWorld.ts
|
|
28905
29083
|
var import_three40 = require("three");
|
|
28906
29084
|
init_Model();
|
|
28907
|
-
|
|
29085
|
+
init_util();
|
|
28908
29086
|
init_AssetKey();
|
|
28909
29087
|
init_BlockStates();
|
|
28910
29088
|
init_CubeFace();
|
|
@@ -29049,7 +29227,7 @@ var MineRenderWorld = class {
|
|
|
29049
29227
|
* Places a structure's blocks at their stored positions, then refreshes neighboring faces.
|
|
29050
29228
|
* Entity NBT is retained by the structure but is not rendered.
|
|
29051
29229
|
*
|
|
29052
|
-
* @param useBatches -
|
|
29230
|
+
* @param useBatches - Yields between chunks by default. Set to `false` for sequential placement.
|
|
29053
29231
|
* @param executor - Optional queue for batched placement. The caller remains responsible for stopping it.
|
|
29054
29232
|
*/
|
|
29055
29233
|
async placeMultiBlock(multiblock, useBatches = true, executor) {
|
|
@@ -29066,8 +29244,10 @@ var MineRenderWorld = class {
|
|
|
29066
29244
|
};
|
|
29067
29245
|
const place = (block) => this.placeBlock(new import_three40.Vector3(...block.position), block, collect);
|
|
29068
29246
|
const keys = /* @__PURE__ */ new Map();
|
|
29247
|
+
const types = /* @__PURE__ */ new Set();
|
|
29069
29248
|
for (const block of multiblock.blocks) {
|
|
29070
|
-
if (Chunk.isAir(block)) continue;
|
|
29249
|
+
if (Chunk.isAir(block) || types.has(block.type)) continue;
|
|
29250
|
+
types.add(block.type);
|
|
29071
29251
|
const key = AssetKey.parse("blockstates", block.type);
|
|
29072
29252
|
keys.set(key.serialize(), key);
|
|
29073
29253
|
}
|
|
@@ -29076,6 +29256,23 @@ var MineRenderWorld = class {
|
|
|
29076
29256
|
for (const block of multiblock.blocks) await place(block);
|
|
29077
29257
|
return;
|
|
29078
29258
|
}
|
|
29259
|
+
if (!executor) {
|
|
29260
|
+
const groups2 = /* @__PURE__ */ new Map();
|
|
29261
|
+
const position = new import_three40.Vector3();
|
|
29262
|
+
for (const block of multiblock.blocks) {
|
|
29263
|
+
const [x, y, z] = block.position;
|
|
29264
|
+
this.validatePosBounds(position.set(x, y, z));
|
|
29265
|
+
const cx = Math.floor(x / 16), cy = Math.floor(y / 16), cz = Math.floor(z / 16);
|
|
29266
|
+
const key = `${cx}_${cy}_${cz}`;
|
|
29267
|
+
const chunk = this._chunks.get(key) ?? (Chunk.isAir(block) ? void 0 : this.getOrCreateChunkAt(position));
|
|
29268
|
+
if (!chunk) continue;
|
|
29269
|
+
let group = groups2.get(chunk);
|
|
29270
|
+
if (!group) groups2.set(chunk, group = []);
|
|
29271
|
+
group.push({ index: (y - cy * 16) * 256 + (z - cz * 16) * 16 + x - cx * 16, block });
|
|
29272
|
+
}
|
|
29273
|
+
await this.placeChunkGroups(groups2, changes);
|
|
29274
|
+
return;
|
|
29275
|
+
}
|
|
29079
29276
|
const positions = /* @__PURE__ */ new Map();
|
|
29080
29277
|
for (const block of multiblock.blocks) {
|
|
29081
29278
|
const key = block.position.join(",");
|
|
@@ -29084,19 +29281,34 @@ var MineRenderWorld = class {
|
|
|
29084
29281
|
else positions.set(key, [block]);
|
|
29085
29282
|
}
|
|
29086
29283
|
const groups = [...positions.values()];
|
|
29087
|
-
|
|
29088
|
-
|
|
29089
|
-
|
|
29090
|
-
|
|
29091
|
-
|
|
29092
|
-
|
|
29093
|
-
|
|
29094
|
-
|
|
29095
|
-
|
|
29096
|
-
|
|
29284
|
+
for (let i = 0; i < groups.length; i += executor.batch) {
|
|
29285
|
+
const results = await Promise.allSettled(groups.slice(i, i + executor.batch).map(
|
|
29286
|
+
(blocks) => executor.submit(async () => {
|
|
29287
|
+
for (const block of blocks) await place(block);
|
|
29288
|
+
})
|
|
29289
|
+
));
|
|
29290
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
29291
|
+
if (failure?.status === "rejected") throw failure.reason;
|
|
29292
|
+
}
|
|
29293
|
+
}
|
|
29294
|
+
async placeChunkGroups(groups, changes) {
|
|
29295
|
+
let sliceStart = performance.now();
|
|
29296
|
+
for (const [chunk, blocks] of groups) {
|
|
29297
|
+
let positions;
|
|
29298
|
+
try {
|
|
29299
|
+
positions = await chunk.placeBlocks(blocks);
|
|
29300
|
+
} finally {
|
|
29301
|
+
positions ?? (positions = blocks.map(({ index }) => new import_three40.Vector3(
|
|
29302
|
+
chunk.x * 16 + index % 16,
|
|
29303
|
+
chunk.y * 16 + Math.floor(index / 256),
|
|
29304
|
+
chunk.z * 16 + Math.floor(index / 16) % 16
|
|
29305
|
+
)));
|
|
29306
|
+
for (const pos of positions) changes.set(`${pos.x},${pos.y},${pos.z}`, pos);
|
|
29307
|
+
}
|
|
29308
|
+
if (performance.now() - sliceStart > 8) {
|
|
29309
|
+
await yieldToEventLoop();
|
|
29310
|
+
sliceStart = performance.now();
|
|
29097
29311
|
}
|
|
29098
|
-
} finally {
|
|
29099
|
-
if (!executor) queue.stop();
|
|
29100
29312
|
}
|
|
29101
29313
|
}
|
|
29102
29314
|
/** Replaces one chunk column's blocks. Entity NBT remains available on the parsed chunk. */
|
|
@@ -29104,7 +29316,24 @@ var MineRenderWorld = class {
|
|
|
29104
29316
|
const changes = /* @__PURE__ */ new Map();
|
|
29105
29317
|
try {
|
|
29106
29318
|
await this.clearChunkColumn(chunk.x, chunk.z, changes);
|
|
29319
|
+
const groups = /* @__PURE__ */ new Map();
|
|
29107
29320
|
for (const section of chunk.sections) {
|
|
29321
|
+
if (!executor) {
|
|
29322
|
+
const blocks2 = [];
|
|
29323
|
+
for (let index = 0; index < 4096; index++) {
|
|
29324
|
+
const block = section.data.get(index);
|
|
29325
|
+
if (block) blocks2.push({ index, block });
|
|
29326
|
+
}
|
|
29327
|
+
if (blocks2.length) {
|
|
29328
|
+
const position = new import_three40.Vector3(chunk.x * 16, section.y * 16, chunk.z * 16);
|
|
29329
|
+
this.validatePosBounds(position);
|
|
29330
|
+
const target = this.getOrCreateChunkAt(position);
|
|
29331
|
+
const previous = groups.get(target);
|
|
29332
|
+
if (previous) previous.push(...blocks2);
|
|
29333
|
+
else groups.set(target, blocks2);
|
|
29334
|
+
}
|
|
29335
|
+
continue;
|
|
29336
|
+
}
|
|
29108
29337
|
const blocks = [];
|
|
29109
29338
|
for (let index = 0; index < 4096; index++) {
|
|
29110
29339
|
const block = section.data.get(index);
|
|
@@ -29119,6 +29348,7 @@ var MineRenderWorld = class {
|
|
|
29119
29348
|
}
|
|
29120
29349
|
await this.placeBlocks({ size: [16, 16, 16], blocks }, true, executor, changes);
|
|
29121
29350
|
}
|
|
29351
|
+
if (!executor) await this.placeChunkGroups(groups, changes);
|
|
29122
29352
|
} finally {
|
|
29123
29353
|
await this.updateCulling([...changes.values()]);
|
|
29124
29354
|
}
|
|
@@ -29159,19 +29389,23 @@ var MineRenderWorld = class {
|
|
|
29159
29389
|
this.sectionModels?.clear();
|
|
29160
29390
|
}
|
|
29161
29391
|
updateCulling(positions) {
|
|
29392
|
+
const neighbor = new import_three40.Vector3();
|
|
29162
29393
|
for (const pos of positions) {
|
|
29163
|
-
|
|
29164
|
-
|
|
29165
|
-
|
|
29166
|
-
this.pendingCulling.set(neighbor.toArray().join(","), neighbor);
|
|
29167
|
-
}
|
|
29394
|
+
const cx = Math.floor(pos.x / 16), cy = Math.floor(pos.y / 16), cz = Math.floor(pos.z / 16);
|
|
29395
|
+
const chunk = this._chunks.get(`${cx}_${cy}_${cz}`);
|
|
29396
|
+
const lx = pos.x - cx * 16, ly = pos.y - cy * 16, lz = pos.z - cz * 16;
|
|
29168
29397
|
for (let y = -1; y <= 1; y++) {
|
|
29169
29398
|
for (let z = -1; z <= 1; z++) {
|
|
29170
29399
|
for (let x = -1; x <= 1; x++) {
|
|
29171
|
-
const
|
|
29172
|
-
|
|
29173
|
-
|
|
29174
|
-
|
|
29400
|
+
const nx = lx + x, ny = ly + y, nz = lz + z;
|
|
29401
|
+
const section = nx >= 0 && nx < 16 && ny >= 0 && ny < 16 && nz >= 0 && nz < 16 ? chunk : this._chunks.get(`${cx + Math.floor(nx / 16)}_${cy + Math.floor(ny / 16)}_${cz + Math.floor(nz / 16)}`);
|
|
29402
|
+
if (!section) continue;
|
|
29403
|
+
const index = (ny & 15) * 256 + (nz & 15) * 16 + (nx & 15);
|
|
29404
|
+
let pending = this.pendingCulling.get(section);
|
|
29405
|
+
if (pending?.has(index)) continue;
|
|
29406
|
+
if (Math.abs(x) + Math.abs(y) + Math.abs(z) > 1 && !section.getBlockAt(neighbor.set(pos.x + x, pos.y + y, pos.z + z))?.object?.fluidKind) continue;
|
|
29407
|
+
if (!pending) this.pendingCulling.set(section, pending = /* @__PURE__ */ new Set());
|
|
29408
|
+
pending.add(index);
|
|
29175
29409
|
}
|
|
29176
29410
|
}
|
|
29177
29411
|
}
|
|
@@ -29179,39 +29413,38 @@ var MineRenderWorld = class {
|
|
|
29179
29413
|
return this.culling ?? (this.culling = Promise.resolve().then(async () => {
|
|
29180
29414
|
try {
|
|
29181
29415
|
while (this.pendingCulling.size) {
|
|
29182
|
-
const batch = [...this.pendingCulling
|
|
29416
|
+
const batch = [...this.pendingCulling];
|
|
29183
29417
|
this.pendingCulling.clear();
|
|
29184
|
-
const
|
|
29185
|
-
|
|
29186
|
-
|
|
29187
|
-
|
|
29188
|
-
|
|
29189
|
-
|
|
29190
|
-
|
|
29191
|
-
|
|
29192
|
-
|
|
29193
|
-
|
|
29194
|
-
|
|
29195
|
-
|
|
29196
|
-
|
|
29197
|
-
|
|
29198
|
-
|
|
29199
|
-
|
|
29200
|
-
|
|
29201
|
-
|
|
29202
|
-
};
|
|
29203
|
-
});
|
|
29204
|
-
}
|
|
29205
|
-
let mask = 0;
|
|
29206
|
-
for (const [face, offset] of CUBE_FACE_OFFSETS.entries()) {
|
|
29207
|
-
const neighbor = pos.clone().add(new import_three40.Vector3(...offset));
|
|
29208
|
-
if (this.getChunkAt(neighbor)?.isOccludingAt(neighbor)) {
|
|
29209
|
-
mask |= 1 << face;
|
|
29418
|
+
for (const [chunk, indices] of batch) {
|
|
29419
|
+
for (const index of indices) {
|
|
29420
|
+
if (!chunk.isBlockVisibleIndex(index)) continue;
|
|
29421
|
+
const lx = index % 16, ly = Math.floor(index / 256), lz = Math.floor(index / 16) % 16;
|
|
29422
|
+
const wx = chunk.x * 16 + lx, wy = chunk.y * 16 + ly, wz = chunk.z * 16 + lz;
|
|
29423
|
+
const block = chunk.getBlockAt(neighbor.set(wx, wy, wz));
|
|
29424
|
+
if (block.object?.fluidKind) {
|
|
29425
|
+
await block.object.updateFluid((x, y, z) => {
|
|
29426
|
+
const nx = lx + x, ny = ly + y, nz = lz + z;
|
|
29427
|
+
const section = nx >= 0 && nx < 16 && ny >= 0 && ny < 16 && nz >= 0 && nz < 16 ? chunk : this._chunks.get(`${chunk.x + Math.floor(nx / 16)}_${chunk.y + Math.floor(ny / 16)}_${chunk.z + Math.floor(nz / 16)}`);
|
|
29428
|
+
const neighborIndex = (ny & 15) * 256 + (nz & 15) * 16 + (nx & 15);
|
|
29429
|
+
const object3 = section?.isBlockVisibleIndex(neighborIndex) ? section.getBlockAt(neighbor.set(wx + x, wy + y, wz + z))?.object : void 0;
|
|
29430
|
+
return {
|
|
29431
|
+
fluid: object3?.fluidKind,
|
|
29432
|
+
level: object3?.fluidLevel,
|
|
29433
|
+
solid: section?.isOccludingIndex(neighborIndex) ?? false
|
|
29434
|
+
};
|
|
29435
|
+
});
|
|
29210
29436
|
}
|
|
29437
|
+
let mask = 0;
|
|
29438
|
+
for (let face = 0; face < CUBE_FACE_OFFSETS.length; face++) {
|
|
29439
|
+
const offset = CUBE_FACE_OFFSETS[face];
|
|
29440
|
+
const nx = lx + offset[0], ny = ly + offset[1], nz = lz + offset[2];
|
|
29441
|
+
const section = nx >= 0 && nx < 16 && ny >= 0 && ny < 16 && nz >= 0 && nz < 16 ? chunk : this._chunks.get(`${chunk.x + Math.floor(nx / 16)}_${chunk.y + Math.floor(ny / 16)}_${chunk.z + Math.floor(nz / 16)}`);
|
|
29442
|
+
if (section?.isOccludingIndex((ny & 15) * 256 + (nz & 15) * 16 + (nx & 15))) mask |= 1 << face;
|
|
29443
|
+
}
|
|
29444
|
+
await chunk.setCullMaskIndex(index, mask);
|
|
29211
29445
|
}
|
|
29212
|
-
await chunk.setCullMaskAt(pos, mask);
|
|
29213
29446
|
}
|
|
29214
|
-
|
|
29447
|
+
await Promise.all(batch.map(([chunk]) => chunk.rebuildSectionMesh()));
|
|
29215
29448
|
}
|
|
29216
29449
|
} finally {
|
|
29217
29450
|
this.culling = void 0;
|
|
@@ -29273,7 +29506,228 @@ init_Model();
|
|
|
29273
29506
|
// src/world/SectionMesh.ts
|
|
29274
29507
|
var import_three41 = require("three");
|
|
29275
29508
|
init_CanvasCompat();
|
|
29509
|
+
init_CubeFace();
|
|
29276
29510
|
init_Materials();
|
|
29511
|
+
|
|
29512
|
+
// src/world/SectionGeometry.ts
|
|
29513
|
+
init_CubeFace();
|
|
29514
|
+
function buildSectionGeometry(input) {
|
|
29515
|
+
const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize } = input;
|
|
29516
|
+
if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
|
|
29517
|
+
if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
|
|
29518
|
+
const visible = [];
|
|
29519
|
+
const visibleAtlases = /* @__PURE__ */ new Set();
|
|
29520
|
+
for (let entry = 0; entry < count; entry++) {
|
|
29521
|
+
const template = templateData[templates[entry]];
|
|
29522
|
+
if (!template.cullFaces.some((direction) => !(cullMasks[entry] & direction))) continue;
|
|
29523
|
+
visible.push(entry);
|
|
29524
|
+
visibleAtlases.add(template.atlas);
|
|
29525
|
+
}
|
|
29526
|
+
const pages = [];
|
|
29527
|
+
const locations = /* @__PURE__ */ new Map();
|
|
29528
|
+
for (const atlas of [...visibleAtlases].sort((a, b) => atlases[b].height - atlases[a].height)) {
|
|
29529
|
+
const { width, height } = atlases[atlas];
|
|
29530
|
+
if (width > maxAtlasSize || height > maxAtlasSize) throw new RangeError("Model atlas exceeds the section atlas size limit");
|
|
29531
|
+
let selected = -1;
|
|
29532
|
+
for (let i = 0; i < pages.length; i++) {
|
|
29533
|
+
const page2 = pages[i];
|
|
29534
|
+
const nextRow = page2.x + width > maxAtlasSize;
|
|
29535
|
+
if ((nextRow ? page2.y + page2.rowHeight : page2.y) + height > maxAtlasSize) continue;
|
|
29536
|
+
if (nextRow) {
|
|
29537
|
+
page2.y += page2.rowHeight;
|
|
29538
|
+
page2.x = 0;
|
|
29539
|
+
page2.rowHeight = 0;
|
|
29540
|
+
}
|
|
29541
|
+
selected = i;
|
|
29542
|
+
break;
|
|
29543
|
+
}
|
|
29544
|
+
if (selected < 0) {
|
|
29545
|
+
selected = pages.length;
|
|
29546
|
+
pages.push({ placements: [], entries: [], x: 0, y: 0, rowHeight: 0, width: 0, height: 0 });
|
|
29547
|
+
}
|
|
29548
|
+
const page = pages[selected];
|
|
29549
|
+
page.placements.push({ atlas, x: page.x, y: page.y });
|
|
29550
|
+
locations.set(atlas, { page: selected, x: page.x, y: page.y });
|
|
29551
|
+
page.x += width;
|
|
29552
|
+
page.rowHeight = Math.max(page.rowHeight, height);
|
|
29553
|
+
page.width = Math.max(page.width, page.x);
|
|
29554
|
+
page.height = Math.max(page.height, page.y + height);
|
|
29555
|
+
}
|
|
29556
|
+
for (const entry of visible) pages[locations.get(templateData[templates[entry]].atlas).page].entries.push(entry);
|
|
29557
|
+
return pages.map((page) => {
|
|
29558
|
+
let faces = 0;
|
|
29559
|
+
for (const entry of page.entries) {
|
|
29560
|
+
const template = templateData[templates[entry]];
|
|
29561
|
+
for (let face = 0; face < CUBE_FACES.length; face++) {
|
|
29562
|
+
if (!(cullMasks[entry] & template.cullFaces[face])) faces++;
|
|
29563
|
+
}
|
|
29564
|
+
}
|
|
29565
|
+
const positions = new Float32Array(faces * 12), normals = new Float32Array(faces * 12);
|
|
29566
|
+
const uvs = new Float32Array(faces * 8), uvBounds = new Float32Array(faces * 16), colors = new Float32Array(faces * 12);
|
|
29567
|
+
const outputIndices = new Uint32Array(faces * 6);
|
|
29568
|
+
let vertex = 0, indexOffset = 0;
|
|
29569
|
+
for (const entry of page.entries) {
|
|
29570
|
+
const template = templateData[templates[entry]];
|
|
29571
|
+
const atlas = atlases[template.atlas];
|
|
29572
|
+
const placement = locations.get(template.atlas);
|
|
29573
|
+
const mapU = (u) => (placement.x + u * atlas.width) / page.width;
|
|
29574
|
+
const mapV = (v) => 1 - (placement.y + (1 - v) * atlas.height) / page.height;
|
|
29575
|
+
const offsetX = indices[entry] % 16 * 16;
|
|
29576
|
+
const offsetY = Math.floor(indices[entry] / 256) * 16;
|
|
29577
|
+
const offsetZ = Math.floor(indices[entry] / 16) % 16 * 16;
|
|
29578
|
+
for (let face = 0; face < CUBE_FACES.length; face++) {
|
|
29579
|
+
if (cullMasks[entry] & template.cullFaces[face]) continue;
|
|
29580
|
+
const start = vertex;
|
|
29581
|
+
for (let corner = 0; corner < 4; corner++, vertex++) {
|
|
29582
|
+
const source = face * 4 + corner;
|
|
29583
|
+
positions[vertex * 3] = template.positions[source * 3] + offsetX;
|
|
29584
|
+
positions[vertex * 3 + 1] = template.positions[source * 3 + 1] + offsetY;
|
|
29585
|
+
positions[vertex * 3 + 2] = template.positions[source * 3 + 2] + offsetZ;
|
|
29586
|
+
normals[vertex * 3] = template.normals[source * 3];
|
|
29587
|
+
normals[vertex * 3 + 1] = template.normals[source * 3 + 1];
|
|
29588
|
+
normals[vertex * 3 + 2] = template.normals[source * 3 + 2];
|
|
29589
|
+
uvs[vertex * 2] = mapU(template.uvs[source * 2]);
|
|
29590
|
+
uvs[vertex * 2 + 1] = mapV(template.uvs[source * 2 + 1]);
|
|
29591
|
+
uvBounds[vertex * 4] = mapU(template.uvBounds[source * 4]);
|
|
29592
|
+
uvBounds[vertex * 4 + 1] = mapV(template.uvBounds[source * 4 + 1]);
|
|
29593
|
+
uvBounds[vertex * 4 + 2] = mapU(template.uvBounds[source * 4 + 2]);
|
|
29594
|
+
uvBounds[vertex * 4 + 3] = mapV(template.uvBounds[source * 4 + 3]);
|
|
29595
|
+
colors[vertex * 3] = template.colors[source * 3];
|
|
29596
|
+
colors[vertex * 3 + 1] = template.colors[source * 3 + 1];
|
|
29597
|
+
colors[vertex * 3 + 2] = template.colors[source * 3 + 2];
|
|
29598
|
+
}
|
|
29599
|
+
for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[face * 6 + corner] - face * 4;
|
|
29600
|
+
}
|
|
29601
|
+
}
|
|
29602
|
+
return { width: page.width, height: page.height, placements: page.placements, positions, normals, uvs, uvBounds, colors, indices: outputIndices };
|
|
29603
|
+
});
|
|
29604
|
+
}
|
|
29605
|
+
function sectionGeometryTransferables(pages) {
|
|
29606
|
+
return pages.flatMap((page) => [page.positions.buffer, page.normals.buffer, page.uvs.buffer, page.uvBounds.buffer, page.colors.buffer, page.indices.buffer]);
|
|
29607
|
+
}
|
|
29608
|
+
|
|
29609
|
+
// src/world/SectionWorker.ts
|
|
29610
|
+
init_Env();
|
|
29611
|
+
var _SectionWorker = class _SectionWorker {
|
|
29612
|
+
constructor(worker) {
|
|
29613
|
+
this.worker = worker;
|
|
29614
|
+
this.nextId = 0;
|
|
29615
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
29616
|
+
worker.addEventListener("message", (event) => {
|
|
29617
|
+
const reply = event.data;
|
|
29618
|
+
const pending = this.pending.get(reply.id);
|
|
29619
|
+
if (!pending) return;
|
|
29620
|
+
this.pending.delete(reply.id);
|
|
29621
|
+
if (reply.type === "pages") pending.resolve(reply.pages);
|
|
29622
|
+
else pending.reject(new Error(reply.message));
|
|
29623
|
+
});
|
|
29624
|
+
worker.addEventListener("error", (event) => this.fail(event.message || "Section worker failed"));
|
|
29625
|
+
worker.addEventListener("messageerror", () => this.fail("Section worker message could not be deserialized"));
|
|
29626
|
+
}
|
|
29627
|
+
/** The shared worker, or undefined when the environment has none or a previous worker failed. */
|
|
29628
|
+
static shared() {
|
|
29629
|
+
if (!this.initialized) {
|
|
29630
|
+
this.initialized = true;
|
|
29631
|
+
const worker = Env.provider.createWorker?.("section");
|
|
29632
|
+
if (worker) this.instance = new _SectionWorker(worker);
|
|
29633
|
+
}
|
|
29634
|
+
return this.instance;
|
|
29635
|
+
}
|
|
29636
|
+
/** Builds geometry without transferring the input buffers away from the caller. */
|
|
29637
|
+
build(input) {
|
|
29638
|
+
if (!this.worker) return Promise.reject(new Error("Section worker terminated"));
|
|
29639
|
+
return new Promise((resolve, reject) => {
|
|
29640
|
+
const id = this.nextId++;
|
|
29641
|
+
this.pending.set(id, { resolve, reject });
|
|
29642
|
+
this.worker.postMessage({ id, type: "build", input });
|
|
29643
|
+
});
|
|
29644
|
+
}
|
|
29645
|
+
/** Rejects pending builds, terminates the worker, and makes shared() return undefined. */
|
|
29646
|
+
terminate() {
|
|
29647
|
+
this.fail("Section worker terminated");
|
|
29648
|
+
}
|
|
29649
|
+
fail(message) {
|
|
29650
|
+
const error = new Error(message);
|
|
29651
|
+
for (const pending of this.pending.values()) pending.reject(error);
|
|
29652
|
+
this.pending.clear();
|
|
29653
|
+
this.worker?.terminate();
|
|
29654
|
+
this.worker = void 0;
|
|
29655
|
+
_SectionWorker.instance = void 0;
|
|
29656
|
+
}
|
|
29657
|
+
};
|
|
29658
|
+
_SectionWorker.initialized = false;
|
|
29659
|
+
var SectionWorker = _SectionWorker;
|
|
29660
|
+
|
|
29661
|
+
// src/world/SectionMesh.ts
|
|
29662
|
+
var templateCache = /* @__PURE__ */ new WeakMap();
|
|
29663
|
+
function toInput(entries, maxAtlasSize) {
|
|
29664
|
+
if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
|
|
29665
|
+
const atlases = [];
|
|
29666
|
+
const atlasIds = /* @__PURE__ */ new Map();
|
|
29667
|
+
const templateIds = /* @__PURE__ */ new Map();
|
|
29668
|
+
const input = {
|
|
29669
|
+
count: entries.length,
|
|
29670
|
+
indices: new Uint16Array(entries.length),
|
|
29671
|
+
templates: new Uint16Array(entries.length),
|
|
29672
|
+
cullMasks: new Uint8Array(entries.length),
|
|
29673
|
+
templateData: [],
|
|
29674
|
+
atlases: [],
|
|
29675
|
+
maxAtlasSize
|
|
29676
|
+
};
|
|
29677
|
+
for (let i = 0; i < entries.length; i++) {
|
|
29678
|
+
const { index, template, cullMask } = entries[i];
|
|
29679
|
+
let id = templateIds.get(template);
|
|
29680
|
+
if (id === void 0) {
|
|
29681
|
+
let atlas = atlasIds.get(template.atlas);
|
|
29682
|
+
if (atlas === void 0) {
|
|
29683
|
+
atlas = atlases.length;
|
|
29684
|
+
atlasIds.set(template.atlas, atlas);
|
|
29685
|
+
atlases.push(template.atlas);
|
|
29686
|
+
input.atlases.push({ width: template.atlas.image.width, height: template.atlas.image.height });
|
|
29687
|
+
}
|
|
29688
|
+
let data = templateCache.get(template);
|
|
29689
|
+
if (!data) {
|
|
29690
|
+
data = {
|
|
29691
|
+
positions: new Float32Array(72),
|
|
29692
|
+
normals: new Float32Array(72),
|
|
29693
|
+
uvs: new Float32Array(48),
|
|
29694
|
+
uvBounds: new Float32Array(96),
|
|
29695
|
+
colors: new Float32Array(72),
|
|
29696
|
+
indices: new Uint16Array(36),
|
|
29697
|
+
cullFaces: new Uint8Array(template.cullFaces),
|
|
29698
|
+
atlas: 0
|
|
29699
|
+
};
|
|
29700
|
+
const position = template.geometry.getAttribute("position");
|
|
29701
|
+
const normal = template.geometry.getAttribute("normal");
|
|
29702
|
+
const uv = template.geometry.getAttribute("uv");
|
|
29703
|
+
const bounds = template.geometry.getAttribute("uvBounds");
|
|
29704
|
+
const color2 = template.geometry.getAttribute("color");
|
|
29705
|
+
for (let vertex = 0; vertex < CUBE_FACES.length * 4; vertex++) {
|
|
29706
|
+
data.positions.set([position.getX(vertex), position.getY(vertex), position.getZ(vertex)], vertex * 3);
|
|
29707
|
+
data.normals.set([normal.getX(vertex), normal.getY(vertex), normal.getZ(vertex)], vertex * 3);
|
|
29708
|
+
data.uvs.set([uv.getX(vertex), uv.getY(vertex)], vertex * 2);
|
|
29709
|
+
data.uvBounds.set([
|
|
29710
|
+
bounds?.getX(vertex) ?? 0,
|
|
29711
|
+
bounds?.getY(vertex) ?? 0,
|
|
29712
|
+
bounds?.getZ(vertex) ?? 1,
|
|
29713
|
+
bounds?.getW(vertex) ?? 1
|
|
29714
|
+
], vertex * 4);
|
|
29715
|
+
data.colors.set([color2?.getX(vertex) ?? 1, color2?.getY(vertex) ?? 1, color2?.getZ(vertex) ?? 1], vertex * 3);
|
|
29716
|
+
}
|
|
29717
|
+
const indices = template.geometry.getIndex();
|
|
29718
|
+
for (let i2 = 0; i2 < data.indices.length; i2++) data.indices[i2] = indices.getX(i2);
|
|
29719
|
+
templateCache.set(template, data);
|
|
29720
|
+
}
|
|
29721
|
+
id = input.templateData.length;
|
|
29722
|
+
templateIds.set(template, id);
|
|
29723
|
+
input.templateData.push({ ...data, atlas });
|
|
29724
|
+
}
|
|
29725
|
+
input.indices[i] = index;
|
|
29726
|
+
input.templates[i] = id;
|
|
29727
|
+
input.cullMasks[i] = cullMask;
|
|
29728
|
+
}
|
|
29729
|
+
return { input, atlases };
|
|
29730
|
+
}
|
|
29277
29731
|
var SectionMesh = class _SectionMesh extends import_three41.Group {
|
|
29278
29732
|
constructor() {
|
|
29279
29733
|
super(...arguments);
|
|
@@ -29281,86 +29735,31 @@ var SectionMesh = class _SectionMesh extends import_three41.Group {
|
|
|
29281
29735
|
}
|
|
29282
29736
|
/** Builds section-local meshes from visible cube faces. `maxAtlasSize` limits each atlas dimension in pixels. */
|
|
29283
29737
|
static build(entries, maxAtlasSize = 2048) {
|
|
29284
|
-
|
|
29285
|
-
|
|
29286
|
-
|
|
29287
|
-
|
|
29288
|
-
|
|
29289
|
-
|
|
29290
|
-
|
|
29291
|
-
|
|
29292
|
-
|
|
29293
|
-
|
|
29294
|
-
|
|
29295
|
-
if ((nextRow ? page.y + page.rowHeight : page.y) + height > maxAtlasSize) continue;
|
|
29296
|
-
if (nextRow) {
|
|
29297
|
-
page.y += page.rowHeight;
|
|
29298
|
-
page.x = 0;
|
|
29299
|
-
page.rowHeight = 0;
|
|
29300
|
-
}
|
|
29301
|
-
selected = page;
|
|
29302
|
-
break;
|
|
29303
|
-
}
|
|
29304
|
-
if (!selected) {
|
|
29305
|
-
selected = { placements: [], entries: [], x: 0, y: 0, rowHeight: 0, width: 0, height: 0 };
|
|
29306
|
-
pages.push(selected);
|
|
29307
|
-
}
|
|
29308
|
-
const placement = { atlas, x: selected.x, y: selected.y };
|
|
29309
|
-
selected.placements.push(placement);
|
|
29310
|
-
selected.x += width;
|
|
29311
|
-
selected.rowHeight = Math.max(selected.rowHeight, height);
|
|
29312
|
-
selected.width = Math.max(selected.width, selected.x);
|
|
29313
|
-
selected.height = Math.max(selected.height, selected.y + height);
|
|
29314
|
-
locations.set(atlas, { page: selected, placement });
|
|
29315
|
-
}
|
|
29316
|
-
for (const entry of visible) locations.get(entry.template.atlas).page.entries.push(entry);
|
|
29738
|
+
const { input, atlases } = toInput(entries, maxAtlasSize);
|
|
29739
|
+
return this.fromPages(buildSectionGeometry(input), atlases);
|
|
29740
|
+
}
|
|
29741
|
+
/** Builds section-local meshes in a browser worker when available, with synchronous fallback. */
|
|
29742
|
+
static async buildAsync(entries, maxAtlasSize = 2048) {
|
|
29743
|
+
const { input, atlases } = toInput(entries, maxAtlasSize);
|
|
29744
|
+
const worker = SectionWorker.shared();
|
|
29745
|
+
const pages = worker ? await worker.build(input).catch(() => buildSectionGeometry(input)) : buildSectionGeometry(input);
|
|
29746
|
+
return this.fromPages(pages, atlases);
|
|
29747
|
+
}
|
|
29748
|
+
static fromPages(pages, atlases) {
|
|
29317
29749
|
const section = new _SectionMesh();
|
|
29318
29750
|
for (const page of pages) {
|
|
29319
29751
|
const canvas = createCanvas(page.width, page.height);
|
|
29320
29752
|
const context = canvas.getContext("2d");
|
|
29321
29753
|
for (const { atlas, x, y } of page.placements) {
|
|
29322
|
-
context.drawImage(atlas.image.canvas, x, y);
|
|
29323
|
-
}
|
|
29324
|
-
const positions = [], normals = [], uvs = [], uvBounds = [], colors = [], indices = [];
|
|
29325
|
-
for (const { index, template, cullMask } of page.entries) {
|
|
29326
|
-
const position = template.geometry.getAttribute("position");
|
|
29327
|
-
const normal = template.geometry.getAttribute("normal");
|
|
29328
|
-
const uv = template.geometry.getAttribute("uv");
|
|
29329
|
-
const bounds = template.geometry.getAttribute("uvBounds");
|
|
29330
|
-
const color2 = template.geometry.getAttribute("color");
|
|
29331
|
-
const sourceIndices = template.geometry.getIndex();
|
|
29332
|
-
const placement = locations.get(template.atlas).placement;
|
|
29333
|
-
const mapU = (u) => (placement.x + u * template.atlas.image.width) / page.width;
|
|
29334
|
-
const mapV = (v) => 1 - (placement.y + (1 - v) * template.atlas.image.height) / page.height;
|
|
29335
|
-
const offsetX = index % 16 * 16;
|
|
29336
|
-
const offsetY = Math.floor(index / 256) * 16;
|
|
29337
|
-
const offsetZ = Math.floor(index / 16) % 16 * 16;
|
|
29338
|
-
for (let face = 0; face < 6; face++) {
|
|
29339
|
-
if (cullMask & template.cullFaces[face]) continue;
|
|
29340
|
-
const start = positions.length / 3;
|
|
29341
|
-
for (let corner = 0; corner < 4; corner++) {
|
|
29342
|
-
const vertex = face * 4 + corner;
|
|
29343
|
-
positions.push(position.getX(vertex) + offsetX, position.getY(vertex) + offsetY, position.getZ(vertex) + offsetZ);
|
|
29344
|
-
normals.push(normal.getX(vertex), normal.getY(vertex), normal.getZ(vertex));
|
|
29345
|
-
uvs.push(mapU(uv.getX(vertex)), mapV(uv.getY(vertex)));
|
|
29346
|
-
uvBounds.push(
|
|
29347
|
-
mapU(bounds?.getX(vertex) ?? 0),
|
|
29348
|
-
mapV(bounds?.getY(vertex) ?? 0),
|
|
29349
|
-
mapU(bounds?.getZ(vertex) ?? 1),
|
|
29350
|
-
mapV(bounds?.getW(vertex) ?? 1)
|
|
29351
|
-
);
|
|
29352
|
-
colors.push(color2?.getX(vertex) ?? 1, color2?.getY(vertex) ?? 1, color2?.getZ(vertex) ?? 1);
|
|
29353
|
-
}
|
|
29354
|
-
for (let corner = 0; corner < 6; corner++) indices.push(start + sourceIndices.getX(face * 6 + corner) - face * 4);
|
|
29355
|
-
}
|
|
29754
|
+
context.drawImage(atlases[atlas].image.canvas, x, y);
|
|
29356
29755
|
}
|
|
29357
29756
|
const geometry = new import_three41.BufferGeometry();
|
|
29358
|
-
geometry.setAttribute("position", new import_three41.Float32BufferAttribute(positions, 3));
|
|
29359
|
-
geometry.setAttribute("normal", new import_three41.Float32BufferAttribute(normals, 3));
|
|
29360
|
-
geometry.setAttribute("uv", new import_three41.Float32BufferAttribute(uvs, 2));
|
|
29361
|
-
geometry.setAttribute("uvBounds", new import_three41.Float32BufferAttribute(uvBounds, 4));
|
|
29362
|
-
geometry.setAttribute("color", new import_three41.Float32BufferAttribute(colors, 3));
|
|
29363
|
-
geometry.setIndex(indices);
|
|
29757
|
+
geometry.setAttribute("position", new import_three41.Float32BufferAttribute(page.positions.buffer, 3));
|
|
29758
|
+
geometry.setAttribute("normal", new import_three41.Float32BufferAttribute(page.normals.buffer, 3));
|
|
29759
|
+
geometry.setAttribute("uv", new import_three41.Float32BufferAttribute(page.uvs.buffer, 2));
|
|
29760
|
+
geometry.setAttribute("uvBounds", new import_three41.Float32BufferAttribute(page.uvBounds.buffer, 4));
|
|
29761
|
+
geometry.setAttribute("color", new import_three41.Float32BufferAttribute(page.colors.buffer, 3));
|
|
29762
|
+
geometry.setIndex(new import_three41.Uint32BufferAttribute(page.indices.buffer, 1));
|
|
29364
29763
|
geometry.computeBoundingBox();
|
|
29365
29764
|
geometry.computeBoundingSphere();
|
|
29366
29765
|
const material = Materials.createShadedCanvasMaterial(canvas, false, false, true);
|
|
@@ -29394,7 +29793,9 @@ var Chunk = class _Chunk {
|
|
|
29394
29793
|
this.renderedBlocks = /* @__PURE__ */ new Map();
|
|
29395
29794
|
this.sectionBlocks = /* @__PURE__ */ new Map();
|
|
29396
29795
|
this.hiddenBlocks = /* @__PURE__ */ new Set();
|
|
29796
|
+
this.blockPosition = new import_three42.Vector3();
|
|
29397
29797
|
this.meshDirty = false;
|
|
29798
|
+
this.meshGeneration = 0;
|
|
29398
29799
|
this.scene = scene;
|
|
29399
29800
|
this.x = x;
|
|
29400
29801
|
this.y = y;
|
|
@@ -29407,7 +29808,7 @@ var Chunk = class _Chunk {
|
|
|
29407
29808
|
if (isTripleArray(posOrX)) {
|
|
29408
29809
|
return this.getBlockAt(new import_three42.Vector3(posOrX[0], posOrX[1], posOrX[2]));
|
|
29409
29810
|
}
|
|
29410
|
-
const pos = this.
|
|
29811
|
+
const pos = this.blockPosition.set(posOrX.x - this.x * 16, posOrX.y - this.y * 16, posOrX.z - this.z * 16);
|
|
29411
29812
|
const index = _Chunk.chunkPosToBlockIndex(pos);
|
|
29412
29813
|
return this.renderedBlocks.get(index);
|
|
29413
29814
|
}
|
|
@@ -29431,48 +29832,81 @@ var Chunk = class _Chunk {
|
|
|
29431
29832
|
worldPos = this.chunkPosToWorldPos(pos);
|
|
29432
29833
|
}
|
|
29433
29834
|
const index = _Chunk.chunkPosToBlockIndex(pos);
|
|
29434
|
-
this.hiddenBlocks.delete(index);
|
|
29435
|
-
this.data.set(index, block);
|
|
29436
|
-
const readBlock = this.data.snapshot(index);
|
|
29437
|
-
this.renderedBlocks.get(index)?.object?.removeFromScene();
|
|
29438
|
-
if (this.sectionBlocks.delete(index)) this.meshDirty = true;
|
|
29439
|
-
this.renderedBlocks.delete(index);
|
|
29440
|
-
let object3;
|
|
29441
29835
|
try {
|
|
29442
|
-
|
|
29443
|
-
|
|
29444
|
-
const blockState = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
|
|
29445
|
-
if (!blockState) {
|
|
29446
|
-
this.data.set(index, void 0);
|
|
29447
|
-
return void 0;
|
|
29448
|
-
}
|
|
29449
|
-
const perBlock = !this.sectionModels || getFluidKind(blockState.key, stored.properties) || blockState.key && BlockEntities.entry(await BlockEntities.getIndex(blockState.key.root), blockState.key.toNamespacedString());
|
|
29450
|
-
const template = perBlock ? void 0 : await this.sectionModels.get(blockState, stored.properties);
|
|
29451
|
-
if (template) {
|
|
29452
|
-
this.sectionBlocks.set(index, { index, template, cullMask: 0 });
|
|
29453
|
-
this.meshDirty = true;
|
|
29454
|
-
} else {
|
|
29455
|
-
object3 = await this.scene.addBlock(blockState, {
|
|
29456
|
-
mergeMeshes: true,
|
|
29457
|
-
instanceMeshes: true,
|
|
29458
|
-
maxInstanceCount: 2e3,
|
|
29459
|
-
initialState: stored.properties
|
|
29460
|
-
});
|
|
29461
|
-
object3.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
|
|
29462
|
-
}
|
|
29463
|
-
const info = { get block() {
|
|
29464
|
-
return readBlock();
|
|
29465
|
-
}, object: object3 };
|
|
29466
|
-
this.renderedBlocks.set(index, info);
|
|
29467
|
-
return info;
|
|
29468
|
-
} catch (error) {
|
|
29469
|
-
this.data.set(index, void 0);
|
|
29470
|
-
object3?.removeFromScene();
|
|
29471
|
-
throw error;
|
|
29836
|
+
await this.placeBlocks([{ index, block }]);
|
|
29837
|
+
return this.renderedBlocks.get(index);
|
|
29472
29838
|
} finally {
|
|
29473
29839
|
await onBlocksChanged?.([worldPos]);
|
|
29474
29840
|
}
|
|
29475
29841
|
}
|
|
29842
|
+
/** Places cells in input order and returns changed world positions without refreshing neighbors. */
|
|
29843
|
+
async placeBlocks(blocks) {
|
|
29844
|
+
const resolutions = /* @__PURE__ */ new Map();
|
|
29845
|
+
for (const { block } of blocks) {
|
|
29846
|
+
if (!block || ChunkData.isAir(block)) continue;
|
|
29847
|
+
const key = ChunkData.paletteKey(block);
|
|
29848
|
+
if (resolutions.has(key)) continue;
|
|
29849
|
+
const stored = { type: block.type, properties: block.properties ? { ...block.properties } : void 0 };
|
|
29850
|
+
const resolved = (async () => {
|
|
29851
|
+
const blockState = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
|
|
29852
|
+
const perBlock = !blockState || !this.sectionModels || !!getFluidKind(blockState.key, stored.properties) || !!(blockState.key && BlockEntities.entry(await BlockEntities.getIndex(blockState.key.root), blockState.key.toNamespacedString()));
|
|
29853
|
+
if (!perBlock) await this.sectionModels.get(blockState, stored.properties);
|
|
29854
|
+
return { blockState, perBlock };
|
|
29855
|
+
})();
|
|
29856
|
+
resolutions.set(key, resolved);
|
|
29857
|
+
void resolved.catch(() => void 0);
|
|
29858
|
+
}
|
|
29859
|
+
const positions = [];
|
|
29860
|
+
let failed = false, failure;
|
|
29861
|
+
for (const { index, block } of blocks) {
|
|
29862
|
+
this.hiddenBlocks.delete(index);
|
|
29863
|
+
this.data.set(index, block);
|
|
29864
|
+
const readBlock = this.data.snapshot(index);
|
|
29865
|
+
this.renderedBlocks.get(index)?.object?.removeFromScene();
|
|
29866
|
+
if (this.sectionBlocks.delete(index)) this.meshDirty = true;
|
|
29867
|
+
this.renderedBlocks.delete(index);
|
|
29868
|
+
const worldPos = new import_three42.Vector3(
|
|
29869
|
+
this.x * 16 + index % 16,
|
|
29870
|
+
this.y * 16 + Math.floor(index / 256),
|
|
29871
|
+
this.z * 16 + Math.floor(index / 16) % 16
|
|
29872
|
+
);
|
|
29873
|
+
positions.push(worldPos);
|
|
29874
|
+
let object3;
|
|
29875
|
+
try {
|
|
29876
|
+
if (!readBlock) continue;
|
|
29877
|
+
const stored = readBlock();
|
|
29878
|
+
const key = ChunkData.paletteKey(stored);
|
|
29879
|
+
const { blockState, perBlock } = await resolutions.get(key);
|
|
29880
|
+
if (!blockState) {
|
|
29881
|
+
this.data.set(index, void 0);
|
|
29882
|
+
continue;
|
|
29883
|
+
}
|
|
29884
|
+
const template = perBlock ? void 0 : await this.sectionModels.get(blockState, stored.properties);
|
|
29885
|
+
if (template) {
|
|
29886
|
+
this.sectionBlocks.set(index, { index, template, cullMask: 0 });
|
|
29887
|
+
this.meshDirty = true;
|
|
29888
|
+
} else {
|
|
29889
|
+
object3 = await this.scene.addBlock(blockState, {
|
|
29890
|
+
mergeMeshes: true,
|
|
29891
|
+
instanceMeshes: true,
|
|
29892
|
+
maxInstanceCount: 2e3,
|
|
29893
|
+
initialState: stored.properties
|
|
29894
|
+
});
|
|
29895
|
+
object3.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
|
|
29896
|
+
}
|
|
29897
|
+
this.renderedBlocks.set(index, { get block() {
|
|
29898
|
+
return readBlock();
|
|
29899
|
+
}, object: object3 });
|
|
29900
|
+
} catch (error) {
|
|
29901
|
+
this.data.set(index, void 0);
|
|
29902
|
+
object3?.removeFromScene();
|
|
29903
|
+
if (!failed) failure = error;
|
|
29904
|
+
failed = true;
|
|
29905
|
+
}
|
|
29906
|
+
}
|
|
29907
|
+
if (failed) throw failure;
|
|
29908
|
+
return positions;
|
|
29909
|
+
}
|
|
29476
29910
|
/** Changes visibility at world block coordinates while preserving the block's data. */
|
|
29477
29911
|
async setBlockVisibleAt(pos, visible) {
|
|
29478
29912
|
const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
|
|
@@ -29493,15 +29927,24 @@ var Chunk = class _Chunk {
|
|
|
29493
29927
|
await this.onBlocksChanged?.([pos]);
|
|
29494
29928
|
}
|
|
29495
29929
|
isBlockVisibleAt(pos) {
|
|
29496
|
-
|
|
29930
|
+
return this.isBlockVisibleIndex(_Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos)));
|
|
29931
|
+
}
|
|
29932
|
+
/** Reports whether a cell has a visible render object or section entry. */
|
|
29933
|
+
isBlockVisibleIndex(index) {
|
|
29497
29934
|
return this.renderedBlocks.has(index) && !this.hiddenBlocks.has(index);
|
|
29498
29935
|
}
|
|
29499
29936
|
isOccludingAt(pos) {
|
|
29500
|
-
|
|
29937
|
+
return this.isOccludingIndex(_Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos)));
|
|
29938
|
+
}
|
|
29939
|
+
/** Reports whether a visible cell hides neighboring cube faces. */
|
|
29940
|
+
isOccludingIndex(index) {
|
|
29501
29941
|
return !this.hiddenBlocks.has(index) && (this.sectionBlocks.has(index) || (this.renderedBlocks.get(index)?.object?.isOccluding ?? false));
|
|
29502
29942
|
}
|
|
29503
29943
|
async setCullMaskAt(pos, mask) {
|
|
29504
|
-
|
|
29944
|
+
await this.setCullMaskIndex(_Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos)), mask);
|
|
29945
|
+
}
|
|
29946
|
+
/** Updates hidden faces for a cell's section entry or individual render object. */
|
|
29947
|
+
async setCullMaskIndex(index, mask) {
|
|
29505
29948
|
const entry = this.sectionBlocks.get(index);
|
|
29506
29949
|
if (entry) {
|
|
29507
29950
|
if (entry.cullMask !== mask) {
|
|
@@ -29512,12 +29955,17 @@ var Chunk = class _Chunk {
|
|
|
29512
29955
|
await this.renderedBlocks.get(index)?.object?.setCullMask(mask);
|
|
29513
29956
|
}
|
|
29514
29957
|
}
|
|
29515
|
-
|
|
29958
|
+
/** Rebuilds visible section geometry and discards results superseded by another rebuild or clear. */
|
|
29959
|
+
async rebuildSectionMesh() {
|
|
29516
29960
|
if (!this.meshDirty) return;
|
|
29517
|
-
|
|
29518
|
-
|
|
29519
|
-
|
|
29520
|
-
) : void 0;
|
|
29961
|
+
this.meshDirty = false;
|
|
29962
|
+
const generation = ++this.meshGeneration;
|
|
29963
|
+
const entries = [...this.sectionBlocks.values()].filter((entry) => !this.hiddenBlocks.has(entry.index));
|
|
29964
|
+
const next = entries.length ? await SectionMesh.buildAsync(entries, this.sectionModels.maxAtlasSize) : void 0;
|
|
29965
|
+
if (generation !== this.meshGeneration) {
|
|
29966
|
+
next?.dispose();
|
|
29967
|
+
return;
|
|
29968
|
+
}
|
|
29521
29969
|
this.sectionMesh?.dispose();
|
|
29522
29970
|
this.sectionMesh = next;
|
|
29523
29971
|
if (next) {
|
|
@@ -29525,7 +29973,6 @@ var Chunk = class _Chunk {
|
|
|
29525
29973
|
this.scene.add(next);
|
|
29526
29974
|
}
|
|
29527
29975
|
this.scene.dirty = true;
|
|
29528
|
-
this.meshDirty = false;
|
|
29529
29976
|
}
|
|
29530
29977
|
/** Removes stored blocks and their render objects, then notifies the owning world of changed positions. */
|
|
29531
29978
|
async clear(onBlocksChanged = this.onBlocksChanged) {
|
|
@@ -29534,6 +29981,7 @@ var Chunk = class _Chunk {
|
|
|
29534
29981
|
));
|
|
29535
29982
|
for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
|
|
29536
29983
|
this.sectionBlocks.clear();
|
|
29984
|
+
this.meshGeneration++;
|
|
29537
29985
|
this.sectionMesh?.dispose();
|
|
29538
29986
|
this.sectionMesh = void 0;
|
|
29539
29987
|
this.meshDirty = false;
|
|
@@ -29570,4 +30018,178 @@ var Chunk = class _Chunk {
|
|
|
29570
30018
|
);
|
|
29571
30019
|
}
|
|
29572
30020
|
};
|
|
30021
|
+
|
|
30022
|
+
// src/world/WorldStreamer.ts
|
|
30023
|
+
var WorldStreamer = class {
|
|
30024
|
+
constructor(world, source, options = {}) {
|
|
30025
|
+
this.world = world;
|
|
30026
|
+
this.source = source;
|
|
30027
|
+
this.loaded = /* @__PURE__ */ new Map();
|
|
30028
|
+
this.owned = /* @__PURE__ */ new Map();
|
|
30029
|
+
this.missing = /* @__PURE__ */ new Map();
|
|
30030
|
+
this.failed = /* @__PURE__ */ new Map();
|
|
30031
|
+
this.desired = [];
|
|
30032
|
+
this.disposed = false;
|
|
30033
|
+
this.loadRadius = options.loadRadius ?? 2;
|
|
30034
|
+
this.unloadRadius = options.unloadRadius ?? Math.min(32, this.loadRadius + 1);
|
|
30035
|
+
if (![this.loadRadius, this.unloadRadius].every((value) => Number.isInteger(value) && value >= 0 && value <= 32) || this.unloadRadius < this.loadRadius) {
|
|
30036
|
+
throw new RangeError("Chunk radii must be integers from 0 to 32, with unloadRadius at least loadRadius");
|
|
30037
|
+
}
|
|
30038
|
+
}
|
|
30039
|
+
/** Returns absolute coordinates of successfully placed columns, including retained columns. */
|
|
30040
|
+
get loadedChunks() {
|
|
30041
|
+
return [...this.loaded.values()].map((position) => ({ ...position }));
|
|
30042
|
+
}
|
|
30043
|
+
/** Returns source failures retained within the unload radius. Failed columns do not block other loads. */
|
|
30044
|
+
get failedChunks() {
|
|
30045
|
+
return [...this.failed.values()].map((failure) => ({ ...failure }));
|
|
30046
|
+
}
|
|
30047
|
+
/** Counts desired columns still waiting for a source read or placement, including the active column. */
|
|
30048
|
+
get pendingChunks() {
|
|
30049
|
+
return this.desired.filter((position) => this.needsLoad(position)).length;
|
|
30050
|
+
}
|
|
30051
|
+
/**
|
|
30052
|
+
* Updates the center in absolute chunk coordinates. Repeated calls share the active drain.
|
|
30053
|
+
* Source failures are recorded in failedChunks; other columns continue loading. Missing and failed
|
|
30054
|
+
* columns are remembered within the retention radius. Call retryFailedChunks to retry source failures.
|
|
30055
|
+
* Placement and unload failures reject the drain; call update again to retry those operations.
|
|
30056
|
+
* Reads outside the latest load radius receive an abort signal; placement finishes before unloading.
|
|
30057
|
+
* Sources should represent an unchanged world during streaming.
|
|
30058
|
+
*/
|
|
30059
|
+
async update(x, z) {
|
|
30060
|
+
if (this.disposed) throw new Error("WorldStreamer is disposed");
|
|
30061
|
+
if (![x, z].every((value) => Number.isSafeInteger(value) && Number.isSafeInteger(value - this.unloadRadius) && Number.isSafeInteger(value + this.unloadRadius))) {
|
|
30062
|
+
throw new RangeError("Chunk coordinates must be safe integers within the streaming radius");
|
|
30063
|
+
}
|
|
30064
|
+
if (!this.center || this.center.x !== x || this.center.z !== z) {
|
|
30065
|
+
this.center = { x, z };
|
|
30066
|
+
this.desired = [];
|
|
30067
|
+
for (let dz = -this.loadRadius; dz <= this.loadRadius; dz++) {
|
|
30068
|
+
for (let dx = -this.loadRadius; dx <= this.loadRadius; dx++) {
|
|
30069
|
+
this.desired.push({ x: x + dx, z: z + dz });
|
|
30070
|
+
}
|
|
30071
|
+
}
|
|
30072
|
+
this.desired.sort((a, b) => (a.x - x) ** 2 + (a.z - z) ** 2 - (b.x - x) ** 2 - (b.z - z) ** 2);
|
|
30073
|
+
if (this.loading && !this.within(this.loading.position, this.loadRadius)) {
|
|
30074
|
+
this.loading.controller.abort();
|
|
30075
|
+
}
|
|
30076
|
+
}
|
|
30077
|
+
return this.running ?? (this.running = Promise.resolve().then(() => this.drain()));
|
|
30078
|
+
}
|
|
30079
|
+
/** Updates from a camera or view-center position in scene units. One chunk spans 256 scene units. */
|
|
30080
|
+
async updatePosition(position) {
|
|
30081
|
+
if (![position.x, position.z].every(Number.isFinite)) throw new RangeError("Scene coordinates must be finite");
|
|
30082
|
+
return this.update(Math.floor(position.x / 256), Math.floor(position.z / 256));
|
|
30083
|
+
}
|
|
30084
|
+
/** Forgets recorded source failures and retries desired columns at the latest center. */
|
|
30085
|
+
async retryFailedChunks() {
|
|
30086
|
+
if (this.disposed) throw new Error("WorldStreamer is disposed");
|
|
30087
|
+
this.failed.clear();
|
|
30088
|
+
if (this.center) await this.update(this.center.x, this.center.z);
|
|
30089
|
+
}
|
|
30090
|
+
/** Aborts source reads, waits for active work, then unloads owned columns. The world and source remain caller-owned. */
|
|
30091
|
+
dispose() {
|
|
30092
|
+
this.disposed = true;
|
|
30093
|
+
this.desired = [];
|
|
30094
|
+
this.center = void 0;
|
|
30095
|
+
this.loading?.controller.abort();
|
|
30096
|
+
return this.disposal ?? (this.disposal = Promise.resolve().then(async () => {
|
|
30097
|
+
await this.running?.catch(() => void 0);
|
|
30098
|
+
let failure;
|
|
30099
|
+
let failed = false;
|
|
30100
|
+
for (const position of this.owned.values()) {
|
|
30101
|
+
try {
|
|
30102
|
+
await this.unload(position);
|
|
30103
|
+
} catch (error) {
|
|
30104
|
+
failed = true;
|
|
30105
|
+
failure ?? (failure = error);
|
|
30106
|
+
}
|
|
30107
|
+
}
|
|
30108
|
+
this.missing.clear();
|
|
30109
|
+
this.failed.clear();
|
|
30110
|
+
if (failed) throw failure;
|
|
30111
|
+
}).catch((error) => {
|
|
30112
|
+
this.disposal = void 0;
|
|
30113
|
+
throw error;
|
|
30114
|
+
}));
|
|
30115
|
+
}
|
|
30116
|
+
async drain() {
|
|
30117
|
+
try {
|
|
30118
|
+
while (!this.disposed) {
|
|
30119
|
+
for (const [key, position2] of this.missing) {
|
|
30120
|
+
if (!this.within(position2, this.unloadRadius)) this.missing.delete(key);
|
|
30121
|
+
}
|
|
30122
|
+
for (const [key, position2] of this.failed) {
|
|
30123
|
+
if (!this.within(position2, this.unloadRadius)) this.failed.delete(key);
|
|
30124
|
+
}
|
|
30125
|
+
const obsolete = [...this.owned.values()].find((position2) => !this.loaded.has(this.key(position2)) || !this.within(position2, this.unloadRadius));
|
|
30126
|
+
if (obsolete) {
|
|
30127
|
+
await this.unload(obsolete);
|
|
30128
|
+
continue;
|
|
30129
|
+
}
|
|
30130
|
+
const position = this.desired.find((position2) => this.needsLoad(position2));
|
|
30131
|
+
if (!position) return;
|
|
30132
|
+
let chunk;
|
|
30133
|
+
const controller = new AbortController();
|
|
30134
|
+
this.loading = { position, controller };
|
|
30135
|
+
try {
|
|
30136
|
+
chunk = await this.source.getChunk(position.x, position.z, controller.signal);
|
|
30137
|
+
if (chunk) this.validateChunk(chunk, position);
|
|
30138
|
+
} catch (error) {
|
|
30139
|
+
if (!controller.signal.aborted && !this.disposed && this.within(position, this.loadRadius)) {
|
|
30140
|
+
this.failed.set(this.key(position), { ...position, error });
|
|
30141
|
+
}
|
|
30142
|
+
continue;
|
|
30143
|
+
} finally {
|
|
30144
|
+
this.loading = void 0;
|
|
30145
|
+
}
|
|
30146
|
+
if (controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) continue;
|
|
30147
|
+
if (!chunk) {
|
|
30148
|
+
this.missing.set(this.key(position), position);
|
|
30149
|
+
continue;
|
|
30150
|
+
}
|
|
30151
|
+
this.owned.set(this.key(position), position);
|
|
30152
|
+
try {
|
|
30153
|
+
await this.world.placeChunk(chunk);
|
|
30154
|
+
this.loaded.set(this.key(position), position);
|
|
30155
|
+
} catch (error) {
|
|
30156
|
+
try {
|
|
30157
|
+
await this.unload(position);
|
|
30158
|
+
} catch (cleanupError) {
|
|
30159
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
30160
|
+
try {
|
|
30161
|
+
Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
|
|
30162
|
+
} catch {
|
|
30163
|
+
throw Object.assign(new Error(failure.message), { cause: cleanupError });
|
|
30164
|
+
}
|
|
30165
|
+
throw failure;
|
|
30166
|
+
}
|
|
30167
|
+
throw error;
|
|
30168
|
+
}
|
|
30169
|
+
}
|
|
30170
|
+
} finally {
|
|
30171
|
+
this.running = void 0;
|
|
30172
|
+
}
|
|
30173
|
+
}
|
|
30174
|
+
needsLoad(position) {
|
|
30175
|
+
const key = this.key(position);
|
|
30176
|
+
return !this.loaded.has(key) && !this.missing.has(key) && !this.failed.has(key);
|
|
30177
|
+
}
|
|
30178
|
+
async unload(position) {
|
|
30179
|
+
await this.world.unloadChunkColumn(position.x, position.z);
|
|
30180
|
+
this.owned.delete(this.key(position));
|
|
30181
|
+
this.loaded.delete(this.key(position));
|
|
30182
|
+
}
|
|
30183
|
+
validateChunk(chunk, position) {
|
|
30184
|
+
if (chunk.x !== position.x || chunk.z !== position.z) {
|
|
30185
|
+
throw new Error(`World source returned chunk ${chunk.x},${chunk.z} for ${position.x},${position.z}`);
|
|
30186
|
+
}
|
|
30187
|
+
}
|
|
30188
|
+
within(position, radius) {
|
|
30189
|
+
return !!this.center && Math.abs(position.x - this.center.x) <= radius && Math.abs(position.z - this.center.z) <= radius;
|
|
30190
|
+
}
|
|
30191
|
+
key(position) {
|
|
30192
|
+
return `${position.x},${position.z}`;
|
|
30193
|
+
}
|
|
30194
|
+
};
|
|
29573
30195
|
//# sourceMappingURL=index.js.map
|