minerender 2.1.0-alpha.141 → 2.1.0-alpha.143

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.
@@ -264,8 +264,6 @@ async function sleep(timeout) {
264
264
  });
265
265
  }
266
266
  function yieldToEventLoop() {
267
- const scheduler = globalThis.scheduler;
268
- if (scheduler?.yield) return scheduler.yield();
269
267
  return new Promise((resolve) => {
270
268
  if (typeof MessageChannel !== "undefined") {
271
269
  const channel = new MessageChannel();
@@ -18669,6 +18667,7 @@ function skipsBlockFace(data, rule, block, neighbor, face) {
18669
18667
  // src/world/MineRenderWorld.ts
18670
18668
  init_log();
18671
18669
  var log19 = logger("MineRenderWorld");
18670
+ var YIELD_INTERVAL_MS = 8;
18672
18671
  var MineRenderWorld = class {
18673
18672
  constructor(scene, options = {}) {
18674
18673
  this._chunks = /* @__PURE__ */ new Map();
@@ -18782,7 +18781,7 @@ var MineRenderWorld = class {
18782
18781
  failures.set(chunk, error);
18783
18782
  if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) === chunk) this.pendingBiomes.set(chunk, void 0);
18784
18783
  }
18785
- if (performance.now() - sliceStart > 8) {
18784
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18786
18785
  await yieldToEventLoop();
18787
18786
  sliceStart = performance.now();
18788
18787
  }
@@ -18937,7 +18936,7 @@ var MineRenderWorld = class {
18937
18936
  )));
18938
18937
  for (const pos of positions) changes.set(`${pos.x},${pos.y},${pos.z}`, pos);
18939
18938
  }
18940
- if (performance.now() - sliceStart > 8) {
18939
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18941
18940
  await yieldToEventLoop();
18942
18941
  sliceStart = performance.now();
18943
18942
  }
@@ -18945,6 +18944,62 @@ var MineRenderWorld = class {
18945
18944
  }
18946
18945
  /** Replaces a chunk column's blocks, biomes, and saved light and, with `renderEntities`, its supported saved mobs. */
18947
18946
  async placeChunk(chunk, executor) {
18947
+ return this.placeColumn(chunk, executor);
18948
+ }
18949
+ /**
18950
+ * Replaces parsed columns in input order, then refreshes neighboring geometry once.
18951
+ * Duplicate coordinates retain their order. A failed column stops placement; queued refreshes finish before rejection.
18952
+ */
18953
+ async placeChunks(chunks) {
18954
+ const columns = [];
18955
+ for (const chunk of chunks) {
18956
+ const { x, z } = chunk ?? {};
18957
+ if (![x, z].every(Number.isSafeInteger)) {
18958
+ throw new RangeError("Chunk column coordinates must be safe integers");
18959
+ }
18960
+ const sections = Array.from(chunk.sections, (section) => {
18961
+ const { y } = section ?? {};
18962
+ if (!Number.isSafeInteger(y)) throw new RangeError("Chunk section coordinates must be safe integers");
18963
+ return { ...section, y };
18964
+ });
18965
+ columns.push({ ...chunk, x, z, sections });
18966
+ }
18967
+ if (!columns.length) return;
18968
+ await this.culling;
18969
+ let failed = false, failure, sliceStart = performance.now();
18970
+ try {
18971
+ for (const column of columns) {
18972
+ await this.placeColumn(column, void 0, true);
18973
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18974
+ await yieldToEventLoop();
18975
+ sliceStart = performance.now();
18976
+ }
18977
+ }
18978
+ } catch (error) {
18979
+ failed = true;
18980
+ failure = error;
18981
+ } finally {
18982
+ try {
18983
+ const failures = await this.refreshBiomes(true);
18984
+ if (failures.size) log19.warn("Failed to refresh biome colors", failures.values().next().value);
18985
+ } catch (error) {
18986
+ if (!failed) {
18987
+ failed = true;
18988
+ failure = error;
18989
+ }
18990
+ }
18991
+ try {
18992
+ await this.updateCulling([]);
18993
+ } catch (error) {
18994
+ if (!failed) {
18995
+ failed = true;
18996
+ failure = error;
18997
+ }
18998
+ }
18999
+ }
19000
+ if (failed) throw failure;
19001
+ }
19002
+ async placeColumn(chunk, executor, deferCulling = false) {
18948
19003
  if (![chunk.x, chunk.z].every(Number.isSafeInteger)) {
18949
19004
  throw new RangeError("Chunk column coordinates must be safe integers");
18950
19005
  }
@@ -19006,7 +19061,7 @@ var MineRenderWorld = class {
19006
19061
  } finally {
19007
19062
  this.markSectionChanged(target);
19008
19063
  }
19009
- if (performance.now() - sliceStart > 8) {
19064
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19010
19065
  await yieldToEventLoop();
19011
19066
  sliceStart = performance.now();
19012
19067
  }
@@ -19029,9 +19084,12 @@ var MineRenderWorld = class {
19029
19084
  await placeEntities?.();
19030
19085
  } finally {
19031
19086
  this.markBiomesChanged(chunk.x, chunk.z, void 0, false);
19032
- const failures = await this.refreshBiomes();
19033
- if (failures.size) log19.warn("Failed to refresh biome colors", failures.values().next().value);
19034
- await this.updateCulling([...changes.values()]);
19087
+ if (deferCulling) await this.updateCulling([...changes.values()], true);
19088
+ else {
19089
+ const failures = await this.refreshBiomes();
19090
+ if (failures.size) log19.warn("Failed to refresh biome colors", failures.values().next().value);
19091
+ await this.updateCulling([...changes.values()]);
19092
+ }
19035
19093
  }
19036
19094
  }
19037
19095
  /**
@@ -19061,7 +19119,7 @@ var MineRenderWorld = class {
19061
19119
  for (const { x, z } of columns.values()) {
19062
19120
  this.entities?.clearColumn(x, z);
19063
19121
  await this.clearChunkColumn(x, z);
19064
- if (performance.now() - sliceStart > 8) {
19122
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19065
19123
  await yieldToEventLoop();
19066
19124
  sliceStart = performance.now();
19067
19125
  }
@@ -19200,15 +19258,25 @@ var MineRenderWorld = class {
19200
19258
  for (const [index, mask] of masks) await chunk.setCullMaskIndex(index, mask);
19201
19259
  await chunk.refreshLight(indices === "all" ? void 0 : indices);
19202
19260
  chunk.refreshAmbientOcclusion(this._ambientOcclusion);
19203
- if ((this._ambientOcclusion || this.ambientOcclusionPending) && performance.now() - sliceStart > 8) {
19261
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19204
19262
  await yieldToEventLoop();
19205
19263
  sliceStart = performance.now();
19206
19264
  }
19207
19265
  }
19208
- const builds = await Promise.allSettled([...batch.keys()].map(async (chunk) => {
19209
- await chunk.rebuildSectionMesh();
19210
- batch.delete(chunk);
19211
- }));
19266
+ const pending = [];
19267
+ for (const chunk of [...batch.keys()]) {
19268
+ const build = (async () => {
19269
+ await chunk.rebuildSectionMesh();
19270
+ batch.delete(chunk);
19271
+ })();
19272
+ void build.catch(() => void 0);
19273
+ pending.push(build);
19274
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19275
+ await yieldToEventLoop();
19276
+ sliceStart = performance.now();
19277
+ }
19278
+ }
19279
+ const builds = await Promise.allSettled(pending);
19212
19280
  const failure = builds.find((result) => result.status === "rejected");
19213
19281
  if (failure?.status === "rejected") throw failure.reason;
19214
19282
  } catch (error) {
@@ -19651,6 +19719,9 @@ _SectionWorker.initialized = false;
19651
19719
  var SectionWorker = _SectionWorker;
19652
19720
 
19653
19721
  // src/world/SectionMesh.ts
19722
+ var BLOCK_SIZE = 16;
19723
+ var QUAD_VERTICES = 4;
19724
+ var QUAD_INDICES = 6;
19654
19725
  var templateCache = /* @__PURE__ */ new WeakMap();
19655
19726
  var WATER_TINT = new import_three46.Color(4159204).toArray();
19656
19727
  function toInput(entries, maxAtlasSize, fluids, lighting) {
@@ -19687,8 +19758,8 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19687
19758
  if (!data) {
19688
19759
  const indices = template.geometry.getIndex();
19689
19760
  const position = template.geometry.getAttribute("position");
19690
- const quads = indices.count / 6;
19691
- if (indices.count % 6 !== 0 || position.count !== quads * 4) {
19761
+ const quads = indices.count / QUAD_INDICES;
19762
+ if (indices.count % QUAD_INDICES !== 0 || position.count !== quads * QUAD_VERTICES) {
19692
19763
  throw new RangeError("Section template geometry must hold four vertices and six indices per quad");
19693
19764
  }
19694
19765
  data = {
@@ -19698,8 +19769,8 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19698
19769
  uvs: new Float32Array(quads * 8),
19699
19770
  uvBounds: new Float32Array(quads * 16),
19700
19771
  colors: new Float32Array(quads * 12),
19701
- shade: new Float32Array(quads * 4),
19702
- indices: new Uint16Array(quads * 6),
19772
+ shade: new Float32Array(quads * QUAD_VERTICES),
19773
+ indices: new Uint16Array(quads * QUAD_INDICES),
19703
19774
  cullFaces: new Uint8Array(template.cullFaces),
19704
19775
  biomeTint: template.biomeTint ? new Uint8Array(template.biomeTint) : void 0,
19705
19776
  atlas: 0,
@@ -19711,7 +19782,7 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19711
19782
  const bounds = template.geometry.getAttribute("uvBounds");
19712
19783
  const color2 = template.geometry.getAttribute("color");
19713
19784
  const shade = template.geometry.getAttribute("shade");
19714
- for (let vertex = 0; vertex < quads * 4; vertex++) {
19785
+ for (let vertex = 0; vertex < quads * QUAD_VERTICES; vertex++) {
19715
19786
  data.positions.set([position.getX(vertex), position.getY(vertex), position.getZ(vertex)], vertex * 3);
19716
19787
  data.normals.set([normal.getX(vertex), normal.getY(vertex), normal.getZ(vertex)], vertex * 3);
19717
19788
  data.uvs.set([uv.getX(vertex), uv.getY(vertex)], vertex * 2);
@@ -19724,7 +19795,7 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19724
19795
  data.colors.set([color2?.getX(vertex) ?? 1, color2?.getY(vertex) ?? 1, color2?.getZ(vertex) ?? 1], vertex * 3);
19725
19796
  data.shade[vertex] = shade?.getX(vertex) ?? 1;
19726
19797
  }
19727
- for (let i2 = 0; i2 < data.indices.length; i2++) data.indices[i2] = indices.getX(i2) - 4 * Math.floor(i2 / 6);
19798
+ for (let i2 = 0; i2 < data.indices.length; i2++) data.indices[i2] = indices.getX(i2) - QUAD_VERTICES * Math.floor(i2 / QUAD_INDICES);
19728
19799
  templateCache.set(template, data);
19729
19800
  }
19730
19801
  id = input.templateData.length;
@@ -19757,6 +19828,54 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
19757
19828
  }
19758
19829
  return { input, atlases: atlases2 };
19759
19830
  }
19831
+ function sortTransparentQuads(mesh) {
19832
+ const index = mesh.geometry.getIndex();
19833
+ if (index.count <= QUAD_INDICES) return;
19834
+ index.setUsage(import_three46.DynamicDrawUsage);
19835
+ const count = index.count / QUAD_INDICES;
19836
+ let source, centers, depths, order;
19837
+ const view = new import_three46.Matrix4(), localCamera = new import_three46.Vector3(), key = new import_three46.Vector3(), previous = new import_three46.Vector3();
19838
+ let previousOrthographic;
19839
+ mesh.onBeforeRender = (_renderer, _scene, camera) => {
19840
+ localCamera.setFromMatrixPosition(camera.matrixWorld).applyMatrix4(view.copy(mesh.matrixWorld).invert());
19841
+ const orthographic = !!camera.isOrthographicCamera;
19842
+ if (orthographic) {
19843
+ view.multiplyMatrices(camera.matrixWorldInverse, mesh.matrixWorld);
19844
+ key.set(view.elements[2], view.elements[6], view.elements[10]);
19845
+ } else {
19846
+ key.copy(localCamera).divideScalar(BLOCK_SIZE).floor();
19847
+ }
19848
+ if (orthographic === previousOrthographic && key.equals(previous)) return;
19849
+ previous.copy(key);
19850
+ previousOrthographic = orthographic;
19851
+ if (!centers) {
19852
+ source = new Uint32Array(index.array);
19853
+ centers = new Float32Array(count * 3);
19854
+ depths = new Float32Array(count);
19855
+ order = Uint32Array.from({ length: count }, (_, quad) => quad);
19856
+ const positions = mesh.geometry.getAttribute("position");
19857
+ for (let quad = 0; quad < count; quad++) {
19858
+ for (let axis = 0; axis < 3; axis++) {
19859
+ let center = 0;
19860
+ for (let corner = 0; corner < QUAD_VERTICES; corner++) center += positions.getComponent(quad * QUAD_VERTICES + corner, axis);
19861
+ centers[quad * 3 + axis] = center / QUAD_VERTICES;
19862
+ }
19863
+ }
19864
+ }
19865
+ for (let quad = 0; quad < count; quad++) {
19866
+ const x = centers[quad * 3], y = centers[quad * 3 + 1], z = centers[quad * 3 + 2];
19867
+ depths[quad] = orthographic ? key.x * x + key.y * y + key.z * z : -((x - localCamera.x) ** 2 + (y - localCamera.y) ** 2 + (z - localCamera.z) ** 2);
19868
+ }
19869
+ order.sort((a, b) => depths[a] - depths[b] || a - b);
19870
+ if (!order.some((quad, offset) => index.array[offset * QUAD_INDICES] !== source[quad * QUAD_INDICES])) return;
19871
+ for (let offset = 0; offset < count; offset++) {
19872
+ for (let indexOffset = 0; indexOffset < QUAD_INDICES; indexOffset++) {
19873
+ index.array[offset * QUAD_INDICES + indexOffset] = source[order[offset] * QUAD_INDICES + indexOffset];
19874
+ }
19875
+ }
19876
+ index.needsUpdate = true;
19877
+ };
19878
+ }
19760
19879
  var SectionMesh = class _SectionMesh extends import_three46.Group {
19761
19880
  constructor() {
19762
19881
  super(...arguments);
@@ -19766,7 +19885,11 @@ var SectionMesh = class _SectionMesh extends import_three46.Group {
19766
19885
  /** Returns the owning section cell for a triangle hit on this mesh's merged geometry. */
19767
19886
  getBlockIndex(hit) {
19768
19887
  if (hit.faceIndex === void 0 || hit.faceIndex === null) return void 0;
19769
- return this.blockIndices.get(hit.object)?.[Math.floor(hit.faceIndex / 2)];
19888
+ const mesh = hit.object;
19889
+ const owners = this.blockIndices.get(mesh);
19890
+ if (!owners) return void 0;
19891
+ const vertex = mesh.geometry.getIndex().getX(hit.faceIndex * 3);
19892
+ return owners[Math.floor(vertex / QUAD_VERTICES)];
19770
19893
  }
19771
19894
  /** Builds section-local meshes from visible quads. `maxAtlasSize` limits each atlas dimension in pixels. */
19772
19895
  static build(entries, maxAtlasSize = 2048, fluids, lighting) {
@@ -19802,13 +19925,14 @@ var SectionMesh = class _SectionMesh extends import_three46.Group {
19802
19925
  const material = Materials.createShadedCanvasMaterial(canvas, transparent, false, true);
19803
19926
  material.side = import_three46.FrontSide;
19804
19927
  if (page.layer === 2) {
19928
+ material.depthWrite = !material.transparent;
19805
19929
  material.side = import_three46.DoubleSide;
19806
19930
  material.forceSinglePass = true;
19807
- material.depthWrite = !material.transparent;
19808
19931
  }
19809
19932
  material.vertexColors = true;
19810
19933
  const texture = material.uniforms?.map?.value ?? material.map;
19811
19934
  const mesh = new import_three46.Mesh(geometry, material);
19935
+ if (transparent) sortTransparentQuads(mesh);
19812
19936
  section.add(mesh);
19813
19937
  section.blockIndices.set(mesh, page.blockIndices);
19814
19938
  const unsubscribe = [];
@@ -20611,6 +20735,7 @@ var Chunk = class _Chunk {
20611
20735
  };
20612
20736
 
20613
20737
  // src/world/WorldStreamer.ts
20738
+ var MAX_PREFETCHED_COLUMNS = 4;
20614
20739
  var WorldStreamer = class {
20615
20740
  constructor(world, source, options = {}) {
20616
20741
  this.world = world;
@@ -20620,6 +20745,7 @@ var WorldStreamer = class {
20620
20745
  this.missing = /* @__PURE__ */ new Map();
20621
20746
  this.failed = /* @__PURE__ */ new Map();
20622
20747
  this.desired = [];
20748
+ this.reads = /* @__PURE__ */ new Map();
20623
20749
  this.disposed = false;
20624
20750
  this.loadRadius = options.loadRadius ?? 2;
20625
20751
  this.unloadRadius = options.unloadRadius ?? Math.min(32, this.loadRadius + 1);
@@ -20664,6 +20790,12 @@ var WorldStreamer = class {
20664
20790
  if (this.loading && !this.within(this.loading.position, this.loadRadius)) {
20665
20791
  this.loading.controller.abort();
20666
20792
  }
20793
+ for (const [key, read] of this.reads) {
20794
+ if (!this.within(read.position, this.loadRadius)) {
20795
+ read.controller.abort();
20796
+ if (read.ready) this.reads.delete(key);
20797
+ }
20798
+ }
20667
20799
  }
20668
20800
  return this.running ?? (this.running = Promise.resolve().then(() => this.drain()));
20669
20801
  }
@@ -20684,8 +20816,11 @@ var WorldStreamer = class {
20684
20816
  this.desired = [];
20685
20817
  this.center = void 0;
20686
20818
  this.loading?.controller.abort();
20819
+ for (const read of this.reads.values()) read.controller.abort();
20687
20820
  return this.disposal ?? (this.disposal = Promise.resolve().then(async () => {
20688
20821
  await this.running?.catch(() => void 0);
20822
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20823
+ this.reads.clear();
20689
20824
  try {
20690
20825
  await this.unloadMany([...this.owned.values()]);
20691
20826
  } finally {
@@ -20711,6 +20846,10 @@ var WorldStreamer = class {
20711
20846
  await this.unloadMany(obsolete);
20712
20847
  continue;
20713
20848
  }
20849
+ if (this.world.placeChunks) {
20850
+ if (await this.placeReadyChunks()) continue;
20851
+ return;
20852
+ }
20714
20853
  const position = this.desired.find((position2) => this.needsLoad(position2));
20715
20854
  if (!position) return;
20716
20855
  let chunk;
@@ -20733,26 +20872,105 @@ var WorldStreamer = class {
20733
20872
  continue;
20734
20873
  }
20735
20874
  this.owned.set(this.key(position), position);
20875
+ await this.placeOwnedColumns([position], () => this.world.placeChunk(chunk));
20876
+ }
20877
+ } finally {
20878
+ for (const [key, read] of this.reads) {
20879
+ read.controller.abort();
20880
+ if (read.ready) this.reads.delete(key);
20881
+ }
20882
+ this.running = void 0;
20883
+ }
20884
+ }
20885
+ fillReadSlots() {
20886
+ while (!this.disposed && this.reads.size < MAX_PREFETCHED_COLUMNS) {
20887
+ const position = this.desired.find((position2) => this.needsLoad(position2) && !this.reads.has(this.key(position2)));
20888
+ if (!position) return;
20889
+ const key = this.key(position);
20890
+ const read = {
20891
+ position,
20892
+ controller: new AbortController(),
20893
+ promise: Promise.resolve(),
20894
+ ready: false,
20895
+ failed: false
20896
+ };
20897
+ this.reads.set(key, read);
20898
+ read.promise = (async () => {
20736
20899
  try {
20737
- await this.world.placeChunk(chunk);
20738
- this.loaded.set(this.key(position), position);
20900
+ read.chunk = await this.source.getChunk(position.x, position.z, read.controller.signal);
20901
+ if (read.chunk) this.validateChunk(read.chunk, position);
20739
20902
  } catch (error) {
20740
- try {
20741
- await this.unloadMany([position]);
20742
- } catch (cleanupError) {
20743
- const failure = error instanceof Error ? error : new Error(String(error));
20744
- try {
20745
- Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
20746
- } catch {
20747
- throw Object.assign(new Error(failure.message), { cause: cleanupError });
20748
- }
20749
- throw failure;
20903
+ read.failed = true;
20904
+ read.error = error;
20905
+ } finally {
20906
+ read.ready = true;
20907
+ if (read.controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) {
20908
+ this.reads.delete(key);
20750
20909
  }
20751
- throw error;
20910
+ }
20911
+ })();
20912
+ }
20913
+ }
20914
+ /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */
20915
+ async placeReadyChunks() {
20916
+ this.fillReadSlots();
20917
+ const position = this.desired.find((position2) => this.needsLoad(position2));
20918
+ if (!position) {
20919
+ if (!this.reads.size) return false;
20920
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20921
+ return true;
20922
+ }
20923
+ const head = this.reads.get(this.key(position));
20924
+ if (!head) {
20925
+ for (let index = this.desired.length - 1; index >= 0; index--) {
20926
+ const key = this.key(this.desired[index]);
20927
+ if (this.reads.get(key)?.ready) {
20928
+ this.reads.delete(key);
20929
+ return true;
20752
20930
  }
20753
20931
  }
20754
- } finally {
20755
- this.running = void 0;
20932
+ await Promise.race([...this.reads.values()].map((read) => read.promise));
20933
+ return true;
20934
+ }
20935
+ if (!head.ready) {
20936
+ await head.promise;
20937
+ return true;
20938
+ }
20939
+ const chunks = [], positions = [];
20940
+ for (const desired of this.desired) {
20941
+ if (!this.needsLoad(desired)) continue;
20942
+ const key = this.key(desired), read = this.reads.get(key);
20943
+ if (!read?.ready) break;
20944
+ this.reads.delete(key);
20945
+ if (read.failed) this.failed.set(key, { ...desired, error: read.error });
20946
+ else if (!read.chunk) this.missing.set(key, desired);
20947
+ else {
20948
+ chunks.push(read.chunk);
20949
+ positions.push(desired);
20950
+ }
20951
+ }
20952
+ if (!chunks.length) return true;
20953
+ for (const position2 of positions) this.owned.set(this.key(position2), position2);
20954
+ await this.placeOwnedColumns(positions, () => this.world.placeChunks(chunks));
20955
+ return true;
20956
+ }
20957
+ async placeOwnedColumns(positions, place) {
20958
+ try {
20959
+ await place();
20960
+ for (const position of positions) this.loaded.set(this.key(position), position);
20961
+ } catch (error) {
20962
+ try {
20963
+ await this.unloadMany(positions);
20964
+ } catch (cleanupError) {
20965
+ const failure = error instanceof Error ? error : new Error(String(error));
20966
+ try {
20967
+ Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
20968
+ } catch {
20969
+ throw Object.assign(new Error(failure.message), { cause: cleanupError });
20970
+ }
20971
+ throw failure;
20972
+ }
20973
+ throw error;
20756
20974
  }
20757
20975
  }
20758
20976
  needsLoad(position) {