minerender 2.1.0-alpha.137 → 2.1.0-alpha.138

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.
@@ -4068,7 +4068,7 @@ declare class Chunk<SectionMeshing extends boolean = false> {
4068
4068
  private ambientOcclusionCells?;
4069
4069
  private readonly blockChanges;
4070
4070
  private biomeChange?;
4071
- constructor(scene: MineRenderScene, x: number, y: number, z: number, onBlocksChanged?: ((positions: Vector3[]) => Promise<void>) | undefined, sectionModels?: SectionModels | undefined, neighborCell?: ((x: number, y: number, z: number) => number) | undefined, biomeAt?: ((x: number, y: number, z: number) => Maybe<string>) | undefined, onBiomesChanged?: ((biomes?: readonly string[]) => Promise<void>) | undefined, lightAt?: ((x: number, y: number, z: number) => number) | undefined, onLightCleared?: (() => Promise<void>) | undefined, ambientLight?: number, biomeBlend?: (() => BiomeBlend | undefined) | undefined);
4071
+ constructor(scene: MineRenderScene, x: number, y: number, z: number, onBlocksChanged?: ((positions: Vector3[], deferCulling?: boolean) => Promise<void>) | undefined, sectionModels?: SectionModels | undefined, neighborCell?: ((x: number, y: number, z: number) => number) | undefined, biomeAt?: ((x: number, y: number, z: number) => Maybe<string>) | undefined, onBiomesChanged?: ((biomes?: readonly string[]) => Promise<void>) | undefined, lightAt?: ((x: number, y: number, z: number) => number) | undefined, onLightCleared?: (() => Promise<void>) | undefined, ambientLight?: number, biomeBlend?: (() => BiomeBlend | undefined) | undefined);
4072
4072
  /** Finds the nearest owned block hit across sections, testing each shared mesh once. */
4073
4073
  static raycast<SectionMeshing extends boolean>(chunks: Iterable<Chunk<SectionMeshing>>, raycaster: Raycaster): WorldBlockIntersection | undefined;
4074
4074
  getBlockAt(x: number, y: number, z: number): Maybe<BlockInfo<SectionMeshing>>;
@@ -4080,11 +4080,14 @@ declare class Chunk<SectionMeshing extends boolean = false> {
4080
4080
  getBiomeAt(pos: TripleArray): Maybe<string>;
4081
4081
  /** Copies 64 biome IDs in x + z * 4 + y * 16 order and refreshes placed blocks. Omit the array to clear biome data. */
4082
4082
  setBiomes(biomes?: readonly string[]): Promise<void>;
4083
- /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
4083
+ /**
4084
+ * Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects.
4085
+ * The owning world can defer neighbor culling until all affected chunks are refreshed.
4086
+ */
4084
4087
  refreshBiomes(columns?: readonly {
4085
4088
  x: number;
4086
4089
  z: number;
4087
- }[]): Promise<void>;
4090
+ }[], deferCulling?: boolean): Promise<void>;
4088
4091
  private refreshBiomeBlocks;
4089
4092
  /**
4090
4093
  * Places a block at integer world block coordinates. Pass `undefined` to remove it.
@@ -4095,7 +4098,7 @@ declare class Chunk<SectionMeshing extends boolean = false> {
4095
4098
  /**
4096
4099
  * Places a block at integer chunk-local coordinates from 0 to 15.
4097
4100
  */
4098
- setBlockInChunkAt(pos: Vector3, block?: Block, worldPos?: Vector3, onBlocksChanged?: ((positions: Vector3[]) => Promise<void>) | undefined): Promise<Maybe<BlockInfo<SectionMeshing>>>;
4101
+ setBlockInChunkAt(pos: Vector3, block?: Block, worldPos?: Vector3, onBlocksChanged?: ((positions: Vector3[], deferCulling?: boolean) => Promise<void>) | undefined): Promise<Maybe<BlockInfo<SectionMeshing>>>;
4099
4102
  /** Places cells in input order and returns changed world positions without refreshing neighbors. */
