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.
@@ -886,6 +886,71 @@ var init_Materials = __esm({
886
886
  alphaTest: 0.5
887
887
  });
888
888
  }
889
+ /** Whether vanilla's render type for the mode culls back faces. */
890
+ static entityModeCulls(mode = "cutout") {
891
+ return mode === "cutout_cull" || mode === "solid" || mode === "eyes" || mode === "water_mask";
892
+ }
893
+ /** UV offset per tick of entity age for the scrolling modes, as vanilla's texture matrix applies it. */
894
+ static entityModeScroll(mode = "cutout") {
895
+ if (mode === "energy_swirl") return [0.01, 0.01];
896
+ if (mode === "breeze_wind") return [0.02, 0];
897
+ return void 0;
898
+ }
899
+ /**
900
+ * Entity material for one of vanilla's render types. Entities are unlit here, so the lit and full-bright modes
901
+ * differ only in blending and depth state. Culling is not a material property: {@link entityModeCulls} decides
902
+ * whether the geometry gets inward faces.
903
+ */
904
+ static createEntityCanvasMaterial(canvas, mode = "cutout", tint) {
905
+ const material = _Materials.createBasicCanvasMaterial(canvas);
906
+ switch (mode) {
907
+ case "cutout":
908
+ case "cutout_cull":
909
+ break;
910
+ case "cutout_z_offset":
911
+ material.polygonOffset = true;
912
+ material.polygonOffsetFactor = -1;
913
+ material.polygonOffsetUnits = -10;
914
+ break;
915
+ case "solid":
916
+ material.alphaTest = 0;
917
+ break;
918
+ case "translucent":
919
+ case "breeze_wind":
920
+ material.transparent = true;
921
+ material.alphaTest = 0.1;
922
+ break;
923
+ case "translucent_emissive":
924
+ material.transparent = true;
925
+ material.alphaTest = 0.1;
926
+ material.depthWrite = false;
927
+ break;
928
+ case "eyes":
929
+ material.transparent = true;
930
+ material.alphaTest = 0;
931
+ material.depthWrite = false;
932
+ break;
933
+ case "energy_swirl":
934
+ material.transparent = true;
935
+ material.alphaTest = 0.1;
936
+ material.blending = import_three5.CustomBlending;
937
+ material.blendSrc = import_three5.OneFactor;
938
+ material.blendDst = import_three5.OneFactor;
939
+ material.blendSrcAlpha = import_three5.ZeroFactor;
940
+ material.blendDstAlpha = import_three5.OneFactor;
941
+ material.color.set(8421504);
942
+ break;
943
+ case "water_mask":
944
+ material.alphaTest = 0;
945
+ material.colorWrite = false;
946
+ break;
947
+ }
948
+ if (_Materials.entityModeScroll(mode)) {
949
+ material.map.wrapS = material.map.wrapT = import_three5.RepeatWrapping;
950
+ }
951
+ if (tint !== void 0) material.color.multiply(new import_three5.Color(tint));
952
+ return material;
953
+ }
889
954
  static createGuiCanvasMaterial(canvas) {
890
955
  const material = new import_three5.MeshBasicMaterial({
891
956
  map: Textures.createCanvasTexture(canvas),
@@ -4970,32 +5035,44 @@ var Entities = class {
4970
5035
  static async getLayerList(modelKey) {
4971
5036
  return Object.keys((await this.getModelFile(modelKey))?.layers ?? {});
4972
5037
  }
5038
+ /** The dataset's extra draws on top of `main`; their `when` labels are the states `EntityModelOptions.when` can enable. */
5039
+ static async getPassList(modelKey) {
5040
+ return (await this.getModelFile(modelKey))?.passes ?? [];
5041
+ }
4973
5042
  static async getEntity(modelKey, textureKey, options) {
4974
5043
  const model = await this.getModelFile(modelKey);
4975
5044
  if (!model) return void 0;
4976
- const names = [...new Set(options?.layers ?? [options?.layer ?? "main"])];
4977
- if (names.length === 0) throw new MineRenderError(`Entity ${model.id} requires at least one layer`);
4978
- const selected = names.map((name) => {
4979
- const layer = model.layers[name];
5045
+ const passes = options?.layers || options?.layer !== void 0 ? [] : (model.passes ?? []).filter((pass) => pass.when === void 0 || options?.when?.includes(pass.when));
5046
+ const draws = [...new Set(options?.layers ?? [options?.layer ?? "main"])].map((layer) => ({ layer }));
5047
+ if (draws.length === 0) throw new MineRenderError(`Entity ${model.id} requires at least one layer`);
5048
+ draws.push(...passes);
5049
+ const names = [];
5050
+ const selected = draws.map((pass) => {
5051
+ const layer = model.layers[pass.layer];
4980
5052
  if (!layer) {
4981
- throw new MineRenderError(`Entity ${model.id} has no layer "${name}". Available layers: ${Object.keys(model.layers).join(", ")}`);
5053
+ throw new MineRenderError(`Entity ${model.id} has no layer "${pass.layer}". Available layers: ${Object.keys(model.layers).join(", ")}`);
4982
5054
  }
4983
- return { name, layer };
5055
+ let name = pass.layer;
5056
+ for (let n = 2; names.includes(name); n++) name = `${pass.layer}#${n}`;
5057
+ names.push(name);
5058
+ return { name, layer, pass };
4984
5059
  });
4985
- const layers = Object.fromEntries(await Promise.all(selected.map(async ({ name, layer }, index) => {
5060
+ const layers = Object.fromEntries(await Promise.all(selected.map(async ({ name, layer, pass }, index) => {
4986
5061
  const override = options?.textures?.[name] ?? (index === 0 ? textureKey : void 0);
5062
+ const textureLocation = pass.textureLocation ?? layer.textureLocation;
4987
5063
  let texture;
4988
5064
  if (override) {
4989
5065
  const path = isAssetKey(override) ? override.getFullPath() : override.path;
4990
5066
  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);
4991
- } else if (layer.textureLocation !== void 0) {
4992
- texture = AssetKey.parse("textures", layer.textureLocation.replace(/^([^:]+:)?textures\//, "$1"));
5067
+ } else if (textureLocation !== void 0) {
5068
+ texture = AssetKey.parse("textures", textureLocation.replace(/^([^:]+:)?textures\//, "$1"));
4993
5069
  } else {
4994
5070
  texture = await this.resolveTexture(modelKey);
4995
5071
  }
4996
- return [name, { key: override ?? modelKey, texture, layer }];
5072
+ const render = pass.render ?? layer.render;
5073
+ return [name, { key: override ?? modelKey, texture, layer, ...render && { render }, ...pass.tint && { tint: pass.tint } }];
4997
5074
  })));
4998
- return { ...layers[names[0]], id: model.id, layers };
5075
+ return { ...layers[names[0]], id: model.id, layers, ...model.transform && { transform: model.transform } };
4999
5076
  }
5000
5077
  static async resolveTexture(modelKey) {
5001
5078
  const path = isAssetKey(modelKey) ? modelKey.getFullPath() : modelKey.path;
@@ -5078,6 +5155,7 @@ init_SceneObject();
5078
5155
  init_Caching();
5079
5156
  var import_ts_deepmerge4 = __toESM(require_dist());
5080
5157
  var import_three12 = require("three");
5158
+ init_Ticker();
5081
5159
  init_model();
5082
5160
  init_ModelTextures();
5083
5161
  init_AssetKey();
@@ -5089,10 +5167,14 @@ var EntityObject = class _EntityObject extends SceneObject {
5089
5167
  this.entity = entity;
5090
5168
  this.isEntityObject = true;
5091
5169
  this.meshesCreated = false;
5170
+ /** Scrolling materials are owned: their texture offset follows this entity's age. */
5171
+ this.scrollMaterials = [];
5172
+ /** Entity age in ticks, as vanilla's `ageInTicks`; drives the scrolling render modes. */
5173
+ this.age = 0;
5092
5174
  this.options = (0, import_ts_deepmerge4.default)({}, _EntityObject.DEFAULT_OPTIONS, options ?? {});
5093
5175
  }
5094
5176
  static {
5095
- this.DEFAULT_OPTIONS = (0, import_ts_deepmerge4.default)({}, SceneObject.DEFAULT_OPTIONS, { flip: true });
5177
+ this.DEFAULT_OPTIONS = (0, import_ts_deepmerge4.default)({}, SceneObject.DEFAULT_OPTIONS);
5096
5178
  }
5097
5179
  async init() {
5098
5180
  this.createMeshes();
@@ -5101,6 +5183,25 @@ var EntityObject = class _EntityObject extends SceneObject {
5101
5183
  dispose() {
5102
5184
  super.dispose();
5103
5185
  }
5186
+ disposeAndRemoveAllChildren() {
5187
+ this.clearScrollMaterials();
5188
+ super.disposeAndRemoveAllChildren();
5189
+ }
5190
+ clearScrollMaterials() {
5191
+ Ticker.remove(this.scrollTicker);
5192
+ this.scrollTicker = void 0;
5193
+ for (const { material } of this.scrollMaterials.splice(0)) {
5194
+ material.map?.dispose();
5195
+ material.dispose();
5196
+ }
5197
+ }
5198
+ /** Applies the entity age to the scrolling materials; the ticker calls this 20 times per second. */
5199
+ updateScroll() {
5200
+ for (const { offset, speed } of this.scrollMaterials) {
5201
+ offset.set(this.age * speed[0] % 1, 0 - this.age * speed[1] % 1);
5202
+ }
5203
+ this.notifyDirty();
5204
+ }
5104
5205
  getLayerGroup(name) {
5105
5206
  return super.getGroupByName(`layer:${name}`);
5106
5207
  }
@@ -5131,16 +5232,29 @@ var EntityObject = class _EntityObject extends SceneObject {
5131
5232
  createMeshes(force = false) {
5132
5233
  if (this.meshesCreated && !force) return;
5133
5234
  const modelRoot = new import_three12.Object3D();
5134
- if (this.options.flip) modelRoot.scale.set(-1, -1, 1);
5235
+ const transform = this.options.flip === void 0 ? this.entity.transform : void 0;
5236
+ if (transform) {
5237
+ for (const op of transform) {
5238
+ const matrix = new import_three12.Matrix4();
5239
+ if ("scale" in op) matrix.makeScale(...op.scale);
5240
+ else if ("translate" in op) matrix.makeTranslation(...op.translate);
5241
+ else matrix.makeRotationFromEuler(new import_three12.Euler(...op.rotate, "ZYX"));
5242
+ modelRoot.matrix.multiply(matrix);
5243
+ }
5244
+ modelRoot.matrixAutoUpdate = false;
5245
+ } else if (this.options.flip ?? true) {
5246
+ modelRoot.scale.set(-1, -1, 1);
5247
+ }
5135
5248
  this.add(modelRoot);
5136
5249
  Object.entries(this.entityLayers).forEach(([name, layer], index) => {
5137
5250
  const group = this.createGroup(`layer:${name}`);
5138
5251
  modelRoot.add(group);
5139
- this.createPart("root", layer.layer.root, group, layer.layer.texture, Materials.MISSING_TEXTURE, index);
5252
+ const inward = !Materials.entityModeCulls(layer.render ?? layer.layer.render);
5253
+ this.createPart("root", layer.layer.root, group, layer.layer.texture, Materials.MISSING_TEXTURE, index, inward);
5140
5254
  });
5141
5255
  this.meshesCreated = true;
5142
5256
  }
5143
- createPart(name, part, parent, textureSize, material, renderOrder) {
5257
+ createPart(name, part, parent, textureSize, material, renderOrder, inward = true) {
5144
5258
  const anchor = this.createGroup(name);
5145
5259
  anchor.position.fromArray(part.pose.offset);
5146
5260
  anchor.rotation.set(...part.pose.rotation, "ZYX");
@@ -5159,7 +5273,7 @@ var EntityObject = class _EntityObject extends SceneObject {
5159
5273
  uv
5160
5274
  ).clone();
5161
5275
  geometry.translate(cube.origin[0] + width / 2, cube.origin[1] + height / 2, cube.origin[2] + depth / 2);
5162
- if (Math.min(width + growX * 2, height + growY * 2, depth + growZ * 2) > 0) {
5276
+ if (inward && Math.min(width + growX * 2, height + growY * 2, depth + growZ * 2) > 0) {
5163
5277
  const index = Array.from(geometry.getIndex().array);
5164
5278
  geometry.setIndex(index.concat(index.slice().reverse()));
5165
5279
  }
@@ -5169,26 +5283,42 @@ var EntityObject = class _EntityObject extends SceneObject {
5169
5283
  if (this.options.wireframe) addWireframeToMesh(geometry, mesh);
5170
5284
  }
5171
5285
  for (const [childName, child] of Object.entries(part.children)) {
5172
- this.createPart(childName, child, anchor, size, material, renderOrder);
5286
+ this.createPart(childName, child, anchor, size, material, renderOrder, inward);
5173
5287
  }
5174
5288
  }
5175
5289
  async applyTextures() {
5290
+ this.clearScrollMaterials();
5176
5291
  await Promise.all(Object.entries(this.entityLayers).map(async ([name, layer]) => {
5292
+ const mode = layer.render ?? layer.layer.render ?? "cutout";
5293
+ const tint = layer.tint === void 0 ? void 0 : this.options.tints?.[layer.tint];
5294
+ const scroll = Materials.entityModeScroll(mode);
5177
5295
  const assetKeyStr = this.getTextureKey(layer).serialize();
5178
- const keyStr = `entity:${assetKeyStr}`;
5179
- let mat = Caching.materialCache.getIfPresent(keyStr);
5296
+ const keyStr = `entity:${mode}:${tint === void 0 ? "" : new import_three12.Color(tint).getHexString()}:${assetKeyStr}`;
5297
+ let mat = scroll ? void 0 : Caching.materialCache.getIfPresent(keyStr);
5180
5298
  if (!mat) {
5181
5299
  const cachedAsset = Caching.textureAssetCache.getIfPresent(assetKeyStr);
5182
5300
  const imageData = await this.loadTextures(layer);
5183
5301
  if (!imageData) return;
5184
5302
  const canvas = imageData.data.canvas;
5185
- const createMaterial = () => Materials.createBasicCanvasMaterial(canvas);
5186
- mat = cachedAsset && Caching.textureAssetCache.getIfPresent(assetKeyStr) === cachedAsset ? Caching.materialCache.get(keyStr, createMaterial) : createMaterial();
5303
+ const createMaterial = () => Materials.createEntityCanvasMaterial(canvas, mode, tint);
5304
+ mat = !scroll && cachedAsset && Caching.textureAssetCache.getIfPresent(assetKeyStr) === cachedAsset ? Caching.materialCache.get(keyStr, createMaterial) : createMaterial();
5305
+ if (scroll) {
5306
+ this.scrollMaterials.push({ material: mat, offset: mat.map.offset, speed: scroll });
5307
+ }
5187
5308
  }
5188
5309
  this.getLayerGroup(name)?.traverse((object) => {
5189
5310
  if (isMesh(object)) object.material = mat;
5190
5311
  });
5191
5312
  }));
5313
+ if (!this.children.length) {
5314
+ this.clearScrollMaterials();
5315
+ } else if (this.scrollMaterials.length) {
5316
+ this.updateScroll();
5317
+ this.scrollTicker = Ticker.add(() => {
5318
+ this.age++;
5319
+ this.updateScroll();
5320
+ });
5321
+ }
5192
5322
  this.notifyDirty();
5193
5323
  }
5194
5324
  };
@@ -7704,11 +7834,69 @@ function copyDataTexture(source) {
7704
7834
  }
7705
7835
 
7706
7836
  // src/gui/GuiHelper.ts
7837
+ init_AssetKey();
7707
7838
  var GuiHelper = class {
7708
7839
  static inventorySlot(slot, origin = [0, 0], offset = [18, 18], rowSize = 9) {
7709
7840
  const [column, row] = typeof slot === "number" ? [slot % rowSize, Math.floor(slot / rowSize)] : slot;
7710
7841
  return [origin[0] + column * offset[0], origin[1] + row * offset[1]];
7711
7842
  }
7843
+ /** Creates crafting-table layers for ingredients and the result; stack counts are not drawn. */
7844
+ static recipe(recipe, options = {}) {
7845
+ const layers = [
7846
+ { name: "background", texture: "minecraft:gui/container/crafting_table", crop: [0, 0, 176, 166] }
7847
+ ];
7848
+ const itemKey = (ingredient) => {
7849
+ const item = typeof ingredient === "string" && !ingredient.startsWith("#") ? ingredient : typeof ingredient === "object" && "item" in ingredient ? ingredient.item : options.resolveIngredient?.(ingredient);
7850
+ if (isAssetKey(item)) return item;
7851
+ if (!item || item.startsWith("#")) {
7852
+ throw new Error(`Select a concrete item for recipe ingredient ${JSON.stringify(ingredient)}`);
7853
+ }
7854
+ const key = AssetKey.parse("models", item);
7855
+ return new AssetKey(key.namespace, key.getFullPath(), "models", "item");
7856
+ };
7857
+ const addIngredient = (ingredient, slot) => {
7858
+ layers.push({
7859
+ name: `ingredient-${slot}`,
7860
+ item: itemKey(ingredient),
7861
+ position: this.inventorySlot(slot, [30, 17], [18, 18], 3)
7862
+ });
7863
+ };
7864
+ switch (recipe.type) {
7865
+ case "minecraft:crafting_shaped":
7866
+ case "crafting_shaped": {
7867
+ const pattern = recipe.pattern;
7868
+ if (!pattern.length || pattern.length > 3 || !pattern[0].length || pattern[0].length > 3 || pattern.some((row) => row.length !== pattern[0].length)) {
7869
+ throw new Error("Crafting patterns must be rectangular, with 1\u20133 rows and columns");
7870
+ }
7871
+ const cells = pattern.flatMap((row, y) => row.split("").map((symbol, x) => ({ symbol, x, y }))).filter((cell) => cell.symbol !== " ");
7872
+ if (!cells.length) throw new Error("Crafting patterns must contain an ingredient");
7873
+ const minX = Math.min(...cells.map((cell) => cell.x)), maxX = Math.max(...cells.map((cell) => cell.x));
7874
+ const minY = Math.min(...cells.map((cell) => cell.y)), maxY = Math.max(...cells.map((cell) => cell.y));
7875
+ const offsetX = minX === maxX ? 1 : 0, offsetY = minY === maxY ? 1 : 0;
7876
+ for (const { symbol, x, y } of cells) {
7877
+ const ingredient = recipe.key[symbol];
7878
+ if (!ingredient) throw new Error(`Missing recipe ingredient for symbol ${JSON.stringify(symbol)}`);
7879
+ addIngredient(ingredient, (y - minY + offsetY) * 3 + x - minX + offsetX);
7880
+ }
7881
+ break;
7882
+ }
7883
+ case "minecraft:crafting_shapeless":
7884
+ case "crafting_shapeless":
7885
+ if (!recipe.ingredients.length || recipe.ingredients.length > 9) {
7886
+ throw new Error("Shapeless crafting recipes require 1\u20139 ingredients");
7887
+ }
7888
+ recipe.ingredients.forEach(addIngredient);
7889
+ break;
7890
+ default:
7891
+ throw new Error(`Unsupported crafting recipe type: ${recipe.type}`);
7892
+ }
7893
+ layers.push({
7894
+ name: "result",
7895
+ item: itemKey("id" in recipe.result ? recipe.result.id : recipe.result.item),
7896
+ position: [124, 35]
7897
+ });
7898
+ return layers;
7899
+ }
7712
7900
  };
7713
7901
 
7714
7902
  // src/gui/scene/GuiObject.ts