minerender 2.1.0-alpha.131 → 2.1.0-alpha.133

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.
@@ -310,6 +310,9 @@ var init_Caching = __esm({
310
310
  static {
311
311
  this.blockEntityIndexCache = _Caching.createAssetCache();
312
312
  }
313
+ static {
314
+ this.unihexCache = _Caching.createAssetCache();
315
+ }
313
316
  static {
314
317
  this.fontCache = _Caching.createAssetCache();
315
318
  }
@@ -352,6 +355,7 @@ var init_Caching = __esm({
352
355
  this.entityAnimationCache,
353
356
  this.blockEntityIndexCache,
354
357
  this.fontCache,
358
+ this.unihexCache,
355
359
  this.bannerPatternCache,
356
360
  this.mineRenderDataCache,
357
361
  this.usernameUuidCache,
@@ -1409,6 +1413,7 @@ var init_AssetParsers = __esm({
1409
1413
  AssetParser2["IMAGE"] = "image";
1410
1414
  AssetParser2["LIST"] = "list";
1411
1415
  AssetParser2["JSON"] = "json";
1416
+ AssetParser2["BINARY"] = "binary";
1412
1417
  return AssetParser2;
1413
1418
  })(AssetParser || {});
1414
1419
  }
@@ -2881,7 +2886,7 @@ var init_parser = __esm({
2881
2886
  });
2882
2887
 
2883
2888
  // src/assets/source/HostedAssetSource.ts
2884
- var p3, DEFAULT_OPTIONS, HostedAssetSource;
2889
+ var BINARY_TIMEOUT, p3, DEFAULT_OPTIONS, HostedAssetSource;
2885
2890
  var init_HostedAssetSource = __esm({
2886
2891
  "src/assets/source/HostedAssetSource.ts"() {
2887
2892
  init_AssetSource();
@@ -2894,6 +2899,7 @@ var init_HostedAssetSource = __esm({
2894
2899
  init_AssetLoader();
2895
2900
  init_parser();
2896
2901
  init_merge();
2902
+ BINARY_TIMEOUT = 6e4;
2897
2903
  p3 = prefix("HostedAssetSource");
2898
2904
  DEFAULT_OPTIONS = {
2899
2905
  retryDefaults: true
@@ -2964,6 +2970,17 @@ var init_HostedAssetSource = __esm({
2964
2970
  }
2965
2971
  };
2966
2972
  }
2973
+ static {
2974
+ this.BINARY = {
2975
+ config(request) {
2976
+ request.timeout = BINARY_TIMEOUT;
2977
+ request.responseType = "arraybuffer";
2978
+ },
2979
+ parse(response) {
2980
+ return { data: new Uint8Array(response.data) };
2981
+ }
2982
+ };
2983
+ }
2967
2984
  static {
2968
2985
  this.PARSER_MAP = /* @__PURE__ */ new Map([
2969
2986
  ["model" /* MODEL */, _HostedAssetSource.MODEL],
@@ -2972,7 +2989,8 @@ var init_HostedAssetSource = __esm({
2972
2989
  ["nbt" /* NBT */, _HostedAssetSource.NBT],
2973
2990
  ["image" /* IMAGE */, _HostedAssetSource.IMAGE],
2974
2991
  ["list" /* LIST */, _HostedAssetSource.LIST],
2975
- ["json" /* JSON */, _HostedAssetSource.JSON]
2992
+ ["json" /* JSON */, _HostedAssetSource.JSON],
2993
+ ["binary" /* BINARY */, _HostedAssetSource.BINARY]
2976
2994
  ]);
2977
2995
  }
2978
2996
  /** Retries missing assets with the `minecraft` namespace and default root. Other failures reject. */
@@ -4858,6 +4876,178 @@ var init_Entities = __esm({
4858
4876
  }
4859
4877
  });
4860
4878
 
4879
+ // src/assets/_fonts/Unihex.ts
4880
+ var Unihex_exports = {};
4881
+ __export(Unihex_exports, {
4882
+ loadUnihex: () => loadUnihex
4883
+ });
4884
+ function readHex(text2, filename, glyphs) {
4885
+ const lines = text2.split("\n");
4886
+ if (lines[lines.length - 1] === "") lines.pop();
4887
+ lines.forEach((line, index) => {
4888
+ const match = /^([0-9A-F]{4,6}):([0-9A-F]+)$/.exec(line);
4889
+ if (!match || ![32, 64, 96, 128].includes(match[2].length)) {
4890
+ throw new Error(`Invalid Unihex record in ${filename} at line ${index + 1}`);
4891
+ }
4892
+ const codepoint = parseInt(match[1], 16);
4893
+ if (codepoint > 1114111) throw new Error(`Invalid Unihex codepoint in ${filename} at line ${index + 1}`);
4894
+ const bitWidth = match[2].length / 4;
4895
+ const rowLength = bitWidth / 4;
4896
+ const rows = new Uint32Array(GLYPH_HEIGHT);
4897
+ let mask = 0;
4898
+ for (let y = 0; y < GLYPH_HEIGHT; y++) {
4899
+ rows[y] = parseInt(match[2].slice(y * rowLength, (y + 1) * rowLength), 16);
4900
+ mask |= rows[y];
4901
+ }
4902
+ let left = 0, right = bitWidth;
4903
+ if (mask) {
4904
+ while (!(mask >>> bitWidth - left - 1 & 1)) left++;
4905
+ right = bitWidth - 1;
4906
+ while (!(mask >>> bitWidth - right - 1 & 1)) right--;
4907
+ }
4908
+ glyphs.set(codepoint, { rows, bitWidth, left, right });
4909
+ });
4910
+ }
4911
+ async function parseUnihex(key) {
4912
+ try {
4913
+ const asset2 = await AssetLoader.get(key, "binary" /* BINARY */);
4914
+ if (!asset2) throw new Error(`Could not load Unihex archive ${key.toNamespacedString()}`);
4915
+ const records = /* @__PURE__ */ new Map();
4916
+ const reader = new import_zip2.ZipReader(new import_zip2.Uint8ArrayReader(asset2.data), { useWebWorkers: false, useCompressionStream: false });
4917
+ try {
4918
+ for (const entry of await reader.getEntries()) {
4919
+ if (!entry.directory && entry.filename.endsWith(".hex")) {
4920
+ readHex(await entry.getData(new import_zip2.TextWriter(), { checkSignature: true }), entry.filename, records);
4921
+ }
4922
+ }
4923
+ } finally {
4924
+ await reader.close();
4925
+ }
4926
+ const indices = new Int32Array(1114112).fill(-1);
4927
+ const rows = new Uint32Array(records.size * GLYPH_HEIGHT), metrics = new Uint8Array(records.size * 3);
4928
+ let index = 0;
4929
+ for (const [codepoint, record] of records) {
4930
+ indices[codepoint] = index;
4931
+ rows.set(record.rows, index * GLYPH_HEIGHT);
4932
+ metrics.set([record.bitWidth, record.left, record.right], index * 3);
4933
+ index++;
4934
+ }
4935
+ return { indices, rows, metrics };
4936
+ } catch (error) {
4937
+ console.warn(`Could not load Unihex file ${key.toNamespacedString()}`, error);
4938
+ return void 0;
4939
+ }
4940
+ }
4941
+ async function loadUnihex(provider, fontKey, characters) {
4942
+ const origin = new AssetKey(DEFAULT_NAMESPACE, "", void 0, void 0, "assets", "", fontKey.root);
4943
+ const key = AssetKey.parse("font", provider.hex_file, origin);
4944
+ key.assetType = void 0;
4945
+ const glyphs = /* @__PURE__ */ new Map();
4946
+ let overrides;
4947
+ try {
4948
+ overrides = (provider.size_overrides ?? []).map((override) => {
4949
+ const from = override.from.codePointAt(0), to = override.to.codePointAt(0);
4950
+ if ([...override.from].length !== 1 || [...override.to].length !== 1 || from >= to || !Number.isInteger(override.left) || !Number.isInteger(override.right) || override.right < override.left) {
4951
+ throw new Error(`Invalid Unihex size override in font ${fontKey.toNamespacedString()}`);
4952
+ }
4953
+ return { ...override, from, to };
4954
+ });
4955
+ } catch (error) {
4956
+ console.warn(`Could not load Unihex file ${key.toNamespacedString()}`, error);
4957
+ return glyphs;
4958
+ }
4959
+ const file = await Caching.unihexCache.get(key.serialize(), () => parseUnihex(key));
4960
+ if (!file) return glyphs;
4961
+ let atlas = atlases.get(provider);
4962
+ if (!atlas) {
4963
+ atlas = new UnihexAtlas();
4964
+ atlases.set(provider, atlas);
4965
+ }
4966
+ for (const character of characters) {
4967
+ const codepoint = character.codePointAt(0), index = file.indices[codepoint];
4968
+ if (index < 0) continue;
4969
+ const record = {
4970
+ rows: file.rows.subarray(index * GLYPH_HEIGHT, (index + 1) * GLYPH_HEIGHT),
4971
+ bitWidth: file.metrics[index * 3],
4972
+ left: file.metrics[index * 3 + 1],
4973
+ right: file.metrics[index * 3 + 2]
4974
+ };
4975
+ const override = overrides.find((range) => codepoint >= range.from && codepoint <= range.to);
4976
+ if (override) {
4977
+ record.left = override.left;
4978
+ record.right = override.right;
4979
+ }
4980
+ const width = record.right - record.left + 1;
4981
+ let rendered;
4982
+ const render = () => rendered ??= atlas.draw(record);
4983
+ glyphs.set(character, {
4984
+ get image() {
4985
+ return render().image;
4986
+ },
4987
+ get x() {
4988
+ return render().x;
4989
+ },
4990
+ get y() {
4991
+ return render().y;
4992
+ },
4993
+ width,
4994
+ height: GLYPH_HEIGHT,
4995
+ scale: 0.5,
4996
+ ascent: 7,
4997
+ advance: Math.floor(width / 2) + 1,
4998
+ boldOffset: 0.5,
4999
+ shadowOffset: 0.5
5000
+ });
5001
+ }
5002
+ return glyphs;
5003
+ }
5004
+ var import_zip2, ATLAS_SIZE, GLYPH_HEIGHT, UnihexAtlas, atlases;
5005
+ var init_Unihex = __esm({
5006
+ "src/assets/_fonts/Unihex.ts"() {
5007
+ import_zip2 = require("@zip.js/zip.js");
5008
+ init_AssetKey();
5009
+ init_AssetLoader();
5010
+ init_Assets();
5011
+ init_Caching();
5012
+ init_AssetParsers();
5013
+ init_CanvasCompat();
5014
+ ATLAS_SIZE = 512;
5015
+ GLYPH_HEIGHT = 16;
5016
+ UnihexAtlas = class {
5017
+ constructor() {
5018
+ this.x = 0;
5019
+ this.y = 0;
5020
+ }
5021
+ draw(glyph) {
5022
+ const width = glyph.right - glyph.left + 1;
5023
+ if (this.image && this.x + width > this.image.width) {
5024
+ this.x = 0;
5025
+ this.y += GLYPH_HEIGHT;
5026
+ }
5027
+ if (!this.image || this.y + GLYPH_HEIGHT > this.image.height || width > this.image.width) {
5028
+ this.image = createCanvas2(Math.max(ATLAS_SIZE, width), ATLAS_SIZE);
5029
+ this.x = 0;
5030
+ this.y = 0;
5031
+ }
5032
+ const context = this.image.getContext("2d");
5033
+ const pixels = context.createImageData(width, GLYPH_HEIGHT);
5034
+ for (let y = 0; y < GLYPH_HEIGHT; y++) {
5035
+ for (let x = 0; x < width; x++) {
5036
+ const column = glyph.left + x;
5037
+ if (column < 0 || column >= glyph.bitWidth || !(glyph.rows[y] >>> glyph.bitWidth - column - 1 & 1)) continue;
5038
+ pixels.data.fill(255, (y * width + x) * 4, (y * width + x + 1) * 4);
5039
+ }
5040
+ }
5041
+ const result = { image: this.image, x: this.x, y: this.y };
5042
+ context.putImageData(pixels, this.x, this.y);
5043
+ this.x += width;
5044
+ return result;
5045
+ }
5046
+ };
5047
+ atlases = /* @__PURE__ */ new WeakMap();
5048
+ }
5049
+ });
5050
+
4861
5051
  // src/assets/Fonts.ts
4862
5052
  var Fonts;
4863
5053
  var init_Fonts = __esm({
@@ -4868,7 +5058,7 @@ var init_Fonts = __esm({
4868
5058
  init_ModelTextures();
4869
5059
  init_Caching();
4870
5060
  Fonts = class {
4871
- /** Combines font definitions in source-priority order. Unsupported Unihex and TrueType providers are skipped. */
5061
+ /** Combines font definitions in source-priority order. TrueType providers are skipped. */
4872
5062
  static async get(key = "minecraft:default") {
4873
5063
  const fontKey = typeof key === "string" ? AssetKey.parse("font", key) : key;
4874
5064
  return await Caching.fontCache.get(fontKey.serialize(), () => this.load(fontKey, []));
@@ -4884,6 +5074,7 @@ var init_Fonts = __esm({
4884
5074
  const stack = await AssetLoader.getAll(key, "json" /* JSON */);
4885
5075
  if (stack.length === 0) throw new Error(`Could not load font ${key.toNamespacedString()}`);
4886
5076
  const glyphs = /* @__PURE__ */ new Map();
5077
+ const providers = [];
4887
5078
  for (const asset2 of stack) {
4888
5079
  for (const provider of asset2.providers) {
4889
5080
  if (provider.filter?.uniform || provider.filter?.jp) continue;
@@ -4902,17 +5093,43 @@ var init_Fonts = __esm({
4902
5093
  break;
4903
5094
  case "reference":
4904
5095
  case "minecraft:reference":
4905
- provided = (await this.load(this.relatedKey("font", provider.id, key), [...parents, serialized])).glyphs;
5096
+ const reference = await this.load(this.relatedKey("font", provider.id, key), [...parents, serialized]);
5097
+ if (reference.resolve) {
5098
+ providers.push(async (characters) => {
5099
+ await reference.resolve(characters);
5100
+ return reference.glyphs;
5101
+ });
5102
+ continue;
5103
+ }
5104
+ provided = reference.glyphs;
4906
5105
  break;
5106
+ case "unihex":
5107
+ case "minecraft:unihex":
5108
+ providers.push(async (characters) => (await Promise.resolve().then(() => (init_Unihex(), Unihex_exports))).loadUnihex(provider, key, characters));
5109
+ continue;
4907
5110
  default:
4908
5111
  continue;
4909
5112
  }
4910
- for (const [character, glyph] of provided) {
5113
+ if (providers.every((provider2) => provider2 instanceof Map)) for (const [character, glyph] of provided) {
4911
5114
  if (character !== "\0" && !glyphs.has(character)) glyphs.set(character, glyph);
4912
5115
  }
5116
+ providers.push(provided);
5117
+ }
5118
+ }
5119
+ return { glyphs, resolve: providers.every((provider) => provider instanceof Map) ? void 0 : async (characters) => {
5120
+ const remaining = new Set([...characters].filter((character) => character !== "\0" && character !== "\n" && character !== "\r" && !glyphs.has(character)));
5121
+ for (const provider of providers) {
5122
+ if (!remaining.size) break;
5123
+ const provided = provider instanceof Map ? provider : await provider([...remaining].join(""));
5124
+ for (const character of remaining) {
5125
+ const glyph = provided.get(character);
5126
+ if (glyph) {
5127
+ glyphs.set(character, glyph);
5128
+ remaining.delete(character);
5129
+ }
5130
+ }
4913
5131
  }
4914
- }
4915
- return { glyphs };
5132
+ } };
4916
5133
  }
4917
5134
  static async bitmap(provider, fontKey) {
4918
5135
  const rows = provider.chars.map((row) => [...row]);
@@ -5005,6 +5222,13 @@ var init_EntityAnimation = __esm({
5005
5222
  });
5006
5223
 
5007
5224
  // src/entity/scene/EntityAnimationPlayer.ts
5225
+ function validateAnimationWeights(weights, count) {
5226
+ const values = Array.from(weights);
5227
+ if (values.length !== count || values.some((weight) => !Number.isFinite(weight) || weight < 0)) {
5228
+ throw new Error("Animation weights must match the clip count and contain only finite nonnegative numbers");
5229
+ }
5230
+ return values;
5231
+ }
5008
5232
  var import_three11, ZERO, EntityAnimationPlayer;
5009
5233
  var init_EntityAnimationPlayer = __esm({
5010
5234
  "src/entity/scene/EntityAnimationPlayer.ts"() {
@@ -5014,12 +5238,20 @@ var init_EntityAnimationPlayer = __esm({
5014
5238
  EntityAnimationPlayer = class {
5015
5239
  constructor() {
5016
5240
  this.parts = [];
5241
+ this._animations = [];
5242
+ this._weights = [];
5243
+ this.loops = [];
5017
5244
  this._time = 0;
5018
- this.loop = false;
5019
5245
  this.speed = 1;
5020
5246
  }
5021
5247
  get animation() {
5022
- return this._animation;
5248
+ return this._animations[0];
5249
+ }
5250
+ get animations() {
5251
+ return this._animations.slice();
5252
+ }
5253
+ get weights() {
5254
+ return this._weights.slice();
5023
5255
  }
5024
5256
  get time() {
5025
5257
  return this._time;
@@ -5029,10 +5261,20 @@ var init_EntityAnimationPlayer = __esm({
5029
5261
  * without them, the current pose is used. Stopping restores the pose from before playback.
5030
5262
  */
5031
5263
  play(layers, animation, options = {}, poses) {
5264
+ this.playAnimations(layers, [animation], options, poses);
5265
+ }
5266
+ /**
5267
+ * Starts clips on matching parts with a shared clock. Weighted offsets add to the supplied baked poses
5268
+ * or, without poses, to each animated part's pose when playback starts.
5269
+ */
5270
+ playAnimations(layers, animations, options = {}, poses) {
5271
+ const weights = validateAnimationWeights(options.weights ?? animations.map(() => 1), animations.length);
5032
5272
  this.stop();
5033
- this._animation = animation;
5273
+ this._animations = animations.slice();
5274
+ this._weights = weights;
5275
+ if (!animations.length) return;
5034
5276
  this._time = options.time ?? 0;
5035
- this.loop = options.loop ?? animation.loop;
5277
+ this.loops = animations.map((animation) => options.loop ?? animation.loop);
5036
5278
  this.speed = options.speed ?? 1;
5037
5279
  for (const layer of layers) {
5038
5280
  const objects = [];
@@ -5041,7 +5283,8 @@ var init_EntityAnimationPlayer = __esm({
5041
5283
  if (poses.has(object4)) objects.push(object4);
5042
5284
  });
5043
5285
  } else {
5044
- for (const bone of Object.keys(animation.bones)) {
5286
+ const bones = new Set(animations.flatMap((animation) => Object.keys(animation.bones)));
5287
+ for (const bone of bones) {
5045
5288
  const object4 = layer.getObjectByName(`group:${bone}`);
5046
5289
  if (object4) objects.push(object4);
5047
5290
  }
@@ -5065,7 +5308,7 @@ var init_EntityAnimationPlayer = __esm({
5065
5308
  }
5066
5309
  /** Restores the poses from before playback. Returns whether an animation was active. */
5067
5310
  stop() {
5068
- if (!this._animation) return false;
5311
+ if (!this._animations.length) return false;
5069
5312
  for (const part of this.parts) {
5070
5313
  part.object.position.copy(part.position);
5071
5314
  part.object.rotation.copy(part.rotation);
@@ -5077,12 +5320,14 @@ var init_EntityAnimationPlayer = __esm({
5077
5320
  /** Forgets the animation without touching the parts, for parts that no longer exist. */
5078
5321
  clear() {
5079
5322
  this.parts = [];
5080
- this._animation = void 0;
5323
+ this._animations = [];
5324
+ this._weights = [];
5325
+ this.loops = [];
5081
5326
  this._time = 0;
5082
5327
  }
5083
5328
  /** Applies the pose at an absolute playback time. Returns `false` when no clip is active. */
5084
5329
  setTime(seconds) {
5085
- if (!this._animation) return false;
5330
+ if (!this._animations.length) return false;
5086
5331
  this._time = seconds;
5087
5332
  this.apply();
5088
5333
  return true;
@@ -5091,8 +5336,30 @@ var init_EntityAnimationPlayer = __esm({
5091
5336
  advance(deltaSeconds) {
5092
5337
  return this.setTime(this._time + deltaSeconds * this.speed);
5093
5338
  }
5339
+ /** Changes weights in clip order and reapplies the current time without restarting playback. */
5340
+ setWeights(weights) {
5341
+ if (!this._animations.length) return false;
5342
+ const validated = validateAnimationWeights(weights, this._animations.length);
5343
+ this._weights = validated;
5344
+ this.apply();
5345
+ return true;
5346
+ }
5094
5347
  apply() {
5095
- const pose = sampleEntityAnimation(this._animation, this._time, this.loop);
5348
+ const pose = /* @__PURE__ */ Object.create(null);
5349
+ for (let i = 0; i < this._animations.length; i++) {
5350
+ const weight = this._weights[i];
5351
+ if (weight === 0) continue;
5352
+ const sampled = sampleEntityAnimation(this._animations[i], this._time, this.loops[i]);
5353
+ for (const [bone, channels] of Object.entries(sampled)) {
5354
+ const blended = pose[bone] ??= {};
5355
+ for (const channel of ENTITY_ANIMATION_CHANNELS) {
5356
+ const value = channels[channel];
5357
+ if (!value) continue;
5358
+ const target = blended[channel] ??= [0, 0, 0];
5359
+ for (let axis = 0; axis < 3; axis++) target[axis] += value[axis] * weight;
5360
+ }
5361
+ }
5362
+ }
5096
5363
  for (const part of this.parts) {
5097
5364
  const { position = ZERO, rotation = ZERO, scale = ZERO } = pose[part.bone] ?? {};
5098
5365
  part.object.position.set(part.basePosition.x + position[0], part.basePosition.y + position[1], part.basePosition.z + position[2]);
@@ -5716,7 +5983,7 @@ var init_EntityObject = __esm({
5716
5983
  for (const geometry of this.geometries) geometry.dispose();
5717
5984
  this.geometries.clear();
5718
5985
  for (const dispose of this.disposeWireframes.splice(0)) dispose();
5719
- for (const player of this.animationPlayers.splice(0)) player.clear();
5986
+ for (const { player } of this.animationPlayers.splice(0)) player.clear();
5720
5987
  this.partPoses.clear();
5721
5988
  super.disposeAndRemoveAllChildren();
5722
5989
  }
@@ -5751,15 +6018,31 @@ var init_EntityObject = __esm({
5751
6018
  //<editor-fold desc="ANIMATION">
5752
6019
  /** The first active clip, until playback is stopped or the object is disposed. */
5753
6020
  get animation() {
5754
- return this.animationPlayers[0]?.animation;
6021
+ return this.animationPlayers[0]?.player.animation;
5755
6022
  }
5756
- /** Clips playing together, each targeting a separate geometry layer. */
6023
+ /** Clips playing together, in the order passed to {@link playAnimations}. */
5757
6024
  get activeAnimations() {
5758
- return this.animationPlayers.map((player) => player.animation);
6025
+ const animations = [];
6026
+ for (const { player, indices } of this.animationPlayers) {
6027
+ player.animations.forEach((animation, i) => {
6028
+ animations[indices[i]] = animation;
6029
+ });
6030
+ }
6031
+ return animations;
6032
+ }
6033
+ /** Weights of the active clips, in the same order as {@link activeAnimations}. */
6034
+ get animationWeights() {
6035
+ const weights = [];
6036
+ for (const { player, indices } of this.animationPlayers) {
6037
+ player.weights.forEach((weight, i) => {
6038
+ weights[indices[i]] = weight;
6039
+ });
6040
+ }
6041
+ return weights;
5759
6042
  }
5760
6043
  /** Seconds since the animation started, before looping. */
5761
6044
  get animationTime() {
5762
- return this.animationPlayers[0]?.time ?? 0;
6045
+ return this.animationPlayers[0]?.player.time ?? 0;
5763
6046
  }
5764
6047
  /**
5765
6048
  * Plays a clip from {@link Entities.getAnimations}, replacing all active clips.
@@ -5770,42 +6053,59 @@ var init_EntityObject = __esm({
5770
6053
  playAnimation(animation, options) {
5771
6054
  this.playAnimations([animation], options);
5772
6055
  }
5773
- /** Plays clips with a shared clock and no blending. Selected layers without their own clip follow `main`. */
6056
+ /**
6057
+ * Plays clips with a shared clock, adding their weighted offsets within each geometry layer.
6058
+ * Weights default to 1 and are not normalized. Selected layers without their own clip follow `main`.
6059
+ */
5774
6060
  playAnimations(animations, options) {
5775
- const targets = /* @__PURE__ */ new Set();
5776
- const selections = animations.map((animation) => {
6061
+ const weights = validateAnimationWeights(options?.weights ?? animations.map(() => 1), animations.length);
6062
+ const selections = /* @__PURE__ */ new Map();
6063
+ animations.forEach((animation, index) => {
5777
6064
  const layer = animation.layer ?? "main";
5778
- if (targets.has(layer)) throw new Error(`Multiple animations target layer "${layer}"`);
5779
- targets.add(layer);
5780
6065
  const names = Object.keys(this.entityLayers).filter((name) => name.split("#")[0] === layer);
5781
6066
  if (!names.length) throw new Error(`Entity ${this.entity.id} has no selected animation layer "${layer}"`);
5782
- return { animation, names };
6067
+ let selection = selections.get(layer);
6068
+ if (!selection) selections.set(layer, selection = { animations: [], names, indices: [] });
6069
+ selection.animations.push(animation);
6070
+ selection.indices.push(index);
5783
6071
  });
5784
- const main = selections.find(({ animation }) => (animation.layer ?? "main") === "main");
5785
- if (main) main.names.push(...Object.keys(this.entityLayers).filter((name) => !targets.has(name.split("#")[0])));
6072
+ const main = selections.get("main");
6073
+ if (main) main.names.push(...Object.keys(this.entityLayers).filter((name) => !selections.has(name.split("#")[0])));
5786
6074
  this.createMeshes();
5787
6075
  this.stopAnimation();
5788
- for (const { animation, names } of selections) {
6076
+ for (const { animations: clips, names, indices } of selections.values()) {
5789
6077
  const player = new EntityAnimationPlayer();
5790
- player.play(names.map((name) => this.getLayerGroup(name)), animation, options, this.partPoses);
5791
- this.animationPlayers.push(player);
6078
+ player.playAnimations(
6079
+ names.map((name) => this.getLayerGroup(name)),
6080
+ clips,
6081
+ { ...options, weights: indices.map((index) => weights[index]) },
6082
+ this.partPoses
6083
+ );
6084
+ this.animationPlayers.push({ player, indices });
5792
6085
  }
5793
6086
  this.notifyDirty();
5794
6087
  }
6088
+ /** Changes weights in active clip order, notifying the scene without restarting the shared clock. Has no effect without an animation. */
6089
+ setAnimationWeights(weights) {
6090
+ if (!this.animationPlayers.length) return;
6091
+ const values = validateAnimationWeights(weights, this.activeAnimations.length);
6092
+ for (const { player, indices } of this.animationPlayers) player.setWeights(indices.map((index) => values[index]));
6093
+ this.notifyDirty();
6094
+ }
5795
6095
  /** Stops all clips and restores the poses from before playback. */
5796
6096
  stopAnimation() {
5797
6097
  const players = this.animationPlayers.splice(0);
5798
- for (const player of players) player.stop();
6098
+ for (const { player } of players) player.stop();
5799
6099
  if (players.length) this.notifyDirty();
5800
6100
  }
5801
6101
  /** Poses the object at an explicit time in seconds. Has no effect without an animation. */
5802
6102
  setAnimationTime(seconds) {
5803
- for (const player of this.animationPlayers) player.setTime(seconds);
6103
+ for (const { player } of this.animationPlayers) player.setTime(seconds);
5804
6104
  if (this.animationPlayers.length) this.notifyDirty();
5805
6105
  }
5806
6106
  /** Advances the animation by a frame delta in seconds, scaled by its speed. Has no effect without an animation. */
5807
6107
  advanceAnimation(deltaSeconds) {
5808
- for (const player of this.animationPlayers) player.advance(deltaSeconds);
6108
+ for (const { player } of this.animationPlayers) player.advance(deltaSeconds);
5809
6109
  if (this.animationPlayers.length) this.notifyDirty();
5810
6110
  }
5811
6111
  //</editor-fold>
@@ -8038,6 +8338,7 @@ async function layoutGuiText(text2, options = {}) {
8038
8338
  }
8039
8339
  const font = await Fonts.get(options.font);
8040
8340
  const runs = typeof text2 === "string" ? [{ text: text2 }] : text2;
8341
+ await font.resolve?.(runs.map((run) => run.text).join(""));
8041
8342
  const glyphs = [], lineWidths = [];
8042
8343
  let line = [], width = 0;
8043
8344
  let trailingNewline = false;
@@ -8064,7 +8365,7 @@ async function layoutGuiText(text2, options = {}) {
8064
8365
  continue;
8065
8366
  }
8066
8367
  const glyph = font.glyphs.get(character);
8067
- const advance = (glyph?.advance ?? 6) + (style.bold ? 1 : 0);
8368
+ const advance = (glyph?.advance ?? 6) + (style.bold ? glyph?.boldOffset ?? 1 : 0);
8068
8369
  line.push({ character, glyph, advance, style, x: 0, y: 0 });
8069
8370
  width += advance;
8070
8371
  while (width > maxWidth && line.length > 1) {
@@ -8092,7 +8393,7 @@ function createGuiTextGeometry(layout, shadow = true) {
8092
8393
  batches.push(batch);
8093
8394
  }
8094
8395
  const color2 = new import_three20.Color(isShadow ? (style.color & 16579836) >> 2 : style.color);
8095
- const offset = isShadow ? 1 : 0;
8396
+ const offset = isShadow ? glyph?.shadowOffset ?? 1 : 0;
8096
8397
  const top = glyph ? 7 - glyph.ascent : 0;
8097
8398
  const height = glyph ? glyph.height * glyph.scale : 8;
8098
8399
  const width = glyph ? glyph.width * glyph.scale : 5;
@@ -8122,7 +8423,7 @@ function createGuiTextGeometry(layout, shadow = true) {
8122
8423
  for (let i = 0; i < 4; i++) batch.colors.push(color2.r, color2.g, color2.b);
8123
8424
  batch.indices.push(base, base + 2, base + 1, base + 2, base + 3, base + 1);
8124
8425
  };
8125
- for (const boldOffset of style.bold ? [0, 1] : [0]) {
8426
+ for (const boldOffset of style.bold ? [0, glyph?.boldOffset ?? 1] : [0]) {
8126
8427
  if (glyph) quad(0, top, width, top + height, boldOffset);
8127
8428
  else {
8128
8429
  quad(0, 0, 5, 1, boldOffset);
@@ -11399,6 +11700,7 @@ __export(index_node_exports, {
11399
11700
  toRadians: () => toRadians,
11400
11701
  toTrianglesDrawMode: () => toTrianglesDrawMode,
11401
11702
  trimCanvas: () => trimCanvas,
11703
+ validateAnimationWeights: () => validateAnimationWeights,
11402
11704
  yieldToEventLoop: () => yieldToEventLoop
11403
11705
  });
11404
11706
  module.exports = __toCommonJS(index_node_exports);
@@ -18937,7 +19239,7 @@ var LIGHT_SAMPLE_OFFSETS = [0, 1, -1, 324, -324, 18, -18];
18937
19239
 
18938
19240
  // src/world/SectionGeometry.ts
18939
19241
  function buildSectionGeometry(input) {
18940
- const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight, ambientOcclusion, biomeTints } = input;
19242
+ const { count, indices, templates, cullMasks, templateData, atlases: atlases2, maxAtlasSize, fluids, light, ambientLight, ambientOcclusion, biomeTints } = input;
18941
19243
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
18942
19244
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
18943
19245
  if (biomeTints && biomeTints.length !== count * 3) throw new RangeError("Section biome tints must contain RGB for each placement");
@@ -18972,8 +19274,8 @@ function buildSectionGeometry(input) {
18972
19274
  const locations = /* @__PURE__ */ new Map();
18973
19275
  for (const [layer, layerAtlases] of [...visibleAtlases].sort(([a], [b]) => a - b)) {
18974
19276
  const firstPage = pages.length;
18975
- for (const atlas of [...layerAtlases].sort((a, b) => Number(atlases[a].animated) - Number(atlases[b].animated) || (atlases[a].animated ? a - b : atlases[b].height - atlases[a].height))) {
18976
- const { width, height, animated } = atlases[atlas];
19277
+ for (const atlas of [...layerAtlases].sort((a, b) => Number(atlases2[a].animated) - Number(atlases2[b].animated) || (atlases2[a].animated ? a - b : atlases2[b].height - atlases2[a].height))) {
19278
+ const { width, height, animated } = atlases2[atlas];
18977
19279
  if (width > maxAtlasSize || height > maxAtlasSize) throw new RangeError("Model atlas exceeds the section atlas size limit");
18978
19280
  let selected = -1;
18979
19281
  for (let i = firstPage; !animated && i < pages.length; i++) {
@@ -19016,7 +19318,7 @@ function buildSectionGeometry(input) {
19016
19318
  });
19017
19319
  if (!quads.sprites.length) continue;
19018
19320
  if (!fluidPages.has(fluid)) {
19019
- const { width, height } = atlases[data.atlas];
19321
+ const { width, height } = atlases2[data.atlas];
19020
19322
  if (width > maxAtlasSize || height > maxAtlasSize) throw new RangeError("Model atlas exceeds the section atlas size limit");
19021
19323
  fluidPages.set(fluid, {
19022
19324
  layer: 2,
@@ -19034,7 +19336,7 @@ function buildSectionGeometry(input) {
19034
19336
  const tint = fluid === "water" && fluids.waterTints ? fluids.waterTints.subarray(tintIndex, tintIndex + 3) : data.tint;
19035
19337
  fluidPages.get(fluid).fluids.push({ quads, x, y, z, data, tint });
19036
19338
  }
19037
- pages.push(...[...fluidPages.values()].sort((a, b) => Number(atlases[a.placements[0].atlas].animated) - Number(atlases[b.placements[0].atlas].animated) || a.placements[0].atlas - b.placements[0].atlas));
19339
+ pages.push(...[...fluidPages.values()].sort((a, b) => Number(atlases2[a.placements[0].atlas].animated) - Number(atlases2[b.placements[0].atlas].animated) || a.placements[0].atlas - b.placements[0].atlas));
19038
19340
  }
19039
19341
  return pages.map((page) => {
19040
19342
  let faces = page.fluids.reduce((sum, fluid) => sum + fluid.quads.sprites.length, 0);
@@ -19051,7 +19353,7 @@ function buildSectionGeometry(input) {
19051
19353
  let vertex = 0, indexOffset = 0;
19052
19354
  for (const entry of page.entries) {
19053
19355
  const template = templateData[templates[entry]];
19054
- const atlas = atlases[template.atlas];
19356
+ const atlas = atlases2[template.atlas];
19055
19357
  const placement = locations.get(template.atlas);
19056
19358
  const mapU = (u) => (placement.x + u * atlas.width) / page.width;
19057
19359
  const mapV = (v) => 1 - (placement.y + (1 - v) * atlas.height) / page.height;
@@ -19090,7 +19392,7 @@ function buildSectionGeometry(input) {
19090
19392
  }
19091
19393
  }
19092
19394
  for (const { quads, x, y, z, data, tint } of page.fluids) {
19093
- const atlas = atlases[data.atlas];
19395
+ const atlas = atlases2[data.atlas];
19094
19396
  const placement = page.placements[0];
19095
19397
  const mapU = (u) => (placement.x + u * atlas.width) / page.width;
19096
19398
  const mapV = (v) => 1 - (placement.y + (1 - v) * atlas.height) / page.height;
@@ -19192,7 +19494,7 @@ var templateCache = /* @__PURE__ */ new WeakMap();
19192
19494
  var WATER_TINT = new import_three46.Color(4159204).toArray();
19193
19495
  function toInput(entries, maxAtlasSize, fluids, lighting) {
19194
19496
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
19195
- const atlases = [];
19497
+ const atlases2 = [];
19196
19498
  const atlasIds = /* @__PURE__ */ new Map();
19197
19499
  const templateIds = /* @__PURE__ */ new Map();
19198
19500
  const input = {
@@ -19215,9 +19517,9 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19215
19517
  if (id === void 0) {
19216
19518
  let atlas = atlasIds.get(template.atlas);
19217
19519
  if (atlas === void 0) {
19218
- atlas = atlases.length;
19520
+ atlas = atlases2.length;
19219
19521
  atlasIds.set(template.atlas, atlas);
19220
- atlases.push(template.atlas);
19522
+ atlases2.push(template.atlas);
19221
19523
  input.atlases.push({ width: template.atlas.image.width, height: template.atlas.image.height, animated: template.atlas.hasAnimation });
19222
19524
  }
19223
19525
  let data = templateCache.get(template);
@@ -19276,9 +19578,9 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19276
19578
  if (!atlas) continue;
19277
19579
  let id = atlasIds.get(atlas);
19278
19580
  if (id === void 0) {
19279
- id = atlases.length;
19581
+ id = atlases2.length;
19280
19582
  atlasIds.set(atlas, id);
19281
- atlases.push(atlas);
19583
+ atlases2.push(atlas);
19282
19584
  input.atlases.push({ width: atlas.image.width, height: atlas.image.height, animated: atlas.hasAnimation });
19283
19585
  }
19284
19586
  input.fluids[kind] = {
@@ -19289,7 +19591,7 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19289
19591
  };
19290
19592
  }
19291
19593
  }
19292
- return { input, atlases };
19594
+ return { input, atlases: atlases2 };
19293
19595
  }
19294
19596
  var SectionMesh = class _SectionMesh extends import_three46.Group {
19295
19597
  constructor() {
@@ -19304,23 +19606,23 @@ var SectionMesh = class _SectionMesh extends import_three46.Group {
19304
19606
  }
19305
19607
  /** Builds section-local meshes from visible quads. `maxAtlasSize` limits each atlas dimension in pixels. */
19306
19608
  static build(entries, maxAtlasSize = 2048, fluids, lighting) {
19307
- const { input, atlases } = toInput(entries, maxAtlasSize, fluids, lighting);
19308
- return this.fromPages(buildSectionGeometry(input), atlases);
19609
+ const { input, atlases: atlases2 } = toInput(entries, maxAtlasSize, fluids, lighting);
19610
+ return this.fromPages(buildSectionGeometry(input), atlases2);
19309
19611
  }
19310
19612
  /** Builds section-local meshes in a browser worker when available, with synchronous fallback. */
19311
19613
  static async buildAsync(entries, maxAtlasSize = 2048, fluids, lighting) {
19312
- const { input, atlases } = toInput(entries, maxAtlasSize, fluids, lighting);
19614
+ const { input, atlases: atlases2 } = toInput(entries, maxAtlasSize, fluids, lighting);
19313
19615
  const worker = SectionWorker.shared();
19314
19616
  const pages = worker ? await worker.build(input).catch(() => buildSectionGeometry(input)) : buildSectionGeometry(input);
19315
- return this.fromPages(pages, atlases);
19617
+ return this.fromPages(pages, atlases2);
19316
19618
  }
19317
- static fromPages(pages, atlases) {
19619
+ static fromPages(pages, atlases2) {
19318
19620
  const section = new _SectionMesh();
19319
19621
  for (const page of pages) {
19320
19622
  const canvas = createCanvas2(page.width, page.height);
19321
19623
  const context = canvas.getContext("2d");
19322
19624
  for (const { atlas, x, y } of page.placements) {
19323
- context.drawImage(atlases[atlas].image.canvas, x, y);
19625
+ context.drawImage(atlases2[atlas].image.canvas, x, y);
19324
19626
  }
19325
19627
  const geometry = new import_three46.BufferGeometry();
19326
19628
  geometry.setAttribute("position", new import_three46.Float32BufferAttribute(page.positions.buffer, 3));
@@ -19331,7 +19633,7 @@ var SectionMesh = class _SectionMesh extends import_three46.Group {
19331
19633
  geometry.setIndex(new import_three46.Uint32BufferAttribute(page.indices.buffer, 1));
19332
19634
  geometry.computeBoundingBox();
19333
19635
  geometry.computeBoundingSphere();
19334
- const transparent = page.layer === 2 ? atlases[page.placements[0].atlas].hasTranslucency : page.layer === 1;
19636
+ const transparent = page.layer === 2 ? atlases2[page.placements[0].atlas].hasTranslucency : page.layer === 1;
19335
19637
  const material = Materials.createShadedCanvasMaterial(canvas, transparent, false, true);
19336
19638
  material.side = import_three46.FrontSide;
19337
19639
  if (page.layer === 2) {
@@ -19346,7 +19648,7 @@ var SectionMesh = class _SectionMesh extends import_three46.Group {
19346
19648
  section.blockIndices.set(mesh, page.blockIndices);
19347
19649
  const unsubscribe = [];
19348
19650
  for (const { atlas: id, x, y } of page.placements) {
19349
- const atlas = atlases[id];
19651
+ const atlas = atlases2[id];
19350
19652
  if (!atlas.hasAnimation) continue;
19351
19653
  unsubscribe.push(atlas.subscribe(() => {
19352
19654
  context.clearRect(x, y, atlas.image.width, atlas.image.height);
@@ -20690,6 +20992,7 @@ var NodeRenderer = class _NodeRenderer extends Renderer {
20690
20992
  toRadians,
20691
20993
  toTrianglesDrawMode,
20692
20994
  trimCanvas,
20995
+ validateAnimationWeights,
20693
20996
  yieldToEventLoop
20694
20997
  });
20695
20998
  //# sourceMappingURL=index.js.map