minerender 2.1.0-alpha.52 → 2.1.0-alpha.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -839,6 +839,71 @@ var init_Materials = __esm({
839
839
  alphaTest: 0.5
840
840
  });
841
841
  }
842
+ /** Whether vanilla's render type for the mode culls back faces. */
843
+ static entityModeCulls(mode = "cutout") {
844
+ return mode === "cutout_cull" || mode === "solid" || mode === "eyes" || mode === "water_mask";
845
+ }
846
+ /** UV offset per tick of entity age for the scrolling modes, as vanilla's texture matrix applies it. */
847
+ static entityModeScroll(mode = "cutout") {
848
+ if (mode === "energy_swirl") return [0.01, 0.01];
849
+ if (mode === "breeze_wind") return [0.02, 0];
850
+ return void 0;
851
+ }
852
+ /**
853
+ * Entity material for one of vanilla's render types. Entities are unlit here, so the lit and full-bright modes
854
+ * differ only in blending and depth state. Culling is not a material property: {@link entityModeCulls} decides
855
+ * whether the geometry gets inward faces.
856
+ */
857
+ static createEntityCanvasMaterial(canvas, mode = "cutout", tint) {
858
+ const material = _Materials.createBasicCanvasMaterial(canvas);
859
+ switch (mode) {
860
+ case "cutout":
861
+ case "cutout_cull":
862
+ break;
863
+ case "cutout_z_offset":
864
+ material.polygonOffset = true;
865
+ material.polygonOffsetFactor = -1;
866
+ material.polygonOffsetUnits = -10;
867
+ break;
868
+ case "solid":
869
+ material.alphaTest = 0;
870
+ break;
871
+ case "translucent":
872
+ case "breeze_wind":
873
+ material.transparent = true;
874
+ material.alphaTest = 0.1;
875
+ break;
876
+ case "translucent_emissive":
877
+ material.transparent = true;
878
+ material.alphaTest = 0.1;
879
+ material.depthWrite = false;
880
+ break;
881
+ case "eyes":
882
+ material.transparent = true;
883
+ material.alphaTest = 0;
884
+ material.depthWrite = false;
885
+ break;
886
+ case "energy_swirl":
887
+ material.transparent = true;
888
+ material.alphaTest = 0.1;
889
+ material.blending = import_three5.CustomBlending;
890
+ material.blendSrc = import_three5.OneFactor;
891
+ material.blendDst = import_three5.OneFactor;
892
+ material.blendSrcAlpha = import_three5.ZeroFactor;
893
+ material.blendDstAlpha = import_three5.OneFactor;
894
+ material.color.set(8421504);
895
+ break;
896
+ case "water_mask":
897
+ material.alphaTest = 0;
898
+ material.colorWrite = false;
899
+ break;
900
+ }
901
+ if (_Materials.entityModeScroll(mode)) {
902
+ material.map.wrapS = material.map.wrapT = import_three5.RepeatWrapping;
903
+ }
904
+ if (tint !== void 0) material.color.multiply(new import_three5.Color(tint));
905
+ return material;
906
+ }
842
907
  static createGuiCanvasMaterial(canvas) {
843
908
  const material = new import_three5.MeshBasicMaterial({
844
909
  map: Textures.createCanvasTexture(canvas),
@@ -3452,48 +3517,6 @@ var init_SceneObject = __esm({
3452
3517
  }
3453
3518
  });
3454
3519
 
3455
- // src/model/ModelCulling.ts
3456
- var import_three22, ModelCulling;
3457
- var init_ModelCulling = __esm({
3458
- "src/model/ModelCulling.ts"() {
3459
- import_three22 = require("three");
3460
- init_CubeFace();
3461
- ModelCulling = class {
3462
- static toLocalMask(worldMask, rotation) {
3463
- let localMask = 0;
3464
- for (const [localFace, offset] of CUBE_FACE_OFFSETS.entries()) {
3465
- const normal = new import_three22.Vector3(...offset).applyEuler(rotation);
3466
- const worldFace = CUBE_FACE_OFFSETS.findIndex(([x, y, z]) => Math.abs(normal.x - x) < 1e-6 && Math.abs(normal.y - y) < 1e-6 && Math.abs(normal.z - z) < 1e-6);
3467
- if (worldFace < 0) return 0;
3468
- if (worldMask & 1 << worldFace) localMask |= 1 << localFace;
3469
- }
3470
- return localMask;
3471
- }
3472
- static isOpaqueFullCube(atlas) {
3473
- if (!atlas || atlas.hasTransparency !== false || atlas.model.elements?.length !== 1) return false;
3474
- const element = atlas.model.elements[0];
3475
- if (element.from.length !== 3 || !element.from.every((value) => value === 0) || element.to.length !== 3 || !element.to.every((value) => value === 16) || element.rotation && element.rotation.angle !== 0 || element.mappedUv?.length !== 48 || !element.mappedUv.every(Number.isFinite)) return false;
3476
- if (!(atlas.image.width > 0 && atlas.image.height > 0)) return false;
3477
- return CUBE_FACES.every((face, faceIndex) => {
3478
- const texture = element.faces[face]?.texture;
3479
- if (!texture?.startsWith("#")) return false;
3480
- const key = texture.slice(1);
3481
- const position = atlas.positions[key];
3482
- const size = atlas.sizes[key];
3483
- if (position?.length !== 2 || !position.every(Number.isFinite) || size?.length !== 2 || !size.every((value) => value > 0 && Number.isFinite(value))) return false;
3484
- for (let vertex = 0; vertex < 4; vertex++) {
3485
- const uvIndex = (faceIndex * 4 + vertex) * 2;
3486
- const x = element.mappedUv[uvIndex] * atlas.image.width;
3487
- const y = (1 - element.mappedUv[uvIndex + 1]) * atlas.image.height;
3488
- if (x < position[0] - 1e-6 || x > position[0] + size[0] + 1e-6 || y < position[1] - 1e-6 || y > position[1] + size[1] + 1e-6) return false;
3489
- }
3490
- return true;
3491
- });
3492
- }
3493
- };
3494
- }
3495
- });
3496
-
3497
3520
  // src/three/BufferGeometryUtils.js
3498
3521
  function computeTangents(geometry) {
3499
3522
  geometry.computeTangents();
@@ -3506,7 +3529,7 @@ function mergeBufferGeometries(geometries, useGroups = false) {
3506
3529
  const attributes = {};
3507
3530
  const morphAttributes = {};
3508
3531
  const morphTargetsRelative = geometries[0].morphTargetsRelative;
3509
- const mergedGeometry = new import_three24.BufferGeometry();
3532
+ const mergedGeometry = new import_three19.BufferGeometry();
3510
3533
  let offset = 0;
3511
3534
  for (let i = 0; i < geometries.length; ++i) {
3512
3535
  const geometry = geometries[i];
@@ -3630,7 +3653,7 @@ function mergeBufferAttributes(attributes) {
3630
3653
  array.set(attributes[i].array, offset);
3631
3654
  offset += attributes[i].array.length;
3632
3655
  }
3633
- return new import_three24.BufferAttribute(array, itemSize, normalized);
3656
+ return new import_three19.BufferAttribute(array, itemSize, normalized);
3634
3657
  }
3635
3658
  function interleaveAttributes(attributes) {
3636
3659
  let TypedArray;
@@ -3646,7 +3669,7 @@ function interleaveAttributes(attributes) {
3646
3669
  arrayLength += attribute.array.length;
3647
3670
  stride += attribute.itemSize;
3648
3671
  }
3649
- const interleavedBuffer = new import_three24.InterleavedBuffer(new TypedArray(arrayLength), stride);
3672
+ const interleavedBuffer = new import_three19.InterleavedBuffer(new TypedArray(arrayLength), stride);
3650
3673
  let offset = 0;
3651
3674
  const res = [];
3652
3675
  const getters = ["getX", "getY", "getZ", "getW"];
@@ -3655,7 +3678,7 @@ function interleaveAttributes(attributes) {
3655
3678
  const attribute = attributes[j];
3656
3679
  const itemSize = attribute.itemSize;
3657
3680
  const count = attribute.count;
3658
- const iba = new import_three24.InterleavedBufferAttribute(interleavedBuffer, itemSize, offset, attribute.normalized);
3681
+ const iba = new import_three19.InterleavedBufferAttribute(interleavedBuffer, itemSize, offset, attribute.normalized);
3659
3682
  res.push(iba);
3660
3683
  offset += itemSize;
3661
3684
  for (let c = 0; c < count; c++) {
@@ -3739,13 +3762,13 @@ function mergeVertices(geometry, tolerance = 1e-4) {
3739
3762
  const name = attributeNames[i];
3740
3763
  const oldAttribute = geometry.getAttribute(name);
3741
3764
  const buffer = new oldAttribute.array.constructor(attrArrays[name]);
3742
- const attribute = new import_three24.BufferAttribute(buffer, oldAttribute.itemSize, oldAttribute.normalized);
3765
+ const attribute = new import_three19.BufferAttribute(buffer, oldAttribute.itemSize, oldAttribute.normalized);
3743
3766
  result.setAttribute(name, attribute);
3744
3767
  if (name in morphAttrsArrays) {
3745
3768
  for (let j = 0; j < morphAttrsArrays[name].length; j++) {
3746
3769
  const oldMorphAttribute = geometry.morphAttributes[name][j];
3747
3770
  const buffer2 = new oldMorphAttribute.array.constructor(morphAttrsArrays[name][j]);
3748
- const morphAttribute = new import_three24.BufferAttribute(buffer2, oldMorphAttribute.itemSize, oldMorphAttribute.normalized);
3771
+ const morphAttribute = new import_three19.BufferAttribute(buffer2, oldMorphAttribute.itemSize, oldMorphAttribute.normalized);
3749
3772
  result.morphAttributes[name][j] = morphAttribute;
3750
3773
  }
3751
3774
  }
@@ -3754,11 +3777,11 @@ function mergeVertices(geometry, tolerance = 1e-4) {
3754
3777
  return result;
3755
3778
  }
3756
3779
  function toTrianglesDrawMode(geometry, drawMode) {
3757
- if (drawMode === import_three24.TrianglesDrawMode) {
3780
+ if (drawMode === import_three19.TrianglesDrawMode) {
3758
3781
  console.warn("THREE.BufferGeometryUtils.toTrianglesDrawMode(): Geometry already defined as triangles.");
3759
3782
  return geometry;
3760
3783
  }
3761
- if (drawMode === import_three24.TriangleFanDrawMode || drawMode === import_three24.TriangleStripDrawMode) {
3784
+ if (drawMode === import_three19.TriangleFanDrawMode || drawMode === import_three19.TriangleStripDrawMode) {
3762
3785
  let index = geometry.getIndex();
3763
3786
  if (index === null) {
3764
3787
  const indices = [];
@@ -3776,7 +3799,7 @@ function toTrianglesDrawMode(geometry, drawMode) {
3776
3799
  }
3777
3800
  const numberOfTriangles = index.count - 2;
3778
3801
  const newIndices = [];
3779
- if (drawMode === import_three24.TriangleFanDrawMode) {
3802
+ if (drawMode === import_three19.TriangleFanDrawMode) {
3780
3803
  for (let i = 1; i <= numberOfTriangles; i++) {
3781
3804
  newIndices.push(index.getX(0));
3782
3805
  newIndices.push(index.getX(i));
@@ -3812,15 +3835,15 @@ function computeMorphedAttributes(object) {
3812
3835
  console.error("THREE.BufferGeometryUtils: Geometry is not of type BufferGeometry.");
3813
3836
  return null;
3814
3837
  }
3815
- const _vA = new import_three24.Vector3();
3816
- const _vB = new import_three24.Vector3();
3817
- const _vC = new import_three24.Vector3();
3818
- const _tempA = new import_three24.Vector3();
3819
- const _tempB = new import_three24.Vector3();
3820
- const _tempC = new import_three24.Vector3();
3821
- const _morphA = new import_three24.Vector3();
3822
- const _morphB = new import_three24.Vector3();
3823
- const _morphC = new import_three24.Vector3();
3838
+ const _vA = new import_three19.Vector3();
3839
+ const _vB = new import_three19.Vector3();
3840
+ const _vC = new import_three19.Vector3();
3841
+ const _tempA = new import_three19.Vector3();
3842
+ const _tempB = new import_three19.Vector3();
3843
+ const _tempC = new import_three19.Vector3();
3844
+ const _morphA = new import_three19.Vector3();
3845
+ const _morphB = new import_three19.Vector3();
3846
+ const _morphC = new import_three19.Vector3();
3824
3847
  function _calculateMorphedAttributeData(object2, material2, attribute, morphAttribute, morphTargetsRelative2, a2, b2, c2, modifiedAttributeArray) {
3825
3848
  _vA.fromBufferAttribute(attribute, a2);
3826
3849
  _vB.fromBufferAttribute(attribute, b2);
@@ -4015,8 +4038,8 @@ function computeMorphedAttributes(object) {
4015
4038
  }
4016
4039
  }
4017
4040
  }
4018
- const morphedPositionAttribute = new import_three24.Float32BufferAttribute(modifiedPosition, 3);
4019
- const morphedNormalAttribute = new import_three24.Float32BufferAttribute(modifiedNormal, 3);
4041
+ const morphedPositionAttribute = new import_three19.Float32BufferAttribute(modifiedPosition, 3);
4042
+ const morphedNormalAttribute = new import_three19.Float32BufferAttribute(modifiedNormal, 3);
4020
4043
  return {
4021
4044
  positionAttribute,
4022
4045
  normalAttribute,
@@ -4024,10 +4047,52 @@ function computeMorphedAttributes(object) {
4024
4047
  morphedNormalAttribute
4025
4048
  };
4026
4049
  }
4027
- var import_three24;
4050
+ var import_three19;
4028
4051
  var init_BufferGeometryUtils = __esm({
4029
4052
  "src/three/BufferGeometryUtils.js"() {
4030
- import_three24 = require("three");
4053
+ import_three19 = require("three");
4054
+ }
4055
+ });
4056
+
4057
+ // src/model/ModelCulling.ts
4058
+ var import_three20, ModelCulling;
4059
+ var init_ModelCulling = __esm({
4060
+ "src/model/ModelCulling.ts"() {
4061
+ import_three20 = require("three");
4062
+ init_CubeFace();
4063
+ ModelCulling = class {
4064
+ static toLocalMask(worldMask, rotation) {
4065
+ let localMask = 0;
4066
+ for (const [localFace, offset] of CUBE_FACE_OFFSETS.entries()) {
4067
+ const normal = new import_three20.Vector3(...offset).applyEuler(rotation);
4068
+ const worldFace = CUBE_FACE_OFFSETS.findIndex(([x, y, z]) => Math.abs(normal.x - x) < 1e-6 && Math.abs(normal.y - y) < 1e-6 && Math.abs(normal.z - z) < 1e-6);
4069
+ if (worldFace < 0) return 0;
4070
+ if (worldMask & 1 << worldFace) localMask |= 1 << localFace;
4071
+ }
4072
+ return localMask;
4073
+ }
4074
+ static isOpaqueFullCube(atlas) {
4075
+ if (!atlas || atlas.hasTransparency !== false || atlas.model.elements?.length !== 1) return false;
4076
+ const element = atlas.model.elements[0];
4077
+ if (element.from.length !== 3 || !element.from.every((value) => value === 0) || element.to.length !== 3 || !element.to.every((value) => value === 16) || element.rotation && element.rotation.angle !== 0 || element.mappedUv?.length !== 48 || !element.mappedUv.every(Number.isFinite)) return false;
4078
+ if (!(atlas.image.width > 0 && atlas.image.height > 0)) return false;
4079
+ return CUBE_FACES.every((face, faceIndex) => {
4080
+ const texture = element.faces[face]?.texture;
4081
+ if (!texture?.startsWith("#")) return false;
4082
+ const key = texture.slice(1);
4083
+ const position = atlas.positions[key];
4084
+ const size = atlas.sizes[key];
4085
+ if (position?.length !== 2 || !position.every(Number.isFinite) || size?.length !== 2 || !size.every((value) => value > 0 && Number.isFinite(value))) return false;
4086
+ for (let vertex = 0; vertex < 4; vertex++) {
4087
+ const uvIndex = (faceIndex * 4 + vertex) * 2;
4088
+ const x = element.mappedUv[uvIndex] * atlas.image.width;
4089
+ const y = (1 - element.mappedUv[uvIndex + 1]) * atlas.image.height;
4090
+ if (x < position[0] - 1e-6 || x > position[0] + size[0] + 1e-6 || y < position[1] - 1e-6 || y > position[1] + size[1] + 1e-6) return false;
4091
+ }
4092
+ return true;
4093
+ });
4094
+ }
4095
+ };
4031
4096
  }
4032
4097
  });
4033
4098
 
@@ -4035,13 +4100,13 @@ var init_BufferGeometryUtils = __esm({
4035
4100
  function isModelObject(obj) {
4036
4101
  return obj.isModelObject;
4037
4102
  }
4038
- var import_three25, import_ts_deepmerge5, p9, _ModelObject, ModelObject;
4103
+ var import_three21, import_ts_deepmerge5, p7, _ModelObject, ModelObject;
4039
4104
  var init_ModelObject = __esm({
4040
4105
  "src/model/scene/ModelObject.ts"() {
4041
4106
  init_SceneObject();
4042
4107
  init_Materials();
4043
4108
  init_UVMapper();
4044
- import_three25 = require("three");
4109
+ import_three21 = require("three");
4045
4110
  init_BufferGeometryUtils();
4046
4111
  init_model();
4047
4112
  import_ts_deepmerge5 = __toESM(require_dist());
@@ -4049,7 +4114,7 @@ var init_ModelObject = __esm({
4049
4114
  init_CubeFace();
4050
4115
  init_DisplayTransforms();
4051
4116
  init_ModelCulling();
4052
- p9 = prefix("ModelObject");
4117
+ p7 = prefix("ModelObject");
4053
4118
  _ModelObject = class _ModelObject extends SceneObject {
4054
4119
  constructor(originalModel, options) {
4055
4120
  super(options);
@@ -4095,7 +4160,7 @@ var init_ModelObject = __esm({
4095
4160
  elGeo.clearGroups();
4096
4161
  }
4097
4162
  if (this.options.uvLockRotation) {
4098
- UVMapper.lockUvs(elGeo, el.faces, this.atlas, new import_three25.Euler(...this.options.uvLockRotation));
4163
+ UVMapper.lockUvs(elGeo, el.faces, this.atlas, new import_three21.Euler(...this.options.uvLockRotation));
4099
4164
  }
4100
4165
  if (this.options.tints) {
4101
4166
  const colors = new Float32Array(elGeo.getAttribute("position").count * 3).fill(1);
@@ -4104,19 +4169,19 @@ var init_ModelObject = __esm({
4104
4169
  if (tintIndex === void 0 || tintIndex < 0) continue;
4105
4170
  const tint = this.options.tints[tintIndex];
4106
4171
  if (tint === void 0) continue;
4107
- const color = new import_three25.Color(tint);
4172
+ const color = new import_three21.Color(tint);
4108
4173
  for (let vertex = 0; vertex < 4; vertex++) {
4109
4174
  color.toArray(colors, (faceIndex * 4 + vertex) * 3);
4110
4175
  }
4111
4176
  }
4112
- elGeo.setAttribute("color", new import_three25.BufferAttribute(colors, 3));
4177
+ elGeo.setAttribute("color", new import_three21.BufferAttribute(colors, 3));
4113
4178
  }
4114
- elGeo.applyMatrix4(new import_three25.Matrix4().makeTranslation((el.to[0] - el.from[0]) / 2, (el.to[1] - el.from[1]) / 2, (el.to[2] - el.from[2]) / 2));
4115
- elGeo.applyMatrix4(new import_three25.Matrix4().makeTranslation(el.from[0], el.from[1], el.from[2]));
4179
+ elGeo.applyMatrix4(new import_three21.Matrix4().makeTranslation((el.to[0] - el.from[0]) / 2, (el.to[1] - el.from[1]) / 2, (el.to[2] - el.from[2]) / 2));
4180
+ elGeo.applyMatrix4(new import_three21.Matrix4().makeTranslation(el.from[0], el.from[1], el.from[2]));
4116
4181
  if (el.rotation) {
4117
4182
  applyElementRotation(el.rotation, elGeo);
4118
4183
  }
4119
- elGeo.applyMatrix4(new import_three25.Matrix4().makeTranslation(-8, -8, -8));
4184
+ elGeo.applyMatrix4(new import_three21.Matrix4().makeTranslation(-8, -8, -8));
4120
4185
  if (displayTransform) DisplayTransforms.apply(elGeo, displayTransform);
4121
4186
  if (this.options.mergeMeshes) {
4122
4187
  allGeos.push(elGeo);
@@ -4128,17 +4193,17 @@ var init_ModelObject = __esm({
4128
4193
  }
4129
4194
  });
4130
4195
  } else {
4131
- console.debug(p9, this.atlas.model, "has no elements");
4196
+ console.debug(p7, this.atlas.model, "has no elements");
4132
4197
  }
4133
4198
  } else {
4134
- console.debug(p9, "Missing atlas for", this);
4199
+ console.debug(p7, "Missing atlas for", this);
4135
4200
  }
4136
4201
  if (this.options.mergeMeshes) {
4137
4202
  let combinedGeo;
4138
4203
  if (allGeos.length > 0) {
4139
4204
  combinedGeo = mergeBufferGeometries(allGeos);
4140
4205
  } else {
4141
- combinedGeo = new import_three25.BoxGeometry(16, 16, 16);
4206
+ combinedGeo = new import_three21.BoxGeometry(16, 16, 16);
4142
4207
  if (displayTransform) DisplayTransforms.apply(combinedGeo, displayTransform);
4143
4208
  }
4144
4209
  combinedGeo.computeBoundingBox();
@@ -4925,32 +4990,44 @@ var Entities = class {
4925
4990
  static async getLayerList(modelKey) {
4926
4991
  return Object.keys((await this.getModelFile(modelKey))?.layers ?? {});
4927
4992
  }
4993
+ /** The dataset's extra draws on top of `main`; their `when` labels are the states `EntityModelOptions.when` can enable. */
4994
+ static async getPassList(modelKey) {
4995
+ return (await this.getModelFile(modelKey))?.passes ?? [];
4996
+ }
4928
4997
  static async getEntity(modelKey, textureKey, options) {
4929
4998
  const model = await this.getModelFile(modelKey);
4930
4999
  if (!model) return void 0;
4931
- const names = [...new Set(options?.layers ?? [options?.layer ?? "main"])];
4932
- if (names.length === 0) throw new MineRenderError(`Entity ${model.id} requires at least one layer`);
4933
- const selected = names.map((name) => {
4934
- const layer = model.layers[name];
5000
+ const passes = options?.layers || options?.layer !== void 0 ? [] : (model.passes ?? []).filter((pass) => pass.when === void 0 || options?.when?.includes(pass.when));
5001
+ const draws = [...new Set(options?.layers ?? [options?.layer ?? "main"])].map((layer) => ({ layer }));
5002
+ if (draws.length === 0) throw new MineRenderError(`Entity ${model.id} requires at least one layer`);
5003
+ draws.push(...passes);
5004
+ const names = [];
5005
+ const selected = draws.map((pass) => {
5006
+ const layer = model.layers[pass.layer];
4935
5007
  if (!layer) {
4936
- throw new MineRenderError(`Entity ${model.id} has no layer "${name}". Available layers: ${Object.keys(model.layers).join(", ")}`);
5008
+ throw new MineRenderError(`Entity ${model.id} has no layer "${pass.layer}". Available layers: ${Object.keys(model.layers).join(", ")}`);
4937
5009
  }
4938
- return { name, layer };
5010
+ let name = pass.layer;
5011
+ for (let n = 2; names.includes(name); n++) name = `${pass.layer}#${n}`;
5012
+ names.push(name);
5013
+ return { name, layer, pass };
4939
5014
  });
4940
- const layers = Object.fromEntries(await Promise.all(selected.map(async ({ name, layer }, index) => {
5015
+ const layers = Object.fromEntries(await Promise.all(selected.map(async ({ name, layer, pass }, index) => {
4941
5016
  const override = options?.textures?.[name] ?? (index === 0 ? textureKey : void 0);
5017
+ const textureLocation = pass.textureLocation ?? layer.textureLocation;
4942
5018
  let texture;
4943
5019
  if (override) {
4944
5020
  const path = isAssetKey(override) ? override.getFullPath() : override.path;
4945
5021
  texture = isAssetKey(override) && override.assetType === "textures" && path.startsWith("entity/") ? override : new AssetKey(override.namespace, path, "textures", "entity", "assets", ".png", isAssetKey(override) ? override.root : void 0);
4946
- } else if (layer.textureLocation !== void 0) {
4947
- texture = AssetKey.parse("textures", layer.textureLocation.replace(/^([^:]+:)?textures\//, "$1"));
5022
+ } else if (textureLocation !== void 0) {
5023
+ texture = AssetKey.parse("textures", textureLocation.replace(/^([^:]+:)?textures\//, "$1"));
4948
5024
  } else {
4949
5025
  texture = await this.resolveTexture(modelKey);
4950
5026
  }
4951
- return [name, { key: override ?? modelKey, texture, layer }];
5027
+ const render = pass.render ?? layer.render;
5028
+ return [name, { key: override ?? modelKey, texture, layer, ...render && { render }, ...pass.tint && { tint: pass.tint } }];
4952
5029
  })));
4953
- return { ...layers[names[0]], id: model.id, layers };
5030
+ return { ...layers[names[0]], id: model.id, layers, ...model.transform && { transform: model.transform } };
4954
5031
  }
4955
5032
  static async resolveTexture(modelKey) {
4956
5033
  const path = isAssetKey(modelKey) ? modelKey.getFullPath() : modelKey.path;
@@ -5033,6 +5110,7 @@ init_SceneObject();
5033
5110
  init_Caching();
5034
5111
  var import_ts_deepmerge4 = __toESM(require_dist());
5035
5112
  var import_three12 = require("three");
5113
+ init_Ticker();
5036
5114
  init_model();
5037
5115
  init_ModelTextures();
5038
5116
  init_AssetKey();
@@ -5044,6 +5122,10 @@ var _EntityObject = class _EntityObject extends SceneObject {
5044
5122
  this.entity = entity;
5045
5123
  this.isEntityObject = true;
5046
5124
  this.meshesCreated = false;
5125
+ /** Scrolling materials are owned: their texture offset follows this entity's age. */
5126
+ this.scrollMaterials = [];
5127
+ /** Entity age in ticks, as vanilla's `ageInTicks`; drives the scrolling render modes. */
5128
+ this.age = 0;
5047
5129
  this.options = (0, import_ts_deepmerge4.default)({}, _EntityObject.DEFAULT_OPTIONS, options ?? {});
5048
5130
  }
5049
5131
  async init() {
@@ -5053,6 +5135,25 @@ var _EntityObject = class _EntityObject extends SceneObject {
5053
5135
  dispose() {
5054
5136
  super.dispose();
5055
5137
  }
5138
+ disposeAndRemoveAllChildren() {
5139
+ this.clearScrollMaterials();
5140
+ super.disposeAndRemoveAllChildren();
5141
+ }
5142
+ clearScrollMaterials() {
5143
+ Ticker.remove(this.scrollTicker);
5144
+ this.scrollTicker = void 0;
5145
+ for (const { material } of this.scrollMaterials.splice(0)) {
5146
+ material.map?.dispose();
5147
+ material.dispose();
5148
+ }
5149
+ }
5150
+ /** Applies the entity age to the scrolling materials; the ticker calls this 20 times per second. */
5151
+ updateScroll() {
5152
+ for (const { offset, speed } of this.scrollMaterials) {
5153
+ offset.set(this.age * speed[0] % 1, 0 - this.age * speed[1] % 1);
5154
+ }
5155
+ this.notifyDirty();
5156
+ }
5056
5157
  getLayerGroup(name) {
5057
5158
  return super.getGroupByName(`layer:${name}`);
5058
5159
  }
@@ -5083,16 +5184,29 @@ var _EntityObject = class _EntityObject extends SceneObject {
5083
5184
  createMeshes(force = false) {
5084
5185
  if (this.meshesCreated && !force) return;
5085
5186
  const modelRoot = new import_three12.Object3D();
5086
- if (this.options.flip) modelRoot.scale.set(-1, -1, 1);
5187
+ const transform = this.options.flip === void 0 ? this.entity.transform : void 0;
5188
+ if (transform) {
5189
+ for (const op of transform) {
5190
+ const matrix = new import_three12.Matrix4();
5191
+ if ("scale" in op) matrix.makeScale(...op.scale);
5192
+ else if ("translate" in op) matrix.makeTranslation(...op.translate);
5193
+ else matrix.makeRotationFromEuler(new import_three12.Euler(...op.rotate, "ZYX"));
5194
+ modelRoot.matrix.multiply(matrix);
5195
+ }
5196
+ modelRoot.matrixAutoUpdate = false;
5197
+ } else if (this.options.flip ?? true) {
5198
+ modelRoot.scale.set(-1, -1, 1);
5199
+ }
5087
5200
  this.add(modelRoot);
5088
5201
  Object.entries(this.entityLayers).forEach(([name, layer], index) => {
5089
5202
  const group = this.createGroup(`layer:${name}`);
5090
5203
  modelRoot.add(group);
5091
- this.createPart("root", layer.layer.root, group, layer.layer.texture, Materials.MISSING_TEXTURE, index);
5204
+ const inward = !Materials.entityModeCulls(layer.render ?? layer.layer.render);
5205
+ this.createPart("root", layer.layer.root, group, layer.layer.texture, Materials.MISSING_TEXTURE, index, inward);
5092
5206
  });
5093
5207
  this.meshesCreated = true;
5094
5208
  }
5095
- createPart(name, part, parent, textureSize, material, renderOrder) {
5209
+ createPart(name, part, parent, textureSize, material, renderOrder, inward = true) {
5096
5210
  const anchor = this.createGroup(name);
5097
5211
  anchor.position.fromArray(part.pose.offset);
5098
5212
  anchor.rotation.set(...part.pose.rotation, "ZYX");
@@ -5111,7 +5225,7 @@ var _EntityObject = class _EntityObject extends SceneObject {
5111
5225
  uv
5112
5226
  ).clone();
5113
5227
  geometry.translate(cube.origin[0] + width / 2, cube.origin[1] + height / 2, cube.origin[2] + depth / 2);
5114
- if (Math.min(width + growX * 2, height + growY * 2, depth + growZ * 2) > 0) {
5228
+ if (inward && Math.min(width + growX * 2, height + growY * 2, depth + growZ * 2) > 0) {
5115
5229
  const index = Array.from(geometry.getIndex().array);
5116
5230
  geometry.setIndex(index.concat(index.slice().reverse()));
5117
5231
  }
@@ -5121,30 +5235,46 @@ var _EntityObject = class _EntityObject extends SceneObject {
5121
5235
  if (this.options.wireframe) addWireframeToMesh(geometry, mesh);
5122
5236
  }
5123
5237
  for (const [childName, child] of Object.entries(part.children)) {
5124
- this.createPart(childName, child, anchor, size, material, renderOrder);
5238
+ this.createPart(childName, child, anchor, size, material, renderOrder, inward);
5125
5239
  }
5126
5240
  }
5127
5241
  async applyTextures() {
5242
+ this.clearScrollMaterials();
5128
5243
  await Promise.all(Object.entries(this.entityLayers).map(async ([name, layer]) => {
5244
+ const mode = layer.render ?? layer.layer.render ?? "cutout";
5245
+ const tint = layer.tint === void 0 ? void 0 : this.options.tints?.[layer.tint];
5246
+ const scroll = Materials.entityModeScroll(mode);
5129
5247
  const assetKeyStr = this.getTextureKey(layer).serialize();
5130
- const keyStr = `entity:${assetKeyStr}`;
5131
- let mat = Caching.materialCache.getIfPresent(keyStr);
5248
+ const keyStr = `entity:${mode}:${tint === void 0 ? "" : new import_three12.Color(tint).getHexString()}:${assetKeyStr}`;
5249
+ let mat = scroll ? void 0 : Caching.materialCache.getIfPresent(keyStr);
5132
5250
  if (!mat) {
5133
5251
  const cachedAsset = Caching.textureAssetCache.getIfPresent(assetKeyStr);
5134
5252
  const imageData = await this.loadTextures(layer);
5135
5253
  if (!imageData) return;
5136
5254
  const canvas = imageData.data.canvas;
5137
- const createMaterial = () => Materials.createBasicCanvasMaterial(canvas);
5138
- mat = cachedAsset && Caching.textureAssetCache.getIfPresent(assetKeyStr) === cachedAsset ? Caching.materialCache.get(keyStr, createMaterial) : createMaterial();
5255
+ const createMaterial = () => Materials.createEntityCanvasMaterial(canvas, mode, tint);
5256
+ mat = !scroll && cachedAsset && Caching.textureAssetCache.getIfPresent(assetKeyStr) === cachedAsset ? Caching.materialCache.get(keyStr, createMaterial) : createMaterial();
5257
+ if (scroll) {
5258
+ this.scrollMaterials.push({ material: mat, offset: mat.map.offset, speed: scroll });
5259
+ }
5139
5260
  }
5140
5261
  this.getLayerGroup(name)?.traverse((object) => {
5141
5262
  if (isMesh(object)) object.material = mat;
5142
5263
  });
5143
5264
  }));
5265
+ if (!this.children.length) {
5266
+ this.clearScrollMaterials();
5267
+ } else if (this.scrollMaterials.length) {
5268
+ this.updateScroll();
5269
+ this.scrollTicker = Ticker.add(() => {
5270
+ this.age++;
5271
+ this.updateScroll();
5272
+ });
5273
+ }
5144
5274
  this.notifyDirty();
5145
5275
  }
5146
5276
  };
5147
- _EntityObject.DEFAULT_OPTIONS = (0, import_ts_deepmerge4.default)({}, SceneObject.DEFAULT_OPTIONS, { flip: true });
5277
+ _EntityObject.DEFAULT_OPTIONS = (0, import_ts_deepmerge4.default)({}, SceneObject.DEFAULT_OPTIONS);
5148
5278
  var EntityObject = _EntityObject;
5149
5279
  function isEntityObject(obj) {
5150
5280
  return obj.isEntityObject;
@@ -7658,27 +7788,97 @@ function copyDataTexture(source) {
7658
7788
  }
7659
7789
 
7660
7790
  // src/gui/GuiHelper.ts
7791
+ init_AssetKey();
7661
7792
  var GuiHelper = class {
7662
7793
  static inventorySlot(slot, origin = [0, 0], offset = [18, 18], rowSize = 9) {
7663
7794
  const [column, row] = typeof slot === "number" ? [slot % rowSize, Math.floor(slot / rowSize)] : slot;
7664
7795
  return [origin[0] + column * offset[0], origin[1] + row * offset[1]];
7665
7796
  }
7797
+ /** Creates crafting-table layers for ingredients and the result; stack counts are not drawn. */
7798
+ static recipe(recipe, options = {}) {
7799
+ const layers = [
7800
+ { name: "background", texture: "minecraft:gui/container/crafting_table", crop: [0, 0, 176, 166] }
7801
+ ];
7802
+ const itemKey = (ingredient) => {
7803
+ const item = typeof ingredient === "string" && !ingredient.startsWith("#") ? ingredient : typeof ingredient === "object" && "item" in ingredient ? ingredient.item : options.resolveIngredient?.(ingredient);
7804
+ if (isAssetKey(item)) return item;
7805
+ if (!item || item.startsWith("#")) {
7806
+ throw new Error(`Select a concrete item for recipe ingredient ${JSON.stringify(ingredient)}`);
7807
+ }
7808
+ const key = AssetKey.parse("models", item);
7809
+ return new AssetKey(key.namespace, key.getFullPath(), "models", "item");
7810
+ };
7811
+ const addIngredient = (ingredient, slot) => {
7812
+ layers.push({
7813
+ name: `ingredient-${slot}`,
7814
+ item: itemKey(ingredient),
7815
+ position: this.inventorySlot(slot, [30, 17], [18, 18], 3)
7816
+ });
7817
+ };
7818
+ switch (recipe.type) {
7819
+ case "minecraft:crafting_shaped":
7820
+ case "crafting_shaped": {
7821
+ const pattern = recipe.pattern;
7822
+ if (!pattern.length || pattern.length > 3 || !pattern[0].length || pattern[0].length > 3 || pattern.some((row) => row.length !== pattern[0].length)) {
7823
+ throw new Error("Crafting patterns must be rectangular, with 1\u20133 rows and columns");
7824
+ }
7825
+ const cells = pattern.flatMap((row, y) => row.split("").map((symbol, x) => ({ symbol, x, y }))).filter((cell) => cell.symbol !== " ");
7826
+ if (!cells.length) throw new Error("Crafting patterns must contain an ingredient");
7827
+ const minX = Math.min(...cells.map((cell) => cell.x)), maxX = Math.max(...cells.map((cell) => cell.x));
7828
+ const minY = Math.min(...cells.map((cell) => cell.y)), maxY = Math.max(...cells.map((cell) => cell.y));
7829
+ const offsetX = minX === maxX ? 1 : 0, offsetY = minY === maxY ? 1 : 0;
7830
+ for (const { symbol, x, y } of cells) {
7831
+ const ingredient = recipe.key[symbol];
7832
+ if (!ingredient) throw new Error(`Missing recipe ingredient for symbol ${JSON.stringify(symbol)}`);
7833
+ addIngredient(ingredient, (y - minY + offsetY) * 3 + x - minX + offsetX);
7834
+ }
7835
+ break;
7836
+ }
7837
+ case "minecraft:crafting_shapeless":
7838
+ case "crafting_shapeless":
7839
+ if (!recipe.ingredients.length || recipe.ingredients.length > 9) {
7840
+ throw new Error("Shapeless crafting recipes require 1\u20139 ingredients");
7841
+ }
7842
+ recipe.ingredients.forEach(addIngredient);
7843
+ break;
7844
+ default:
7845
+ throw new Error(`Unsupported crafting recipe type: ${recipe.type}`);
7846
+ }
7847
+ layers.push({
7848
+ name: "result",
7849
+ item: itemKey("id" in recipe.result ? recipe.result.id : recipe.result.item),
7850
+ position: [124, 35]
7851
+ });
7852
+ return layers;
7853
+ }
7666
7854
  };
7667
7855
 
7668
7856
  // src/gui/scene/GuiObject.ts
7669
- var import_three19 = require("three");
7857
+ var import_three22 = require("three");
7670
7858
  init_AssetKey();
7671
7859
  init_ModelTextures();
7860
+ init_Models();
7672
7861
  init_Caching();
7673
7862
  init_SceneObject();
7674
7863
  init_Materials();
7864
+ init_ModelObject();
7865
+ init_DisplayPosition();
7866
+
7867
+ // src/model/GuiLight.ts
7868
+ var GuiLight = /* @__PURE__ */ ((GuiLight2) => {
7869
+ GuiLight2["FRONT"] = "front";
7870
+ GuiLight2["SIDE"] = "side";
7871
+ return GuiLight2;
7872
+ })(GuiLight || {});
7873
+
7874
+ // src/gui/scene/GuiObject.ts
7675
7875
  var GuiObject = class extends SceneObject {
7676
7876
  constructor(textureLayers, options) {
7677
7877
  super({ ...options, instanceMeshes: false, mergeMeshes: false });
7678
7878
  this.textureLayers = textureLayers;
7679
7879
  this.isGuiObject = true;
7680
7880
  /** Local bounds in GUI pixels, with y growing down from the origin. */
7681
- this.bounds = new import_three19.Box2();
7881
+ this.bounds = new import_three22.Box2();
7682
7882
  this.geometries = /* @__PURE__ */ new Set();
7683
7883
  this.ownedMaterials = /* @__PURE__ */ new Set();
7684
7884
  this.initialized = false;
@@ -7686,14 +7886,35 @@ var GuiObject = class extends SceneObject {
7686
7886
  async init() {
7687
7887
  if (this.initialized) return;
7688
7888
  try {
7889
+ let depth = 0;
7689
7890
  for (const [index, layer] of this.textureLayers.entries()) {
7891
+ const [x, y] = layer.position ?? [0, 0];
7892
+ if ("item" in layer) {
7893
+ const [width2, height2] = layer.size ?? [16, 16];
7894
+ const item = await this.createItem(layer, index);
7895
+ item.position.set(x + width2 / 2, -y - height2 / 2, 0);
7896
+ item.scale.set(width2 / 16, height2 / 16, width2 / 16);
7897
+ item.updateMatrix();
7898
+ const bounds = new import_three22.Box3();
7899
+ item.iterateAllMeshes((mesh2) => {
7900
+ mesh2.geometry.computeBoundingBox();
7901
+ bounds.union(mesh2.geometry.boundingBox);
7902
+ });
7903
+ bounds.applyMatrix4(item.matrix);
7904
+ item.position.z = depth - bounds.min.z;
7905
+ depth += bounds.max.z - bounds.min.z + 0.01;
7906
+ this.bounds.expandByPoint(new import_three22.Vector2(x, y));
7907
+ this.bounds.expandByPoint(new import_three22.Vector2(x + width2, y + height2));
7908
+ this.bounds.expandByPoint(new import_three22.Vector2(bounds.min.x, -bounds.max.y));
7909
+ this.bounds.expandByPoint(new import_three22.Vector2(bounds.max.x, -bounds.min.y));
7910
+ continue;
7911
+ }
7690
7912
  const key = typeof layer.texture === "string" ? AssetKey.parse("textures", layer.texture) : layer.texture;
7691
7913
  const material = await this.loadMaterial(key);
7692
7914
  const image = material.map.image;
7693
7915
  const [cropX, cropY, cropWidth, cropHeight] = layer.crop ?? [0, 0, image.width, image.height];
7694
7916
  const [width, height] = layer.size ?? [cropWidth, cropHeight];
7695
- const [x, y] = layer.position ?? [0, 0];
7696
- const geometry = new import_three19.PlaneGeometry(width, height);
7917
+ const geometry = new import_three22.PlaneGeometry(width, height);
7697
7918
  this.geometries.add(geometry);
7698
7919
  const uv = geometry.getAttribute("uv");
7699
7920
  for (let i = 0; i < uv.count; i++) {
@@ -7703,14 +7924,16 @@ var GuiObject = class extends SceneObject {
7703
7924
  1 - (cropY + (1 - uv.getY(i)) * cropHeight) / image.height
7704
7925
  );
7705
7926
  }
7706
- const mesh = new import_three19.Mesh(geometry, material);
7927
+ const mesh = new import_three22.Mesh(geometry, material);
7707
7928
  mesh.name = `mesh:${layer.name ?? index}`;
7708
- mesh.position.set(x + width / 2, -y - height / 2, 0);
7929
+ mesh.position.set(x + width / 2, -y - height / 2, depth);
7709
7930
  mesh.renderOrder = index;
7710
7931
  this.add(mesh);
7711
- this.bounds.expandByPoint(new import_three19.Vector2(x, y));
7712
- this.bounds.expandByPoint(new import_three19.Vector2(x + width, y + height));
7932
+ this.bounds.expandByPoint(new import_three22.Vector2(x, y));
7933
+ this.bounds.expandByPoint(new import_three22.Vector2(x + width, y + height));
7934
+ depth += 0.01;
7713
7935
  }
7936
+ for (const child of this.children) child.position.z -= depth - 0.01;
7714
7937
  this.initialized = true;
7715
7938
  this.notifyDirty();
7716
7939
  } catch (error) {
@@ -7718,6 +7941,32 @@ var GuiObject = class extends SceneObject {
7718
7941
  throw error;
7719
7942
  }
7720
7943
  }
7944
+ async createItem(layer, index) {
7945
+ const key = typeof layer.item === "string" ? AssetKey.parse("models", layer.item) : layer.item;
7946
+ const model = await Models.getMerged(key);
7947
+ if (!model) throw new Error(`Could not load GUI item ${key.toNamespacedString()}`);
7948
+ const item = new ModelObject(model, {
7949
+ displayPosition: "gui" /* GUI */,
7950
+ tints: layer.tints,
7951
+ instanceMeshes: false,
7952
+ mergeMeshes: true
7953
+ });
7954
+ item.name = `group:${layer.name ?? index}`;
7955
+ this.add(item);
7956
+ await item.init();
7957
+ item.iterateAllMeshes((mesh) => {
7958
+ this.geometries.add(mesh.geometry);
7959
+ mesh.name = `mesh:${layer.name ?? index}`;
7960
+ mesh.renderOrder = index;
7961
+ for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
7962
+ material.transparent = true;
7963
+ if (model.gui_light === "front" /* FRONT */ && material.uniforms?.SHADE) {
7964
+ material.uniforms.SHADE.value = false;
7965
+ }
7966
+ }
7967
+ });
7968
+ return item;
7969
+ }
7721
7970
  async loadMaterial(key) {
7722
7971
  const assetKey = key.serialize();
7723
7972
  const materialKey = `gui:${assetKey}`;
@@ -7756,21 +8005,21 @@ function isGuiObject(obj) {
7756
8005
  init_image();
7757
8006
 
7758
8007
  // src/inspector/SceneInspector.ts
7759
- var import_three20 = require("three");
8008
+ var import_three23 = require("three");
7760
8009
  init_SceneObject();
7761
8010
  init_util();
7762
8011
  init_log();
7763
8012
  init_three();
7764
- var p7 = prefix("SceneInspector");
8013
+ var p8 = prefix("SceneInspector");
7765
8014
  var help = "<span>Ctrl/Cmd+Click to Select<br/></span><br/>";
7766
8015
  var SceneInspector = class {
7767
8016
  constructor(renderer) {
7768
8017
  this.renderer = renderer;
7769
- this.mouse = new import_three20.Vector2();
8018
+ this.mouse = new import_three23.Vector2();
7770
8019
  this.listeners = new AbortController();
7771
8020
  this.objectInfoContainer = document.createElement("div");
7772
8021
  this.objectControlsContainer = document.createElement("div");
7773
- this.raycaster = new import_three20.Raycaster();
8022
+ this.raycaster = new import_three23.Raycaster();
7774
8023
  this.init();
7775
8024
  }
7776
8025
  init() {
@@ -7855,7 +8104,7 @@ var SceneInspector = class {
7855
8104
  }
7856
8105
  selectObject(targetObject, targetIntersection, instance = this.getIntersectionInstance(targetIntersection)) {
7857
8106
  this.selectedObject = targetObject;
7858
- console.log(p7, "selected", targetObject);
8107
+ console.log(p8, "selected", targetObject);
7859
8108
  this.redraw(targetObject, targetIntersection, instance);
7860
8109
  }
7861
8110
  redraw(targetObject, targetIntersection, instance) {
@@ -8114,7 +8363,7 @@ var SceneStatsDisplay = class {
8114
8363
 
8115
8364
  // src/instance/InstanceManager.ts
8116
8365
  init_log();
8117
- var p8 = prefix("InstanceManager");
8366
+ var p9 = prefix("InstanceManager");
8118
8367
  var InstanceManager = class {
8119
8368
  constructor() {
8120
8369
  this.isInstanceManager = true;
@@ -8122,7 +8371,7 @@ var InstanceManager = class {
8122
8371
  }
8123
8372
  async get(key) {
8124
8373
  if (key in this.instanceCache) {
8125
- console.debug(p8, "key in cache", key);
8374
+ console.debug(p9, "key in cache", key);
8126
8375
  return (await this.instanceCache[key]).nextInstance();
8127
8376
  }
8128
8377
  return void 0;
@@ -8131,7 +8380,7 @@ var InstanceManager = class {
8131
8380
  if (key in this.instanceCache) {
8132
8381
  return await this.get(key);
8133
8382
  }
8134
- console.debug(p8, "key not in cache", key);
8383
+ console.debug(p9, "key not in cache", key);
8135
8384
  const pending = Promise.resolve().then(supplier).then((obj) => obj.nextInstance());
8136
8385
  this.instanceCache[key] = pending;
8137
8386
  try {
@@ -8165,13 +8414,6 @@ function isInstanceable(obj) {
8165
8414
  init_DisplayPosition();
8166
8415
  init_DisplayTransforms();
8167
8416
 
8168
- // src/model/GuiLight.ts
8169
- var GuiLight = /* @__PURE__ */ ((GuiLight2) => {
8170
- GuiLight2["FRONT"] = "front";
8171
- GuiLight2["SIDE"] = "side";
8172
- return GuiLight2;
8173
- })(GuiLight || {});
8174
-
8175
8417
  // src/model/Model.ts
8176
8418
  var ITEM_GENERATED = "item/generated";
8177
8419
  var BUILTIN_GENERATED = "builtin/generated";
@@ -8193,7 +8435,7 @@ init_ModelGenerator();
8193
8435
  init_ModelMerger();
8194
8436
 
8195
8437
  // src/model/block/BlockStateResolver.ts
8196
- var import_three23 = require("three");
8438
+ var import_three25 = require("three");
8197
8439
  init_BlockStates();
8198
8440
  init_MineRenderError();
8199
8441
  init_util();
@@ -8271,7 +8513,7 @@ var BlockStateResolver = class {
8271
8513
  return variants[variants.length - 1];
8272
8514
  }
8273
8515
  static rotation(variant) {
8274
- const rotation = new import_three23.Euler();
8516
+ const rotation = new import_three25.Euler();
8275
8517
  if (typeof variant.x !== "undefined") {
8276
8518
  rotation.x = toRadians(clampRotationDegrees(variant.x));
8277
8519
  }