minerender 2.1.0-alpha.53 → 2.1.0-alpha.54

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.
@@ -839,6 +839,71 @@ var init_Materials = __esm({
839
839
  alphaTest: 0.5
840
840
  });
841
841
  }
842
+ /** Whether vanilla's render type for the mode culls back faces. */
843
+ static entityModeCulls(mode = "cutout") {
844
+ return mode === "cutout_cull" || mode === "solid" || mode === "eyes" || mode === "water_mask";
845
+ }
846
+ /** UV offset per tick of entity age for the scrolling modes, as vanilla's texture matrix applies it. */
847
+ static entityModeScroll(mode = "cutout") {
848
+ if (mode === "energy_swirl") return [0.01, 0.01];
849
+ if (mode === "breeze_wind") return [0.02, 0];
850
+ return void 0;
851
+ }
852
+ /**
853
+ * Entity material for one of vanilla's render types. Entities are unlit here, so the lit and full-bright modes
854
+ * differ only in blending and depth state. Culling is not a material property: {@link entityModeCulls} decides
855
+ * whether the geometry gets inward faces.
856
+ */
857
+ static createEntityCanvasMaterial(canvas, mode = "cutout", tint) {
858
+ const material = _Materials.createBasicCanvasMaterial(canvas);
859
+ switch (mode) {
860
+ case "cutout":
861
+ case "cutout_cull":
862
+ break;
863
+ case "cutout_z_offset":
864
+ material.polygonOffset = true;
865
+ material.polygonOffsetFactor = -1;
866
+ material.polygonOffsetUnits = -10;
867
+ break;
868
+ case "solid":
869
+ material.alphaTest = 0;
870
+ break;
871
+ case "translucent":
872
+ case "breeze_wind":
873
+ material.transparent = true;
874
+ material.alphaTest = 0.1;
875
+ break;
876
+ case "translucent_emissive":
877
+ material.transparent = true;
878
+ material.alphaTest = 0.1;
879
+ material.depthWrite = false;
880
+ break;
881
+ case "eyes":
882
+ material.transparent = true;
883
+ material.alphaTest = 0;
884
+ material.depthWrite = false;
885
+ break;
886
+ case "energy_swirl":
887
+ material.transparent = true;
888
+ material.alphaTest = 0.1;
889
+ material.blending = import_three5.CustomBlending;
890
+ material.blendSrc = import_three5.OneFactor;
891
+ material.blendDst = import_three5.OneFactor;
892
+ material.blendSrcAlpha = import_three5.ZeroFactor;
893
+ material.blendDstAlpha = import_three5.OneFactor;
894
+ material.color.set(8421504);
895
+ break;
896
+ case "water_mask":
897
+ material.alphaTest = 0;
898
+ material.colorWrite = false;
899
+ break;
900
+ }
901
+ if (_Materials.entityModeScroll(mode)) {
902
+ material.map.wrapS = material.map.wrapT = import_three5.RepeatWrapping;
903
+ }
904
+ if (tint !== void 0) material.color.multiply(new import_three5.Color(tint));
905
+ return material;
906
+ }
842
907
  static createGuiCanvasMaterial(canvas) {
843
908
  const material = new import_three5.MeshBasicMaterial({
844
909
  map: Textures.createCanvasTexture(canvas),
@@ -4925,32 +4990,44 @@ var Entities = class {
4925
4990
  static async getLayerList(modelKey) {
4926
4991
  return Object.keys((await this.getModelFile(modelKey))?.layers ?? {});
4927
4992
  }
4993
+ /** The dataset's extra draws on top of `main`; their `when` labels are the states `EntityModelOptions.when` can enable. */
4994
+ static async getPassList(modelKey) {
4995
+ return (await this.getModelFile(modelKey))?.passes ?? [];
4996
+ }
4928
4997
  static async getEntity(modelKey, textureKey, options) {
4929
4998
  const model = await this.getModelFile(modelKey);
4930
4999
  if (!model) return void 0;
4931
- const names = [...new Set(options?.layers ?? [options?.layer ?? "main"])];
4932
- if (names.length === 0) throw new MineRenderError(`Entity ${model.id} requires at least one layer`);
4933
- const selected = names.map((name) => {
4934
- const layer = model.layers[name];
5000
+ const passes = options?.layers || options?.layer !== void 0 ? [] : (model.passes ?? []).filter((pass) => pass.when === void 0 || options?.when?.includes(pass.when));
5001
+ const draws = [...new Set(options?.layers ?? [options?.layer ?? "main"])].map((layer) => ({ layer }));
5002
+ if (draws.length === 0) throw new MineRenderError(`Entity ${model.id} requires at least one layer`);
5003
+ draws.push(...passes);
5004
+ const names = [];
5005
+ const selected = draws.map((pass) => {
5006
+ const layer = model.layers[pass.layer];
4935
5007
  if (!layer) {
4936
- throw new MineRenderError(`Entity ${model.id} has no layer "${name}". Available layers: ${Object.keys(model.layers).join(", ")}`);
5008
+ throw new MineRenderError(`Entity ${model.id} has no layer "${pass.layer}". Available layers: ${Object.keys(model.layers).join(", ")}`);
4937
5009
  }
4938
- return { name, layer };
5010
+ let name = pass.layer;
5011
+ for (let n = 2; names.includes(name); n++) name = `${pass.layer}#${n}`;
5012
+ names.push(name);
5013
+ return { name, layer, pass };
4939
5014
  });
4940
- const layers = Object.fromEntries(await Promise.all(selected.map(async ({ name, layer }, index) => {
5015
+ const layers = Object.fromEntries(await Promise.all(selected.map(async ({ name, layer, pass }, index) => {
4941
5016
  const override = options?.textures?.[name] ?? (index === 0 ? textureKey : void 0);
5017
+ const textureLocation = pass.textureLocation ?? layer.textureLocation;
4942
5018
  let texture;
4943
5019
  if (override) {
4944
5020
  const path = isAssetKey(override) ? override.getFullPath() : override.path;
4945
5021
  texture = isAssetKey(override) && override.assetType === "textures" && path.startsWith("entity/") ? override : new AssetKey(override.namespace, path, "textures", "entity", "assets", ".png", isAssetKey(override) ? override.root : void 0);
4946
- } else if (layer.textureLocation !== void 0) {
4947
- texture = AssetKey.parse("textures", layer.textureLocation.replace(/^([^:]+:)?textures\//, "$1"));
5022
+ } else if (textureLocation !== void 0) {
5023
+ texture = AssetKey.parse("textures", textureLocation.replace(/^([^:]+:)?textures\//, "$1"));
4948
5024
  } else {
4949
5025
  texture = await this.resolveTexture(modelKey);
4950
5026
  }
4951
- return [name, { key: override ?? modelKey, texture, layer }];
5027
+ const render = pass.render ?? layer.render;
5028
+ return [name, { key: override ?? modelKey, texture, layer, ...render && { render }, ...pass.tint && { tint: pass.tint } }];
4952
5029
  })));
4953
- return { ...layers[names[0]], id: model.id, layers };
5030
+ return { ...layers[names[0]], id: model.id, layers, ...model.transform && { transform: model.transform } };
4954
5031
  }
4955
5032
  static async resolveTexture(modelKey) {
4956
5033
  const path = isAssetKey(modelKey) ? modelKey.getFullPath() : modelKey.path;
@@ -5033,6 +5110,7 @@ init_SceneObject();
5033
5110
  init_Caching();
5034
5111
  var import_ts_deepmerge4 = __toESM(require_dist());
5035
5112
  var import_three12 = require("three");
5113
+ init_Ticker();
5036
5114
  init_model();
5037
5115
  init_ModelTextures();
5038
5116
  init_AssetKey();
@@ -5044,6 +5122,10 @@ var _EntityObject = class _EntityObject extends SceneObject {
5044
5122
  this.entity = entity;
5045
5123
  this.isEntityObject = true;
5046
5124
  this.meshesCreated = false;
5125
+ /** Scrolling materials are owned: their texture offset follows this entity's age. */
5126
+ this.scrollMaterials = [];
5127
+ /** Entity age in ticks, as vanilla's `ageInTicks`; drives the scrolling render modes. */
5128
+ this.age = 0;
5047
5129
  this.options = (0, import_ts_deepmerge4.default)({}, _EntityObject.DEFAULT_OPTIONS, options ?? {});
5048
5130
  }
5049
5131
  async init() {
@@ -5053,6 +5135,25 @@ var _EntityObject = class _EntityObject extends SceneObject {
5053
5135
  dispose() {
5054
5136
  super.dispose();
5055
5137
  }
5138
+ disposeAndRemoveAllChildren() {
5139
+ this.clearScrollMaterials();
5140
+ super.disposeAndRemoveAllChildren();
5141
+ }
5142
+ clearScrollMaterials() {
5143
+ Ticker.remove(this.scrollTicker);
5144
+ this.scrollTicker = void 0;
5145
+ for (const { material } of this.scrollMaterials.splice(0)) {
5146
+ material.map?.dispose();
5147
+ material.dispose();
5148
+ }
5149
+ }
5150
+ /** Applies the entity age to the scrolling materials; the ticker calls this 20 times per second. */
5151
+ updateScroll() {
5152
+ for (const { offset, speed } of this.scrollMaterials) {
5153
+ offset.set(this.age * speed[0] % 1, 0 - this.age * speed[1] % 1);
5154
+ }
5155
+ this.notifyDirty();
5156
+ }
5056
5157
  getLayerGroup(name) {
5057
5158
  return super.getGroupByName(`layer:${name}`);
5058
5159
  }
@@ -5083,16 +5184,29 @@ var _EntityObject = class _EntityObject extends SceneObject {
5083
5184
  createMeshes(force = false) {
5084
5185
  if (this.meshesCreated && !force) return;
5085
5186
  const modelRoot = new import_three12.Object3D();
5086
- if (this.options.flip) modelRoot.scale.set(-1, -1, 1);
5187
+ const transform = this.options.flip === void 0 ? this.entity.transform : void 0;
5188
+ if (transform) {
5189
+ for (const op of transform) {
5190
+ const matrix = new import_three12.Matrix4();
5191
+ if ("scale" in op) matrix.makeScale(...op.scale);
5192
+ else if ("translate" in op) matrix.makeTranslation(...op.translate);
5193
+ else matrix.makeRotationFromEuler(new import_three12.Euler(...op.rotate, "ZYX"));
5194
+ modelRoot.matrix.multiply(matrix);
5195
+ }
5196
+ modelRoot.matrixAutoUpdate = false;
5197
+ } else if (this.options.flip ?? true) {
5198
+ modelRoot.scale.set(-1, -1, 1);
5199
+ }
5087
5200
  this.add(modelRoot);
5088
5201
  Object.entries(this.entityLayers).forEach(([name, layer], index) => {
5089
5202
  const group = this.createGroup(`layer:${name}`);
5090
5203
  modelRoot.add(group);
5091
- this.createPart("root", layer.layer.root, group, layer.layer.texture, Materials.MISSING_TEXTURE, index);
5204
+ const inward = !Materials.entityModeCulls(layer.render ?? layer.layer.render);
5205
+ this.createPart("root", layer.layer.root, group, layer.layer.texture, Materials.MISSING_TEXTURE, index, inward);
5092
5206
  });
5093
5207
  this.meshesCreated = true;
5094
5208
  }
5095
- createPart(name, part, parent, textureSize, material, renderOrder) {
5209
+ createPart(name, part, parent, textureSize, material, renderOrder, inward = true) {
5096
5210
  const anchor = this.createGroup(name);
5097
5211
  anchor.position.fromArray(part.pose.offset);
5098
5212
  anchor.rotation.set(...part.pose.rotation, "ZYX");
@@ -5111,7 +5225,7 @@ var _EntityObject = class _EntityObject extends SceneObject {
5111
5225
  uv
5112
5226
  ).clone();
5113
5227
  geometry.translate(cube.origin[0] + width / 2, cube.origin[1] + height / 2, cube.origin[2] + depth / 2);
5114
- if (Math.min(width + growX * 2, height + growY * 2, depth + growZ * 2) > 0) {
5228
+ if (inward && Math.min(width + growX * 2, height + growY * 2, depth + growZ * 2) > 0) {
5115
5229
  const index = Array.from(geometry.getIndex().array);
5116
5230
  geometry.setIndex(index.concat(index.slice().reverse()));
5117
5231
  }
@@ -5121,30 +5235,46 @@ var _EntityObject = class _EntityObject extends SceneObject {
5121
5235
  if (this.options.wireframe) addWireframeToMesh(geometry, mesh);
5122
5236
  }
5123
5237
  for (const [childName, child] of Object.entries(part.children)) {
5124
- this.createPart(childName, child, anchor, size, material, renderOrder);
5238
+ this.createPart(childName, child, anchor, size, material, renderOrder, inward);
5125
5239
  }
5126
5240
  }
5127
5241
  async applyTextures() {
5242
+ this.clearScrollMaterials();
5128
5243
  await Promise.all(Object.entries(this.entityLayers).map(async ([name, layer]) => {
5244
+ const mode = layer.render ?? layer.layer.render ?? "cutout";
5245
+ const tint = layer.tint === void 0 ? void 0 : this.options.tints?.[layer.tint];
5246
+ const scroll = Materials.entityModeScroll(mode);
5129
5247
  const assetKeyStr = this.getTextureKey(layer).serialize();
5130
- const keyStr = `entity:${assetKeyStr}`;
5131
- let mat = Caching.materialCache.getIfPresent(keyStr);
5248
+ const keyStr = `entity:${mode}:${tint === void 0 ? "" : new import_three12.Color(tint).getHexString()}:${assetKeyStr}`;
5249
+ let mat = scroll ? void 0 : Caching.materialCache.getIfPresent(keyStr);
5132
5250
  if (!mat) {
5133
5251
  const cachedAsset = Caching.textureAssetCache.getIfPresent(assetKeyStr);
5134
5252
  const imageData = await this.loadTextures(layer);
5135
5253
  if (!imageData) return;
5136
5254
  const canvas = imageData.data.canvas;
5137
- const createMaterial = () => Materials.createBasicCanvasMaterial(canvas);
5138
- mat = cachedAsset && Caching.textureAssetCache.getIfPresent(assetKeyStr) === cachedAsset ? Caching.materialCache.get(keyStr, createMaterial) : createMaterial();
5255
+ const createMaterial = () => Materials.createEntityCanvasMaterial(canvas, mode, tint);
5256
+ mat = !scroll && cachedAsset && Caching.textureAssetCache.getIfPresent(assetKeyStr) === cachedAsset ? Caching.materialCache.get(keyStr, createMaterial) : createMaterial();
5257
+ if (scroll) {
5258
+ this.scrollMaterials.push({ material: mat, offset: mat.map.offset, speed: scroll });
5259
+ }
5139
5260
  }
5140
5261
  this.getLayerGroup(name)?.traverse((object) => {
5141
5262
  if (isMesh(object)) object.material = mat;
5142
5263
  });
5143
5264
  }));
5265
+ if (!this.children.length) {
5266
+ this.clearScrollMaterials();
5267
+ } else if (this.scrollMaterials.length) {
5268
+ this.updateScroll();
5269
+ this.scrollTicker = Ticker.add(() => {
5270
+ this.age++;
5271
+ this.updateScroll();
5272
+ });
5273
+ }
5144
5274
  this.notifyDirty();
5145
5275
  }
5146
5276
  };
5147
- _EntityObject.DEFAULT_OPTIONS = (0, import_ts_deepmerge4.default)({}, SceneObject.DEFAULT_OPTIONS, { flip: true });
5277
+ _EntityObject.DEFAULT_OPTIONS = (0, import_ts_deepmerge4.default)({}, SceneObject.DEFAULT_OPTIONS);
5148
5278
  var EntityObject = _EntityObject;
5149
5279
  function isEntityObject(obj) {
5150
5280
  return obj.isEntityObject;
@@ -7658,11 +7788,69 @@ function copyDataTexture(source) {
7658
7788
  }
7659
7789
 
7660
7790
  // src/gui/GuiHelper.ts
7791
+ init_AssetKey();
7661
7792
  var GuiHelper = class {
7662
7793
  static inventorySlot(slot, origin = [0, 0], offset = [18, 18], rowSize = 9) {
7663
7794
  const [column, row] = typeof slot === "number" ? [slot % rowSize, Math.floor(slot / rowSize)] : slot;
7664
7795
  return [origin[0] + column * offset[0], origin[1] + row * offset[1]];
7665
7796
  }
7797
+ /** Creates crafting-table layers for ingredients and the result; stack counts are not drawn. */
7798
+ static recipe(recipe, options = {}) {
7799
+ const layers = [
7800
+ { name: "background", texture: "minecraft:gui/container/crafting_table", crop: [0, 0, 176, 166] }
7801
+ ];
7802
+ const itemKey = (ingredient) => {
7803
+ const item = typeof ingredient === "string" && !ingredient.startsWith("#") ? ingredient : typeof ingredient === "object" && "item" in ingredient ? ingredient.item : options.resolveIngredient?.(ingredient);
7804
+ if (isAssetKey(item)) return item;
7805
+ if (!item || item.startsWith("#")) {
7806
+ throw new Error(`Select a concrete item for recipe ingredient ${JSON.stringify(ingredient)}`);
7807
+ }
7808
+ const key = AssetKey.parse("models", item);
7809
+ return new AssetKey(key.namespace, key.getFullPath(), "models", "item");
7810
+ };
7811
+ const addIngredient = (ingredient, slot) => {
7812
+ layers.push({
7813
+ name: `ingredient-${slot}`,
7814
+ item: itemKey(ingredient),
7815
+ position: this.inventorySlot(slot, [30, 17], [18, 18], 3)
7816
+ });
7817
+ };
7818
+ switch (recipe.type) {
7819
+ case "minecraft:crafting_shaped":
7820
+ case "crafting_shaped": {
7821
+ const pattern = recipe.pattern;
7822
+ if (!pattern.length || pattern.length > 3 || !pattern[0].length || pattern[0].length > 3 || pattern.some((row) => row.length !== pattern[0].length)) {
7823
+ throw new Error("Crafting patterns must be rectangular, with 1\u20133 rows and columns");
7824
+ }
7825
+ const cells = pattern.flatMap((row, y) => row.split("").map((symbol, x) => ({ symbol, x, y }))).filter((cell) => cell.symbol !== " ");
7826
+ if (!cells.length) throw new Error("Crafting patterns must contain an ingredient");
7827
+ const minX = Math.min(...cells.map((cell) => cell.x)), maxX = Math.max(...cells.map((cell) => cell.x));
7828
+ const minY = Math.min(...cells.map((cell) => cell.y)), maxY = Math.max(...cells.map((cell) => cell.y));
7829
+ const offsetX = minX === maxX ? 1 : 0, offsetY = minY === maxY ? 1 : 0;
7830
+ for (const { symbol, x, y } of cells) {
7831
+ const ingredient = recipe.key[symbol];
7832
+ if (!ingredient) throw new Error(`Missing recipe ingredient for symbol ${JSON.stringify(symbol)}`);
7833
+ addIngredient(ingredient, (y - minY + offsetY) * 3 + x - minX + offsetX);
7834
+ }
7835
+ break;
7836
+ }
7837
+ case "minecraft:crafting_shapeless":
7838
+ case "crafting_shapeless":
7839
+ if (!recipe.ingredients.length || recipe.ingredients.length > 9) {
7840
+ throw new Error("Shapeless crafting recipes require 1\u20139 ingredients");
7841
+ }
7842
+ recipe.ingredients.forEach(addIngredient);
7843
+ break;
7844
+ default:
7845
+ throw new Error(`Unsupported crafting recipe type: ${recipe.type}`);
7846
+ }
7847
+ layers.push({
7848
+ name: "result",
7849
+ item: itemKey("id" in recipe.result ? recipe.result.id : recipe.result.item),
7850
+ position: [124, 35]
7851
+ });
7852
+ return layers;
7853
+ }
7666
7854
  };
7667
7855
 
7668
7856
  // src/gui/scene/GuiObject.ts