minerender 2.1.0-alpha.77 → 2.1.0-alpha.79

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.
@@ -1528,6 +1528,8 @@ var init_ModelMerger = __esm({
1528
1528
  ModelMerger = class {
1529
1529
  /** Loads the parent chain and returns a merged model without modifying the supplied definition. */
1530
1530
  static async mergeWithParents(model) {
1531
+ const parts = model.parts;
1532
+ if (parts) return { ...model, parts: await Promise.all(parts.map((part) => this.mergeWithParents(part))) };
1531
1533
  const models = await this.collectAllParents(model);
1532
1534
  let merged = {};
1533
1535
  for (const source of [...models, model]) {
@@ -1588,6 +1590,7 @@ var init_Models = __esm({
1588
1590
  init_PersistentCache();
1589
1591
  init_AssetKey();
1590
1592
  init_AssetParsers();
1593
+ init_DisplayPosition();
1591
1594
  Models = class _Models {
1592
1595
  // opened lazily: touching the store at import time would hit IndexedDB/disk just for loading
1593
1596
  // the library, and would require an EnvProvider before any entry has had a chance to register
@@ -1602,34 +1605,174 @@ var init_Models = __esm({
1602
1605
  return (await AssetLoader.getFirst([key, legacyKey], "list" /* LIST */))?.asset;
1603
1606
  }).then((r) => r?.files ?? []);
1604
1607
  }
1605
- static async loadAndMerge(key) {
1606
- const model = key.type === "item" ? await this.getItemModel(key) : await this.getRaw(key);
1608
+ static async loadAndMerge(key, context = {}) {
1609
+ const model = key.type === "item" ? await this.getItemModel(key, context) : await this.getRaw(key);
1607
1610
  if (!model) {
1608
1611
  return void 0;
1609
1612
  }
1610
1613
  return ModelMerger.mergeWithParents(model);
1611
1614
  }
1612
- static async getItemModel(key) {
1615
+ static async getItemModel(key, context = {}) {
1616
+ const preview = this.snapshotContext(key, context);
1613
1617
  const itemKey = new AssetKey(key.namespace, key.path, "items", void 0, key.rootType, ".json", key.root);
1614
- const model = await this.PERSISTENT_CACHE.getOrLoad(`item-v2:${AssetLoader.persistentKey(itemKey.serialize())}`, async () => {
1618
+ const cacheKey = itemKey.serialize() + this.contextKey(preview);
1619
+ const model = await this.PERSISTENT_CACHE.getOrLoad(`item-v3:${AssetLoader.persistentKey(cacheKey)}`, async () => {
1615
1620
  const result = await AssetLoader.getFirst([itemKey, key], "json" /* JSON */);
1616
1621
  if (!result) return void 0;
1617
1622
  if (result.key.assetType !== "items") return { ...result.asset, key };
1618
- const selected = this.defaultItemModel(result.asset.model, key);
1619
- const modelKey = AssetKey.parse("models", selected.model);
1620
- modelKey.root = key.root;
1621
- const model2 = await this.getRaw(modelKey);
1622
- if (!model2) throw new Error(`Item ${key.toNamespacedString()} references missing model ${selected.model}`);
1623
- return { ...model2, ...selected.special && { special: selected.special }, ...selected.tints && { tints: selected.tints } };
1623
+ const load = async (selected) => {
1624
+ if ("parts" in selected) {
1625
+ return { key, parts: await Promise.all(selected.parts.map(load)) };
1626
+ }
1627
+ if ("item" in selected) {
1628
+ const model3 = await this.getItemModel(selected.item, { displayContext: preview.displayContext });
1629
+ if (!model3) throw new Error(`Item ${key.toNamespacedString()} references missing item ${selected.item.toNamespacedString()}`);
1630
+ return model3;
1631
+ }
1632
+ const modelKey = AssetKey.parse("models", selected.model);
1633
+ modelKey.root = key.root;
1634
+ const model2 = await this.getRaw(modelKey);
1635
+ if (!model2) throw new Error(`Item ${key.toNamespacedString()} references missing model ${selected.model}`);
1636
+ return { ...model2, ...selected.special && { special: selected.special }, ...selected.tints && { tints: selected.tints } };
1637
+ };
1638
+ return load(this.selectItemModel(result.asset.model, key, preview));
1639
+ });
1640
+ const restore = (model2) => ({
1641
+ ...model2,
1642
+ key: Object.assign(new AssetKey("", ""), model2.key),
1643
+ ...model2.parts && { parts: model2.parts.map(restore) }
1624
1644
  });
1625
- return model ? { ...model, key: Object.assign(new AssetKey("", ""), model.key) } : void 0;
1645
+ return model ? restore(model) : void 0;
1646
+ }
1647
+ static contextIdentifier(id) {
1648
+ if (typeof id !== "string" || !/^(?:[a-z0-9_.-]+:)?[a-z0-9_./-]+$/.test(id)) {
1649
+ throw new Error(`Invalid item-preview identifier: ${id}`);
1650
+ }
1651
+ return id.includes(":") ? id : `minecraft:${id}`;
1652
+ }
1653
+ static snapshotContext(key, context) {
1654
+ const displayContext = context.displayContext ?? "gui" /* GUI */;
1655
+ if (displayContext !== "none" && !Object.values(DisplayPosition).includes(displayContext)) {
1656
+ throw new Error(`Unsupported item display context: ${displayContext}`);
1657
+ }
1658
+ for (const map of [context.properties, context.components, context.itemReferences]) {
1659
+ if (map !== void 0 && (!map || typeof map !== "object" || Array.isArray(map))) {
1660
+ throw new Error("Item-preview properties, components, and references must be objects keyed by identifier");
1661
+ }
1662
+ }
1663
+ const count = context.count === void 0 ? 1 : context.count;
1664
+ if (!Number.isSafeInteger(count) || count < 0) throw new Error("Item-preview count must be a nonnegative safe integer");
1665
+ const properties = {};
1666
+ for (const [id, value] of Object.entries(context.properties ?? {})) {
1667
+ const property = this.contextIdentifier(id);
1668
+ if (!["boolean", "string", "number"].includes(typeof value) || typeof value === "number" && !Number.isFinite(value)) {
1669
+ throw new Error(`Item-preview property ${property} must be a boolean, string, or finite number`);
1670
+ }
1671
+ if (Object.prototype.hasOwnProperty.call(properties, property)) throw new Error(`Duplicate item-preview property: ${property}`);
1672
+ properties[property] = value;
1673
+ }
1674
+ const components = {};
1675
+ for (const [id, value] of Object.entries(context.components ?? {})) {
1676
+ const component = this.contextIdentifier(id);
1677
+ if (Object.prototype.hasOwnProperty.call(components, component)) throw new Error(`Duplicate item-preview component: ${component}`);
1678
+ components[component] = this.snapshotJson(value);
1679
+ }
1680
+ const itemReferences = {};
1681
+ for (const [id, value] of Object.entries(context.itemReferences ?? {})) {
1682
+ const reference = this.contextIdentifier(id);
1683
+ if (!isAssetKey(value) || value.assetType !== "models" || value.type !== "item") {
1684
+ throw new Error(`Item-preview reference ${reference} must be an item-model AssetKey`);
1685
+ }
1686
+ this.contextIdentifier(`${value.namespace}:${value.path}`);
1687
+ if (Object.prototype.hasOwnProperty.call(itemReferences, reference)) throw new Error(`Duplicate item-preview reference: ${reference}`);
1688
+ itemReferences[reference] = Object.assign(new AssetKey("", ""), value, { root: value.root ?? key.root });
1689
+ }
1690
+ return { displayContext, properties, components, count, itemReferences };
1691
+ }
1692
+ static contextKey(context) {
1693
+ const itemReferences = Object.fromEntries(Object.entries(context.itemReferences).map(([id, key]) => [id, key.serialize()]));
1694
+ return `|item-v3:${JSON.stringify(this.snapshotJson({ ...context, itemReferences }))}`;
1695
+ }
1696
+ static snapshotJson(value, ancestors = /* @__PURE__ */ new Set()) {
1697
+ if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return value;
1698
+ if (typeof value !== "object" || ancestors.has(value) || !Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
1699
+ throw new Error("Item-preview components must contain finite JSON values without circular references");
1700
+ }
1701
+ ancestors.add(value);
1702
+ const copy = Array.isArray(value) ? Array.from(value, (entry) => this.snapshotJson(entry, ancestors)) : Object.fromEntries(Object.keys(value).sort().map((key) => [key, this.snapshotJson(value[key], ancestors)]));
1703
+ ancestors.delete(value);
1704
+ return copy;
1705
+ }
1706
+ static propertyValue(node, context) {
1707
+ const kind = node.type.replace(/^minecraft:/, "");
1708
+ const property = node.property ? this.contextIdentifier(node.property) : void 0;
1709
+ if (kind === "select" && property === "minecraft:display_context") return context.displayContext;
1710
+ if (property && Object.prototype.hasOwnProperty.call(context.properties, property)) return context.properties[property];
1711
+ const fallback = kind === "condition" ? false : kind === "range_dispatch" ? 0 : void 0;
1712
+ const component = (id) => context.components[this.contextIdentifier(id)];
1713
+ if (property === "minecraft:custom_model_data") {
1714
+ const index = node.index ?? 0;
1715
+ if (!Number.isSafeInteger(index) || index < 0) throw new Error("Item-model custom_model_data index must be a nonnegative integer");
1716
+ const data = component("custom_model_data");
1717
+ if (data === void 0) return fallback;
1718
+ if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error("Item-preview custom_model_data must be an object");
1719
+ const field = kind === "condition" ? "flags" : kind === "select" ? "strings" : "floats";
1720
+ const values = data[field];
1721
+ if (values === void 0) return fallback;
1722
+ if (!Array.isArray(values)) throw new Error(`Item-preview custom_model_data.${field} must be an array`);
1723
+ return index < values.length ? values[index] : fallback;
1724
+ }
1725
+ if (kind === "condition" && property === "minecraft:has_component") {
1726
+ if (node.ignore_default) throw new Error("Item-preview has_component with ignore_default requires an explicit properties override; item defaults are unavailable");
1727
+ return Object.prototype.hasOwnProperty.call(context.components, this.contextIdentifier(node.component));
1728
+ }
1729
+ if (kind === "select") {
1730
+ switch (property) {
1731
+ case "minecraft:component":
1732
+ return component(node.component);
1733
+ case "minecraft:block_state": {
1734
+ const state = component("block_state");
1735
+ if (state === void 0) return void 0;
1736
+ if (!state || typeof state !== "object" || Array.isArray(state)) throw new Error("Item-preview block_state must be an object");
1737
+ if (typeof node.block_state_property !== "string") throw new Error("Item-model block_state requires block_state_property");
1738
+ return Object.prototype.hasOwnProperty.call(state, node.block_state_property) ? state[node.block_state_property] : void 0;
1739
+ }
1740
+ case "minecraft:charge_type": {
1741
+ const projectiles = component("charged_projectiles");
1742
+ if (projectiles === void 0) return "none";
1743
+ if (!Array.isArray(projectiles)) throw new Error("Item-preview charged_projectiles must be an array of item stacks");
1744
+ const ids = projectiles.map((stack) => {
1745
+ if (!stack || typeof stack !== "object" || Array.isArray(stack)) throw new Error("Item-preview charged_projectiles must contain item-stack objects");
1746
+ return this.contextIdentifier(stack.id);
1747
+ });
1748
+ return !ids.length ? "none" : ids.includes("minecraft:firework_rocket") ? "rocket" : "arrow";
1749
+ }
1750
+ }
1751
+ }
1752
+ if (kind === "range_dispatch" && (property === "minecraft:damage" || property === "minecraft:count")) {
1753
+ const damage = property === "minecraft:damage";
1754
+ const maximumComponent = component(damage ? "max_damage" : "max_stack_size");
1755
+ const maximum = maximumComponent === void 0 ? damage ? 0 : 1 : maximumComponent;
1756
+ const damageComponent = component("damage");
1757
+ const value = damage ? damageComponent === void 0 ? 0 : damageComponent : context.count;
1758
+ if (typeof value !== "number" || typeof maximum !== "number" || maximum < 0) throw new Error(`Item-preview ${property} requires numeric components and a nonnegative maximum`);
1759
+ const clamped = Math.max(0, Math.min(maximum, value));
1760
+ return node.normalize === false ? clamped : clamped / maximum;
1761
+ }
1762
+ return fallback;
1626
1763
  }
1627
- // Item previews use the GUI context, false conditions, and zero numeric properties.
1628
- static defaultItemModel(node, key) {
1764
+ static selectItemModel(node, key, context) {
1629
1765
  if (!node || typeof node.type !== "string") {
1630
1766
  throw new Error(`Unsupported item model definition for ${key.toNamespacedString()}`);
1631
1767
  }
1632
1768
  switch (node.type.replace(/^minecraft:/, "")) {
1769
+ case "composite":
1770
+ if (Array.isArray(node.models)) return { parts: node.models.map((child) => this.selectItemModel(child, key, context)) };
1771
+ break;
1772
+ case "bundle/selected_item": {
1773
+ const item = context.itemReferences["minecraft:bundle/selected_item"];
1774
+ return item ? { item } : { parts: [] };
1775
+ }
1633
1776
  case "model":
1634
1777
  if (typeof node.model === "string" && node.model) return { model: node.model, tints: node.tints };
1635
1778
  break;
@@ -1650,17 +1793,29 @@ var init_Models = __esm({
1650
1793
  }
1651
1794
  throw new Error(`Unsupported special item renderer ${special.type} for ${key.toNamespacedString()}`);
1652
1795
  }
1653
- case "condition":
1654
- return this.defaultItemModel(node.on_false, key);
1796
+ case "condition": {
1797
+ const value = this.propertyValue(node, context);
1798
+ if (typeof value !== "boolean") throw new Error(`Item-preview condition ${node.property} requires a boolean`);
1799
+ return this.selectItemModel(value ? node.on_true : node.on_false, key, context);
1800
+ }
1655
1801
  case "select": {
1656
- const selected = node.property?.replace(/^minecraft:/, "") === "display_context" ? node.cases?.find((entry) => Array.isArray(entry.when) ? entry.when.includes("gui") : entry.when === "gui")?.model : void 0;
1657
- return this.defaultItemModel(selected ?? node.fallback, key);
1802
+ const value = this.propertyValue(node, context);
1803
+ const component = node.property && this.contextIdentifier(node.property) === "minecraft:component";
1804
+ if (!component && value !== void 0 && typeof value !== "string") throw new Error(`Item-preview selector ${node.property} requires a string`);
1805
+ const matches = (candidate) => component ? JSON.stringify(this.snapshotJson(candidate)) === JSON.stringify(this.snapshotJson(value)) : candidate === value;
1806
+ const selected = value === void 0 ? void 0 : node.cases?.find((entry) => matches(entry.when) || Array.isArray(entry.when) && entry.when.some(matches))?.model;
1807
+ return this.selectItemModel(selected ?? node.fallback, key, context);
1658
1808
  }
1659
1809
  case "range_dispatch": {
1810
+ const value = this.propertyValue(node, context);
1811
+ if (typeof value !== "number") throw new Error(`Item-preview range ${node.property} requires a number`);
1812
+ if (Number.isNaN(value)) return this.selectItemModel(node.fallback, key, context);
1813
+ const scaled = value * (node.scale ?? 1);
1814
+ if (!Number.isFinite(scaled)) throw new Error(`Item-preview range ${node.property} requires a finite value and scale`);
1660
1815
  const selected = node.entries?.reduce((match, entry) => {
1661
- return entry.threshold <= 0 && (!match || entry.threshold >= match.threshold) ? entry : match;
1816
+ return entry.threshold <= scaled && (!match || entry.threshold >= match.threshold) ? entry : match;
1662
1817
  }, void 0);
1663
- return this.defaultItemModel(selected?.model ?? node.fallback, key);
1818
+ return this.selectItemModel(selected?.model ?? node.fallback, key, context);
1664
1819
  }
1665
1820
  }
1666
1821
  throw new Error(`Unsupported item model ${node?.type ?? "definition"} for ${key.toNamespacedString()}`);
@@ -1687,25 +1842,28 @@ var init_Models = __esm({
1687
1842
  }
1688
1843
  /**
1689
1844
  * Loads a model and resolves its parent chain. Returns `undefined` when the model is missing.
1690
- * Item keys use GUI context, false conditions, and zero numeric properties.
1845
+ * Item keys default to GUI context and a stack count of 1. Unresolved conditions are false and numeric properties are zero.
1846
+ * Pass `context` to supply component values, property overrides, and item references for a preview.
1847
+ * Composite items retain independently merged children in `ItemModel.parts`.
1691
1848
  *
1692
1849
  * @param key - Model key, for example `AssetKey.parse("models", "minecraft:item/diamond_sword")`.
1693
1850
  */
1694
- static async getMerged(key) {
1851
+ static async getMerged(key, context = {}) {
1695
1852
  if (!key.assetType) {
1696
1853
  key.assetType = "models";
1697
1854
  }
1698
1855
  if (!key.extension) {
1699
1856
  key.extension = ".json";
1700
1857
  }
1701
- const keyStr = key.serialize();
1858
+ const preview = this.snapshotContext(key, context);
1859
+ const keyStr = key.serialize() + (key.type === "item" ? this.contextKey(preview) : "");
1702
1860
  return Caching.mergedModelCache.get(keyStr, (k) => {
1703
- return _Models.loadAndMerge(key);
1861
+ return _Models.loadAndMerge(key, preview);
1704
1862
  });
1705
1863
  }
1706
1864
  /** Alias for {@link getMerged}. */
1707
- static async get(key) {
1708
- return this.getMerged(key);
1865
+ static async get(key, context = {}) {
1866
+ return this.getMerged(key, context);
1709
1867
  }
1710
1868
  /** Clears persisted models. Use {@link Caching.clear} to also discard in-memory assets. */
1711
1869
  static async clearCache() {
@@ -5576,9 +5734,10 @@ var init_ModelObject = __esm({
5576
5734
  this.originalModel = originalModel;
5577
5735
  this.isModelObject = true;
5578
5736
  this.specialMaterials = /* @__PURE__ */ new Map();
5737
+ this.geometries = /* @__PURE__ */ new Set();
5579
5738
  this.meshesCreated = false;
5580
5739
  this.options = (0, import_ts_deepmerge5.default)({}, _ModelObject.DEFAULT_OPTIONS, options ?? {});
5581
- if (originalModel.special) this.options.instanceMeshes = false;
5740
+ if (originalModel.special || originalModel.parts) this.options.instanceMeshes = false;
5582
5741
  if (this.options.tints) this.options.tints = { ...this.options.tints };
5583
5742
  this.addEventListener("added", () => this.updateAnimationSubscription());
5584
5743
  this.addEventListener("removed", () => this.updateAnimationSubscription());
@@ -5587,14 +5746,36 @@ var init_ModelObject = __esm({
5587
5746
  this.DEFAULT_OPTIONS = (0, import_ts_deepmerge5.default)({}, SceneObject.DEFAULT_OPTIONS, {});
5588
5747
  }
5589
5748
  async init() {
5749
+ const parts = this.originalModel.parts;
5750
+ if (parts) {
5751
+ try {
5752
+ for (const part of parts) {
5753
+ const object3 = new _ModelObject(part, { ...this.options, instanceMeshes: false });
5754
+ this.add(object3);
5755
+ await object3.init();
5756
+ }
5757
+ let order = 0;
5758
+ this.iterateAllMeshes((mesh) => {
5759
+ mesh.renderOrder = order++;
5760
+ for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
5761
+ material.transparent = true;
5762
+ }
5763
+ });
5764
+ } catch (error) {
5765
+ this.disposeAndRemoveAllChildren();
5766
+ throw error;
5767
+ }
5768
+ this.notifyDirty();
5769
+ return;
5770
+ }
5590
5771
  this.options.tints = await ItemTints.get(this.originalModel, this.options.tints);
5591
5772
  const special = this.originalModel.special;
5592
5773
  if (special) {
5593
- const parts = await SpecialItems.getParts(special, this.originalModel.key?.root);
5774
+ const parts2 = await SpecialItems.getParts(special, this.originalModel.key?.root);
5594
5775
  const transform = this.options.displayPosition ? DisplayTransforms.getMatrix(this.originalModel.display, this.options.displayPosition) : new import_three25.Matrix4();
5595
5776
  transform.multiply(new import_three25.Matrix4().makeTranslation(-8, -8, -8));
5596
5777
  try {
5597
- for (const part of parts) {
5778
+ for (const part of parts2) {
5598
5779
  const object3 = new EntityObject(part.model, { flip: false, wireframe: this.options.wireframe });
5599
5780
  object3.matrix.copy(transform).multiply(part.transform);
5600
5781
  object3.matrixWorldNeedsUpdate = true;
@@ -5645,6 +5826,7 @@ var init_ModelObject = __esm({
5645
5826
  if (this.atlas.model.elements) {
5646
5827
  this.atlas.model.elements?.forEach((el) => {
5647
5828
  const elGeo = this._getBoxGeometryFromElement(el).clone();
5829
+ this.geometries.add(elGeo);
5648
5830
  if (this.options.cullMask) {
5649
5831
  const indices = Array.from(elGeo.getIndex().array);
5650
5832
  elGeo.setIndex(indices.filter((_, index) => {
@@ -5698,10 +5880,14 @@ var init_ModelObject = __esm({
5698
5880
  let combinedGeo;
5699
5881
  if (allGeos.length > 0) {
5700
5882
  combinedGeo = mergeBufferGeometries(allGeos);
5883
+ for (const geometry of allGeos) {
5884
+ this.geometries.delete(geometry);
5885
+ }
5701
5886
  } else {
5702
5887
  combinedGeo = new import_three25.BoxGeometry(16, 16, 16);
5703
5888
  if (displayTransform) DisplayTransforms.apply(combinedGeo, displayTransform);
5704
5889
  }
5890
+ this.geometries.add(combinedGeo);
5705
5891
  combinedGeo.computeBoundingBox();
5706
5892
  let mesh;
5707
5893
  if (this.options.instanceMeshes) {
@@ -5726,6 +5912,9 @@ var init_ModelObject = __esm({
5726
5912
  const mat = Materials.createShadedCanvasMaterial(this.atlas.image.canvas, this.atlas.hasTransparency, false, true);
5727
5913
  this.atlasMaterial = mat;
5728
5914
  this.atlasTexture = mat.uniforms?.map?.value ?? mat.map;
5915
+ if (this.options.displayPosition === "gui" /* GUI */ && this.originalModel.gui_light === "front" /* FRONT */ && mat.uniforms?.SHADE) {
5916
+ mat.uniforms.SHADE.value = false;
5917
+ }
5729
5918
  this.iterateAllMeshes((mesh) => {
5730
5919
  if (mesh.geometry.hasAttribute("color")) mat.vertexColors = true;
5731
5920
  mesh.material = mat;
@@ -5734,7 +5923,9 @@ var init_ModelObject = __esm({
5734
5923
  }
5735
5924
  }
5736
5925
  updateAnimationSubscription() {
5737
- const active = !!this.parent && (!this.isInstanced || this.instanceCounter > 0);
5926
+ let root = this;
5927
+ while (root.parent && isModelObject(root.parent)) root = root.parent;
5928
+ const active = !!root.parent && (!this.isInstanced || this.instanceCounter > 0);
5738
5929
  if (active && this.atlas?.hasAnimation && this.atlasTexture) {
5739
5930
  if (!this.unsubscribeAtlas) {
5740
5931
  this.atlasTexture.needsUpdate = true;
@@ -5750,6 +5941,9 @@ var init_ModelObject = __esm({
5750
5941
  this.unsubscribeAtlas?.();
5751
5942
  this.unsubscribeAtlas = void 0;
5752
5943
  }
5944
+ for (const child of this.children) {
5945
+ if (isModelObject(child)) child.updateAnimationSubscription();
5946
+ }
5753
5947
  }
5754
5948
  nextInstance() {
5755
5949
  const reference = super.nextInstance();
@@ -5769,6 +5963,8 @@ var init_ModelObject = __esm({
5769
5963
  this.atlasMaterial = void 0;
5770
5964
  for (const material of this.specialMaterials.values()) material.dispose();
5771
5965
  this.specialMaterials.clear();
5966
+ for (const geometry of this.geometries) geometry.dispose();
5967
+ this.geometries.clear();
5772
5968
  super.disposeAndRemoveAllChildren();
5773
5969
  }
5774
5970
  };
@@ -6060,6 +6256,7 @@ __export(index_node_exports, {
6060
6256
  Env: () => Env,
6061
6257
  FluidModelObject: () => FluidModelObject,
6062
6258
  Fonts: () => Fonts,
6259
+ GUI_CONTAINER_LAYOUTS: () => GUI_CONTAINER_LAYOUTS,
6063
6260
  Geometries: () => Geometries,
6064
6261
  GuiHelper: () => GuiHelper,
6065
6262
  GuiLight: () => GuiLight,
@@ -9454,6 +9651,7 @@ function copyDataTexture(source) {
9454
9651
 
9455
9652
  // src/gui/GuiHelper.ts
9456
9653
  init_AssetKey();
9654
+ init_ModelTextures();
9457
9655
 
9458
9656
  // src/gui/GuiText.ts
9459
9657
  var import_three20 = require("three");
@@ -9563,7 +9761,128 @@ function createGuiTextGeometry(layout, shadow = true) {
9563
9761
  }
9564
9762
 
9565
9763
  // src/gui/GuiHelper.ts
9764
+ var GUI_CONTAINER_LAYOUTS = {
9765
+ generic_54: {
9766
+ texture: "minecraft:gui/container/generic_54",
9767
+ crop: [0, 0, 176, 222],
9768
+ slotOrigin: [8, 18],
9769
+ slotOffset: [18, 18],
9770
+ rowSize: 9,
9771
+ slotCount: 54
9772
+ },
9773
+ crafting_table: {
9774
+ texture: "minecraft:gui/container/crafting_table",
9775
+ crop: [0, 0, 176, 166],
9776
+ slotOrigin: [30, 17],
9777
+ slotOffset: [18, 18],
9778
+ rowSize: 3,
9779
+ slotCount: 9,
9780
+ resultPosition: [124, 35]
9781
+ }
9782
+ };
9566
9783
  var GuiHelper = class {
9784
+ /**
9785
+ * Creates a 182×5 boss bar using the active resource pack. Finite progress is clamped to 0–1.
9786
+ * The fill crops the source image; explicit crops do not apply `.mcmeta` sprite scaling.
9787
+ */
9788
+ static async bossBar(options = {}) {
9789
+ const { color: color2 = "pink", progress = 1, position = [0, 0] } = options;
9790
+ this.validatePosition(position);
9791
+ if (!["pink", "blue", "red", "green", "yellow", "purple", "white"].includes(color2)) {
9792
+ throw new Error(`Unsupported boss bar color: ${color2}`);
9793
+ }
9794
+ if (!Number.isFinite(progress)) throw new Error("Boss bar progress must be finite");
9795
+ const value = Math.max(0, Math.min(1, progress));
9796
+ const width = value > 0 ? Math.floor(value * 181) + 1 : 0;
9797
+ const texture = `minecraft:gui/sprites/boss_bar/${color2}`;
9798
+ const layers = [{
9799
+ name: "boss-bar-background",
9800
+ texture: `${texture}_background`,
9801
+ position: [...position],
9802
+ size: [182, 5]
9803
+ }];
9804
+ if (width > 0) {
9805
+ layers.push({
9806
+ ...await this.cropTexture(`${texture}_progress`, [0, 0, width, 5], [182, 5], position),
9807
+ name: "boss-bar-progress"
9808
+ });
9809
+ }
9810
+ return layers;
9811
+ }
9812
+ /**
9813
+ * Creates a 192×192 book page with optional arrows and supplied text. Buttons are hidden by default.
9814
+ * Callers choose the page content and button states; this helper does not paginate or handle clicks.
9815
+ */
9816
+ static async book(options = {}) {
9817
+ const {
9818
+ position = [0, 0],
9819
+ previous = "hidden",
9820
+ next = "hidden",
9821
+ text: text2,
9822
+ color: color2 = 0,
9823
+ shadow = false,
9824
+ maxWidth = 114,
9825
+ lineHeight = 9,
9826
+ ...style
9827
+ } = options;
9828
+ this.validatePosition(position);
9829
+ for (const state of [previous, next]) {
9830
+ if (!["hidden", "normal", "hover"].includes(state)) throw new Error(`Unsupported book button state: ${state}`);
9831
+ }
9832
+ const layers = [{
9833
+ ...await this.cropTexture("minecraft:gui/book", [0, 0, 192, 192], [256, 256], position),
9834
+ name: "book-background"
9835
+ }];
9836
+ for (const [name, direction, state, x] of [
9837
+ ["previous", "backward", previous, 43],
9838
+ ["next", "forward", next, 116]
9839
+ ]) {
9840
+ if (state === "hidden") continue;
9841
+ layers.push({
9842
+ name: `book-${name}`,
9843
+ texture: `minecraft:gui/sprites/widget/page_${direction}${state === "hover" ? "_highlighted" : ""}`,
9844
+ position: [position[0] + x, position[1] + 157],
9845
+ size: [23, 13]
9846
+ });
9847
+ }
9848
+ if (text2 !== void 0) {
9849
+ layers.push({
9850
+ name: "book-text",
9851
+ text: text2,
9852
+ position: [position[0] + 36, position[1] + 30],
9853
+ color: color2,
9854
+ shadow,
9855
+ maxWidth,
9856
+ lineHeight,
9857
+ ...style
9858
+ });
9859
+ }
9860
+ return layers;
9861
+ }
9862
+ /** Creates a container background. Use {@link GUI_CONTAINER_LAYOUTS} with {@link inventorySlot} to place its items. */
9863
+ static async container(type, position = [0, 0]) {
9864
+ this.validatePosition(position);
9865
+ if (type !== "generic_54" && type !== "crafting_table") throw new Error(`Unsupported GUI container: ${type}`);
9866
+ const layout = GUI_CONTAINER_LAYOUTS[type];
9867
+ return [{ ...await this.cropTexture(layout.texture, layout.crop, [256, 256], position), name: "background" }];
9868
+ }
9869
+ static validatePosition(position) {
9870
+ if (!Array.isArray(position) || position.length !== 2 || !position.every(Number.isFinite)) {
9871
+ throw new Error("GUI position must contain two finite coordinates");
9872
+ }
9873
+ }
9874
+ static async cropTexture(texture, crop, referenceSize, position) {
9875
+ const image = await ModelTextures.preload(AssetKey.parse("textures", texture));
9876
+ if (!image) throw new Error(`Could not load GUI texture ${texture}`);
9877
+ const [x, y, width, height] = crop;
9878
+ const scaleX = image.width / referenceSize[0], scaleY = image.height / referenceSize[1];
9879
+ return {
9880
+ texture,
9881
+ crop: [x * scaleX, y * scaleY, width * scaleX, height * scaleY],
9882
+ position: [...position],
9883
+ size: [width, height]
9884
+ };
9885
+ }
9567
9886
  /** Sizes a tooltip around supplied text; the first entry is its title. */
9568
9887
  static async tooltip(lines, options = {}) {
9569
9888
  if (!lines.length) return [];
@@ -9598,8 +9917,9 @@ var GuiHelper = class {
9598
9917
  }
9599
9918
  /** Creates crafting-table layers for ingredients and the result; stack counts are not drawn. */
9600
9919
  static recipe(recipe, options = {}) {
9920
+ const layout = GUI_CONTAINER_LAYOUTS.crafting_table;
9601
9921
  const layers = [
9602
- { name: "background", texture: "minecraft:gui/container/crafting_table", crop: [0, 0, 176, 166] }
9922
+ { name: "background", texture: layout.texture, crop: [...layout.crop] }
9603
9923
  ];
9604
9924
  const itemKey = (ingredient) => {
9605
9925
  const item = typeof ingredient === "string" && !ingredient.startsWith("#") ? ingredient : typeof ingredient === "object" && "item" in ingredient ? ingredient.item : options.resolveIngredient?.(ingredient);
@@ -9614,7 +9934,7 @@ var GuiHelper = class {
9614
9934
  layers.push({
9615
9935
  name: `ingredient-${slot}`,
9616
9936
  item: itemKey(ingredient),
9617
- position: this.inventorySlot(slot, [30, 17], [18, 18], 3)
9937
+ position: this.inventorySlot(slot, layout.slotOrigin, layout.slotOffset, layout.rowSize)
9618
9938
  });
9619
9939
  };
9620
9940
  switch (recipe.type) {
@@ -9649,7 +9969,7 @@ var GuiHelper = class {
9649
9969
  layers.push({
9650
9970
  name: "result",
9651
9971
  item: itemKey("id" in recipe.result ? recipe.result.id : recipe.result.item),
9652
- position: [124, 35]
9972
+ position: [...layout.resultPosition]
9653
9973
  });
9654
9974
  return layers;
9655
9975
  }
@@ -9744,7 +10064,6 @@ init_SceneObject();
9744
10064
  init_Materials();
9745
10065
  init_ModelObject();
9746
10066
  init_DisplayPosition();
9747
- init_GuiLight();
9748
10067
  var GuiObject = class extends SceneObject {
9749
10068
  constructor(textureLayers, options) {
9750
10069
  super({ ...options, instanceMeshes: false, mergeMeshes: false });
@@ -9812,12 +10131,16 @@ var GuiObject = class extends SceneObject {
9812
10131
  mesh2.geometry.computeBoundingBox();
9813
10132
  bounds.union(mesh2.geometry.boundingBox.clone().applyMatrix4(new import_three26.Matrix4().multiplyMatrices(inverse, mesh2.matrixWorld)));
9814
10133
  });
9815
- item.position.z = depth - bounds.min.z;
9816
- depth += bounds.max.z - bounds.min.z + 0.01;
10134
+ item.position.z = depth;
10135
+ if (!bounds.isEmpty()) {
10136
+ item.position.z -= bounds.min.z;
10137
+ depth += bounds.max.z - bounds.min.z;
10138
+ this.bounds.expandByPoint(new import_three26.Vector2(bounds.min.x, -bounds.max.y));
10139
+ this.bounds.expandByPoint(new import_three26.Vector2(bounds.max.x, -bounds.min.y));
10140
+ }
10141
+ depth += 0.01;
9817
10142
  this.bounds.expandByPoint(new import_three26.Vector2(x, y));
9818
10143
  this.bounds.expandByPoint(new import_three26.Vector2(x + width2, y + height2));
9819
- this.bounds.expandByPoint(new import_three26.Vector2(bounds.min.x, -bounds.max.y));
9820
- this.bounds.expandByPoint(new import_three26.Vector2(bounds.max.x, -bounds.min.y));
9821
10144
  continue;
9822
10145
  }
9823
10146
  const key = typeof layer.texture === "string" ? AssetKey.parse("textures", layer.texture) : layer.texture;
@@ -9857,15 +10180,13 @@ var GuiObject = class extends SceneObject {
9857
10180
  item.name = `group:${layer.name ?? index}`;
9858
10181
  this.add(item);
9859
10182
  await item.init();
9860
- item.iterateAllMeshes((mesh) => {
9861
- if (!model.special) this.geometries.add(mesh.geometry);
10183
+ const meshes = [];
10184
+ item.iterateAllMeshes((mesh) => meshes.push(mesh));
10185
+ meshes.forEach((mesh, partIndex) => {
9862
10186
  mesh.name = `mesh:${layer.name ?? index}`;
9863
- mesh.renderOrder = index;
10187
+ mesh.renderOrder = index + partIndex / (meshes.length + 1) * 0.5;
9864
10188
  for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
9865
10189
  material.transparent = true;
9866
- if (model.gui_light === "front" /* FRONT */ && material.uniforms?.SHADE) {
9867
- material.uniforms.SHADE.value = false;
9868
- }
9869
10190
  }
9870
10191
  });
9871
10192
  return item;
@@ -16621,6 +16942,7 @@ var Chunk = class _Chunk {
16621
16942
  Env,
16622
16943
  FluidModelObject,
16623
16944
  Fonts,
16945
+ GUI_CONTAINER_LAYOUTS,
16624
16946
  Geometries,
16625
16947
  GuiHelper,
16626
16948
  GuiLight,