minerender 2.1.0-alpha.87 → 2.1.0-alpha.89

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,12 +18795,14 @@ __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,
18784
18802
  SkinTextures: () => SkinTextures,
18785
18803
  Skins: () => Skins,
18786
18804
  SpecialItems: () => SpecialItems,
18805
+ SpongeSchematicParser: () => SpongeSchematicParser,
18787
18806
  StructureParser: () => StructureParser,
18788
18807
  TextureAtlas: () => TextureAtlas,
18789
18808
  TextureLoader: () => TextureLoader,
@@ -18802,6 +18821,7 @@ __export(index_browser_exports, {
18802
18821
  axisToVec3: () => axisToVec3,
18803
18822
  base64decode: () => base64decode,
18804
18823
  base64encode: () => base64encode,
18824
+ buildSectionGeometry: () => buildSectionGeometry,
18805
18825
  capeTextureSizes: () => capeTextureSizes,
18806
18826
  captureException: () => captureException,
18807
18827
  changeEvent: () => changeEvent,
@@ -18854,6 +18874,7 @@ __export(index_browser_exports, {
18854
18874
  sampleEntityAnimation: () => sampleEntityAnimation,
18855
18875
  sampleEntityKeyframes: () => sampleEntityKeyframes,
18856
18876
  sampleFluid: () => sampleFluid,
18877
+ sectionGeometryTransferables: () => sectionGeometryTransferables,
18857
18878
  serializeBoxGeometryKey: () => serializeBoxGeometryKey,
18858
18879
  serializeImageKey: () => serializeImageKey,
18859
18880
  serializeMaterialKey: () => serializeMaterialKey,
@@ -18871,7 +18892,8 @@ __export(index_browser_exports, {
18871
18892
  toDegrees: () => toDegrees,
18872
18893
  toRadians: () => toRadians,
18873
18894
  toTrianglesDrawMode: () => toTrianglesDrawMode,
18874
- trimCanvas: () => trimCanvas
18895
+ trimCanvas: () => trimCanvas,
18896
+ yieldToEventLoop: () => yieldToEventLoop
18875
18897
  });
18876
18898
  module.exports = __toCommonJS(index_browser_exports);
18877
18899
 
@@ -18956,6 +18978,7 @@ function probeImageSize(data) {
18956
18978
  }
18957
18979
 
18958
18980
  // src/env/browser/BrowserEnv.ts
18981
+ var import_meta = {};
18959
18982
  var BrowserEnv = class {
18960
18983
  constructor() {
18961
18984
  this.name = "browser";
@@ -18983,6 +19006,14 @@ var BrowserEnv = class {
18983
19006
  version: version2
18984
19007
  }));
18985
19008
  }
19009
+ createWorker(name) {
19010
+ if (name !== "section" || typeof Worker === "undefined") return void 0;
19011
+ try {
19012
+ return new Worker(new URL("./section.worker.mjs", import_meta.url), { type: "module" });
19013
+ } catch {
19014
+ return void 0;
19015
+ }
19016
+ }
18986
19017
  };
18987
19018
  function registerBrowserEnv() {
18988
19019
  const env = new BrowserEnv();
@@ -23311,46 +23342,46 @@ function matchesCondition(condition, state) {
23311
23342
  }
23312
23343
  var BlockStateResolver = class {
23313
23344
  /** Loads vanilla property defaults, falling back to preview values inferred from the blockstate file. */
23314
- static async defaults(blockState) {
23315
- const defaults = blockState.key ? await BlockStates.getDefaultState(blockState.key) : void 0;
23345
+ static async defaults(blockState2) {
23346
+ const defaults = blockState2.key ? await BlockStates.getDefaultState(blockState2.key) : void 0;
23316
23347
  const state = {};
23317
23348
  if (defaults && Object.keys(defaults).length > 0) {
23318
23349
  for (const key in defaults) state[key] = defaults[key].default;
23319
- } else if (blockState.variants) {
23320
- const variant = Object.keys(blockState.variants)[0];
23350
+ } else if (blockState2.variants) {
23351
+ const variant = Object.keys(blockState2.variants)[0];
23321
23352
  if (variant) {
23322
23353
  for (const property of variant.split(",")) {
23323
23354
  const [key, value] = property.split("=");
23324
23355
  state[key] = value;
23325
23356
  }
23326
23357
  }
23327
- } else if (blockState.multipart) {
23328
- const condition = blockState.multipart.map((part) => part.when).find((when) => when !== void 0 && Object.values(when).every((value) => typeof value === "string"));
23358
+ } else if (blockState2.multipart) {
23359
+ const condition = blockState2.multipart.map((part) => part.when).find((when) => when !== void 0 && Object.values(when).every((value) => typeof value === "string"));
23329
23360
  for (const [key, value] of Object.entries(condition ?? {})) state[key] = value.split("|")[0];
23330
23361
  }
23331
23362
  return state;
23332
23363
  }
23333
23364
  /** Selects matching models. Supply `choose` to control selection within each alternatives array. */
23334
- static select(blockState, state, choose = this.choose) {
23335
- return this.matching(blockState, state).map(choose);
23365
+ static select(blockState2, state, choose = this.choose) {
23366
+ return this.matching(blockState2, state).map(choose);
23336
23367
  }
23337
23368
  /** Returns matching model groups before choosing weighted alternatives. */
23338
- static matching(blockState, state) {
23369
+ static matching(blockState2, state) {
23339
23370
  const out = [];
23340
- if (blockState.variants) {
23341
- if (Object.keys(blockState.variants).length === 1 && "" in blockState.variants) {
23342
- out.push(blockState.variants[""]);
23371
+ if (blockState2.variants) {
23372
+ if (Object.keys(blockState2.variants).length === 1 && "" in blockState2.variants) {
23373
+ out.push(blockState2.variants[""]);
23343
23374
  } else {
23344
- for (const variantKey in blockState.variants) {
23375
+ for (const variantKey in blockState2.variants) {
23345
23376
  const matches = variantKey.split(",").every((property) => {
23346
23377
  const [key, value] = property.split("=");
23347
23378
  return `${state[key]}` === `${value}`;
23348
23379
  });
23349
- if (matches) out.push(blockState.variants[variantKey]);
23380
+ if (matches) out.push(blockState2.variants[variantKey]);
23350
23381
  }
23351
23382
  }
23352
- } else if (blockState.multipart) {
23353
- for (const part of blockState.multipart) {
23383
+ } else if (blockState2.multipart) {
23384
+ for (const part of blockState2.multipart) {
23354
23385
  if (part.apply && (!part.when || matchesCondition(part.when, state))) {
23355
23386
  out.push(part.apply);
23356
23387
  }
@@ -23478,9 +23509,9 @@ init_ModelCulling();
23478
23509
  init_FluidGeometry();
23479
23510
  init_Entities();
23480
23511
  var _BlockObject = class _BlockObject extends SceneObject {
23481
- constructor(blockState, options) {
23512
+ constructor(blockState2, options) {
23482
23513
  super(options);
23483
- this.blockState = blockState;
23514
+ this.blockState = blockState2;
23484
23515
  this.isBlockObject = true;
23485
23516
  this._previousState = {};
23486
23517
  this._state = {};
@@ -25659,6 +25690,157 @@ function compoundList(tags, name) {
25659
25690
  throw new MineRenderError(`Schematic ${name} must be a compound list`);
25660
25691
  }
25661
25692
 
25693
+ // src/model/multiblock/SpongeSchematicParser.ts
25694
+ init_MineRenderError();
25695
+ var SpongeSchematicParser = class {
25696
+ /**
25697
+ * Parses decoded NBT with a local block palette, omitting air blocks.
25698
+ * Applies Offset to block and entity positions; size remains the source dimensions.
25699
+ * Biomes and metadata are not imported, and DataVersion does not trigger migration.
25700
+ */
25701
+ static async parse(nbt) {
25702
+ const tags = nbt.value.Schematic === void 0 ? nbt.value : compound(nbt.value.Schematic, "Schematic");
25703
+ const version2 = integer2(tags.Version, "Version");
25704
+ if (version2 !== 2 && version2 !== 3) {
25705
+ throw new MineRenderError(`Unsupported Sponge schematic version ${version2}; expected 2 or 3`);
25706
+ }
25707
+ const dataVersion = integer2(tags.DataVersion, "DataVersion");
25708
+ const size = ["Width", "Height", "Length"].map((name) => {
25709
+ const tag = tags[name];
25710
+ if (tag?.type !== "short" || !Number.isInteger(tag.value) || tag.value < -32768 || tag.value > 65535 || tag.value === 0) {
25711
+ throw new MineRenderError(`Sponge schematic ${name} must be a positive unsigned short`);
25712
+ }
25713
+ return tag.value & 65535;
25714
+ });
25715
+ const offset = tags.Offset === void 0 ? [0, 0, 0] : intPosition(tags.Offset, "Offset");
25716
+ const blocks = [];
25717
+ const container = version2 === 2 ? tags : tags.Blocks === void 0 ? void 0 : compound(tags.Blocks, "Blocks");
25718
+ if (container) {
25719
+ const paletteTag = container.Palette;
25720
+ if (!paletteTag) throw new MineRenderError("Sponge schematic requires a local Palette; global block IDs are not supported");
25721
+ const palette = /* @__PURE__ */ new Map();
25722
+ const paletteMax = version2 === 2 && tags.PaletteMax !== void 0 ? integer2(tags.PaletteMax, "PaletteMax") : void 0;
25723
+ if (paletteMax !== void 0 && paletteMax <= 0) throw new MineRenderError("Sponge schematic PaletteMax must be positive");
25724
+ for (const [state, tag] of Object.entries(compound(paletteTag, "Palette"))) {
25725
+ const id = integer2(tag, "Palette index");
25726
+ if (id < 0 || palette.has(id) || paletteMax !== void 0 && id >= paletteMax) {
25727
+ throw new MineRenderError(`Invalid or duplicate Sponge schematic palette index ${id}`);
25728
+ }
25729
+ palette.set(id, blockState(state));
25730
+ }
25731
+ const [width, height, length] = size;
25732
+ const volume = width * height * length;
25733
+ const data = container[version2 === 2 ? "BlockData" : "Data"];
25734
+ if (data?.type !== "byteArray" || !Array.isArray(data.value) || data.value.length < volume) {
25735
+ throw new MineRenderError(`Sponge schematic block data must contain ${volume} varints`);
25736
+ }
25737
+ const blockEntities = /* @__PURE__ */ new Map();
25738
+ for (const entity of compoundList2(container, "BlockEntities")) {
25739
+ const pos = intPosition(entity.Pos, "BlockEntities Pos");
25740
+ if (pos.some((value2, axis) => value2 < 0 || value2 >= size[axis])) {
25741
+ throw new MineRenderError("Sponge schematic block entity position is outside its dimensions");
25742
+ }
25743
+ const index = pos[0] + pos[2] * width + pos[1] * width * length;
25744
+ if (blockEntities.has(index)) throw new MineRenderError(`Duplicate Sponge schematic block entity at index ${index}`);
25745
+ const value = entityData(entity, version2);
25746
+ for (const [axis, name] of ["x", "y", "z"].entries()) {
25747
+ value[name] = { type: "int", value: pos[axis] + offset[axis] };
25748
+ }
25749
+ blockEntities.set(index, { type: "compound", value });
25750
+ }
25751
+ let cursor = 0;
25752
+ for (let index = 0; index < volume; index++) {
25753
+ let id = 0;
25754
+ for (let shift = 0; ; shift += 7) {
25755
+ const byte = data.value[cursor++];
25756
+ if (!Number.isInteger(byte) || byte < -128 || byte > 255) {
25757
+ throw new MineRenderError(`Invalid or truncated Sponge schematic varint at index ${index}`);
25758
+ }
25759
+ const unsigned = byte & 255;
25760
+ if (shift === 28 && unsigned > 7) {
25761
+ throw new MineRenderError(`Sponge schematic varint exceeds a nonnegative int at index ${index}`);
25762
+ }
25763
+ id += (unsigned & 127) * 2 ** shift;
25764
+ if ((unsigned & 128) === 0) break;
25765
+ }
25766
+ const state = palette.get(id);
25767
+ if (!state) throw new MineRenderError(`Sponge schematic palette index ${id} does not exist`);
25768
+ if (["minecraft:air", "minecraft:cave_air", "minecraft:void_air"].includes(state.type)) continue;
25769
+ blocks.push({
25770
+ type: state.type,
25771
+ properties: { ...state.properties },
25772
+ position: [
25773
+ index % width + offset[0],
25774
+ Math.floor(index / (width * length)) + offset[1],
25775
+ Math.floor(index / width) % length + offset[2]
25776
+ ],
25777
+ nbt: blockEntities.get(index)
25778
+ });
25779
+ }
25780
+ if (cursor !== data.value.length) throw new MineRenderError("Sponge schematic block data has trailing bytes");
25781
+ }
25782
+ const entities = compoundList2(tags, "Entities").map((entity) => {
25783
+ const pos = entity.Pos;
25784
+ if (pos?.type !== "list" || pos.value.type !== "double" || pos.value.value.length !== 3 || !pos.value.value.every(Number.isFinite)) {
25785
+ throw new MineRenderError("Sponge schematic entity Pos must contain three doubles");
25786
+ }
25787
+ const position = pos.value.value.map((value2, axis) => value2 + offset[axis]);
25788
+ const value = entityData(entity, version2);
25789
+ value.Pos = { type: "list", value: { type: "double", value: [...position] } };
25790
+ return { position, nbt: { type: "compound", value } };
25791
+ });
25792
+ return { size, blocks, entities, dataVersion };
25793
+ }
25794
+ };
25795
+ function integer2(tag, name) {
25796
+ if (tag?.type !== "int" || !Number.isInteger(tag.value) || tag.value < -2147483648 || tag.value > 2147483647) {
25797
+ throw new MineRenderError(`Sponge schematic ${name} must be an int`);
25798
+ }
25799
+ return tag.value;
25800
+ }
25801
+ function compound(tag, name) {
25802
+ if (tag?.type !== "compound" || !tag.value || typeof tag.value !== "object" || Array.isArray(tag.value)) {
25803
+ throw new MineRenderError(`Sponge schematic ${name} must be a compound`);
25804
+ }
25805
+ return tag.value;
25806
+ }
25807
+ function intPosition(tag, name) {
25808
+ if (tag?.type !== "intArray" || !Array.isArray(tag.value) || tag.value.length !== 3 || !tag.value.every((value) => Number.isInteger(value) && value >= -2147483648 && value <= 2147483647)) {
25809
+ throw new MineRenderError(`Sponge schematic ${name} must contain three ints`);
25810
+ }
25811
+ return [...tag.value];
25812
+ }
25813
+ function compoundList2(tags, name) {
25814
+ const tag = tags[name];
25815
+ if (tag === void 0) return [];
25816
+ if (tag.type === "list" && Array.isArray(tag.value.value)) {
25817
+ if (tag.value.value.length === 0) return [];
25818
+ if (tag.value.type === "compound") return tag.value.value;
25819
+ }
25820
+ throw new MineRenderError(`Sponge schematic ${name} must be a compound list`);
25821
+ }
25822
+ function blockState(state) {
25823
+ const match = /^((?:[a-z0-9_.-]+:)?[a-z0-9_./-]+)(?:\[([^\[\]]*)\])?$/.exec(state);
25824
+ if (!match) throw new MineRenderError(`Invalid Sponge schematic block state ${state}`);
25825
+ const properties = {};
25826
+ if (match[2]) {
25827
+ for (const entry of match[2].split(",")) {
25828
+ const pair = /^([a-z0-9_]+)=([^\s=,\[\]]+)$/.exec(entry);
25829
+ if (!pair || Object.prototype.hasOwnProperty.call(properties, pair[1])) throw new MineRenderError(`Invalid Sponge schematic block state ${state}`);
25830
+ Object.defineProperty(properties, pair[1], { value: pair[2], enumerable: true, writable: true, configurable: true });
25831
+ }
25832
+ }
25833
+ return { type: match[1].includes(":") ? match[1] : `minecraft:${match[1]}`, properties };
25834
+ }
25835
+ function entityData(entity, version2) {
25836
+ const id = entity.Id;
25837
+ if (id?.type !== "string" || !id.value) throw new MineRenderError("Sponge schematic entity Id must be a nonempty string");
25838
+ const { Id, Pos, ...extra } = entity;
25839
+ const value = version2 === 2 ? extra : entity.Data === void 0 ? {} : { ...compound(entity.Data, "entity Data") };
25840
+ value.id = { type: "string", value: id.value.includes(":") ? id.value : `minecraft:${id.value}` };
25841
+ return value;
25842
+ }
25843
+
25662
25844
  // src/model/multiblock/StructureParser.ts
25663
25845
  init_MineRenderError();
25664
25846
  var StructureParser = class {
@@ -26883,8 +27065,8 @@ var _MineRenderScene = class _MineRenderScene extends import_three34.Scene {
26883
27065
  * Set `options.initialState` to choose block properties such as `facing` or `axis`.
26884
27066
  * Use the returned object's `setPosition` method to move all of its model parts.
26885
27067
  */
26886
- async addBlock(blockState, options, parent = this) {
26887
- return this.addSceneObject(blockState, () => new BlockObject(blockState, options), options, parent);
27068
+ async addBlock(blockState2, options, parent = this) {
27069
+ return this.addSceneObject(blockState2, () => new BlockObject(blockState2, options), options, parent);
26888
27070
  }
26889
27071
  /**
26890
27072
  * Loads a skin and adds an initialized player model.
@@ -28636,6 +28818,16 @@ init_NBTHelper();
28636
28818
 
28637
28819
  // src/world/ChunkData.ts
28638
28820
  init_AssetKey();
28821
+ var blockKeys = /* @__PURE__ */ new Map();
28822
+ function blockKey(type) {
28823
+ let key = blockKeys.get(type);
28824
+ if (!key) {
28825
+ key = AssetKey.parse("blockstates", type);
28826
+ if (blockKeys.size >= 4096) blockKeys.clear();
28827
+ blockKeys.set(type, key);
28828
+ }
28829
+ return key;
28830
+ }
28639
28831
  var ChunkData = class _ChunkData {
28640
28832
  constructor() {
28641
28833
  this.indices = new Uint16Array(4096);
@@ -28663,10 +28855,10 @@ var ChunkData = class _ChunkData {
28663
28855
  let key;
28664
28856
  let nbt;
28665
28857
  if (block && !_ChunkData.isAir(block)) {
28666
- const type = AssetKey.parse("blockstates", block.type).toNamespacedString();
28858
+ const type = blockKey(block.type).toNamespacedString();
28667
28859
  const properties = Object.fromEntries(Object.keys(block.properties ?? {}).sort().map((name) => [name, block.properties[name]]));
28668
28860
  state = Object.freeze(Object.keys(properties).length ? { type, properties: Object.freeze(properties) } : { type });
28669
- key = JSON.stringify([type, properties]);
28861
+ key = _ChunkData.paletteKey(block);
28670
28862
  if (block.nbt !== void 0) nbt = structuredClone(block.nbt);
28671
28863
  }
28672
28864
  const previousId = this.indices[index];
@@ -28703,9 +28895,14 @@ var ChunkData = class _ChunkData {
28703
28895
  }
28704
28896
  static isAir(block) {
28705
28897
  if (typeof block === "undefined" || typeof block.type === "undefined") return true;
28706
- const key = AssetKey.parse("blockstates", block.type);
28898
+ const key = blockKey(block.type);
28707
28899
  return key.namespace === "minecraft" && !key.type && ["air", "cave_air", "void_air"].includes(key.path);
28708
28900
  }
28901
+ /** Identifies a normalized block type and its sorted properties, excluding per-cell NBT. */
28902
+ static paletteKey(block) {
28903
+ const properties = Object.fromEntries(Object.keys(block.properties ?? {}).sort().map((name) => [name, block.properties[name]]));
28904
+ return JSON.stringify([blockKey(block.type).toNamespacedString(), properties]);
28905
+ }
28709
28906
  static copyBlock(state, nbt) {
28710
28907
  const block = { type: state.type };
28711
28908
  if (state.properties) block.properties = { ...state.properties };
@@ -28885,14 +29082,17 @@ var AnvilParser = class {
28885
29082
  const perLong = Math.floor(64 / bits);
28886
29083
  const expected = padded ? Math.ceil(4096 / perLong) : Math.ceil(4096 * bits / 64);
28887
29084
  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;
29085
+ const words = tag.value.map(([high, low]) => [high >>> 0, low >>> 0]);
29086
+ const mask = (1 << bits) - 1;
28890
29087
  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);
29088
+ const bitPos = index * bits;
29089
+ const word = padded ? Math.floor(index / perLong) : bitPos >>> 6;
29090
+ const shift = padded ? index % perLong * bits : bitPos & 63;
29091
+ const [high, low] = words[word];
29092
+ let value = shift < 32 ? low >>> shift : high >>> shift - 32;
29093
+ if (shift < 32 && shift + bits > 32) value += high * 2 ** (32 - shift);
29094
+ if (!padded && shift + bits > 64) value += words[word + 1][1] * 2 ** (64 - shift);
29095
+ indices[index] = value & mask;
28896
29096
  }
28897
29097
  return indices;
28898
29098
  }
@@ -29034,7 +29234,7 @@ init_AssetKey();
29034
29234
  // src/world/MineRenderWorld.ts
29035
29235
  var import_three40 = require("three");
29036
29236
  init_Model();
29037
- init_BatchedExecutor();
29237
+ init_util();
29038
29238
  init_AssetKey();
29039
29239
  init_BlockStates();
29040
29240
  init_CubeFace();
@@ -29071,18 +29271,18 @@ var SectionModels = class {
29071
29271
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("maxAtlasSize must be a positive integer");
29072
29272
  }
29073
29273
  /** Returns a cube template, or `undefined` when the block requires an individual render object. */
29074
- async get(blockState, properties = {}) {
29075
- if (!blockState.variants || blockState.multipart) return void 0;
29076
- let states = this.states.get(blockState);
29077
- if (!states) this.states.set(blockState, states = /* @__PURE__ */ new Map());
29274
+ async get(blockState2, properties = {}) {
29275
+ if (!blockState2.variants || blockState2.multipart) return void 0;
29276
+ let states = this.states.get(blockState2);
29277
+ if (!states) this.states.set(blockState2, states = /* @__PURE__ */ new Map());
29078
29278
  const key = JSON.stringify(Object.entries(properties).sort(([a], [b]) => a.localeCompare(b)));
29079
- const prepared = await cached(states, key, () => this.prepare(blockState, properties));
29279
+ const prepared = await cached(states, key, () => this.prepare(blockState2, properties));
29080
29280
  if (!prepared) return void 0;
29081
29281
  return prepared.templates.get(BlockStateResolver.choose(prepared.variants));
29082
29282
  }
29083
- async prepare(blockState, properties) {
29084
- const state = { ...await BlockStateResolver.defaults(blockState), ...properties };
29085
- const groups = BlockStateResolver.matching(blockState, state);
29283
+ async prepare(blockState2, properties) {
29284
+ const state = { ...await BlockStateResolver.defaults(blockState2), ...properties };
29285
+ const groups = BlockStateResolver.matching(blockState2, state);
29086
29286
  if (groups.length !== 1) return void 0;
29087
29287
  const variants = groups[0];
29088
29288
  const choices = Array.isArray(variants) ? variants : [variants];
@@ -29094,7 +29294,7 @@ var SectionModels = class {
29094
29294
  const atlas = await UVMapper.getAtlas(model);
29095
29295
  const rotation = BlockStateResolver.rotation(variant);
29096
29296
  if (!atlas || atlas.hasAnimation || !ModelCulling.isOpaqueFullCube(atlas) || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize || ModelCulling.toLocalMask(63, rotation) !== 63) return void 0;
29097
- const tints = await BlockTints.get(blockState.key, state, model);
29297
+ const tints = await BlockTints.get(blockState2.key, state, model);
29098
29298
  let models = this.models.get(model);
29099
29299
  if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
29100
29300
  const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
@@ -29179,7 +29379,7 @@ var MineRenderWorld = class {
29179
29379
  * Places a structure's blocks at their stored positions, then refreshes neighboring faces.
29180
29380
  * Entity NBT is retained by the structure but is not rendered.
29181
29381
  *
29182
- * @param useBatches - Uses bounded batches by default. Set to `false` for sequential placement.
29382
+ * @param useBatches - Yields between chunks by default. Set to `false` for sequential placement.
29183
29383
  * @param executor - Optional queue for batched placement. The caller remains responsible for stopping it.
29184
29384
  */
29185
29385
  async placeMultiBlock(multiblock, useBatches = true, executor) {
@@ -29196,8 +29396,10 @@ var MineRenderWorld = class {
29196
29396
  };
29197
29397
  const place = (block) => this.placeBlock(new import_three40.Vector3(...block.position), block, collect);
29198
29398
  const keys = /* @__PURE__ */ new Map();
29399
+ const types = /* @__PURE__ */ new Set();
29199
29400
  for (const block of multiblock.blocks) {
29200
- if (Chunk.isAir(block)) continue;
29401
+ if (Chunk.isAir(block) || types.has(block.type)) continue;
29402
+ types.add(block.type);
29201
29403
  const key = AssetKey.parse("blockstates", block.type);
29202
29404
  keys.set(key.serialize(), key);
29203
29405
  }
@@ -29206,6 +29408,23 @@ var MineRenderWorld = class {
29206
29408
  for (const block of multiblock.blocks) await place(block);
29207
29409
  return;
29208
29410
  }
29411
+ if (!executor) {
29412
+ const groups2 = /* @__PURE__ */ new Map();
29413
+ const position = new import_three40.Vector3();
29414
+ for (const block of multiblock.blocks) {
29415
+ const [x, y, z] = block.position;
29416
+ this.validatePosBounds(position.set(x, y, z));
29417
+ const cx = Math.floor(x / 16), cy = Math.floor(y / 16), cz = Math.floor(z / 16);
29418
+ const key = `${cx}_${cy}_${cz}`;
29419
+ const chunk = this._chunks.get(key) ?? (Chunk.isAir(block) ? void 0 : this.getOrCreateChunkAt(position));
29420
+ if (!chunk) continue;
29421
+ let group = groups2.get(chunk);
29422
+ if (!group) groups2.set(chunk, group = []);
29423
+ group.push({ index: (y - cy * 16) * 256 + (z - cz * 16) * 16 + x - cx * 16, block });
29424
+ }
29425
+ await this.placeChunkGroups(groups2, changes);
29426
+ return;
29427
+ }
29209
29428
  const positions = /* @__PURE__ */ new Map();
29210
29429
  for (const block of multiblock.blocks) {
29211
29430
  const key = block.position.join(",");
@@ -29214,19 +29433,34 @@ var MineRenderWorld = class {
29214
29433
  else positions.set(key, [block]);
29215
29434
  }
29216
29435
  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;
29436
+ for (let i = 0; i < groups.length; i += executor.batch) {
29437
+ const results = await Promise.allSettled(groups.slice(i, i + executor.batch).map(
29438
+ (blocks) => executor.submit(async () => {
29439
+ for (const block of blocks) await place(block);
29440
+ })
29441
+ ));
29442
+ const failure = results.find((result) => result.status === "rejected");
29443
+ if (failure?.status === "rejected") throw failure.reason;
29444
+ }
29445
+ }
29446
+ async placeChunkGroups(groups, changes) {
29447
+ let sliceStart = performance.now();
29448
+ for (const [chunk, blocks] of groups) {
29449
+ let positions;
29450
+ try {
29451
+ positions = await chunk.placeBlocks(blocks);
29452
+ } finally {
29453
+ positions ?? (positions = blocks.map(({ index }) => new import_three40.Vector3(
29454
+ chunk.x * 16 + index % 16,
29455
+ chunk.y * 16 + Math.floor(index / 256),
29456
+ chunk.z * 16 + Math.floor(index / 16) % 16
29457
+ )));
29458
+ for (const pos of positions) changes.set(`${pos.x},${pos.y},${pos.z}`, pos);
29459
+ }
29460
+ if (performance.now() - sliceStart > 8) {
29461
+ await yieldToEventLoop();
29462
+ sliceStart = performance.now();
29227
29463
  }
29228
- } finally {
29229
- if (!executor) queue.stop();
29230
29464
  }
29231
29465
  }
29232
29466
  /** Replaces one chunk column's blocks. Entity NBT remains available on the parsed chunk. */
@@ -29234,7 +29468,24 @@ var MineRenderWorld = class {
29234
29468
  const changes = /* @__PURE__ */ new Map();
29235
29469
  try {
29236
29470
  await this.clearChunkColumn(chunk.x, chunk.z, changes);
29471
+ const groups = /* @__PURE__ */ new Map();
29237
29472
  for (const section of chunk.sections) {
29473
+ if (!executor) {
29474
+ const blocks2 = [];
29475
+ for (let index = 0; index < 4096; index++) {
29476
+ const block = section.data.get(index);
29477
+ if (block) blocks2.push({ index, block });
29478
+ }
29479
+ if (blocks2.length) {
29480
+ const position = new import_three40.Vector3(chunk.x * 16, section.y * 16, chunk.z * 16);
29481
+ this.validatePosBounds(position);
29482
+ const target = this.getOrCreateChunkAt(position);
29483
+ const previous = groups.get(target);
29484
+ if (previous) previous.push(...blocks2);
29485
+ else groups.set(target, blocks2);
29486
+ }
29487
+ continue;
29488
+ }
29238
29489
  const blocks = [];
29239
29490
  for (let index = 0; index < 4096; index++) {
29240
29491
  const block = section.data.get(index);
@@ -29249,6 +29500,7 @@ var MineRenderWorld = class {
29249
29500
  }
29250
29501
  await this.placeBlocks({ size: [16, 16, 16], blocks }, true, executor, changes);
29251
29502
  }
29503
+ if (!executor) await this.placeChunkGroups(groups, changes);
29252
29504
  } finally {
29253
29505
  await this.updateCulling([...changes.values()]);
29254
29506
  }
@@ -29289,19 +29541,23 @@ var MineRenderWorld = class {
29289
29541
  this.sectionModels?.clear();
29290
29542
  }
29291
29543
  updateCulling(positions) {
29544
+ const neighbor = new import_three40.Vector3();
29292
29545
  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
- }
29546
+ const cx = Math.floor(pos.x / 16), cy = Math.floor(pos.y / 16), cz = Math.floor(pos.z / 16);
29547
+ const chunk = this._chunks.get(`${cx}_${cy}_${cz}`);
29548
+ const lx = pos.x - cx * 16, ly = pos.y - cy * 16, lz = pos.z - cz * 16;
29298
29549
  for (let y = -1; y <= 1; y++) {
29299
29550
  for (let z = -1; z <= 1; z++) {
29300
29551
  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
- }
29552
+ const nx = lx + x, ny = ly + y, nz = lz + z;
29553
+ 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)}`);
29554
+ if (!section) continue;
29555
+ const index = (ny & 15) * 256 + (nz & 15) * 16 + (nx & 15);
29556
+ let pending = this.pendingCulling.get(section);
29557
+ if (pending?.has(index)) continue;
29558
+ 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;
29559
+ if (!pending) this.pendingCulling.set(section, pending = /* @__PURE__ */ new Set());
29560
+ pending.add(index);
29305
29561
  }
29306
29562
  }
29307
29563
  }
@@ -29309,39 +29565,38 @@ var MineRenderWorld = class {
29309
29565
  return this.culling ?? (this.culling = Promise.resolve().then(async () => {
29310
29566
  try {
29311
29567
  while (this.pendingCulling.size) {
29312
- const batch = [...this.pendingCulling.values()];
29568
+ const batch = [...this.pendingCulling];
29313
29569
  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;
29570
+ for (const [chunk, indices] of batch) {
29571
+ for (const index of indices) {
29572
+ if (!chunk.isBlockVisibleIndex(index)) continue;
29573
+ const lx = index % 16, ly = Math.floor(index / 256), lz = Math.floor(index / 16) % 16;
29574
+ const wx = chunk.x * 16 + lx, wy = chunk.y * 16 + ly, wz = chunk.z * 16 + lz;
29575
+ const block = chunk.getBlockAt(neighbor.set(wx, wy, wz));
29576
+ if (block.object?.fluidKind) {
29577
+ await block.object.updateFluid((x, y, z) => {
29578
+ const nx = lx + x, ny = ly + y, nz = lz + z;
29579
+ 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)}`);
29580
+ const neighborIndex = (ny & 15) * 256 + (nz & 15) * 16 + (nx & 15);
29581
+ const object3 = section?.isBlockVisibleIndex(neighborIndex) ? section.getBlockAt(neighbor.set(wx + x, wy + y, wz + z))?.object : void 0;
29582
+ return {
29583
+ fluid: object3?.fluidKind,
29584
+ level: object3?.fluidLevel,
29585
+ solid: section?.isOccludingIndex(neighborIndex) ?? false
29586
+ };
29587
+ });
29340
29588
  }
29589
+ let mask = 0;
29590
+ for (let face = 0; face < CUBE_FACE_OFFSETS.length; face++) {
29591
+ const offset = CUBE_FACE_OFFSETS[face];
29592
+ const nx = lx + offset[0], ny = ly + offset[1], nz = lz + offset[2];
29593
+ 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)}`);
29594
+ if (section?.isOccludingIndex((ny & 15) * 256 + (nz & 15) * 16 + (nx & 15))) mask |= 1 << face;
29595
+ }
29596
+ await chunk.setCullMaskIndex(index, mask);
29341
29597
  }
29342
- await chunk.setCullMaskAt(pos, mask);
29343
29598
  }
29344
- for (const chunk of changed) chunk.rebuildSectionMesh();
29599
+ await Promise.all(batch.map(([chunk]) => chunk.rebuildSectionMesh()));
29345
29600
  }
29346
29601
  } finally {
29347
29602
  this.culling = void 0;
@@ -29403,7 +29658,228 @@ init_Model();
29403
29658
  // src/world/SectionMesh.ts
29404
29659
  var import_three41 = require("three");
29405
29660
  init_CanvasCompat();
29661
+ init_CubeFace();
29406
29662
  init_Materials();
29663
+
29664
+ // src/world/SectionGeometry.ts
29665
+ init_CubeFace();
29666
+ function buildSectionGeometry(input) {
29667
+ const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize } = input;
29668
+ if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
29669
+ if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
29670
+ const visible = [];
29671
+ const visibleAtlases = /* @__PURE__ */ new Set();
29672
+ for (let entry = 0; entry < count; entry++) {
29673
+ const template = templateData[templates[entry]];
29674
+ if (!template.cullFaces.some((direction) => !(cullMasks[entry] & direction))) continue;
29675
+ visible.push(entry);
29676
+ visibleAtlases.add(template.atlas);
29677
+ }
29678
+ const pages = [];
29679
+ const locations = /* @__PURE__ */ new Map();
29680
+ for (const atlas of [...visibleAtlases].sort((a, b) => atlases[b].height - atlases[a].height)) {
29681
+ const { width, height } = atlases[atlas];
29682
+ if (width > maxAtlasSize || height > maxAtlasSize) throw new RangeError("Model atlas exceeds the section atlas size limit");
29683
+ let selected = -1;
29684
+ for (let i = 0; i < pages.length; i++) {
29685
+ const page2 = pages[i];
29686
+ const nextRow = page2.x + width > maxAtlasSize;
29687
+ if ((nextRow ? page2.y + page2.rowHeight : page2.y) + height > maxAtlasSize) continue;
29688
+ if (nextRow) {
29689
+ page2.y += page2.rowHeight;
29690
+ page2.x = 0;
29691
+ page2.rowHeight = 0;
29692
+ }
29693
+ selected = i;
29694
+ break;
29695
+ }
29696
+ if (selected < 0) {
29697
+ selected = pages.length;
29698
+ pages.push({ placements: [], entries: [], x: 0, y: 0, rowHeight: 0, width: 0, height: 0 });
29699
+ }
29700
+ const page = pages[selected];
29701
+ page.placements.push({ atlas, x: page.x, y: page.y });
29702
+ locations.set(atlas, { page: selected, x: page.x, y: page.y });
29703
+ page.x += width;
29704
+ page.rowHeight = Math.max(page.rowHeight, height);
29705
+ page.width = Math.max(page.width, page.x);
29706
+ page.height = Math.max(page.height, page.y + height);
29707
+ }
29708
+ for (const entry of visible) pages[locations.get(templateData[templates[entry]].atlas).page].entries.push(entry);
29709
+ return pages.map((page) => {
29710
+ let faces = 0;
29711
+ for (const entry of page.entries) {
29712
+ const template = templateData[templates[entry]];
29713
+ for (let face = 0; face < CUBE_FACES.length; face++) {
29714
+ if (!(cullMasks[entry] & template.cullFaces[face])) faces++;
29715
+ }
29716
+ }
29717
+ const positions = new Float32Array(faces * 12), normals = new Float32Array(faces * 12);
29718
+ const uvs = new Float32Array(faces * 8), uvBounds = new Float32Array(faces * 16), colors = new Float32Array(faces * 12);
29719
+ const outputIndices = new Uint32Array(faces * 6);
29720
+ let vertex = 0, indexOffset = 0;
29721
+ for (const entry of page.entries) {
29722
+ const template = templateData[templates[entry]];
29723
+ const atlas = atlases[template.atlas];
29724
+ const placement = locations.get(template.atlas);
29725
+ const mapU = (u) => (placement.x + u * atlas.width) / page.width;
29726
+ const mapV = (v) => 1 - (placement.y + (1 - v) * atlas.height) / page.height;
29727
+ const offsetX = indices[entry] % 16 * 16;
29728
+ const offsetY = Math.floor(indices[entry] / 256) * 16;
29729
+ const offsetZ = Math.floor(indices[entry] / 16) % 16 * 16;
29730
+ for (let face = 0; face < CUBE_FACES.length; face++) {
29731
+ if (cullMasks[entry] & template.cullFaces[face]) continue;
29732
+ const start = vertex;
29733
+ for (let corner = 0; corner < 4; corner++, vertex++) {
29734
+ const source = face * 4 + corner;
29735
+ positions[vertex * 3] = template.positions[source * 3] + offsetX;
29736
+ positions[vertex * 3 + 1] = template.positions[source * 3 + 1] + offsetY;
29737
+ positions[vertex * 3 + 2] = template.positions[source * 3 + 2] + offsetZ;
29738
+ normals[vertex * 3] = template.normals[source * 3];
29739
+ normals[vertex * 3 + 1] = template.normals[source * 3 + 1];
29740
+ normals[vertex * 3 + 2] = template.normals[source * 3 + 2];
29741
+ uvs[vertex * 2] = mapU(template.uvs[source * 2]);
29742
+ uvs[vertex * 2 + 1] = mapV(template.uvs[source * 2 + 1]);
29743
+ uvBounds[vertex * 4] = mapU(template.uvBounds[source * 4]);
29744
+ uvBounds[vertex * 4 + 1] = mapV(template.uvBounds[source * 4 + 1]);
29745
+ uvBounds[vertex * 4 + 2] = mapU(template.uvBounds[source * 4 + 2]);
29746
+ uvBounds[vertex * 4 + 3] = mapV(template.uvBounds[source * 4 + 3]);
29747
+ colors[vertex * 3] = template.colors[source * 3];
29748
+ colors[vertex * 3 + 1] = template.colors[source * 3 + 1];
29749
+ colors[vertex * 3 + 2] = template.colors[source * 3 + 2];
29750
+ }
29751
+ for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[face * 6 + corner] - face * 4;
29752
+ }
29753
+ }
29754
+ return { width: page.width, height: page.height, placements: page.placements, positions, normals, uvs, uvBounds, colors, indices: outputIndices };
29755
+ });
29756
+ }
29757
+ function sectionGeometryTransferables(pages) {
29758
+ return pages.flatMap((page) => [page.positions.buffer, page.normals.buffer, page.uvs.buffer, page.uvBounds.buffer, page.colors.buffer, page.indices.buffer]);
29759
+ }
29760
+
29761
+ // src/world/SectionWorker.ts
29762
+ init_Env();
29763
+ var _SectionWorker = class _SectionWorker {
29764
+ constructor(worker) {
29765
+ this.worker = worker;
29766
+ this.nextId = 0;
29767
+ this.pending = /* @__PURE__ */ new Map();
29768
+ worker.addEventListener("message", (event) => {
29769
+ const reply = event.data;
29770
+ const pending = this.pending.get(reply.id);
29771
+ if (!pending) return;
29772
+ this.pending.delete(reply.id);
29773
+ if (reply.type === "pages") pending.resolve(reply.pages);
29774
+ else pending.reject(new Error(reply.message));
29775
+ });
29776
+ worker.addEventListener("error", (event) => this.fail(event.message || "Section worker failed"));
29777
+ worker.addEventListener("messageerror", () => this.fail("Section worker message could not be deserialized"));
29778
+ }
29779
+ /** The shared worker, or undefined when the environment has none or a previous worker failed. */
29780
+ static shared() {
29781
+ if (!this.initialized) {
29782
+ this.initialized = true;
29783
+ const worker = Env.provider.createWorker?.("section");
29784
+ if (worker) this.instance = new _SectionWorker(worker);
29785
+ }
29786
+ return this.instance;
29787
+ }
29788
+ /** Builds geometry without transferring the input buffers away from the caller. */
29789
+ build(input) {
29790
+ if (!this.worker) return Promise.reject(new Error("Section worker terminated"));
29791
+ return new Promise((resolve, reject) => {
29792
+ const id = this.nextId++;
29793
+ this.pending.set(id, { resolve, reject });
29794
+ this.worker.postMessage({ id, type: "build", input });
29795
+ });
29796
+ }
29797
+ /** Rejects pending builds, terminates the worker, and makes shared() return undefined. */
29798
+ terminate() {
29799
+ this.fail("Section worker terminated");
29800
+ }
29801
+ fail(message) {
29802
+ const error = new Error(message);
29803
+ for (const pending of this.pending.values()) pending.reject(error);
29804
+ this.pending.clear();
29805
+ this.worker?.terminate();
29806
+ this.worker = void 0;
29807
+ _SectionWorker.instance = void 0;
29808
+ }
29809
+ };
29810
+ _SectionWorker.initialized = false;
29811
+ var SectionWorker = _SectionWorker;
29812
+
29813
+ // src/world/SectionMesh.ts
29814
+ var templateCache = /* @__PURE__ */ new WeakMap();
29815
+ function toInput(entries, maxAtlasSize) {
29816
+ if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
29817
+ const atlases = [];
29818
+ const atlasIds = /* @__PURE__ */ new Map();
29819
+ const templateIds = /* @__PURE__ */ new Map();
29820
+ const input = {
29821
+ count: entries.length,
29822
+ indices: new Uint16Array(entries.length),
29823
+ templates: new Uint16Array(entries.length),
29824
+ cullMasks: new Uint8Array(entries.length),
29825
+ templateData: [],
29826
+ atlases: [],
29827
+ maxAtlasSize
29828
+ };
29829
+ for (let i = 0; i < entries.length; i++) {
29830
+ const { index, template, cullMask } = entries[i];
29831
+ let id = templateIds.get(template);
29832
+ if (id === void 0) {
29833
+ let atlas = atlasIds.get(template.atlas);
29834
+ if (atlas === void 0) {
29835
+ atlas = atlases.length;
29836
+ atlasIds.set(template.atlas, atlas);
29837
+ atlases.push(template.atlas);
29838
+ input.atlases.push({ width: template.atlas.image.width, height: template.atlas.image.height });
29839
+ }
29840
+ let data = templateCache.get(template);
29841
+ if (!data) {
29842
+ data = {
29843
+ positions: new Float32Array(72),
29844
+ normals: new Float32Array(72),
29845
+ uvs: new Float32Array(48),
29846
+ uvBounds: new Float32Array(96),
29847
+ colors: new Float32Array(72),
29848
+ indices: new Uint16Array(36),
29849
+ cullFaces: new Uint8Array(template.cullFaces),
29850
+ atlas: 0
29851
+ };
29852
+ const position = template.geometry.getAttribute("position");
29853
+ const normal = template.geometry.getAttribute("normal");
29854
+ const uv = template.geometry.getAttribute("uv");
29855
+ const bounds = template.geometry.getAttribute("uvBounds");
29856
+ const color2 = template.geometry.getAttribute("color");
29857
+ for (let vertex = 0; vertex < CUBE_FACES.length * 4; vertex++) {
29858
+ data.positions.set([position.getX(vertex), position.getY(vertex), position.getZ(vertex)], vertex * 3);
29859
+ data.normals.set([normal.getX(vertex), normal.getY(vertex), normal.getZ(vertex)], vertex * 3);
29860
+ data.uvs.set([uv.getX(vertex), uv.getY(vertex)], vertex * 2);
29861
+ data.uvBounds.set([
29862
+ bounds?.getX(vertex) ?? 0,
29863
+ bounds?.getY(vertex) ?? 0,
29864
+ bounds?.getZ(vertex) ?? 1,
29865
+ bounds?.getW(vertex) ?? 1
29866
+ ], vertex * 4);
29867
+ data.colors.set([color2?.getX(vertex) ?? 1, color2?.getY(vertex) ?? 1, color2?.getZ(vertex) ?? 1], vertex * 3);
29868
+ }
29869
+ const indices = template.geometry.getIndex();
29870
+ for (let i2 = 0; i2 < data.indices.length; i2++) data.indices[i2] = indices.getX(i2);
29871
+ templateCache.set(template, data);
29872
+ }
29873
+ id = input.templateData.length;
29874
+ templateIds.set(template, id);
29875
+ input.templateData.push({ ...data, atlas });
29876
+ }
29877
+ input.indices[i] = index;
29878
+ input.templates[i] = id;
29879
+ input.cullMasks[i] = cullMask;
29880
+ }
29881
+ return { input, atlases };
29882
+ }
29407
29883
  var SectionMesh = class _SectionMesh extends import_three41.Group {
29408
29884
  constructor() {
29409
29885
  super(...arguments);
@@ -29411,86 +29887,31 @@ var SectionMesh = class _SectionMesh extends import_three41.Group {
29411
29887
  }
29412
29888
  /** Builds section-local meshes from visible cube faces. `maxAtlasSize` limits each atlas dimension in pixels. */
29413
29889
  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);
29890
+ const { input, atlases } = toInput(entries, maxAtlasSize);
29891
+ return this.fromPages(buildSectionGeometry(input), atlases);
29892
+ }
29893
+ /** Builds section-local meshes in a browser worker when available, with synchronous fallback. */
29894
+ static async buildAsync(entries, maxAtlasSize = 2048) {
29895
+ const { input, atlases } = toInput(entries, maxAtlasSize);
29896
+ const worker = SectionWorker.shared();
29897
+ const pages = worker ? await worker.build(input).catch(() => buildSectionGeometry(input)) : buildSectionGeometry(input);
29898
+ return this.fromPages(pages, atlases);
29899
+ }
29900
+ static fromPages(pages, atlases) {
29447
29901
  const section = new _SectionMesh();
29448
29902
  for (const page of pages) {
29449
29903
  const canvas = createCanvas(page.width, page.height);
29450
29904
  const context = canvas.getContext("2d");
29451
29905
  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
- }
29906
+ context.drawImage(atlases[atlas].image.canvas, x, y);
29486
29907
  }
29487
29908
  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);
29909
+ geometry.setAttribute("position", new import_three41.Float32BufferAttribute(page.positions.buffer, 3));
29910
+ geometry.setAttribute("normal", new import_three41.Float32BufferAttribute(page.normals.buffer, 3));
29911
+ geometry.setAttribute("uv", new import_three41.Float32BufferAttribute(page.uvs.buffer, 2));
29912
+ geometry.setAttribute("uvBounds", new import_three41.Float32BufferAttribute(page.uvBounds.buffer, 4));
29913
+ geometry.setAttribute("color", new import_three41.Float32BufferAttribute(page.colors.buffer, 3));
29914
+ geometry.setIndex(new import_three41.Uint32BufferAttribute(page.indices.buffer, 1));
29494
29915
  geometry.computeBoundingBox();
29495
29916
  geometry.computeBoundingSphere();
29496
29917
  const material = Materials.createShadedCanvasMaterial(canvas, false, false, true);
@@ -29524,7 +29945,9 @@ var Chunk = class _Chunk {
29524
29945
  this.renderedBlocks = /* @__PURE__ */ new Map();
29525
29946
  this.sectionBlocks = /* @__PURE__ */ new Map();
29526
29947
  this.hiddenBlocks = /* @__PURE__ */ new Set();
29948
+ this.blockPosition = new import_three42.Vector3();
29527
29949
  this.meshDirty = false;
29950
+ this.meshGeneration = 0;
29528
29951
  this.scene = scene;
29529
29952
  this.x = x;
29530
29953
  this.y = y;
@@ -29537,7 +29960,7 @@ var Chunk = class _Chunk {
29537
29960
  if (isTripleArray(posOrX)) {
29538
29961
  return this.getBlockAt(new import_three42.Vector3(posOrX[0], posOrX[1], posOrX[2]));
29539
29962
  }
29540
- const pos = this.worldPosToChunkPos(posOrX);
29963
+ const pos = this.blockPosition.set(posOrX.x - this.x * 16, posOrX.y - this.y * 16, posOrX.z - this.z * 16);
29541
29964
  const index = _Chunk.chunkPosToBlockIndex(pos);
29542
29965
  return this.renderedBlocks.get(index);
29543
29966
  }
@@ -29561,48 +29984,81 @@ var Chunk = class _Chunk {
29561
29984
  worldPos = this.chunkPosToWorldPos(pos);
29562
29985
  }
29563
29986
  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
29987
  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;
29988
+ await this.placeBlocks([{ index, block }]);
29989
+ return this.renderedBlocks.get(index);
29602
29990
  } finally {
29603
29991
  await onBlocksChanged?.([worldPos]);
29604
29992
  }
29605
29993
  }
29994
+ /** Places cells in input order and returns changed world positions without refreshing neighbors. */
29995
+ async placeBlocks(blocks) {
29996
+ const resolutions = /* @__PURE__ */ new Map();
29997
+ for (const { block } of blocks) {
29998
+ if (!block || ChunkData.isAir(block)) continue;
29999
+ const key = ChunkData.paletteKey(block);
30000
+ if (resolutions.has(key)) continue;
30001
+ const stored = { type: block.type, properties: block.properties ? { ...block.properties } : void 0 };
30002
+ const resolved = (async () => {
30003
+ const blockState2 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
30004
+ const perBlock = !blockState2 || !this.sectionModels || !!getFluidKind(blockState2.key, stored.properties) || !!(blockState2.key && BlockEntities.entry(await BlockEntities.getIndex(blockState2.key.root), blockState2.key.toNamespacedString()));
30005
+ if (!perBlock) await this.sectionModels.get(blockState2, stored.properties);
30006
+ return { blockState: blockState2, perBlock };
30007
+ })();
30008
+ resolutions.set(key, resolved);
30009
+ void resolved.catch(() => void 0);
30010
+ }
30011
+ const positions = [];
30012
+ let failed = false, failure;
30013
+ for (const { index, block } of blocks) {
30014
+ this.hiddenBlocks.delete(index);
30015
+ this.data.set(index, block);
30016
+ const readBlock = this.data.snapshot(index);
30017
+ this.renderedBlocks.get(index)?.object?.removeFromScene();
30018
+ if (this.sectionBlocks.delete(index)) this.meshDirty = true;
30019
+ this.renderedBlocks.delete(index);
30020
+ const worldPos = new import_three42.Vector3(
30021
+ this.x * 16 + index % 16,
30022
+ this.y * 16 + Math.floor(index / 256),
30023
+ this.z * 16 + Math.floor(index / 16) % 16
30024
+ );
30025
+ positions.push(worldPos);
30026
+ let object3;
30027
+ try {
30028
+ if (!readBlock) continue;
30029
+ const stored = readBlock();
30030
+ const key = ChunkData.paletteKey(stored);
30031
+ const { blockState: blockState2, perBlock } = await resolutions.get(key);
30032
+ if (!blockState2) {
30033
+ this.data.set(index, void 0);
30034
+ continue;
30035
+ }
30036
+ const template = perBlock ? void 0 : await this.sectionModels.get(blockState2, stored.properties);
30037
+ if (template) {
30038
+ this.sectionBlocks.set(index, { index, template, cullMask: 0 });
30039
+ this.meshDirty = true;
30040
+ } else {
30041
+ object3 = await this.scene.addBlock(blockState2, {
30042
+ mergeMeshes: true,
30043
+ instanceMeshes: true,
30044
+ maxInstanceCount: 2e3,
30045
+ initialState: stored.properties
30046
+ });
30047
+ object3.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
30048
+ }
30049
+ this.renderedBlocks.set(index, { get block() {
30050
+ return readBlock();
30051
+ }, object: object3 });
30052
+ } catch (error) {
30053
+ this.data.set(index, void 0);
30054
+ object3?.removeFromScene();
30055
+ if (!failed) failure = error;
30056
+ failed = true;
30057
+ }
30058
+ }
30059
+ if (failed) throw failure;
30060
+ return positions;
30061
+ }
29606
30062
  /** Changes visibility at world block coordinates while preserving the block's data. */
29607
30063
  async setBlockVisibleAt(pos, visible) {
29608
30064
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
@@ -29623,15 +30079,24 @@ var Chunk = class _Chunk {
29623
30079
  await this.onBlocksChanged?.([pos]);
29624
30080
  }
29625
30081
  isBlockVisibleAt(pos) {
29626
- const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
30082
+ return this.isBlockVisibleIndex(_Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos)));
30083
+ }
30084
+ /** Reports whether a cell has a visible render object or section entry. */
30085
+ isBlockVisibleIndex(index) {
29627
30086
  return this.renderedBlocks.has(index) && !this.hiddenBlocks.has(index);
29628
30087
  }
29629
30088
  isOccludingAt(pos) {
29630
- const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
30089
+ return this.isOccludingIndex(_Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos)));
30090
+ }
30091
+ /** Reports whether a visible cell hides neighboring cube faces. */
30092
+ isOccludingIndex(index) {
29631
30093
  return !this.hiddenBlocks.has(index) && (this.sectionBlocks.has(index) || (this.renderedBlocks.get(index)?.object?.isOccluding ?? false));
29632
30094
  }
29633
30095
  async setCullMaskAt(pos, mask) {
29634
- const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
30096
+ await this.setCullMaskIndex(_Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos)), mask);
30097
+ }
30098
+ /** Updates hidden faces for a cell's section entry or individual render object. */
30099
+ async setCullMaskIndex(index, mask) {
29635
30100
  const entry = this.sectionBlocks.get(index);
29636
30101
  if (entry) {
29637
30102
  if (entry.cullMask !== mask) {
@@ -29642,12 +30107,17 @@ var Chunk = class _Chunk {
29642
30107
  await this.renderedBlocks.get(index)?.object?.setCullMask(mask);
29643
30108
  }
29644
30109
  }
29645
- rebuildSectionMesh() {
30110
+ /** Rebuilds visible section geometry and discards results superseded by another rebuild or clear. */
30111
+ async rebuildSectionMesh() {
29646
30112
  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;
30113
+ this.meshDirty = false;
30114
+ const generation = ++this.meshGeneration;
30115
+ const entries = [...this.sectionBlocks.values()].filter((entry) => !this.hiddenBlocks.has(entry.index));
30116
+ const next = entries.length ? await SectionMesh.buildAsync(entries, this.sectionModels.maxAtlasSize) : void 0;
30117
+ if (generation !== this.meshGeneration) {
30118
+ next?.dispose();
30119
+ return;
30120
+ }
29651
30121
  this.sectionMesh?.dispose();
29652
30122
  this.sectionMesh = next;
29653
30123
  if (next) {
@@ -29655,7 +30125,6 @@ var Chunk = class _Chunk {
29655
30125
  this.scene.add(next);
29656
30126
  }
29657
30127
  this.scene.dirty = true;
29658
- this.meshDirty = false;
29659
30128
  }
29660
30129
  /** Removes stored blocks and their render objects, then notifies the owning world of changed positions. */
29661
30130
  async clear(onBlocksChanged = this.onBlocksChanged) {
@@ -29664,6 +30133,7 @@ var Chunk = class _Chunk {
29664
30133
  ));
29665
30134
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
29666
30135
  this.sectionBlocks.clear();
30136
+ this.meshGeneration++;
29667
30137
  this.sectionMesh?.dispose();
29668
30138
  this.sectionMesh = void 0;
29669
30139
  this.meshDirty = false;