minerender 2.1.0-alpha.53 → 2.1.0-alpha.55

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.
package/dist/bundle.js CHANGED
@@ -32176,7 +32176,9 @@ var MineRender = (() => {
32176
32176
  this.defaultBlockStatesCache,
32177
32177
  this.blockTintCache,
32178
32178
  this.entityModelCache,
32179
- this.entityTextureCache
32179
+ this.entityTextureCache,
32180
+ this.entityAnimationCache,
32181
+ this.blockEntityIndexCache
32180
32182
  ];
32181
32183
  }
32182
32184
  static clear() {
@@ -32221,6 +32223,8 @@ var MineRender = (() => {
32221
32223
  _Caching.blockTintCache = _Caching.createAssetCache();
32222
32224
  _Caching.entityModelCache = _Caching.createAssetCache();
32223
32225
  _Caching.entityTextureCache = _Caching.createAssetCache();
32226
+ _Caching.entityAnimationCache = _Caching.createAssetCache();
32227
+ _Caching.blockEntityIndexCache = _Caching.createAssetCache();
32224
32228
  Caching = _Caching;
32225
32229
  }
32226
32230
  });
@@ -33153,6 +33157,71 @@ var MineRender = (() => {
33153
33157
  alphaTest: 0.5
33154
33158
  });
33155
33159
  }
