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.
@@ -1478,6 +1478,8 @@ var init_ModelMerger = __esm({
1478
1478
  ModelMerger = class {
1479
1479
  /** Loads the parent chain and returns a merged model without modifying the supplied definition. */
1480
1480
  static async mergeWithParents(model) {
1481
+ const parts = model.parts;
1482
+ if (parts) return { ...model, parts: await Promise.all(parts.map((part) => this.mergeWithParents(part))) };
1481
1483
  const models = await this.collectAllParents(model);
1482
1484
  let merged = {};
1483
1485
  for (const source of [...models, model]) {
@@ -1538,6 +1540,7 @@ var init_Models = __esm({
1538
1540
  init_PersistentCache();
1539
1541
  init_AssetKey();
1540
1542
  init_AssetParsers();
1543
+ init_DisplayPosition();
1541
1544
  Models = class _Models {
1542
1545
  // opened lazily: touching the store at import time would hit IndexedDB/disk just for loading
1543
1546
  // the library, and would require an EnvProvider before any entry has had a chance to register
@@ -1552,34 +1555,174 @@ var init_Models = __esm({
1552
1555
  return (await AssetLoader.getFirst([key, legacyKey], "list" /* LIST */))?.asset;
1553
1556
  }).then((r) => r?.files ?? []);
1554
1557
  }
1555
- static async loadAndMerge(key) {
1556
- const model = key.type === "item" ? await this.getItemModel(key) : await this.getRaw(key);
1558
+ static async loadAndMerge(key, context = {}) {
1559
+ const model = key.type === "item" ? await this.getItemModel(key, context) : await this.getRaw(key);
1557
1560
  if (!model) {
1558
1561
  return void 0;
1559
1562
  }
1560
1563
  return ModelMerger.mergeWithParents(model);
1561
1564
  }
1562
- static async getItemModel(key) {
1565
+ static async getItemModel(key, context = {}) {
1566
+ const preview = this.snapshotContext(key, context);
1563
1567
  const itemKey = new AssetKey(key.namespace, key.path, "items", void 0, key.rootType, ".json", key.root);
1564
- const model = await this.PERSISTENT_CACHE.getOrLoad(`item-v2:${AssetLoader.persistentKey(itemKey.serialize())}`, async () => {
1568
+ const cacheKey = itemKey.serialize() + this.contextKey(preview);
1569
+ const model = await this.PERSISTENT_CACHE.getOrLoad(`item-v3:${AssetLoader.persistentKey(cacheKey)}`, async () => {
1565
1570
  const result = await AssetLoader.getFirst([itemKey, key], "json" /* JSON */);
1566
1571
  if (!result) return void 0;
1567
1572
  if (result.key.assetType !== "items") return { ...result.asset, key };
1568
- const selected = this.defaultItemModel(result.asset.model, key);
1569
- const modelKey = AssetKey.parse("models", selected.model);
1570
- modelKey.root = key.root;
1571
- const model2 = await this.getRaw(modelKey);
1572
- if (!model2) throw new Error(`Item ${key.toNamespacedString()} references missing model ${selected.model}`);
1573
- return { ...model2, ...selected.special && { special: selected.special }, ...selected.tints && { tints: selected.tints } };
1573
+ const load = async (selected) => {
1574
+ if ("parts" in selected) {
1575
+ return { key, parts: await Promise.all(selected.parts.map(load)) };
1576
+ }
1577
+ if ("item" in selected) {
1578
+ const model3 = await this.getItemModel(selected.item, { displayContext: preview.displayContext });
1579
+ if (!model3) throw new Error(`Item ${key.toNamespacedString()} references missing item ${selected.item.toNamespacedString()}`);
1580
+ return model3;
1581
+ }
1582
+ const modelKey = AssetKey.parse("models", selected.model);
1583
+ modelKey.root = key.root;
1584
+ const model2 = await this.getRaw(modelKey);
1585
+ if (!model2) throw new Error(`Item ${key.toNamespacedString()} references missing model ${selected.model}`);
1586
+ return { ...model2, ...selected.special && { special: selected.special }, ...selected.tints && { tints: selected.tints } };
1587
+ };
1588
+ return load(this.selectItemModel(result.asset.model, key, preview));
1589
+ });
1590
+ const restore = (model2) => ({
1591
+ ...model2,
1592
+ key: Object.assign(new AssetKey("", ""), model2.key),
1593
+ ...model2.parts && { parts: model2.parts.map(restore) }
1574
1594
  });
1575
- return model ? { ...model, key: Object.assign(new AssetKey("", ""), model.key) } : void 0;
1595
+ return model ? restore(model) : void 0;
1596
+ }
1597
+ static contextIdentifier(id) {
1598
+ if (typeof id !== "string" || !/^(?:[a-z0-9_.-]+:)?[a-z0-9_./-]+$/.test(id)) {
1599
+ throw new Error(`Invalid item-preview identifier: ${id}`);
1600
+ }
1601
+ return id.includes(":") ? id : `minecraft:${id}`;
1602
+ }
1603
+ static snapshotContext(key, context) {
1604
+ const displayContext = context.displayContext ?? "gui" /* GUI */;
1605
+ if (displayContext !== "none" && !Object.values(DisplayPosition).includes(displayContext)) {
1606
+ throw new Error(`Unsupported item display context: ${displayContext}`);
1607
+ }
1608
+ for (const map of [context.properties, context.components, context.itemReferences]) {
1609
+ if (map !== void 0 && (!map || typeof map !== "object" || Array.isArray(map))) {
1610
+ throw new Error("Item-preview properties, components, and references must be objects keyed by identifier");
1611
+ }
1612
+ }
1613
+ const count = context.count === void 0 ? 1 : context.count;
1614
+ if (!Number.isSafeInteger(count) || count < 0) throw new Error("Item-preview count must be a nonnegative safe integer");
1615
+ const properties = {};
1616
+ for (const [id, value] of Object.entries(context.properties ?? {})) {
1617
+ const property = this.contextIdentifier(id);
1618
+ if (!["boolean", "string", "number"].includes(typeof value) || typeof value === "number" && !Number.isFinite(value)) {
1619
+ throw new Error(`Item-preview property ${property} must be a boolean, string, or finite number`);
1620
+ }
1621
+ if (Object.prototype.hasOwnProperty.call(properties, property)) throw new Error(`Duplicate item-preview property: ${property}`);
1622
+ properties[property] = value;
1623
+ }
1624
+ const components = {};
1625
+ for (const [id, value] of Object.entries(context.components ?? {})) {
1626
+ const component = this.contextIdentifier(id);
1627
+ if (Object.prototype.hasOwnProperty.call(components, component)) throw new Error(`Duplicate item-preview component: ${component}`);
1628
+ components[component] = this.snapshotJson(value);
1629
+ }
1630
+ const itemReferences = {};
1631
+ for (const [id, value] of Object.entries(context.itemReferences ?? {})) {
1632
+ const reference = this.contextIdentifier(id);
1633
+ if (!isAssetKey(value) || value.assetType !== "models" || value.type !== "item") {
1634
+ throw new Error(`Item-preview reference ${reference} must be an item-model AssetKey`);
1635
+ }
1636
+ this.contextIdentifier(`${value.namespace}:${value.path}`);
1637
+ if (Object.prototype.hasOwnProperty.call(itemReferences, reference)) throw new Error(`Duplicate item-preview reference: ${reference}`);
1638
+ itemReferences[reference] = Object.assign(new AssetKey("", ""), value, { root: value.root ?? key.root });
1639
+ }
1640
+ return { displayContext, properties, components, count, itemReferences };
1641
+ }
1642
+ static contextKey(context) {
1643
+ const itemReferences = Object.fromEntries(Object.entries(context.itemReferences).map(([id, key]) => [id, key.serialize()]));
1644
+ return `|item-v3:${JSON.stringify(this.snapshotJson({ ...context, itemReferences }))}`;
1645
+ }
1646
+ static snapshotJson(value, ancestors = /* @__PURE__ */ new Set()) {
1647
+ if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return value;
1648
+ if (typeof value !== "object" || ancestors.has(value) || !Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
1649
+ throw new Error("Item-preview components must contain finite JSON values without circular references");
1650
+ }
1651
+ ancestors.add(value);
1652
+ 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)]));
1653
+ ancestors.delete(value);
1654
+ return copy;
1655
+ }
1656
+ static propertyValue(node, context) {
1657
+ const kind = node.type.replace(/^minecraft:/, "");
1658
+ const property = node.property ? this.contextIdentifier(node.property) : void 0;
1659
+ if (kind === "select" && property === "minecraft:display_context") return context.displayContext;
1660
+ if (property && Object.prototype.hasOwnProperty.call(context.properties, property)) return context.properties[property];
1661
+ const fallback = kind === "condition" ? false : kind === "range_dispatch" ? 0 : void 0;
1662
+ const component = (id) => context.components[this.contextIdentifier(id)];
1663
+ if (property === "minecraft:custom_model_data") {
1664
+ const index = node.index ?? 0;
1665
+ if (!Number.isSafeInteger(index) || index < 0) throw new Error("Item-model custom_model_data index must be a nonnegative integer");
1666
+ const data = component("custom_model_data");
1667
+ if (data === void 0) return fallback;
1668
+ if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error("Item-preview custom_model_data must be an object");
1669
+ const field = kind === "condition" ? "flags" : kind === "select" ? "strings" : "floats";
1670
+ const values = data[field];
1671
+ if (values === void 0) return fallback;
1672
+ if (!Array.isArray(values)) throw new Error(`Item-preview custom_model_data.${field} must be an array`);
1673
+ return index < values.length ? values[index] : fallback;
1674
+ }
1675
+ if (kind === "condition" && property === "minecraft:has_component") {
1676
+ if (node.ignore_default) throw new Error("Item-preview has_component with ignore_default requires an explicit properties override; item defaults are unavailable");
1677
+ return Object.prototype.hasOwnProperty.call(context.components, this.contextIdentifier(node.component));
1678
+ }
1679
+ if (kind === "select") {
1680
+ switch (property) {
1681
+ case "minecraft:component":
1682
+ return component(node.component);
1683
+ case "minecraft:block_state": {
1684
+ const state = component("block_state");
1685
+ if (state === void 0) return void 0;
1686
+ if (!state || typeof state !== "object" || Array.isArray(state)) throw new Error("Item-preview block_state must be an object");
1687
+ if (typeof node.block_state_property !== "string") throw new Error("Item-model block_state requires block_state_property");
1688
+ return Object.prototype.hasOwnProperty.call(state, node.block_state_property) ? state[node.block_state_property] : void 0;
1689
+ }
1690
+ case "minecraft:charge_type": {
1691
+ const projectiles = component("charged_projectiles");
1692
+ if (projectiles === void 0) return "none";
1693
+ if (!Array.isArray(projectiles)) throw new Error("Item-preview charged_projectiles must be an array of item stacks");
1694
+ const ids = projectiles.map((stack) => {
1695
+ if (!stack || typeof stack !== "object" || Array.isArray(stack)) throw new Error("Item-preview charged_projectiles must contain item-stack objects");
1696
+ return this.contextIdentifier(stack.id);
1697
+ });
1698
+ return !ids.length ? "none" : ids.includes("minecraft:firework_rocket") ? "rocket" : "arrow";
1699
+ }
1700
+ }
1701
+ }
1702
+ if (kind === "range_dispatch" && (property === "minecraft:damage" || property === "minecraft:count")) {
1703
+ const damage = property === "minecraft:damage";
1704
+ const maximumComponent = component(damage ? "max_damage" : "max_stack_size");
1705
+ const maximum = maximumComponent === void 0 ? damage ? 0 : 1 : maximumComponent;
1706
+ const damageComponent = component("damage");
1707
+ const value = damage ? damageComponent === void 0 ? 0 : damageComponent : context.count;
1708
+ if (typeof value !== "number" || typeof maximum !== "number" || maximum < 0) throw new Error(`Item-preview ${property} requires numeric components and a nonnegative maximum`);
1709
+ const clamped = Math.max(0, Math.min(maximum, value));
1710
+ return node.normalize === false ? clamped : clamped / maximum;
1711
+ }
1712
+ return fallback;
1576
1713
  }
1577
- // Item previews use the GUI context, false conditions, and zero numeric properties.
1578
- static defaultItemModel(node, key) {
1714
+ static selectItemModel(node, key, context) {
1579
1715
  if (!node || typeof node.type !== "string") {
1580
1716
  throw new Error(`Unsupported item model definition for ${key.toNamespacedString()}`);
1581
1717
  }
1582
1718
  switch (node.type.replace(/^minecraft:/, "")) {
1719
+ case "composite":
1720
+ if (Array.isArray(node.models)) return { parts: node.models.map((child) => this.selectItemModel(child, key, context)) };
1721
+ break;
1722
+ case "bundle/selected_item": {
1723
+ const item = context.itemReferences["minecraft:bundle/selected_item"];
1724
+ return item ? { item } : { parts: [] };
1725
+ }
1583
1726
  case "model":
1584
1727
  if (typeof node.model === "string" && node.model) return { model: node.model, tints: node.tints };
1585
1728
  break;
@@ -1600,17 +1743,29 @@ var init_Models = __esm({
1600
1743
  }
1601
1744
  throw new Error(`Unsupported special item renderer ${special.type} for ${key.toNamespacedString()}`);
1602
1745
  }
1603
- case "condition":
1604
- return this.defaultItemModel(node.on_false, key);
1746
+ case "condition": {
1747
+ const value = this.propertyValue(node, context);
1748
+ if (typeof value !== "boolean") throw new Error(`Item-preview condition ${node.property} requires a boolean`);
1749
+ return this.selectItemModel(value ? node.on_true : node.on_false, key, context);
1750
+ }
1605
1751
  case "select": {
1606
- 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;
1607
- return this.defaultItemModel(selected ?? node.fallback, key);
1752
+ const value = this.propertyValue(node, context);
1753
+ const component = node.property && this.contextIdentifier(node.property) === "minecraft:component";
1754
+ if (!component && value !== void 0 && typeof value !== "string") throw new Error(`Item-preview selector ${node.property} requires a string`);
1755
+ const matches = (candidate) => component ? JSON.stringify(this.snapshotJson(candidate)) === JSON.stringify(this.snapshotJson(value)) : candidate === value;
1756
+ const selected = value === void 0 ? void 0 : node.cases?.find((entry) => matches(entry.when) || Array.isArray(entry.when) && entry.when.some(matches))?.model;
1757
+ return this.selectItemModel(selected ?? node.fallback, key, context);
1608
1758
  }
1609
1759
  case "range_dispatch": {
1760
+ const value = this.propertyValue(node, context);
1761
+ if (typeof value !== "number") throw new Error(`Item-preview range ${node.property} requires a number`);
1762
+ if (Number.isNaN(value)) return this.selectItemModel(node.fallback, key, context);
1763
+ const scaled = value * (node.scale ?? 1);
1764
+ if (!Number.isFinite(scaled)) throw new Error(`Item-preview range ${node.property} requires a finite value and scale`);
1610
1765
  const selected = node.entries?.reduce((match, entry) => {
1611
- return entry.threshold <= 0 && (!match || entry.threshold >= match.threshold) ? entry : match;
1766
+ return entry.threshold <= scaled && (!match || entry.threshold >= match.threshold) ? entry : match;
1612
1767
  }, void 0);
1613
- return this.defaultItemModel(selected?.model ?? node.fallback, key);
1768
+ return this.selectItemModel(selected?.model ?? node.fallback, key, context);
1614
1769
  }
1615
1770
  }
1616
1771
  throw new Error(`Unsupported item model ${node?.type ?? "definition"} for ${key.toNamespacedString()}`);
@@ -1637,25 +1792,28 @@ var init_Models = __esm({
1637
1792
  }
1638
1793
  /**
1639
1794
  * Loads a model and resolves its parent chain. Returns `undefined` when the model is missing.
1640
- * Item keys use GUI context, false conditions, and zero numeric properties.
1795
+ * Item keys default to GUI context and a stack count of 1. Unresolved conditions are false and numeric properties are zero.
1796
+ * Pass `context` to supply component values, property overrides, and item references for a preview.
1797
+ * Composite items retain independently merged children in `ItemModel.parts`.
1641
1798
  *
1642
1799
  * @param key - Model key, for example `AssetKey.parse("models", "minecraft:item/diamond_sword")`.
1643
1800
  */
1644
- static async getMerged(key) {
1801
+ static async getMerged(key, context = {}) {
1645
1802
  if (!key.assetType) {
1646
1803
  key.assetType = "models";
1647
1804
  }
1648
1805
  if (!key.extension) {
1649
1806
  key.extension = ".json";
1650
1807
  }
1651
- const keyStr = key.serialize();
1808
+ const preview = this.snapshotContext(key, context);
1809
+ const keyStr = key.serialize() + (key.type === "item" ? this.contextKey(preview) : "");
1652
1810
  return Caching.mergedModelCache.get(keyStr, (k) => {
1653
- return _Models.loadAndMerge(key);
1811
+ return _Models.loadAndMerge(key, preview);
1654
1812
  });
1655
1813
  }
1656
1814
  /** Alias for {@link getMerged}. */
1657
- static async get(key) {
1658
- return this.getMerged(key);
1815
+ static async get(key, context = {}) {
1816
+ return this.getMerged(key, context);
1659
1817
  }
1660
1818
  /** Clears persisted models. Use {@link Caching.clear} to also discard in-memory assets. */
1661
1819
  static async clearCache() {
@@ -5485,22 +5643,45 @@ var init_ModelObject = __esm({
5485
5643
  this.originalModel = originalModel;
5486
5644
  this.isModelObject = true;
5487
5645
  this.specialMaterials = /* @__PURE__ */ new Map();
5646
+ this.geometries = /* @__PURE__ */ new Set();
5488
5647
  this.meshesCreated = false;
5489
5648
  this.options = (0, import_ts_deepmerge5.default)({}, _ModelObject.DEFAULT_OPTIONS, options ?? {});
5490
- if (originalModel.special) this.options.instanceMeshes = false;
5649
+ if (originalModel.special || originalModel.parts) this.options.instanceMeshes = false;
5491
5650
  if (this.options.tints) this.options.tints = { ...this.options.tints };
5492
5651
  this.addEventListener("added", () => this.updateAnimationSubscription());
5493
5652
  this.addEventListener("removed", () => this.updateAnimationSubscription());
5494
5653
  }
5495
5654
  async init() {
5655
+ const parts = this.originalModel.parts;
5656
+ if (parts) {
5657
+ try {
5658
+ for (const part of parts) {
5659
+ const object3 = new _ModelObject(part, { ...this.options, instanceMeshes: false });
5660
+ this.add(object3);
5661
+ await object3.init();
5662
+ }
5663
+ let order = 0;
5664
+ this.iterateAllMeshes((mesh) => {
5665
+ mesh.renderOrder = order++;
5666
+ for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
5667
+ material.transparent = true;
5668
+ }
5669
+ });
5670
+ } catch (error) {
5671
+ this.disposeAndRemoveAllChildren();
5672
+ throw error;
5673
+ }
5674
+ this.notifyDirty();
5675
+ return;
5676
+ }
5496
5677
  this.options.tints = await ItemTints.get(this.originalModel, this.options.tints);
5497
5678
  const special = this.originalModel.special;
5498
5679
  if (special) {
5499
- const parts = await SpecialItems.getParts(special, this.originalModel.key?.root);
5680
+ const parts2 = await SpecialItems.getParts(special, this.originalModel.key?.root);
5500
5681
  const transform = this.options.displayPosition ? DisplayTransforms.getMatrix(this.originalModel.display, this.options.displayPosition) : new import_three25.Matrix4();
5501
5682
  transform.multiply(new import_three25.Matrix4().makeTranslation(-8, -8, -8));
5502
5683
  try {
5503
- for (const part of parts) {
5684
+ for (const part of parts2) {
5504
5685
  const object3 = new EntityObject(part.model, { flip: false, wireframe: this.options.wireframe });
5505
5686
  object3.matrix.copy(transform).multiply(part.transform);
5506
5687
  object3.matrixWorldNeedsUpdate = true;
@@ -5551,6 +5732,7 @@ var init_ModelObject = __esm({
5551
5732
  if (this.atlas.model.elements) {
5552
5733
  this.atlas.model.elements?.forEach((el) => {
5553
5734
  const elGeo = this._getBoxGeometryFromElement(el).clone();
5735
+ this.geometries.add(elGeo);
5554
5736
  if (this.options.cullMask) {
5555
5737
  const indices = Array.from(elGeo.getIndex().array);
5556
5738
  elGeo.setIndex(indices.filter((_, index) => {
@@ -5604,10 +5786,14 @@ var init_ModelObject = __esm({
5604
5786
  let combinedGeo;
5605
5787
  if (allGeos.length > 0) {
5606
5788
  combinedGeo = mergeBufferGeometries(allGeos);
5789
+ for (const geometry of allGeos) {
5790
+ this.geometries.delete(geometry);
5791
+ }
5607
5792
  } else {
5608
5793
  combinedGeo = new import_three25.BoxGeometry(16, 16, 16);
5609
5794
  if (displayTransform) DisplayTransforms.apply(combinedGeo, displayTransform);
5610
5795
  }
5796
+ this.geometries.add(combinedGeo);
5611
5797
  combinedGeo.computeBoundingBox();
5612
5798
  let mesh;
5613
5799
  if (this.options.instanceMeshes) {
@@ -5632,6 +5818,9 @@ var init_ModelObject = __esm({
5632
5818
  const mat = Materials.createShadedCanvasMaterial(this.atlas.image.canvas, this.atlas.hasTransparency, false, true);
5633
5819
  this.atlasMaterial = mat;
5634
5820
  this.atlasTexture = mat.uniforms?.map?.value ?? mat.map;
5821
+ if (this.options.displayPosition === "gui" /* GUI */ && this.originalModel.gui_light === "front" /* FRONT */ && mat.uniforms?.SHADE) {
5822
+ mat.uniforms.SHADE.value = false;
5823
+ }
5635
5824
  this.iterateAllMeshes((mesh) => {
5636
5825
  if (mesh.geometry.hasAttribute("color")) mat.vertexColors = true;
5637
5826
  mesh.material = mat;
@@ -5640,7 +5829,9 @@ var init_ModelObject = __esm({
5640
5829
  }
5641
5830
  }
5642
5831
  updateAnimationSubscription() {
5643
- const active = !!this.parent && (!this.isInstanced || this.instanceCounter > 0);
5832
+ let root = this;
5833
+ while (root.parent && isModelObject(root.parent)) root = root.parent;
5834
+ const active = !!root.parent && (!this.isInstanced || this.instanceCounter > 0);
5644
5835
  if (active && this.atlas?.hasAnimation && this.atlasTexture) {
5645
5836
  if (!this.unsubscribeAtlas) {
5646
5837
  this.atlasTexture.needsUpdate = true;
@@ -5656,6 +5847,9 @@ var init_ModelObject = __esm({
5656
5847
  this.unsubscribeAtlas?.();
5657
5848
  this.unsubscribeAtlas = void 0;
5658
5849
  }
5850
+ for (const child of this.children) {
5851
+ if (isModelObject(child)) child.updateAnimationSubscription();
5852
+ }
5659
5853
  }
5660
5854
  nextInstance() {
5661
5855
  const reference = super.nextInstance();
@@ -5675,6 +5869,8 @@ var init_ModelObject = __esm({
5675
5869
  this.atlasMaterial = void 0;
5676
5870
  for (const material of this.specialMaterials.values()) material.dispose();
5677
5871
  this.specialMaterials.clear();
5872
+ for (const geometry of this.geometries) geometry.dispose();
5873
+ this.geometries.clear();
5678
5874
  super.disposeAndRemoveAllChildren();
5679
5875
  }
5680
5876
  };
@@ -5970,6 +6166,7 @@ __export(index_browser_exports, {
5970
6166
  Env: () => Env,
5971
6167
  FluidModelObject: () => FluidModelObject,
5972
6168
  Fonts: () => Fonts,
6169
+ GUI_CONTAINER_LAYOUTS: () => GUI_CONTAINER_LAYOUTS,
5973
6170
  Geometries: () => Geometries,
5974
6171
  GuiHelper: () => GuiHelper,
5975
6172
  GuiLight: () => GuiLight,
@@ -9401,6 +9598,7 @@ function copyDataTexture(source) {
9401
9598
 
9402
9599
  // src/gui/GuiHelper.ts
9403
9600
  init_AssetKey();
9601
+ init_ModelTextures();
9404
9602
 
9405
9603
  // src/gui/GuiText.ts
9406
9604
  var import_three20 = require("three");
@@ -9510,7 +9708,128 @@ function createGuiTextGeometry(layout, shadow = true) {
9510
9708
  }
9511
9709
 
9512
9710
  // src/gui/GuiHelper.ts
9711
+ var GUI_CONTAINER_LAYOUTS = {
9712
+ generic_54: {
9713
+ texture: "minecraft:gui/container/generic_54",
9714
+ crop: [0, 0, 176, 222],
9715
+ slotOrigin: [8, 18],
9716
+ slotOffset: [18, 18],
9717
+ rowSize: 9,
9718
+ slotCount: 54
9719
+ },
9720
+ crafting_table: {
9721
+ texture: "minecraft:gui/container/crafting_table",
9722
+ crop: [0, 0, 176, 166],
9723
+ slotOrigin: [30, 17],
9724
+ slotOffset: [18, 18],
9725
+ rowSize: 3,
9726
+ slotCount: 9,
9727
+ resultPosition: [124, 35]
9728
+ }
9729
+ };
9513
9730
  var GuiHelper = class {
9731
+ /**
9732
+ * Creates a 182×5 boss bar using the active resource pack. Finite progress is clamped to 0–1.
9733
+ * The fill crops the source image; explicit crops do not apply `.mcmeta` sprite scaling.
9734
+ */
9735
+ static async bossBar(options = {}) {
9736
+ const { color: color2 = "pink", progress = 1, position = [0, 0] } = options;
9737
+ this.validatePosition(position);
9738
+ if (!["pink", "blue", "red", "green", "yellow", "purple", "white"].includes(color2)) {
9739
+ throw new Error(`Unsupported boss bar color: ${color2}`);
9740
+ }
9741
+ if (!Number.isFinite(progress)) throw new Error("Boss bar progress must be finite");
9742
+ const value = Math.max(0, Math.min(1, progress));
9743
+ const width = value > 0 ? Math.floor(value * 181) + 1 : 0;
9744
+ const texture = `minecraft:gui/sprites/boss_bar/${color2}`;
9745
+ const layers = [{
9746
+ name: "boss-bar-background",
9747
+ texture: `${texture}_background`,
9748
+ position: [...position],
9749
+ size: [182, 5]
9750
+ }];
9751
+ if (width > 0) {
9752
+ layers.push({
9753
+ ...await this.cropTexture(`${texture}_progress`, [0, 0, width, 5], [182, 5], position),
9754
+ name: "boss-bar-progress"
9755
+ });
9756
+ }
9757
+ return layers;
9758
+ }
9759
+ /**
9760
+ * Creates a 192×192 book page with optional arrows and supplied text. Buttons are hidden by default.
9761
+ * Callers choose the page content and button states; this helper does not paginate or handle clicks.
9762
+ */
9763
+ static async book(options = {}) {
9764
+ const {
9765
+ position = [0, 0],
9766
+ previous = "hidden",
9767
+ next = "hidden",
9768
+ text: text2,
9769
+ color: color2 = 0,
9770
+ shadow = false,
9771
+ maxWidth = 114,
9772
+ lineHeight = 9,
9773
+ ...style
9774
+ } = options;
9775
+ this.validatePosition(position);
9776
+ for (const state of [previous, next]) {
9777
+ if (!["hidden", "normal", "hover"].includes(state)) throw new Error(`Unsupported book button state: ${state}`);
9778
+ }
9779
+ const layers = [{
9780
+ ...await this.cropTexture("minecraft:gui/book", [0, 0, 192, 192], [256, 256], position),
9781
+ name: "book-background"
9782
+ }];
9783
+ for (const [name, direction, state, x] of [
9784
+ ["previous", "backward", previous, 43],
9785
+ ["next", "forward", next, 116]
9786
+ ]) {
9787
+ if (state === "hidden") continue;
9788
+ layers.push({
9789
+ name: `book-${name}`,
9790
+ texture: `minecraft:gui/sprites/widget/page_${direction}${state === "hover" ? "_highlighted" : ""}`,
9791
+ position: [position[0] + x, position[1] + 157],
9792
+ size: [23, 13]
9793
+ });
9794
+ }
9795
+ if (text2 !== void 0) {
9796
+ layers.push({
9797
+ name: "book-text",
9798
+ text: text2,
9799
+ position: [position[0] + 36, position[1] + 30],
9800
+ color: color2,
9801
+ shadow,
9802
+ maxWidth,
9803
+ lineHeight,
9804
+ ...style
9805
+ });
9806
+ }
9807
+ return layers;
9808
+ }
9809
+ /** Creates a container background. Use {@link GUI_CONTAINER_LAYOUTS} with {@link inventorySlot} to place its items. */
9810
+ static async container(type, position = [0, 0]) {
9811
+ this.validatePosition(position);
9812
+ if (type !== "generic_54" && type !== "crafting_table") throw new Error(`Unsupported GUI container: ${type}`);
9813
+ const layout = GUI_CONTAINER_LAYOUTS[type];
9814
+ return [{ ...await this.cropTexture(layout.texture, layout.crop, [256, 256], position), name: "background" }];
9815
+ }
9816
+ static validatePosition(position) {
9817
+ if (!Array.isArray(position) || position.length !== 2 || !position.every(Number.isFinite)) {
9818
+ throw new Error("GUI position must contain two finite coordinates");
9819
+ }
9820
+ }
9821
+ static async cropTexture(texture, crop, referenceSize, position) {
9822
+ const image = await ModelTextures.preload(AssetKey.parse("textures", texture));
9823
+ if (!image) throw new Error(`Could not load GUI texture ${texture}`);
9824
+ const [x, y, width, height] = crop;
9825
+ const scaleX = image.width / referenceSize[0], scaleY = image.height / referenceSize[1];
9826
+ return {
9827
+ texture,
9828
+ crop: [x * scaleX, y * scaleY, width * scaleX, height * scaleY],
9829
+ position: [...position],
9830
+ size: [width, height]
9831
+ };
9832
+ }
9514
9833
  /** Sizes a tooltip around supplied text; the first entry is its title. */
9515
9834
  static async tooltip(lines, options = {}) {
9516
9835
  if (!lines.length) return [];
@@ -9545,8 +9864,9 @@ var GuiHelper = class {
9545
9864
  }
9546
9865
  /** Creates crafting-table layers for ingredients and the result; stack counts are not drawn. */
9547
9866
  static recipe(recipe, options = {}) {
9867
+ const layout = GUI_CONTAINER_LAYOUTS.crafting_table;
9548
9868
  const layers = [
9549
- { name: "background", texture: "minecraft:gui/container/crafting_table", crop: [0, 0, 176, 166] }
9869
+ { name: "background", texture: layout.texture, crop: [...layout.crop] }
9550
9870
  ];
9551
9871
  const itemKey = (ingredient) => {
9552
9872
  const item = typeof ingredient === "string" && !ingredient.startsWith("#") ? ingredient : typeof ingredient === "object" && "item" in ingredient ? ingredient.item : options.resolveIngredient?.(ingredient);
@@ -9561,7 +9881,7 @@ var GuiHelper = class {
9561
9881
  layers.push({
9562
9882
  name: `ingredient-${slot}`,
9563
9883
  item: itemKey(ingredient),
9564
- position: this.inventorySlot(slot, [30, 17], [18, 18], 3)
9884
+ position: this.inventorySlot(slot, layout.slotOrigin, layout.slotOffset, layout.rowSize)
9565
9885
  });
9566
9886
  };
9567
9887
  switch (recipe.type) {
@@ -9596,7 +9916,7 @@ var GuiHelper = class {
9596
9916
  layers.push({
9597
9917
  name: "result",
9598
9918
  item: itemKey("id" in recipe.result ? recipe.result.id : recipe.result.item),
9599
- position: [124, 35]
9919
+ position: [...layout.resultPosition]
9600
9920
  });
9601
9921
  return layers;
9602
9922
  }
@@ -9691,7 +10011,6 @@ init_SceneObject();
9691
10011
  init_Materials();
9692
10012
  init_ModelObject();
9693
10013
  init_DisplayPosition();
9694
- init_GuiLight();
9695
10014
  var GuiObject = class extends SceneObject {
9696
10015
  constructor(textureLayers, options) {
9697
10016
  super({ ...options, instanceMeshes: false, mergeMeshes: false });
@@ -9759,12 +10078,16 @@ var GuiObject = class extends SceneObject {
9759
10078
  mesh2.geometry.computeBoundingBox();
9760
10079
  bounds.union(mesh2.geometry.boundingBox.clone().applyMatrix4(new import_three26.Matrix4().multiplyMatrices(inverse, mesh2.matrixWorld)));
9761
10080
  });
9762
- item.position.z = depth - bounds.min.z;
9763
- depth += bounds.max.z - bounds.min.z + 0.01;
10081
+ item.position.z = depth;
10082
+ if (!bounds.isEmpty()) {
10083
+ item.position.z -= bounds.min.z;
10084
+ depth += bounds.max.z - bounds.min.z;
10085
+ this.bounds.expandByPoint(new import_three26.Vector2(bounds.min.x, -bounds.max.y));
10086
+ this.bounds.expandByPoint(new import_three26.Vector2(bounds.max.x, -bounds.min.y));
10087
+ }
10088
+ depth += 0.01;
9764
10089
  this.bounds.expandByPoint(new import_three26.Vector2(x, y));
9765
10090
  this.bounds.expandByPoint(new import_three26.Vector2(x + width2, y + height2));
9766
- this.bounds.expandByPoint(new import_three26.Vector2(bounds.min.x, -bounds.max.y));
9767
- this.bounds.expandByPoint(new import_three26.Vector2(bounds.max.x, -bounds.min.y));
9768
10091
  continue;
9769
10092
  }
9770
10093
  const key = typeof layer.texture === "string" ? AssetKey.parse("textures", layer.texture) : layer.texture;
@@ -9804,15 +10127,13 @@ var GuiObject = class extends SceneObject {
9804
10127
  item.name = `group:${layer.name ?? index}`;
9805
10128
  this.add(item);
9806
10129
  await item.init();
9807
- item.iterateAllMeshes((mesh) => {
9808
- if (!model.special) this.geometries.add(mesh.geometry);
10130
+ const meshes = [];
10131
+ item.iterateAllMeshes((mesh) => meshes.push(mesh));
10132
+ meshes.forEach((mesh, partIndex) => {
9809
10133
  mesh.name = `mesh:${layer.name ?? index}`;
9810
- mesh.renderOrder = index;
10134
+ mesh.renderOrder = index + partIndex / (meshes.length + 1) * 0.5;
9811
10135
  for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
9812
10136
  material.transparent = true;
9813
- if (model.gui_light === "front" /* FRONT */ && material.uniforms?.SHADE) {
9814
- material.uniforms.SHADE.value = false;
9815
- }
9816
10137
  }
9817
10138
  });
9818
10139
  return item;