minerender 2.1.0-alpha.87 → 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
  }
@@ -18778,6 +18795,7 @@ __export(index_browser_exports, {
18778
18795
  SchematicParser: () => SchematicParser,
18779
18796
  SectionMesh: () => SectionMesh,
18780
18797
  SectionModels: () => SectionModels,
18798
+ SectionWorker: () => SectionWorker,
18781
18799
  SkinImage: () => SkinImage,
18782
18800
  SkinObject: () => SkinObject,
18783
18801
  SkinPart: () => SkinPart,
@@ -18802,6 +18820,7 @@ __export(index_browser_exports, {
18802
18820
  axisToVec3: () => axisToVec3,
18803
18821
  base64decode: () => base64decode,
18804
18822
  base64encode: () => base64encode,
18823
+ buildSectionGeometry: () => buildSectionGeometry,
18805
18824
  capeTextureSizes: () => capeTextureSizes,
18806
18825
  captureException: () => captureException,
18807
18826
  changeEvent: () => changeEvent,
@@ -18854,6 +18873,7 @@ __export(index_browser_exports, {
18854
18873
  sampleEntityAnimation: () => sampleEntityAnimation,
18855
18874
  sampleEntityKeyframes: () => sampleEntityKeyframes,
18856
18875
  sampleFluid: () => sampleFluid,
18876
+ sectionGeometryTransferables: () => sectionGeometryTransferables,
18857
18877
  serializeBoxGeometryKey: () => serializeBoxGeometryKey,
18858
18878
  serializeImageKey: () => serializeImageKey,
18859
18879
  serializeMaterialKey: () => serializeMaterialKey,
@@ -18871,7 +18891,8 @@ __export(index_browser_exports, {
18871
18891
  toDegrees: () => toDegrees,
18872
18892
  toRadians: () => toRadians,
18873
18893
  toTrianglesDrawMode: () => toTrianglesDrawMode,
18874
- trimCanvas: () => trimCanvas
18894
+ trimCanvas: () => trimCanvas,
18895
+ yieldToEventLoop: () => yieldToEventLoop
18875
18896
  });
18876
18897
  module.exports = __toCommonJS(index_browser_exports);
18877
18898
 
@@ -18956,6 +18977,7 @@ function probeImageSize(data) {
18956
18977
  }
18957
18978
 
18958
18979
  // src/env/browser/BrowserEnv.ts
18980
+ var import_meta = {};
18959
18981
  var BrowserEnv = class {
18960
18982
  constructor() {
18961
18983
  this.name = "browser";
@@ -18983,6 +19005,14 @@ var BrowserEnv = class {
18983
19005
  version: version2
18984
19006
  }));
18985
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
+ }
18986
19016
  };
18987
19017
  function registerBrowserEnv() {
18988
19018
  const env = new BrowserEnv();
@@ -28636,6 +28666,16 @@ init_NBTHelper();
28636
28666
 
28637
28667
  // src/world/ChunkData.ts
28638
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
+ }
28639
28679
  var ChunkData = class _ChunkData {
28640
28680
  constructor() {
28641
28681
  this.indices = new Uint16Array(4096);
@@ -28663,10 +28703,10 @@ var ChunkData = class _ChunkData {
28663
28703
  let key;
28664
28704
  let nbt;
28665
28705
  if (block && !_ChunkData.isAir(block)) {
28666
- const type = AssetKey.parse("blockstates", block.type).toNamespacedString();
28706
+ const type = blockKey(block.type).toNamespacedString();
28667
28707
  const properties = Object.fromEntries(Object.keys(block.properties ?? {}).sort().map((name) => [name, block.properties[name]]));
28668
28708
  state = Object.freeze(Object.keys(properties).length ? { type, properties: Object.freeze(properties) } : { type });
28669
- key = JSON.stringify([type, properties]);
28709
+ key = _ChunkData.paletteKey(block);
28670
28710
  if (block.nbt !== void 0) nbt = structuredClone(block.nbt);
28671
28711
  }
28672
28712
  const previousId = this.indices[index];
@@ -28703,9 +28743,14 @@ var ChunkData = class _ChunkData {
28703
28743
  }
28704
28744
  static isAir(block) {
28705
28745
  if (typeof block === "undefined" || typeof block.type === "undefined") return true;
28706
- const key = AssetKey.parse("blockstates", block.type);
28746
+ const key = blockKey(block.type);
28707
28747
  return key.namespace === "minecraft" && !key.type && ["air", "cave_air", "void_air"].includes(key.path);
28708
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
+ }
28709
28754
  static copyBlock(state, nbt) {
28710
28755
  const block = { type: state.type };
28711
28756
  if (state.properties) block.properties = { ...state.properties };
@@ -28885,14 +28930,17 @@ var AnvilParser = class {
28885
28930
  const perLong = Math.floor(64 / bits);
28886
28931
  const expected = padded ? Math.ceil(4096 / perLong) : Math.ceil(4096 * bits / 64);
28887
28932
  if (tag.value.length !== expected) throw new MineRenderError(`Invalid Anvil block-state array length: expected ${expected}`);
28888
- const words = tag.value.map(([high, low]) => BigInt(high >>> 0) << 32n | BigInt(low >>> 0));
28889
- const mask = (1n << BigInt(bits)) - 1n;
28933
+ const words = tag.value.map(([high, low]) => [high >>> 0, low >>> 0]);
28934
+ const mask = (1 << bits) - 1;
28890
28935
  for (let index = 0; index < 4096; index++) {
28891
- const word = padded ? Math.floor(index / perLong) : Math.floor(index * bits / 64);
28892
- const shift = padded ? index % perLong * bits : index * bits % 64;
28893
- let value = words[word] >> BigInt(shift);
28894
- if (!padded && shift + bits > 64) value |= words[word + 1] << BigInt(64 - shift);
28895
- 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;
28896
28944
  }
28897
28945
  return indices;
28898
28946
  }
@@ -29034,7 +29082,7 @@ init_AssetKey();
29034
29082
  // src/world/MineRenderWorld.ts
29035
29083
  var import_three40 = require("three");
29036
29084
  init_Model();
29037
- init_BatchedExecutor();
29085
+ init_util();
29038
29086
  init_AssetKey();
29039
29087
  init_BlockStates();
29040
29088
  init_CubeFace();
@@ -29179,7 +29227,7 @@ var MineRenderWorld = class {
29179
29227
  * Places a structure's blocks at their stored positions, then refreshes neighboring faces.
29180
29228
  * Entity NBT is retained by the structure but is not rendered.
29181
29229
  *
29182
- * @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.
29183
29231
  * @param executor - Optional queue for batched placement. The caller remains responsible for stopping it.
29184
29232
  */
29185
29233
  async placeMultiBlock(multiblock, useBatches = true, executor) {
@@ -29196,8 +29244,10 @@ var MineRenderWorld = class {
29196
29244
  };
29197
29245
  const place = (block) => this.placeBlock(new import_three40.Vector3(...block.position), block, collect);
29198
29246
  const keys = /* @__PURE__ */ new Map();
29247
+ const types = /* @__PURE__ */ new Set();
29199
29248
  for (const block of multiblock.blocks) {
29200
- if (Chunk.isAir(block)) continue;
29249
+ if (Chunk.isAir(block) || types.has(block.type)) continue;
29250
+ types.add(block.type);
29201
29251
  const key = AssetKey.parse("blockstates", block.type);
29202
29252
  keys.set(key.serialize(), key);
29203
29253
  }
@@ -29206,6 +29256,23 @@ var MineRenderWorld = class {
29206
29256
  for (const block of multiblock.blocks) await place(block);
29207
29257
  return;
29208
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
+ }
29209
29276
  const positions = /* @__PURE__ */ new Map();
29210
29277
  for (const block of multiblock.blocks) {
29211
29278
  const key = block.position.join(",");
@@ -29214,19 +29281,34 @@ var MineRenderWorld = class {
29214
29281
  else positions.set(key, [block]);
29215
29282
  }
29216
29283
  const groups = [...positions.values()];
29217
- const queue = executor ?? new BatchedExecutor();
29218
- try {
29219
- for (let i = 0; i < groups.length; i += queue.batch) {
29220
- const results = await Promise.allSettled(groups.slice(i, i + queue.batch).map(
29221
- (blocks) => queue.submit(async () => {
29222
- for (const block of blocks) await place(block);
29223
- })
29224
- ));
29225
- const failure = results.find((result) => result.status === "rejected");
29226
- 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();
29227
29311
  }
29228
- } finally {
29229
- if (!executor) queue.stop();
29230
29312
  }
29231
29313
  }
29232
29314
  /** Replaces one chunk column's blocks. Entity NBT remains available on the parsed chunk. */
@@ -29234,7 +29316,24 @@ var MineRenderWorld = class {
29234
29316
  const changes = /* @__PURE__ */ new Map();
29235
29317
  try {
29236
29318
  await this.clearChunkColumn(chunk.x, chunk.z, changes);
29319
+ const groups = /* @__PURE__ */ new Map();
29237
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
+ }
29238
29337
  const blocks = [];
29239
29338
  for (let index = 0; index < 4096; index++) {
29240
29339
  const block = section.data.get(index);
@@ -29249,6 +29348,7 @@ var MineRenderWorld = class {
29249
29348
  }
29250
29349
  await this.placeBlocks({ size: [16, 16, 16], blocks }, true, executor, changes);
29251
29350
  }
29351
+ if (!executor) await this.placeChunkGroups(groups, changes);
29252
29352
  } finally {
29253
29353
  await this.updateCulling([...changes.values()]);
29254
29354
  }
@@ -29289,19 +29389,23 @@ var MineRenderWorld = class {
29289
29389
  this.sectionModels?.clear();
29290
29390
  }
29291
29391
  updateCulling(positions) {
29392
+ const neighbor = new import_three40.Vector3();
29292
29393
  for (const pos of positions) {
29293
- this.pendingCulling.set(pos.toArray().join(","), pos.clone());
29294
- for (const offset of CUBE_FACE_OFFSETS) {
29295
- const neighbor = pos.clone().add(new import_three40.Vector3(...offset));
29296
- this.pendingCulling.set(neighbor.toArray().join(","), neighbor);
29297
- }
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;
29298
29397
  for (let y = -1; y <= 1; y++) {
29299
29398
  for (let z = -1; z <= 1; z++) {
29300
29399
  for (let x = -1; x <= 1; x++) {
29301
- const neighbor = pos.clone().add(new import_three40.Vector3(x, y, z));
29302
- if (this.getBlockAt(neighbor)?.object?.fluidKind) {
29303
- this.pendingCulling.set(neighbor.toArray().join(","), neighbor);
29304
- }
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);
29305
29409
  }
29306
29410
  }
29307
29411
  }
@@ -29309,39 +29413,38 @@ var MineRenderWorld = class {
29309
29413
  return this.culling ?? (this.culling = Promise.resolve().then(async () => {
29310
29414
  try {
29311
29415
  while (this.pendingCulling.size) {
29312
- const batch = [...this.pendingCulling.values()];
29416
+ const batch = [...this.pendingCulling];
29313
29417
  this.pendingCulling.clear();
29314
- const changed = /* @__PURE__ */ new Set();
29315
- for (const pos of batch) {
29316
- const chunk = this.getChunkAt(pos);
29317
- if (!chunk) continue;
29318
- changed.add(chunk);
29319
- const block = chunk.getBlockAt(pos);
29320
- if (!block || !chunk.isBlockVisibleAt(pos)) {
29321
- continue;
29322
- }
29323
- if (block.object?.fluidKind) {
29324
- await block.object.updateFluid((x, y, z) => {
29325
- const neighbor = pos.clone().add(new import_three40.Vector3(x, y, z));
29326
- const section = this.getChunkAt(neighbor);
29327
- const object3 = section?.isBlockVisibleAt(neighbor) ? section.getBlockAt(neighbor)?.object : void 0;
29328
- return {
29329
- fluid: object3?.fluidKind,
29330
- level: object3?.fluidLevel,
29331
- solid: section?.isOccludingAt(neighbor) ?? false
29332
- };
29333
- });
29334
- }
29335
- let mask = 0;
29336
- for (const [face, offset] of CUBE_FACE_OFFSETS.entries()) {
29337
- const neighbor = pos.clone().add(new import_three40.Vector3(...offset));
29338
- if (this.getChunkAt(neighbor)?.isOccludingAt(neighbor)) {
29339
- 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
+ });
29340
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);
29341
29445
  }
29342
- await chunk.setCullMaskAt(pos, mask);
29343
29446
  }
29344
- for (const chunk of changed) chunk.rebuildSectionMesh();
29447
+ await Promise.all(batch.map(([chunk]) => chunk.rebuildSectionMesh()));
29345
29448
  }
29346
29449
  } finally {
29347
29450
  this.culling = void 0;
@@ -29403,7 +29506,228 @@ init_Model();
29403
29506
  // src/world/SectionMesh.ts
29404
29507
  var import_three41 = require("three");
29405
29508
  init_CanvasCompat();
29509
+ init_CubeFace();
29406
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
+ }
29407
29731
  var SectionMesh = class _SectionMesh extends import_three41.Group {
29408
29732
  constructor() {
29409
29733
  super(...arguments);
@@ -29411,86 +29735,31 @@ var SectionMesh = class _SectionMesh extends import_three41.Group {
29411
29735
  }
29412
29736
  /** Builds section-local meshes from visible cube faces. `maxAtlasSize` limits each atlas dimension in pixels. */
29413
29737
  static build(entries, maxAtlasSize = 2048) {
29414
- if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
29415
- const visible = entries.filter((entry) => entry.template.cullFaces.some((direction) => !(entry.cullMask & direction)));
29416
- const atlases = [...new Set(visible.map((entry) => entry.template.atlas))].sort((a, b) => b.image.height - a.image.height);
29417
- const pages = [];
29418
- const locations = /* @__PURE__ */ new Map();
29419
- for (const atlas of atlases) {
29420
- const { width, height } = atlas.image;
29421
- if (width > maxAtlasSize || height > maxAtlasSize) throw new RangeError("Model atlas exceeds the section atlas size limit");
29422
- let selected;
29423
- for (const page of pages) {
29424
- const nextRow = page.x + width > maxAtlasSize;
29425
- if ((nextRow ? page.y + page.rowHeight : page.y) + height > maxAtlasSize) continue;
29426
- if (nextRow) {
29427
- page.y += page.rowHeight;
29428
- page.x = 0;
29429
- page.rowHeight = 0;
29430
- }
29431
- selected = page;
29432
- break;
29433
- }
29434
- if (!selected) {
29435
- selected = { placements: [], entries: [], x: 0, y: 0, rowHeight: 0, width: 0, height: 0 };
29436
- pages.push(selected);
29437
- }
29438
- const placement = { atlas, x: selected.x, y: selected.y };
29439
- selected.placements.push(placement);
29440
- selected.x += width;
29441
- selected.rowHeight = Math.max(selected.rowHeight, height);
29442
- selected.width = Math.max(selected.width, selected.x);
29443
- selected.height = Math.max(selected.height, selected.y + height);
29444
- locations.set(atlas, { page: selected, placement });
29445
- }
29446
- 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) {
29447
29749
  const section = new _SectionMesh();
29448
29750
  for (const page of pages) {
29449
29751
  const canvas = createCanvas(page.width, page.height);
29450
29752
  const context = canvas.getContext("2d");
29451
29753
  for (const { atlas, x, y } of page.placements) {
29452
- context.drawImage(atlas.image.canvas, x, y);
29453
- }
29454
- const positions = [], normals = [], uvs = [], uvBounds = [], colors = [], indices = [];
29455
- for (const { index, template, cullMask } of page.entries) {
29456
- const position = template.geometry.getAttribute("position");
29457
- const normal = template.geometry.getAttribute("normal");
29458
- const uv = template.geometry.getAttribute("uv");
29459
- const bounds = template.geometry.getAttribute("uvBounds");
29460
- const color2 = template.geometry.getAttribute("color");
29461
- const sourceIndices = template.geometry.getIndex();
29462
- const placement = locations.get(template.atlas).placement;
29463
- const mapU = (u) => (placement.x + u * template.atlas.image.width) / page.width;
29464
- const mapV = (v) => 1 - (placement.y + (1 - v) * template.atlas.image.height) / page.height;
29465
- const offsetX = index % 16 * 16;
29466
- const offsetY = Math.floor(index / 256) * 16;
29467
- const offsetZ = Math.floor(index / 16) % 16 * 16;
29468
- for (let face = 0; face < 6; face++) {
29469
- if (cullMask & template.cullFaces[face]) continue;
29470
- const start = positions.length / 3;
29471
- for (let corner = 0; corner < 4; corner++) {
29472
- const vertex = face * 4 + corner;
29473
- positions.push(position.getX(vertex) + offsetX, position.getY(vertex) + offsetY, position.getZ(vertex) + offsetZ);
29474
- normals.push(normal.getX(vertex), normal.getY(vertex), normal.getZ(vertex));
29475
- uvs.push(mapU(uv.getX(vertex)), mapV(uv.getY(vertex)));
29476
- uvBounds.push(
29477
- mapU(bounds?.getX(vertex) ?? 0),
29478
- mapV(bounds?.getY(vertex) ?? 0),
29479
- mapU(bounds?.getZ(vertex) ?? 1),
29480
- mapV(bounds?.getW(vertex) ?? 1)
29481
- );
29482
- colors.push(color2?.getX(vertex) ?? 1, color2?.getY(vertex) ?? 1, color2?.getZ(vertex) ?? 1);
29483
- }
29484
- for (let corner = 0; corner < 6; corner++) indices.push(start + sourceIndices.getX(face * 6 + corner) - face * 4);
29485
- }
29754
+ context.drawImage(atlases[atlas].image.canvas, x, y);
29486
29755
  }
29487
29756
  const geometry = new import_three41.BufferGeometry();
29488
- geometry.setAttribute("position", new import_three41.Float32BufferAttribute(positions, 3));
29489
- geometry.setAttribute("normal", new import_three41.Float32BufferAttribute(normals, 3));
29490
- geometry.setAttribute("uv", new import_three41.Float32BufferAttribute(uvs, 2));
29491
- geometry.setAttribute("uvBounds", new import_three41.Float32BufferAttribute(uvBounds, 4));
29492
- geometry.setAttribute("color", new import_three41.Float32BufferAttribute(colors, 3));
29493
- 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));
29494
29763
  geometry.computeBoundingBox();
29495
29764
  geometry.computeBoundingSphere();
29496
29765
  const material = Materials.createShadedCanvasMaterial(canvas, false, false, true);
@@ -29524,7 +29793,9 @@ var Chunk = class _Chunk {
29524
29793
  this.renderedBlocks = /* @__PURE__ */ new Map();
29525
29794
  this.sectionBlocks = /* @__PURE__ */ new Map();
29526
29795
  this.hiddenBlocks = /* @__PURE__ */ new Set();
29796
+ this.blockPosition = new import_three42.Vector3();
29527
29797
  this.meshDirty = false;
29798
+ this.meshGeneration = 0;
29528
29799
  this.scene = scene;
29529
29800
  this.x = x;
29530
29801
  this.y = y;
@@ -29537,7 +29808,7 @@ var Chunk = class _Chunk {
29537
29808
  if (isTripleArray(posOrX)) {
29538
29809
  return this.getBlockAt(new import_three42.Vector3(posOrX[0], posOrX[1], posOrX[2]));
29539
29810
  }
29540
- 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);
29541
29812
  const index = _Chunk.chunkPosToBlockIndex(pos);
29542
29813
  return this.renderedBlocks.get(index);
29543
29814
  }
@@ -29561,48 +29832,81 @@ var Chunk = class _Chunk {
29561
29832
  worldPos = this.chunkPosToWorldPos(pos);
29562
29833
  }
29563
29834
  const index = _Chunk.chunkPosToBlockIndex(pos);
29564
- this.hiddenBlocks.delete(index);
29565
- this.data.set(index, block);
29566
- const readBlock = this.data.snapshot(index);
29567
- this.renderedBlocks.get(index)?.object?.removeFromScene();
29568
- if (this.sectionBlocks.delete(index)) this.meshDirty = true;
29569
- this.renderedBlocks.delete(index);
29570
- let object3;
29571
29835
  try {
29572
- if (!readBlock) return void 0;
29573
- const stored = readBlock();
29574
- const blockState = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
29575
- if (!blockState) {
29576
- this.data.set(index, void 0);
29577
- return void 0;
29578
- }
29579
- const perBlock = !this.sectionModels || getFluidKind(blockState.key, stored.properties) || blockState.key && BlockEntities.entry(await BlockEntities.getIndex(blockState.key.root), blockState.key.toNamespacedString());
29580
- const template = perBlock ? void 0 : await this.sectionModels.get(blockState, stored.properties);
29581
- if (template) {
29582
- this.sectionBlocks.set(index, { index, template, cullMask: 0 });
29583
- this.meshDirty = true;
29584
- } else {
29585
- object3 = await this.scene.addBlock(blockState, {
29586
- mergeMeshes: true,
29587
- instanceMeshes: true,
29588
- maxInstanceCount: 2e3,
29589
- initialState: stored.properties
29590
- });
29591
- object3.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
29592
- }
29593
- const info = { get block() {
29594
- return readBlock();
29595
- }, object: object3 };
29596
- this.renderedBlocks.set(index, info);
29597
- return info;
29598
- } catch (error) {
29599
- this.data.set(index, void 0);
29600
- object3?.removeFromScene();
29601
- throw error;
29836
+ await this.placeBlocks([{ index, block }]);
29837
+ return this.renderedBlocks.get(index);
29602
29838
  } finally {
29603
29839
  await onBlocksChanged?.([worldPos]);
29604
29840
  }
29605
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
+ }
29606
29910
  /** Changes visibility at world block coordinates while preserving the block's data. */
29607
29911
  async setBlockVisibleAt(pos, visible) {
29608
29912
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
@@ -29623,15 +29927,24 @@ var Chunk = class _Chunk {
29623
29927
  await this.onBlocksChanged?.([pos]);
29624
29928
  }
29625
29929
  isBlockVisibleAt(pos) {
29626
- 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) {
29627
29934
  return this.renderedBlocks.has(index) && !this.hiddenBlocks.has(index);
29628
29935
  }
29629
29936
  isOccludingAt(pos) {
29630
- 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) {
29631
29941
  return !this.hiddenBlocks.has(index) && (this.sectionBlocks.has(index) || (this.renderedBlocks.get(index)?.object?.isOccluding ?? false));
29632
29942
  }
29633
29943
  async setCullMaskAt(pos, mask) {
29634
- 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) {
29635
29948
  const entry = this.sectionBlocks.get(index);
29636
29949
  if (entry) {
29637
29950
  if (entry.cullMask !== mask) {
@@ -29642,12 +29955,17 @@ var Chunk = class _Chunk {
29642
29955
  await this.renderedBlocks.get(index)?.object?.setCullMask(mask);
29643
29956
  }
29644
29957
  }
29645
- rebuildSectionMesh() {
29958
+ /** Rebuilds visible section geometry and discards results superseded by another rebuild or clear. */
29959
+ async rebuildSectionMesh() {
29646
29960
  if (!this.meshDirty) return;
29647
- const next = this.sectionBlocks.size ? SectionMesh.build(
29648
- [...this.sectionBlocks.values()].filter((entry) => !this.hiddenBlocks.has(entry.index)),
29649
- this.sectionModels.maxAtlasSize
29650
- ) : 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
+ }
29651
29969
  this.sectionMesh?.dispose();
29652
29970
  this.sectionMesh = next;
29653
29971
  if (next) {
@@ -29655,7 +29973,6 @@ var Chunk = class _Chunk {
29655
29973
  this.scene.add(next);
29656
29974
  }
29657
29975
  this.scene.dirty = true;
29658
- this.meshDirty = false;
29659
29976
  }
29660
29977
  /** Removes stored blocks and their render objects, then notifies the owning world of changed positions. */
29661
29978
  async clear(onBlocksChanged = this.onBlocksChanged) {
@@ -29664,6 +29981,7 @@ var Chunk = class _Chunk {
29664
29981
  ));
29665
29982
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
29666
29983
  this.sectionBlocks.clear();
29984
+ this.meshGeneration++;
29667
29985
  this.sectionMesh?.dispose();
29668
29986
  this.sectionMesh = void 0;
29669
29987
  this.meshDirty = false;