33160
+ /** Whether vanilla's render type for the mode culls back faces. */
33161
+ static entityModeCulls(mode2 = "cutout") {
33162
+ return mode2 === "cutout_cull" || mode2 === "solid" || mode2 === "eyes" || mode2 === "water_mask";
33163
+ }
33164
+ /** UV offset per tick of entity age for the scrolling modes, as vanilla's texture matrix applies it. */
33165
+ static entityModeScroll(mode2 = "cutout") {
33166
+ if (mode2 === "energy_swirl") return [0.01, 0.01];
33167
+ if (mode2 === "breeze_wind") return [0.02, 0];
33168
+ return void 0;
33169
+ }
33170
+ /**
33171
+ * Entity material for one of vanilla's render types. Entities are unlit here, so the lit and full-bright modes
33172
+ * differ only in blending and depth state. Culling is not a material property: {@link entityModeCulls} decides
33173
+ * whether the geometry gets inward faces.
33174
+ */
33175
+ static createEntityCanvasMaterial(canvas, mode2 = "cutout", tint) {
33176
+ const material = _Materials.createBasicCanvasMaterial(canvas);
33177
+ switch (mode2) {
33178
+ case "cutout":
33179
+ case "cutout_cull":
33180
+ break;
33181
+ case "cutout_z_offset":
33182
+ material.polygonOffset = true;
33183
+ material.polygonOffsetFactor = -1;
33184
+ material.polygonOffsetUnits = -10;
33185
+ break;
33186
+ case "solid":
33187
+ material.alphaTest = 0;
33188
+ break;
33189
+ case "translucent":
33190
+ case "breeze_wind":
33191
+ material.transparent = true;
33192
+ material.alphaTest = 0.1;
33193
+ break;
33194
+ case "translucent_emissive":
33195
+ material.transparent = true;
33196
+ material.alphaTest = 0.1;
33197
+ material.depthWrite = false;
33198
+ break;
33199
+ case "eyes":
33200
+ material.transparent = true;
33201
+ material.alphaTest = 0;
33202
+ material.depthWrite = false;
33203
+ break;
33204
+ case "energy_swirl":
33205
+ material.transparent = true;
33206
+ material.alphaTest = 0.1;
33207
+ material.blending = CustomBlending;
33208
+ material.blendSrc = OneFactor;
33209
+ material.blendDst = OneFactor;
33210
+ material.blendSrcAlpha = ZeroFactor;
33211
+ material.blendDstAlpha = OneFactor;
33212
+ material.color.set(8421504);
33213
+ break;
33214
+ case "water_mask":
33215
+ material.alphaTest = 0;
33216
+ material.colorWrite = false;
33217
+ break;
33218
+ }
33219
+ if (_Materials.entityModeScroll(mode2)) {
33220
+ material.map.wrapS = material.map.wrapT = RepeatWrapping;
33221
+ }
33222
+ if (tint !== void 0) material.color.multiply(new Color(tint));
33223
+ return material;
33224
+ }
33156
33225
  static createGuiCanvasMaterial(canvas) {
33157
33226
  const material = new MeshBasicMaterial({
33158
33227
  map: Textures.createCanvasTexture(canvas),
@@ -82977,6 +83046,7 @@ ${key}` : key;
82977
83046
  BUILTIN_GENERATED: () => BUILTIN_GENERATED,
82978
83047
  BasicAssetKey: () => BasicAssetKey,
82979
83048
  BatchedExecutor: () => BatchedExecutor,
83049
+ BlockEntities: () => BlockEntities,
82980
83050
  BlockObject: () => BlockObject,
82981
83051
  BlockStateResolver: () => BlockStateResolver,
82982
83052
  BlockStates: () => BlockStates,
@@ -83000,7 +83070,9 @@ ${key}` : key;
83000
83070
  DefaultBlockStates: () => DefaultBlockStates,
83001
83071
  DisplayPosition: () => DisplayPosition,
83002
83072
  DisplayTransforms: () => DisplayTransforms,
83073
+ ENTITY_ANIMATION_CHANNELS: () => ENTITY_ANIMATION_CHANNELS,
83003
83074
  Entities: () => Entities,
83075
+ EntityAnimationPlayer: () => EntityAnimationPlayer,
83004
83076
  EntityObject: () => EntityObject,
83005
83077
  Env: () => Env,
83006
83078
  FluidModelObject: () => FluidModelObject,
@@ -83074,6 +83146,7 @@ ${key}` : key;
83074
83146
  createCanvas: () => createCanvas,
83075
83147
  createFluidGeometry: () => createFluidGeometry,
83076
83148
  createImage: () => createImage,
83149
+ entityAnimationTime: () => entityAnimationTime,
83077
83150
  estimateBytesUsed: () => estimateBytesUsed,
83078
83151
  getBlockFluidState: () => getBlockFluidState,
83079
83152
  getFluidKind: () => getFluidKind,
@@ -83107,6 +83180,8 @@ ${key}` : key;
83107
83180
  prefix: () => prefix,
83108
83181
  probeImageSize: () => probeImageSize,
83109
83182
  registerBrowserEnv: () => registerBrowserEnv,
83183
+ sampleEntityAnimation: () => sampleEntityAnimation,
83184
+ sampleEntityKeyframes: () => sampleEntityKeyframes,
83110
83185
  sampleFluid: () => sampleFluid,
83111
83186
  serializeBoxGeometryKey: () => serializeBoxGeometryKey,
83112
83187
  serializeImageKey: () => serializeImageKey,
@@ -83478,6 +83553,62 @@ ${key}` : key;
83478
83553
  init_AssetKey();
83479
83554
  init_AssetLoader();
83480
83555
  init_Assets();
83556
+
83557
+ // src/assets/BlockEntities.ts
83558
+ init_dirname();
83559
+ init_buffer2();
83560
+ init_process2();
83561
+ init_three_module();
83562
+ init_AssetKey();
83563
+ init_AssetLoader();
83564
+ init_source();
83565
+ init_Caching();
83566
+ var BlockEntities = class {
83567
+ /**
83568
+ * Loads the block index of a version. Versions and sources without one yield an empty index, and so does
83569
+ * a failed load: the index is optional, and blocks must not retry or fail with it one by one. The empty
83570
+ * result is cached like any other; `Caching.clear()` or `AssetLoader.setVersion()` forces a new attempt.
83571
+ */
83572
+ static async getIndex(root) {
83573
+ const key = new AssetKey("entity-models", "blocks", void 0, void 0, void 0, ".json", root);
83574
+ key.rootType = void 0;
83575
+ const index = await Caching.blockEntityIndexCache.get(key.serialize(), async () => {
83576
+ try {
83577
+ return await AssetLoader.get(key, "json" /* JSON */) ?? {};
83578
+ } catch (error) {
83579
+ console.warn("Could not load the block entity index; block entities are drawn as plain block models", error);
83580
+ return {};
83581
+ }
83582
+ });
83583
+ return index ?? {};
83584
+ }
83585
+ /** The index entry of a block ID such as `minecraft:chest`. */
83586
+ static entry(index, block) {
83587
+ const entry = Object.prototype.hasOwnProperty.call(index, block) ? index[block] : void 0;
83588
+ return Array.isArray(entry?.parts) ? entry : void 0;
83589
+ }
83590
+ /** Selects the parts and placement for a block ID such as `minecraft:chest`; undefined when nothing is drawn. */
83591
+ static resolve(index, block, state = {}) {
83592
+ const entry = this.entry(index, block);
83593
+ if (!entry) return void 0;
83594
+ const value = (property) => String(state[property]).toLowerCase();
83595
+ const parts = entry.parts.filter((part) => Object.entries(part.when ?? {}).every(([property, values]) => String(values).split("|").includes(value(property))));
83596
+ if (!parts.length) return void 0;
83597
+ let rotation = 0;
83598
+ if (entry.rotation) {
83599
+ const selected = value(entry.rotation.property);
83600
+ rotation = "degrees" in entry.rotation ? entry.rotation.degrees[selected] : Number(selected) * entry.rotation.step;
83601
+ if (!Number.isFinite(rotation)) rotation = 0;
83602
+ }
83603
+ return { parts, rotation, translation: entry.translation ?? [0, 0, 0] };
83604
+ }
83605
+ /** Placement in block space (0..16 per axis), applied on top of the model's own transform. */
83606
+ static matrix(resolved, matrix = new Matrix4()) {
83607
+ return matrix.makeTranslation(8, 0, 8).multiply(new Matrix4().makeRotationY(resolved.rotation * Math.PI / 180)).multiply(new Matrix4().makeTranslation(resolved.translation[0] - 8, resolved.translation[1], resolved.translation[2] - 8));
83608
+ }
83609
+ };
83610
+
83611
+ // src/assets/index.ts
83481
83612
  init_BlockStates();
83482
83613
 
83483
83614
  // src/assets/Entities.ts
@@ -83512,32 +83643,53 @@ ${key}` : key;
83512
83643
  static async getLayerList(modelKey) {
83513
83644
  return Object.keys((await this.getModelFile(modelKey))?.layers ?? {});
83514
83645
  }
83646
+ /** The dataset's extra draws on top of `main`; their `when` labels are the states `EntityModelOptions.when` can enable. */
83647
+ static async getPassList(modelKey) {
83648
+ return (await this.getModelFile(modelKey))?.passes ?? [];
83649
+ }
83515
83650
  static async getEntity(modelKey, textureKey, options) {
83516
83651
  const model = await this.getModelFile(modelKey);
83517
83652
  if (!model) return void 0;
83518
- const names = [...new Set(options?.layers ?? [options?.layer ?? "main"])];
83519
- if (names.length === 0) throw new MineRenderError(`Entity ${model.id} requires at least one layer`);
83520
- const selected = names.map((name2) => {
83521
- const layer = model.layers[name2];
83653
+ const passes = options?.layers || options?.layer !== void 0 ? [] : (model.passes ?? []).filter((pass) => pass.when === void 0 || options?.when?.includes(pass.when));
83654
+ const draws = [...new Set(options?.layers ?? [options?.layer ?? "main"])].map((layer) => ({ layer }));
83655
+ if (draws.length === 0) throw new MineRenderError(`Entity ${model.id} requires at least one layer`);
83656
+ draws.push(...passes);
83657
+ const names = [];
83658
+ const selected = draws.map((pass) => {
83659
+ const layer = model.layers[pass.layer];
83522
83660
  if (!layer) {
83523
- throw new MineRenderError(`Entity ${model.id} has no layer "${name2}". Available layers: ${Object.keys(model.layers).join(", ")}`);
83661
+ throw new MineRenderError(`Entity ${model.id} has no layer "${pass.layer}". Available layers: ${Object.keys(model.layers).join(", ")}`);
83524
83662
  }
83525
- return { name: name2, layer };
83663
+ let name2 = pass.layer;
83664
+ for (let n4 = 2; names.includes(name2); n4++) name2 = `${pass.layer}#${n4}`;
83665
+ names.push(name2);
83666
+ return { name: name2, layer, pass };
83526
83667
  });
83527
- const layers = Object.fromEntries(await Promise.all(selected.map(async ({ name: name2, layer }, index) => {
83668
+ const layers = Object.fromEntries(await Promise.all(selected.map(async ({ name: name2, layer, pass }, index) => {
83528
83669
  const override = options?.textures?.[name2] ?? (index === 0 ? textureKey : void 0);
83670
+ const textureLocation = pass.textureLocation ?? layer.textureLocation;
83529
83671
  let texture;
83530
83672
  if (override) {
83531
83673
  const path = isAssetKey(override) ? override.getFullPath() : override.path;
83532
83674
  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);
83533
- } else if (layer.textureLocation !== void 0) {
83534
- texture = AssetKey.parse("textures", layer.textureLocation.replace(/^([^:]+:)?textures\//, "$1"));
83675
+ } else if (textureLocation !== void 0) {
83676
+ texture = AssetKey.parse("textures", textureLocation.replace(/^([^:]+:)?textures\//, "$1"));
83535
83677
  } else {
83536
83678
  texture = await this.resolveTexture(modelKey);
83537
83679
  }
83538
- return [name2, { key: override ?? modelKey, texture, layer }];
83680
+ const render = pass.render ?? layer.render;
83681
+ return [name2, { key: override ?? modelKey, texture, layer, ...render && { render }, ...pass.tint && { tint: pass.tint } }];
83539
83682
  })));
83540
- return { ...layers[names[0]], id: model.id, layers };
83683
+ return { ...layers[names[0]], id: model.id, layers, ...model.transform && { transform: model.transform } };
83684
+ }
83685
+ /**
83686
+ * Vanilla's keyframe animations of a model by name, or undefined when the model or the selected version has none.
83687
+ */
83688
+ static async getAnimations(modelKey) {
83689
+ const path = isAssetKey(modelKey) ? modelKey.getFullPath() : modelKey.path;
83690
+ const key = new AssetKey(modelKey.namespace, path, void 0, void 0, "entity-models/animations", ".json");
83691
+ const file = await Caching.entityAnimationCache.get(key.serialize(), () => AssetLoader.get(key, "json" /* JSON */));
83692
+ return file?.animations;
83541
83693
  }
83542
83694
  static async resolveTexture(modelKey) {
83543
83695
  const path = isAssetKey(modelKey) ? modelKey.getFullPath() : modelKey.path;
@@ -83654,6 +83806,51 @@ ${key}` : key;
83654
83806
  init_buffer2();
83655
83807
  init_process2();
83656
83808
 
83809
+ // src/entity/EntityAnimation.ts
83810
+ init_dirname();
83811
+ init_buffer2();
83812
+ init_process2();
83813
+ var ENTITY_ANIMATION_CHANNELS = ["position", "rotation", "scale"];
83814
+ function catmullRom(delta, p0, p1, p22, p32) {
83815
+ return 0.5 * (2 * p1 + (p22 - p0) * delta + (2 * p0 - 5 * p1 + 4 * p22 - p32) * delta * delta + (3 * p1 - p0 - 3 * p22 + p32) * delta * delta * delta);
83816
+ }
83817
+ function sampleEntityKeyframes(keyframes, time) {
83818
+ if (keyframes.length === 0) return [0, 0, 0];
83819
+ let low = 0, high = keyframes.length;
83820
+ while (low < high) {
83821
+ const middle = low + high >>> 1;
83822
+ if (time <= keyframes[middle].time) high = middle;
83823
+ else low = middle + 1;
83824
+ }
83825
+ const a4 = Math.max(0, low - 1);
83826
+ const b4 = Math.min(keyframes.length - 1, a4 + 1);
83827
+ const from = keyframes[a4];
83828
+ const to = keyframes[b4];
83829
+ const delta = a4 === b4 ? 0 : Math.min(1, Math.max(0, (time - from.time) / (to.time - from.time)));
83830
+ if (to.interpolation === "catmullrom") {
83831
+ const before = keyframes[Math.max(0, a4 - 1)].value;
83832
+ const after = keyframes[Math.min(keyframes.length - 1, b4 + 1)].value;
83833
+ return [0, 1, 2].map((i4) => catmullRom(delta, before[i4], from.value[i4], to.value[i4], after[i4]));
83834
+ }
83835
+ const target = to.pre ?? to.value;
83836
+ return [0, 1, 2].map((i4) => from.value[i4] + (target[i4] - from.value[i4]) * delta);
83837
+ }
83838
+ function entityAnimationTime(animation, time, loop = animation.loop) {
83839
+ return loop && animation.length > 0 ? time % animation.length : time;
83840
+ }
83841
+ function sampleEntityAnimation(animation, time, loop = animation.loop) {
83842
+ const elapsed = entityAnimationTime(animation, time, loop);
83843
+ const pose = {};
83844
+ for (const [name2, bone] of Object.entries(animation.bones)) {
83845
+ const sampled = pose[name2] = {};
83846
+ for (const channel of ENTITY_ANIMATION_CHANNELS) {
83847
+ const keyframes = bone[channel];
83848
+ if (keyframes?.length) sampled[channel] = sampleEntityKeyframes(keyframes, elapsed);
83849
+ }
83850
+ }
83851
+ return pose;
83852
+ }
83853
+
83657
83854
  // src/entity/EntityModel.ts
83658
83855
  init_dirname();
83659
83856
  init_buffer2();
@@ -83664,6 +83861,80 @@ ${key}` : key;
83664
83861
  init_buffer2();
83665
83862
  init_process2();
83666
83863
 
83864
+ // src/entity/scene/EntityAnimationPlayer.ts
83865
+ init_dirname();
83866
+ init_buffer2();
83867
+ init_process2();
83868
+ var ZERO = [0, 0, 0];
83869
+ var EntityAnimationPlayer = class {
83870
+ constructor() {
83871
+ this.parts = [];
83872
+ this._time = 0;
83873
+ this.loop = false;
83874
+ this.speed = 1;
83875
+ }
83876
+ get animation() {
83877
+ return this._animation;
83878
+ }
83879
+ get time() {
83880
+ return this._time;
83881
+ }
83882
+ /**
83883
+ * Starts an animation on the named parts below each layer root. The current pose of a part becomes its default;
83884
+ * bones without a matching part are skipped.
83885
+ */
83886
+ play(layers, animation, options = {}) {
83887
+ this.stop();
83888
+ this._animation = animation;
83889
+ this._time = options.time ?? 0;
83890
+ this.loop = options.loop ?? animation.loop;
83891
+ this.speed = options.speed ?? 1;
83892
+ for (const layer of layers) {
83893
+ for (const bone of Object.keys(animation.bones)) {
83894
+ const object = layer.getObjectByName(`group:${bone}`);
83895
+ if (!object) continue;
83896
+ this.parts.push({ bone, object, position: object.position.clone(), rotation: object.rotation.clone(), scale: object.scale.clone() });
83897
+ }
83898
+ }
83899
+ this.apply();
83900
+ }
83901
+ /** Restores the default poses. Returns whether an animation was active. */
83902
+ stop() {
83903
+ if (!this._animation) return false;
83904
+ for (const part of this.parts) {
83905
+ part.object.position.copy(part.position);
83906
+ part.object.rotation.copy(part.rotation);
83907
+ part.object.scale.copy(part.scale);
83908
+ }
83909
+ this.clear();
83910
+ return true;
83911
+ }
83912
+ /** Forgets the animation without touching the parts, for parts that no longer exist. */
83913
+ clear() {
83914
+ this.parts = [];
83915
+ this._animation = void 0;
83916
+ this._time = 0;
83917
+ }
83918
+ setTime(seconds) {
83919
+ if (!this._animation) return false;
83920
+ this._time = seconds;
83921
+ this.apply();
83922
+ return true;
83923
+ }
83924
+ advance(deltaSeconds) {
83925
+ return this.setTime(this._time + deltaSeconds * this.speed);
83926
+ }
83927
+ apply() {
83928
+ const pose = sampleEntityAnimation(this._animation, this._time, this.loop);
83929
+ for (const part of this.parts) {
83930
+ const { position = ZERO, rotation = ZERO, scale = ZERO } = pose[part.bone];
83931
+ part.object.position.set(part.position.x + position[0], part.position.y + position[1], part.position.z + position[2]);
83932
+ part.object.rotation.set(part.rotation.x + rotation[0], part.rotation.y + rotation[1], part.rotation.z + rotation[2], part.rotation.order);
83933
+ part.object.scale.set(part.scale.x + scale[0], part.scale.y + scale[1], part.scale.z + scale[2]);
83934
+ }
83935
+ }
83936
+ };
83937
+
83667
83938
  // src/entity/scene/EntityObject.ts
83668
83939
  init_dirname();
83669
83940
  init_buffer2();
@@ -83672,6 +83943,7 @@ ${key}` : key;
83672
83943
  init_Caching();
83673
83944
  var import_ts_deepmerge4 = __toESM(require_dist());
83674
83945
  init_three_module();
83946
+ init_Ticker();
83675
83947
  init_model();
83676
83948
  init_ModelTextures();
83677
83949
  init_AssetKey();
@@ -83683,6 +83955,11 @@ ${key}` : key;
83683
83955
  this.entity = entity;
83684
83956
  this.isEntityObject = true;
83685
83957
  this.meshesCreated = false;
83958
+ /** Scrolling materials are owned: their texture offset follows this entity's age. */
83959
+ this.scrollMaterials = [];
83960
+ /** Entity age in ticks, as vanilla's `ageInTicks`; drives the scrolling render modes. */
83961
+ this.age = 0;
83962
+ this.animationPlayer = new EntityAnimationPlayer();
83686
83963
  this.options = (0, import_ts_deepmerge4.default)({}, _EntityObject.DEFAULT_OPTIONS, options ?? {});
83687
83964
  }
83688
83965
  async init() {
@@ -83692,6 +83969,59 @@ ${key}` : key;
83692
83969
  dispose() {
83693
83970
  super.dispose();
83694
83971
  }
83972
+ disposeAndRemoveAllChildren() {
83973
+ this.clearScrollMaterials();
83974
+ this.animationPlayer.clear();
83975
+ super.disposeAndRemoveAllChildren();
83976
+ }
83977
+ clearScrollMaterials() {
83978
+ Ticker.remove(this.scrollTicker);
83979
+ this.scrollTicker = void 0;
83980
+ for (const { material } of this.scrollMaterials.splice(0)) {
83981
+ material.map?.dispose();
83982
+ material.dispose();
83983
+ }
83984
+ }
83985
+ /** Applies the entity age to the scrolling materials; the ticker calls this 20 times per second. */
83986
+ updateScroll() {
83987
+ for (const { offset, speed } of this.scrollMaterials) {
83988
+ offset.set(this.age * speed[0] % 1, 0 - this.age * speed[1] % 1);
83989
+ }
83990
+ this.notifyDirty();
83991
+ }
83992
+ //<editor-fold desc="ANIMATION">
83993
+ /** The animation started by {@link playAnimation}, until {@link stopAnimation} or disposal. */
83994
+ get animation() {
83995
+ return this.animationPlayer.animation;
83996
+ }
83997
+ /** Seconds since the animation started, before looping. */
83998
+ get animationTime() {
83999
+ return this.animationPlayer.time;
84000
+ }
84001
+ /**
84002
+ * Poses the object from a keyframe animation of {@link Entities.getAnimations}, replacing the current one.
84003
+ * Parts of every layer are matched by name and their current pose is the default the offsets add to.
84004
+ * The object owns no clock: call {@link advanceAnimation} per frame, or {@link setAnimationTime}.
84005
+ */
84006
+ playAnimation(animation, options) {
84007
+ this.createMeshes();
84008
+ const layers = Object.keys(this.entityLayers).map((name2) => this.getLayerGroup(name2)).filter((layer) => !!layer);
84009
+ this.animationPlayer.play(layers, animation, options);
84010
+ this.notifyDirty();
84011
+ }
84012
+ /** Stops the animation and restores the default pose. */
84013
+ stopAnimation() {
84014
+ if (this.animationPlayer.stop()) this.notifyDirty();
84015
+ }
84016
+ /** Poses the object at an explicit time in seconds. Has no effect without an animation. */
84017
+ setAnimationTime(seconds) {
84018
+ if (this.animationPlayer.setTime(seconds)) this.notifyDirty();
84019
+ }
84020
+ /** Advances the animation by a frame delta in seconds, scaled by its speed. Has no effect without an animation. */
84021
+ advanceAnimation(deltaSeconds) {
84022
+ if (this.animationPlayer.advance(deltaSeconds)) this.notifyDirty();
84023
+ }
84024
+ //</editor-fold>
83695
84025
  getLayerGroup(name2) {
83696
84026
  return super.getGroupByName(`layer:${name2}`);
83697
84027
  }
@@ -83722,16 +84052,29 @@ ${key}` : key;
83722
84052
  createMeshes(force = false) {
83723
84053
  if (this.meshesCreated && !force) return;
83724
84054
  const modelRoot = new Object3D();
83725
- if (this.options.flip) modelRoot.scale.set(-1, -1, 1);
84055
+ const transform = this.options.flip === void 0 ? this.entity.transform : void 0;
84056
+ if (transform) {
84057
+ for (const op of transform) {
84058
+ const matrix = new Matrix4();
84059
+ if ("scale" in op) matrix.makeScale(...op.scale);
84060
+ else if ("translate" in op) matrix.makeTranslation(...op.translate);
84061
+ else matrix.makeRotationFromEuler(new Euler(...op.rotate, "ZYX"));
84062
+ modelRoot.matrix.multiply(matrix);
84063
+ }
84064
+ modelRoot.matrixAutoUpdate = false;
84065
+ } else if (this.options.flip ?? true) {
84066
+ modelRoot.scale.set(-1, -1, 1);
84067
+ }
83726
84068
  this.add(modelRoot);
83727
84069
  Object.entries(this.entityLayers).forEach(([name2, layer], index) => {
83728
84070
  const group = this.createGroup(`layer:${name2}`);
83729
84071
  modelRoot.add(group);
83730
- this.createPart("root", layer.layer.root, group, layer.layer.texture, Materials.MISSING_TEXTURE, index);
84072
+ const inward = !Materials.entityModeCulls(layer.render ?? layer.layer.render);
84073
+ this.createPart("root", layer.layer.root, group, layer.layer.texture, Materials.MISSING_TEXTURE, index, inward);
83731
84074
  });
83732
84075
  this.meshesCreated = true;
83733
84076
  }
83734
- createPart(name2, part, parent, textureSize, material, renderOrder) {
84077
+ createPart(name2, part, parent, textureSize, material, renderOrder, inward = true) {
83735
84078
  const anchor = this.createGroup(name2);
83736
84079
  anchor.position.fromArray(part.pose.offset);
83737
84080
  anchor.rotation.set(...part.pose.rotation, "ZYX");
@@ -83750,7 +84093,7 @@ ${key}` : key;
83750
84093
  uv
83751
84094
  ).clone();
83752
84095
  geometry.translate(cube.origin[0] + width / 2, cube.origin[1] + height / 2, cube.origin[2] + depth / 2);
83753
- if (Math.min(width + growX * 2, height + growY * 2, depth + growZ * 2) > 0) {
84096
+ if (inward && Math.min(width + growX * 2, height + growY * 2, depth + growZ * 2) > 0) {
83754
84097
  const index = Array.from(geometry.getIndex().array);
83755
84098
  geometry.setIndex(index.concat(index.slice().reverse()));
83756
84099
  }
@@ -83760,30 +84103,46 @@ ${key}` : key;
83760
84103
  if (this.options.wireframe) addWireframeToMesh(geometry, mesh);
83761
84104
  }
83762
84105
  for (const [childName, child] of Object.entries(part.children)) {
83763
- this.createPart(childName, child, anchor, size, material, renderOrder);
84106
+ this.createPart(childName, child, anchor, size, material, renderOrder, inward);
83764
84107
  }
83765
84108
  }
83766
84109
  async applyTextures() {
84110
+ this.clearScrollMaterials();
83767
84111
  await Promise.all(Object.entries(this.entityLayers).map(async ([name2, layer]) => {
84112
+ const mode2 = layer.render ?? layer.layer.render ?? "cutout";
84113
+ const tint = layer.tint === void 0 ? void 0 : this.options.tints?.[layer.tint];
84114
+ const scroll = Materials.entityModeScroll(mode2);
83768
84115
  const assetKeyStr = this.getTextureKey(layer).serialize();
83769
- const keyStr = `entity:${assetKeyStr}`;
83770
- let mat = Caching.materialCache.getIfPresent(keyStr);
84116
+ const keyStr = `entity:${mode2}:${tint === void 0 ? "" : new Color(tint).getHexString()}:${assetKeyStr}`;
84117
+ let mat = scroll ? void 0 : Caching.materialCache.getIfPresent(keyStr);
83771
84118
  if (!mat) {
83772
84119
  const cachedAsset = Caching.textureAssetCache.getIfPresent(assetKeyStr);
83773
84120
  const imageData = await this.loadTextures(layer);
83774
84121
  if (!imageData) return;
83775
84122
  const canvas = imageData.data.canvas;
83776
- const createMaterial = () => Materials.createBasicCanvasMaterial(canvas);
83777
- mat = cachedAsset && Caching.textureAssetCache.getIfPresent(assetKeyStr) === cachedAsset ? Caching.materialCache.get(keyStr, createMaterial) : createMaterial();
84123
+ const createMaterial = () => Materials.createEntityCanvasMaterial(canvas, mode2, tint);
84124
+ mat = !scroll && cachedAsset && Caching.textureAssetCache.getIfPresent(assetKeyStr) === cachedAsset ? Caching.materialCache.get(keyStr, createMaterial) : createMaterial();
84125
+ if (scroll) {
84126
+ this.scrollMaterials.push({ material: mat, offset: mat.map.offset, speed: scroll });
84127
+ }
83778
84128
  }
83779
84129
  this.getLayerGroup(name2)?.traverse((object) => {
83780
84130
  if (isMesh(object)) object.material = mat;
83781
84131
  });
83782
84132
  }));
84133
+ if (!this.children.length) {
84134
+ this.clearScrollMaterials();
84135
+ } else if (this.scrollMaterials.length) {
84136
+ this.updateScroll();
84137
+ this.scrollTicker = Ticker.add(() => {
84138
+ this.age++;
84139
+ this.updateScroll();
84140
+ });
84141
+ }
83783
84142
  this.notifyDirty();
83784
84143
  }
83785
84144
  };
83786
- _EntityObject.DEFAULT_OPTIONS = (0, import_ts_deepmerge4.default)({}, SceneObject.DEFAULT_OPTIONS, { flip: true });
84145
+ _EntityObject.DEFAULT_OPTIONS = (0, import_ts_deepmerge4.default)({}, SceneObject.DEFAULT_OPTIONS);
83787
84146
  var EntityObject = _EntityObject;
83788
84147
  function isEntityObject(obj) {
83789
84148
  return obj.isEntityObject;
@@ -86328,11 +86687,69 @@ property list uchar int vertex_index
86328
86687
  init_dirname();
86329
86688
  init_buffer2();
86330
86689
  init_process2();
86690
+ init_AssetKey();
86331
86691
  var GuiHelper = class {
86332
86692
  static inventorySlot(slot, origin = [0, 0], offset = [18, 18], rowSize = 9) {
86333
86693
  const [column, row] = typeof slot === "number" ? [slot % rowSize, Math.floor(slot / rowSize)] : slot;
86334
86694
  return [origin[0] + column * offset[0], origin[1] + row * offset[1]];
86335
86695
  }
86696
+ /** Creates crafting-table layers for ingredients and the result; stack counts are not drawn. */
86697
+ static recipe(recipe, options = {}) {
86698
+ const layers = [
86699
+ { name: "background", texture: "minecraft:gui/container/crafting_table", crop: [0, 0, 176, 166] }
86700
+ ];
86701
+ const itemKey = (ingredient) => {
86702
+ const item = typeof ingredient === "string" && !ingredient.startsWith("#") ? ingredient : typeof ingredient === "object" && "item" in ingredient ? ingredient.item : options.resolveIngredient?.(ingredient);
86703
+ if (isAssetKey(item)) return item;
86704
+ if (!item || item.startsWith("#")) {
86705
+ throw new Error(`Select a concrete item for recipe ingredient ${JSON.stringify(ingredient)}`);
86706
+ }
86707
+ const key = AssetKey.parse("models", item);
86708
+ return new AssetKey(key.namespace, key.getFullPath(), "models", "item");
86709
+ };
86710
+ const addIngredient = (ingredient, slot) => {
86711
+ layers.push({
86712
+ name: `ingredient-${slot}`,
86713
+ item: itemKey(ingredient),
86714
+ position: this.inventorySlot(slot, [30, 17], [18, 18], 3)
86715
+ });
86716
+ };
86717
+ switch (recipe.type) {
86718
+ case "minecraft:crafting_shaped":
86719
+ case "crafting_shaped": {
86720
+ const pattern = recipe.pattern;
86721
+ if (!pattern.length || pattern.length > 3 || !pattern[0].length || pattern[0].length > 3 || pattern.some((row) => row.length !== pattern[0].length)) {
86722
+ throw new Error("Crafting patterns must be rectangular, with 1\u20133 rows and columns");
86723
+ }
86724
+ const cells = pattern.flatMap((row, y5) => row.split("").map((symbol, x4) => ({ symbol, x: x4, y: y5 }))).filter((cell) => cell.symbol !== " ");
86725
+ if (!cells.length) throw new Error("Crafting patterns must contain an ingredient");
86726
+ const minX = Math.min(...cells.map((cell) => cell.x)), maxX = Math.max(...cells.map((cell) => cell.x));
86727
+ const minY = Math.min(...cells.map((cell) => cell.y)), maxY = Math.max(...cells.map((cell) => cell.y));
86728
+ const offsetX = minX === maxX ? 1 : 0, offsetY = minY === maxY ? 1 : 0;
86729
+ for (const { symbol, x: x4, y: y5 } of cells) {
86730
+ const ingredient = recipe.key[symbol];
86731
+ if (!ingredient) throw new Error(`Missing recipe ingredient for symbol ${JSON.stringify(symbol)}`);
86732
+ addIngredient(ingredient, (y5 - minY + offsetY) * 3 + x4 - minX + offsetX);
86733
+ }
86734
+ break;
86735
+ }
86736
+ case "minecraft:crafting_shapeless":
86737
+ case "crafting_shapeless":
86738
+ if (!recipe.ingredients.length || recipe.ingredients.length > 9) {
86739
+ throw new Error("Shapeless crafting recipes require 1\u20139 ingredients");
86740
+ }
86741
+ recipe.ingredients.forEach(addIngredient);
86742
+ break;
86743
+ default:
86744
+ throw new Error(`Unsupported crafting recipe type: ${recipe.type}`);
86745
+ }
86746
+ layers.push({
86747
+ name: "result",
86748
+ item: itemKey("id" in recipe.result ? recipe.result.id : recipe.result.item),
86749
+ position: [124, 35]
86750
+ });
86751
+ return layers;
86752
+ }
86336
86753
  };
86337
86754
 
86338
86755
  // src/gui/GuiLayer.ts
@@ -87212,6 +87629,9 @@ property list uchar int vertex_index
87212
87629
  this._variants = [];
87213
87630
  this._models = [];
87214
87631
  this._cullMask = 0;
87632
+ this._entities = [];
87633
+ this._entityPlacement = new Matrix4();
87634
+ this._entityGeneration = 0;
87215
87635
  this.options = (0, import_ts_deepmerge6.default)({}, _BlockObject.DEFAULT_OPTIONS, options ?? {});
87216
87636
  }
87217
87637
  async init() {
@@ -87231,6 +87651,8 @@ property list uchar int vertex_index
87231
87651
  }
87232
87652
  clearModels() {
87233
87653
  this.removeModels(this._models.splice(0));
87654
+ this._entityGeneration++;
87655
+ this.removeEntities(this._entities.splice(0));
87234
87656
  this._fluidKey = void 0;
87235
87657
  this._fluidModel = void 0;
87236
87658
  this._isInstanced = false;
@@ -87242,6 +87664,57 @@ property list uchar int vertex_index
87242
87664
  if (!isInstanceReference(model)) model.dispose();
87243
87665
  }
87244
87666
  }
87667
+ removeEntities(entities) {
87668
+ for (const entity of entities) {
87669
+ entity.removeFromScene();
87670
+ entity.dispose();
87671
+ }
87672
+ }
87673
+ /** Entity models drawn for this block instead of its block model; they are scene children, unlike the block. */
87674
+ get blockEntities() {
87675
+ return this._entities;
87676
+ }
87677
+ /**
87678
+ * Draws the block through the dataset's block-entity index. Returns false, leaving nothing behind,
87679
+ * when the block is not listed or one of its models cannot be loaded.
87680
+ */
87681
+ async createBlockEntities() {
87682
+ const key = this.blockState.key;
87683
+ if (!key || this.options.displayPosition) return false;
87684
+ const generation = this._entityGeneration;
87685
+ const created = [];
87686
+ let resolved;
87687
+ try {
87688
+ resolved = BlockEntities.resolve(await BlockEntities.getIndex(key.root), key.toNamespacedString(), this.state);
87689
+ if (!resolved) return false;
87690
+ for (const part of resolved.parts) {
87691
+ const [namespace, path] = part.model.includes(":") ? part.model.split(":") : [key.namespace, part.model];
87692
+ const texture = part.textureLocation === void 0 ? void 0 : AssetKey.parse("textures", part.textureLocation.replace(/^([^:]+:)?textures\//, "$1"));
87693
+ if (texture) texture.root = key.root;
87694
+ const entity = await Entities.getEntity(new BasicAssetKey(namespace, path), texture, { layer: part.layer });
87695
+ if (!entity) throw new MineRenderError(`Missing entity model ${part.model}`);
87696
+ created.push(await this.scene.addEntity(entity, { wireframe: this.options.wireframe }));
87697
+ }
87698
+ } catch (error) {
87699
+ console.warn(`Could not draw block entity ${key.toNamespacedString()}, using its block model`, error);
87700
+ this.removeEntities(created);
87701
+ return false;
87702
+ }
87703
+ if (generation !== this._entityGeneration) {
87704
+ this.removeEntities(created);
87705
+ return true;
87706
+ }
87707
+ BlockEntities.matrix(resolved, this._entityPlacement);
87708
+ this._entities = created;
87709
+ this.placeEntities();
87710
+ return true;
87711
+ }
87712
+ placeEntities() {
87713
+ const matrix = new Matrix4().makeTranslation(this.position.x - 8, this.position.y - 8, this.position.z - 8).multiply(this._entityPlacement);
87714
+ for (const entity of this._entities) {
87715
+ matrix.decompose(entity.position, entity.quaternion, entity.scale);
87716
+ }
87717
+ }
87245
87718
  get isOccluding() {
87246
87719
  return this._models.some((model) => {
87247
87720
  const object = isInstanceReference(model) ? model.instanceable : model;
@@ -87353,13 +87826,20 @@ property list uchar int vertex_index
87353
87826
  await this.updateFluid(this._fluidSampler);
87354
87827
  return;
87355
87828
  }
87829
+ const hasEntities = await this.createBlockEntities();
87356
87830
  const variantsToCreate = await this.mapStateToVariant(this.state);
87357
87831
  this._variants = variantsToCreate;
87358
87832
  for (let blockStateVariant of variantsToCreate) {
87833
+ if (hasEntities && !await this.hasGeometry(blockStateVariant)) continue;
87359
87834
  this._models.push(await this.createVariant(blockStateVariant));
87360
87835
  }
87361
87836
  await this.updateFluid(this._fluidSampler);
87362
87837
  }
87838
+ async hasGeometry(variant) {
87839
+ const modelKey = AssetKey.parse("models", variant.model);
87840
+ modelKey.root = this.blockState.key?.root;
87841
+ return !!(await Models.getMerged(modelKey))?.elements?.length;
87842
+ }
87363
87843
  getSingleVariant(variants) {
87364
87844
  return BlockStateResolver.choose(variants);
87365
87845
  }
@@ -87479,6 +87959,7 @@ property list uchar int vertex_index
87479
87959
  for (let model of this._models) {
87480
87960
  model.setPosition(position);
87481
87961
  }
87962
+ this.placeEntities();
87482
87963
  this.notifyDirty();
87483
87964
  }
87484
87965
  //TODO: should override rotation + scale methods
@@ -94365,7 +94846,8 @@ uniform float opacity;varying vec2 vUv;void main(){vec4 texel=texture2D(inputBuf
94365
94846
  this.data.set(index, void 0);
94366
94847
  return void 0;
94367
94848
  }
94368
- const template = getFluidKind(blockState.key, stored.properties) ? void 0 : await this.sectionModels?.get(blockState, stored.properties);
94849
+ const perBlock = !this.sectionModels || getFluidKind(blockState.key, stored.properties) || blockState.key && BlockEntities.entry(await BlockEntities.getIndex(blockState.key.root), blockState.key.toNamespacedString());
94850
+ const template = perBlock ? void 0 : await this.sectionModels.get(blockState, stored.properties);
94369
94851
  if (template) {
94370
94852
  this.sectionBlocks.set(index, { index, template, cullMask: 0 });
94371
94853
  this.meshDirty = true;