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.
@@ -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 = AssetKey.parse("blockstates", block.type).toNamespacedString();
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 = JSON.stringify([type, properties]);
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 = AssetKey.parse("blockstates", block.type);
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]) => BigInt(high >>> 0) << 32n | BigInt(low >>> 0));
28887
- const mask = (1n << BigInt(bits)) - 1n;
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 word = padded ? Math.floor(index / perLong) : Math.floor(index * bits / 64);
28890
- const shift = padded ? index % perLong * bits : index * bits % 64;
28891
- let value = words[word] >> BigInt(shift);
28892
- if (!padded && shift + bits > 64) value |= words[word + 1] << BigInt(64 - shift);
28893
- indices[index] = Number(value & mask);
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
- init_BatchedExecutor();
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 - Uses bounded batches by default. Set to `false` for sequential placement.
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
- const queue = executor ?? new BatchedExecutor();
29088
- try {
29089
- for (let i = 0; i < groups.length; i += queue.batch) {
29090
- const results = await Promise.allSettled(groups.slice(i, i + queue.batch).map(
29091
- (blocks) => queue.submit(async () => {
29092
- for (const block of blocks) await place(block);
29093
- })
29094
- ));
29095
- const failure = results.find((result) => result.status === "rejected");
29096
- if (failure?.status === "rejected") throw failure.reason;
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
- this.pendingCulling.set(pos.toArray().join(","), pos.clone());
29164
- for (const offset of CUBE_FACE_OFFSETS) {
29165
- const neighbor = pos.clone().add(new import_three40.Vector3(...offset));
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 neighbor = pos.clone().add(new import_three40.Vector3(x, y, z));
29172
- if (this.getBlockAt(neighbor)?.object?.fluidKind) {
29173
- this.pendingCulling.set(neighbor.toArray().join(","), neighbor);
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.values()];
29416
+ const batch = [...this.pendingCulling];
29183
29417
  this.pendingCulling.clear();
29184
- const changed = /* @__PURE__ */ new Set();
29185
- for (const pos of batch) {
29186
- const chunk = this.getChunkAt(pos);
29187
- if (!chunk) continue;
29188
- changed.add(chunk);
29189
- const block = chunk.getBlockAt(pos);
29190
- if (!block || !chunk.isBlockVisibleAt(pos)) {
29191
- continue;
29192
- }
29193
- if (block.object?.fluidKind) {
29194
- await block.object.updateFluid((x, y, z) => {
29195
- const neighbor = pos.clone().add(new import_three40.Vector3(x, y, z));
29196
- const section = this.getChunkAt(neighbor);
29197
- const object3 = section?.isBlockVisibleAt(neighbor) ? section.getBlockAt(neighbor)?.object : void 0;
29198
- return {
29199
- fluid: object3?.fluidKind,
29200
- level: object3?.fluidLevel,
29201
- solid: section?.isOccludingAt(neighbor) ?? false
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
- for (const chunk of changed) chunk.rebuildSectionMesh();
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
- if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
29285
- const visible = entries.filter((entry) => entry.template.cullFaces.some((direction) => !(entry.cullMask & direction)));
29286
- const atlases = [...new Set(visible.map((entry) => entry.template.atlas))].sort((a, b) => b.image.height - a.image.height);
29287
- const pages = [];
29288
- const locations = /* @__PURE__ */ new Map();
29289
- for (const atlas of atlases) {
29290
- const { width, height } = atlas.image;
29291
- if (width > maxAtlasSize || height > maxAtlasSize) throw new RangeError("Model atlas exceeds the section atlas size limit");
29292
- let selected;
29293
- for (const page of pages) {
29294
- const nextRow = page.x + width > maxAtlasSize;
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.worldPosToChunkPos(posOrX);
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
- if (!readBlock) return void 0;
29443
- const stored = readBlock();
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
- const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
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
- const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
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
- const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
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
- rebuildSectionMesh() {
29958
+ /** Rebuilds visible section geometry and discards results superseded by another rebuild or clear. */
29959
+ async rebuildSectionMesh() {
29516
29960
  if (!this.meshDirty) return;
29517
- const next = this.sectionBlocks.size ? SectionMesh.build(
29518
- [...this.sectionBlocks.values()].filter((entry) => !this.hiddenBlocks.has(entry.index)),
29519
- this.sectionModels.maxAtlasSize
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