4100
4103
  placeBlocks(blocks: readonly {
4101
4104
  index: number;
@@ -4260,6 +4263,14 @@ declare class MineRenderWorld<SectionMeshing extends boolean = false> {
4260
4263
  * Coordinates match AnvilChunk.x/z, not block positions. Missing columns are ignored.
4261
4264
  */
4262
4265
  unloadChunkColumn(x: number, z: number): Promise<void>;
4266
+ /**
4267
+ * Removes chunk columns together and refreshes surviving neighbors once.
4268
+ * Coordinates are absolute chunk columns. Duplicate and missing columns are ignored.
4269
+ */
4270
+ unloadChunkColumns(positions: readonly {
4271
+ x: number;
4272
+ z: number;
4273
+ }[]): Promise<void>;
4263
4274
  private clearChunkColumn;
4264
4275
  private markSectionChanged;
4265
4276
  /** Removes all chunks and releases their blocks, section meshes, and owned entities. */
@@ -4337,7 +4348,7 @@ declare class WorldStreamer {
4337
4348
  private running?;
4338
4349
  private disposal?;
4339
4350
  private disposed;
4340
- constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumn">, source: WorldChunkSource, options?: WorldStreamerOptions);
4351
+ constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumns">, source: WorldChunkSource, options?: WorldStreamerOptions);
4341
4352
  /** Returns absolute coordinates of successfully placed columns, including retained columns. */
4342
4353
  get loadedChunks(): readonly {
4343
4354
  x: number;
@@ -4371,7 +4382,7 @@ declare class WorldStreamer {
4371
4382
  dispose(): Promise<void>;
4372
4383
  private drain;
4373
4384
  private needsLoad;
4374
- private unload;
4385
+ private unloadMany;
4375
4386
  private validateChunk;
4376
4387
  private within;
4377
4388
  private key;
@@ -4068,7 +4068,7 @@ declare class Chunk<SectionMeshing extends boolean = false> {
4068
4068
  private ambientOcclusionCells?;
4069
4069
  private readonly blockChanges;
4070
4070
  private biomeChange?;
4071
- constructor(scene: MineRenderScene, x: number, y: number, z: number, onBlocksChanged?: ((positions: Vector3[]) => Promise<void>) | undefined, sectionModels?: SectionModels | undefined, neighborCell?: ((x: number, y: number, z: number) => number) | undefined, biomeAt?: ((x: number, y: number, z: number) => Maybe<string>) | undefined, onBiomesChanged?: ((biomes?: readonly string[]) => Promise<void>) | undefined, lightAt?: ((x: number, y: number, z: number) => number) | undefined, onLightCleared?: (() => Promise<void>) | undefined, ambientLight?: number, biomeBlend?: (() => BiomeBlend | undefined) | undefined);
4071
+ constructor(scene: MineRenderScene, x: number, y: number, z: number, onBlocksChanged?: ((positions: Vector3[], deferCulling?: boolean) => Promise<void>) | undefined, sectionModels?: SectionModels | undefined, neighborCell?: ((x: number, y: number, z: number) => number) | undefined, biomeAt?: ((x: number, y: number, z: number) => Maybe<string>) | undefined, onBiomesChanged?: ((biomes?: readonly string[]) => Promise<void>) | undefined, lightAt?: ((x: number, y: number, z: number) => number) | undefined, onLightCleared?: (() => Promise<void>) | undefined, ambientLight?: number, biomeBlend?: (() => BiomeBlend | undefined) | undefined);
4072
4072
  /** Finds the nearest owned block hit across sections, testing each shared mesh once. */
4073
4073
  static raycast<SectionMeshing extends boolean>(chunks: Iterable<Chunk<SectionMeshing>>, raycaster: Raycaster): WorldBlockIntersection | undefined;
4074
4074
  getBlockAt(x: number, y: number, z: number): Maybe<BlockInfo<SectionMeshing>>;
@@ -4080,11 +4080,14 @@ declare class Chunk<SectionMeshing extends boolean = false> {
4080
4080
  getBiomeAt(pos: TripleArray): Maybe<string>;
4081
4081
  /** Copies 64 biome IDs in x + z * 4 + y * 16 order and refreshes placed blocks. Omit the array to clear biome data. */
4082
4082
  setBiomes(biomes?: readonly string[]): Promise<void>;
4083
- /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
4083
+ /**
4084
+ * Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects.
4085
+ * The owning world can defer neighbor culling until all affected chunks are refreshed.
4086
+ */
4084
4087
  refreshBiomes(columns?: readonly {
4085
4088
  x: number;
4086
4089
  z: number;
4087
- }[]): Promise<void>;
4090
+ }[], deferCulling?: boolean): Promise<void>;
4088
4091
  private refreshBiomeBlocks;
4089
4092
  /**
4090
4093
  * Places a block at integer world block coordinates. Pass `undefined` to remove it.
@@ -4095,7 +4098,7 @@ declare class Chunk<SectionMeshing extends boolean = false> {
4095
4098
  /**
4096
4099
  * Places a block at integer chunk-local coordinates from 0 to 15.
4097
4100
  */
4098
- setBlockInChunkAt(pos: Vector3, block?: Block, worldPos?: Vector3, onBlocksChanged?: ((positions: Vector3[]) => Promise<void>) | undefined): Promise<Maybe<BlockInfo<SectionMeshing>>>;
4101
+ setBlockInChunkAt(pos: Vector3, block?: Block, worldPos?: Vector3, onBlocksChanged?: ((positions: Vector3[], deferCulling?: boolean) => Promise<void>) | undefined): Promise<Maybe<BlockInfo<SectionMeshing>>>;
4099
4102
  /** Places cells in input order and returns changed world positions without refreshing neighbors. */
4100
4103
  placeBlocks(blocks: readonly {
4101
4104
  index: number;
@@ -4260,6 +4263,14 @@ declare class MineRenderWorld<SectionMeshing extends boolean = false> {
4260
4263
  * Coordinates match AnvilChunk.x/z, not block positions. Missing columns are ignored.
4261
4264
  */
4262
4265
  unloadChunkColumn(x: number, z: number): Promise<void>;
4266
+ /**
4267
+ * Removes chunk columns together and refreshes surviving neighbors once.
4268
+ * Coordinates are absolute chunk columns. Duplicate and missing columns are ignored.
4269
+ */
4270
+ unloadChunkColumns(positions: readonly {
4271
+ x: number;
4272
+ z: number;
4273
+ }[]): Promise<void>;
4263
4274
  private clearChunkColumn;
4264
4275
  private markSectionChanged;
4265
4276
  /** Removes all chunks and releases their blocks, section meshes, and owned entities. */
@@ -4337,7 +4348,7 @@ declare class WorldStreamer {
4337
4348
  private running?;
4338
4349
  private disposal?;
4339
4350
  private disposed;
4340
- constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumn">, source: WorldChunkSource, options?: WorldStreamerOptions);
4351
+ constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumns">, source: WorldChunkSource, options?: WorldStreamerOptions);
4341
4352
  /** Returns absolute coordinates of successfully placed columns, including retained columns. */
4342
4353
  get loadedChunks(): readonly {
4343
4354
  x: number;
@@ -4371,7 +4382,7 @@ declare class WorldStreamer {
4371
4382
  dispose(): Promise<void>;
4372
4383
  private drain;
4373
4384
  private needsLoad;
4374
- private unload;
4385
+ private unloadMany;
4375
4386
  private validateChunk;
4376
4387
  private within;
4377
4388
  private key;
@@ -18711,8 +18711,14 @@ var MineRenderWorld = class {
18711
18711
  }
18712
18712
  return queued;
18713
18713
  }
18714
- async refreshBiomes() {
18715
- if (this.biomeRefreshing) await this.biomeRefreshing;
18714
+ async refreshBiomes(deferCulling = false) {
18715
+ if (this.biomeRefreshing) {
18716
+ await this.biomeRefreshing;
18717
+ if (!deferCulling) {
18718
+ if (this.pendingCulling.size) await this.updateCulling([]);
18719
+ else await this.culling;
18720
+ }
18721
+ }
18716
18722
  if (!this.pendingBiomes.size) return /* @__PURE__ */ new Map();
18717
18723
  return this.biomeRefreshing ??= Promise.resolve().then(async () => {
18718
18724
  const failures = /* @__PURE__ */ new Map();
@@ -18724,7 +18730,7 @@ var MineRenderWorld = class {
18724
18730
  this.pendingBiomes.delete(chunk);
18725
18731
  if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) !== chunk) continue;
18726
18732
  try {
18727
- await chunk.refreshBiomes(columns);
18733
+ await chunk.refreshBiomes(columns, deferCulling);
18728
18734
  } catch (error) {
18729
18735
  failures.set(chunk, error);
18730
18736
  if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) === chunk) this.pendingBiomes.set(chunk, void 0);
@@ -18986,15 +18992,35 @@ var MineRenderWorld = class {
18986
18992
  * Coordinates match AnvilChunk.x/z, not block positions. Missing columns are ignored.
18987
18993
  */
18988
18994
  async unloadChunkColumn(x, z) {
18989
- if (!Number.isInteger(x) || !Number.isInteger(z)) {
18990
- throw new RangeError("Chunk column coordinates must be integers");
18995
+ return this.unloadChunkColumns([{ x, z }]);
18996
+ }
18997
+ /**
18998
+ * Removes chunk columns together and refreshes surviving neighbors once.
18999
+ * Coordinates are absolute chunk columns. Duplicate and missing columns are ignored.
19000
+ */
19001
+ async unloadChunkColumns(positions) {
19002
+ const columns = /* @__PURE__ */ new Map();
19003
+ for (const position of positions) {
19004
+ const { x, z } = position ?? {};
19005
+ if (!Number.isInteger(x) || !Number.isInteger(z)) {
19006
+ throw new RangeError("Chunk column coordinates must be integers");
19007
+ }
19008
+ columns.set(`${x}_${z}`, { x, z });
18991
19009
  }
18992
- this.entities?.clearColumn(x, z);
19010
+ if (!columns.size) return;
18993
19011
  await this.culling;
19012
+ let sliceStart = performance.now();
18994
19013
  try {
18995
- await this.clearChunkColumn(x, z);
19014
+ for (const { x, z } of columns.values()) {
19015
+ this.entities?.clearColumn(x, z);
19016
+ await this.clearChunkColumn(x, z);
19017
+ if (performance.now() - sliceStart > 8) {
19018
+ await yieldToEventLoop();
19019
+ sliceStart = performance.now();
19020
+ }
19021
+ }
18996
19022
  } finally {
18997
- const failures = await this.refreshBiomes();
19023
+ const failures = await this.refreshBiomes(true);
18998
19024
  if (failures.size) log18.warn("Failed to refresh biome colors", failures.values().next().value);
18999
19025
  await this.updateCulling([]);
19000
19026
  }
@@ -19057,7 +19083,7 @@ var MineRenderWorld = class {
19057
19083
  }
19058
19084
  this.sectionModels?.clear();
19059
19085
  }
19060
- updateCulling(positions) {
19086
+ updateCulling(positions, defer = false) {
19061
19087
  const neighbor = new import_three45.Vector3();
19062
19088
  for (const pos of positions) {
19063
19089
  const cx = Math.floor(pos.x / 16), cy = Math.floor(pos.y / 16), cz = Math.floor(pos.z / 16);
@@ -19079,58 +19105,72 @@ var MineRenderWorld = class {
19079
19105
  }
19080
19106
  }
19081
19107
  }
19108
+ if (defer) return Promise.resolve();
19082
19109
  return this.culling ??= Promise.resolve().then(async () => {
19083
19110
  let sliceStart = performance.now();
19084
19111
  try {
19085
19112
  const culling = await loadBlockCulling();
19086
19113
  while (this.pendingCulling.size) {
19087
- const batch = [...this.pendingCulling];
19114
+ const batch = new Map(this.pendingCulling);
19088
19115
  this.pendingCulling.clear();
19089
- for (const [chunk, indices] of batch) {
19090
- const neighbors = CUBE_FACE_OFFSETS.map(([x, y, z]) => this._chunks.get(`${chunk.x + x}_${chunk.y + y}_${chunk.z + z}`));
19091
- const neighborAt = (x, y, z) => {
19092
- const lx = x - chunk.x * 16, ly = y - chunk.y * 16, lz = z - chunk.z * 16;
19093
- const section = neighbors[lx >= 16 ? 0 : lx < 0 ? 1 : ly >= 16 ? 2 : ly < 0 ? 3 : lz >= 16 ? 4 : 5];
19094
- return [section, (ly & 15) * 256 + (lz & 15) * 16 + (lx & 15)];
19095
- };
19096
- const masks = chunk.updateCullMasks(indices === "all" ? void 0 : indices, (x, y, z) => {
19097
- const [section, i] = neighborAt(x, y, z);
19098
- return section?.isOccludingIndex(i) ?? false;
19099
- }, culling, (x, y, z) => {
19100
- const [section, i] = neighborAt(x, y, z);
19101
- return section?.visibleBlockIndex(i);
19102
- });
19103
- const fluidIndices = indices === "all" ? function* () {
19104
- for (let index = 0; index < 4096; index++) yield index;
19105
- }() : indices;
19106
- for (const index of fluidIndices) {
19107
- if (!chunk.fluidByteIndex(index) || !chunk.isBlockVisibleIndex(index)) continue;
19108
- const lx = index % 16, ly = Math.floor(index / 256), lz = Math.floor(index / 16) % 16;
19109
- const wx = chunk.x * 16 + lx, wy = chunk.y * 16 + ly, wz = chunk.z * 16 + lz;
19110
- const block = chunk.getBlockAt(neighbor.set(wx, wy, wz));
19111
- if (block.object?.fluidKind) {
19112
- await block.object.updateFluid((x, y, z) => {
19113
- const nx = lx + x, ny = ly + y, nz = lz + z;
19114
- 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)}`);
19115
- const neighborIndex = (ny & 15) * 256 + (nz & 15) * 16 + (nx & 15);
19116
- const byte = section?.fluidByteIndex(neighborIndex) ?? 0;
19117
- return {
19118
- fluid: fluidKindOf(byte),
19119
- level: byte & 15,
19120
- solid: section?.isOccludingIndex(neighborIndex) ?? false
19121
- };
19122
- });
19116
+ try {
19117
+ for (const [chunk, indices] of batch) {
19118
+ const neighbors = CUBE_FACE_OFFSETS.map(([x, y, z]) => this._chunks.get(`${chunk.x + x}_${chunk.y + y}_${chunk.z + z}`));
19119
+ const neighborAt = (x, y, z) => {
19120
+ const lx = x - chunk.x * 16, ly = y - chunk.y * 16, lz = z - chunk.z * 16;
19121
+ const section = neighbors[lx >= 16 ? 0 : lx < 0 ? 1 : ly >= 16 ? 2 : ly < 0 ? 3 : lz >= 16 ? 4 : 5];
19122
+ return [section, (ly & 15) * 256 + (lz & 15) * 16 + (lx & 15)];
19123
+ };
19124
+ const masks = chunk.updateCullMasks(indices === "all" ? void 0 : indices, (x, y, z) => {
19125
+ const [section, i] = neighborAt(x, y, z);
19126
+ return section?.isOccludingIndex(i) ?? false;
19127
+ }, culling, (x, y, z) => {
19128
+ const [section, i] = neighborAt(x, y, z);
19129
+ return section?.visibleBlockIndex(i);
19130
+ });
19131
+ const fluidIndices = indices === "all" ? function* () {
19132
+ for (let index = 0; index < 4096; index++) yield index;
19133
+ }() : indices;
19134
+ for (const index of fluidIndices) {
19135
+ if (!chunk.fluidByteIndex(index) || !chunk.isBlockVisibleIndex(index)) continue;
19136
+ const lx = index % 16, ly = Math.floor(index / 256), lz = Math.floor(index / 16) % 16;
19137
+ const wx = chunk.x * 16 + lx, wy = chunk.y * 16 + ly, wz = chunk.z * 16 + lz;
19138
+ const block = chunk.getBlockAt(neighbor.set(wx, wy, wz));
19139
+ if (block.object?.fluidKind) {
19140
+ await block.object.updateFluid((x, y, z) => {
19141
+ const nx = lx + x, ny = ly + y, nz = lz + z;
19142
+ 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)}`);
19143
+ const neighborIndex = (ny & 15) * 256 + (nz & 15) * 16 + (nx & 15);
19144
+ const byte = section?.fluidByteIndex(neighborIndex) ?? 0;
19145
+ return {
19146
+ fluid: fluidKindOf(byte),
19147
+ level: byte & 15,
19148
+ solid: section?.isOccludingIndex(neighborIndex) ?? false
19149
+ };
19150
+ });
19151
+ }
19152
+ }
19153
+ for (const [index, mask] of masks) await chunk.setCullMaskIndex(index, mask);
19154
+ await chunk.refreshLight(indices === "all" ? void 0 : indices);
19155
+ chunk.refreshAmbientOcclusion(this._ambientOcclusion);
19156
+ if ((this._ambientOcclusion || this.ambientOcclusionPending) && performance.now() - sliceStart > 8) {
19157
+ await yieldToEventLoop();
19158
+ sliceStart = performance.now();
19123
19159
  }
19124
19160
  }
19125
- for (const [index, mask] of masks) await chunk.setCullMaskIndex(index, mask);
19126
- await chunk.refreshLight(indices === "all" ? void 0 : indices);
19127
- chunk.refreshAmbientOcclusion(this._ambientOcclusion);
19128
- if ((this._ambientOcclusion || this.ambientOcclusionPending) && performance.now() - sliceStart > 8) {
19129
- await yieldToEventLoop();
19130
- sliceStart = performance.now();
19161
+ const builds = await Promise.allSettled([...batch.keys()].map(async (chunk) => {
19162
+ await chunk.rebuildSectionMesh();
19163
+ batch.delete(chunk);
19164
+ }));
19165
+ const failure = builds.find((result) => result.status === "rejected");
19166
+ if (failure?.status === "rejected") throw failure.reason;
19167
+ } catch (error) {
19168
+ for (const [chunk, indices] of batch) {
19169
+ const pending = this.pendingCulling.get(chunk);
19170
+ this.pendingCulling.set(chunk, indices === "all" || pending === "all" ? "all" : /* @__PURE__ */ new Set([...indices, ...pending ?? []]));
19131
19171
  }
19172
+ throw error;
19132
19173
  }
19133
- await Promise.all(batch.map(([chunk]) => chunk.rebuildSectionMesh()));
19134
19174
  }
19135
19175
  } finally {
19136
19176
  this.culling = void 0;
@@ -19147,7 +19187,7 @@ var MineRenderWorld = class {
19147
19187
  x,
19148
19188
  y,
19149
19189
  z,
19150
- (positions) => this.updateCulling(positions),
19190
+ (positions, defer) => this.updateCulling(positions, defer),
19151
19191
  this.sectionModels,
19152
19192
  (x2, y2, z2) => {
19153
19193
  const cx = Math.floor(x2 / 16), cy = Math.floor(y2 / 16), cz = Math.floor(z2 / 16);
@@ -19882,11 +19922,14 @@ var Chunk = class _Chunk {
19882
19922
  this.biomes = copy;
19883
19923
  return this.refreshBiomes();
19884
19924
  }
19885
- /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
19886
- refreshBiomes(columns) {
19887
- return this.changeBlocks(() => this.refreshBiomeBlocks(columns), true);
19925
+ /**
19926
+ * Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects.
19927
+ * The owning world can defer neighbor culling until all affected chunks are refreshed.
19928
+ */
19929
+ refreshBiomes(columns, deferCulling = false) {
19930
+ return this.changeBlocks(() => this.refreshBiomeBlocks(columns, deferCulling), true);
19888
19931
  }
19889
- async refreshBiomeBlocks(columns) {
19932
+ async refreshBiomeBlocks(columns, deferCulling = false) {
19890
19933
  const blend = columns && this.biomeBlend?.();
19891
19934
  const ownColumn = columns?.some((column) => column.x === this.x && column.z === this.z);
19892
19935
  const affected = /* @__PURE__ */ new Map();
@@ -19943,7 +19986,7 @@ var Chunk = class _Chunk {
19943
19986
  this.renderedBlocks.get(index)?.object?.setVisible(false);
19944
19987
  }
19945
19988
  const positions = blocks.map(({ index }) => new import_three47.Vector3(...this.positionAt(index)));
19946
- if (this.onBlocksChanged) await this.onBlocksChanged(positions);
19989
+ if (this.onBlocksChanged) await this.onBlocksChanged(positions, deferCulling);
19947
19990
  else await this.rebuildSectionMesh();
19948
19991
  }
19949
19992
  }
@@ -20597,19 +20640,12 @@ var WorldStreamer = class {
20597
20640
  this.loading?.controller.abort();
20598
20641
  return this.disposal ??= Promise.resolve().then(async () => {
20599
20642
  await this.running?.catch(() => void 0);
20600
- let failure;
20601
- let failed = false;
20602
- for (const position of this.owned.values()) {
20603
- try {
20604
- await this.unload(position);
20605
- } catch (error) {
20606
- failed = true;
20607
- failure ??= error;
20608
- }
20643
+ try {
20644
+ await this.unloadMany([...this.owned.values()]);
20645
+ } finally {
20646
+ this.missing.clear();
20647
+ this.failed.clear();
20609
20648
  }
20610
- this.missing.clear();
20611
- this.failed.clear();
20612
- if (failed) throw failure;
20613
20649
  }).catch((error) => {
20614
20650
  this.disposal = void 0;
20615
20651
  throw error;
@@ -20624,9 +20660,9 @@ var WorldStreamer = class {
20624
20660
  for (const [key, position2] of this.failed) {
20625
20661
  if (!this.within(position2, this.unloadRadius)) this.failed.delete(key);
20626
20662
  }
20627
- const obsolete = [...this.owned.values()].find((position2) => !this.loaded.has(this.key(position2)) || !this.within(position2, this.unloadRadius));
20628
- if (obsolete) {
20629
- await this.unload(obsolete);
20663
+ const obsolete = [...this.owned.values()].filter((position2) => !this.loaded.has(this.key(position2)) || !this.within(position2, this.unloadRadius));
20664
+ if (obsolete.length) {
20665
+ await this.unloadMany(obsolete);
20630
20666
  continue;
20631
20667
  }
20632
20668
  const position = this.desired.find((position2) => this.needsLoad(position2));
@@ -20656,7 +20692,7 @@ var WorldStreamer = class {
20656
20692
  this.loaded.set(this.key(position), position);
20657
20693
  } catch (error) {
20658
20694
  try {
20659
- await this.unload(position);
20695
+ await this.unloadMany([position]);
20660
20696
  } catch (cleanupError) {
20661
20697
  const failure = error instanceof Error ? error : new Error(String(error));
20662
20698
  try {
@@ -20677,10 +20713,18 @@ var WorldStreamer = class {
20677
20713
  const key = this.key(position);
20678
20714
  return !this.loaded.has(key) && !this.missing.has(key) && !this.failed.has(key);
20679
20715
  }
20680
- async unload(position) {
20681
- await this.world.unloadChunkColumn(position.x, position.z);
20682
- this.owned.delete(this.key(position));
20683
- this.loaded.delete(this.key(position));
20716
+ async unloadMany(positions) {
20717
+ if (!positions.length) return;
20718
+ try {
20719
+ await this.world.unloadChunkColumns(positions.map(({ x, z }) => ({ x, z })));
20720
+ } catch (error) {
20721
+ for (const position of positions) this.loaded.delete(this.key(position));
20722
+ throw error;
20723
+ }
20724
+ for (const position of positions) {
20725
+ this.owned.delete(this.key(position));
20726
+ this.loaded.delete(this.key(position));
20727
+ }
20684
20728
  }
20685
20729
  validateChunk(chunk, position) {
20686
20730
  if (chunk.x !== position.x || chunk.z !== position.z) {