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.
@@ -1,9 +1,12 @@
1
1
  // src/world/SectionMesh.ts
2
- import { BufferGeometry, Color, DoubleSide, Float32BufferAttribute, FrontSide, Group, Mesh, Uint32BufferAttribute } from "three";
2
+ import { BufferGeometry, Color, DoubleSide, DynamicDrawUsage, Float32BufferAttribute, FrontSide, Group, Matrix4, Mesh, Uint32BufferAttribute, Vector3 } from "three";
3
3
  import { createCanvas } from "../canvas/CanvasCompat.mjs";
4
4
  import { Materials } from "../Materials.mjs";
5
5
  import { buildSectionGeometry } from "./SectionGeometry.mjs";
6
6
  import { SectionWorker } from "./SectionWorker.mjs";
7
+ var BLOCK_SIZE = 16;
8
+ var QUAD_VERTICES = 4;
9
+ var QUAD_INDICES = 6;
7
10
  var templateCache = /* @__PURE__ */ new WeakMap();
8
11
  var WATER_TINT = new Color(4159204).toArray();
9
12
  function toInput(entries, maxAtlasSize, fluids, lighting) {
@@ -40,8 +43,8 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
40
43
  if (!data) {
41
44
  const indices = template.geometry.getIndex();
42
45
  const position = template.geometry.getAttribute("position");
43
- const quads = indices.count / 6;
44
- if (indices.count % 6 !== 0 || position.count !== quads * 4) {
46
+ const quads = indices.count / QUAD_INDICES;
47
+ if (indices.count % QUAD_INDICES !== 0 || position.count !== quads * QUAD_VERTICES) {
45
48
  throw new RangeError("Section template geometry must hold four vertices and six indices per quad");
46
49
  }
47
50
  data = {
@@ -51,8 +54,8 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
51
54
  uvs: new Float32Array(quads * 8),
52
55
  uvBounds: new Float32Array(quads * 16),
53
56
  colors: new Float32Array(quads * 12),
54
- shade: new Float32Array(quads * 4),
55
- indices: new Uint16Array(quads * 6),
57
+ shade: new Float32Array(quads * QUAD_VERTICES),
58
+ indices: new Uint16Array(quads * QUAD_INDICES),
56
59
  cullFaces: new Uint8Array(template.cullFaces),
57
60
  biomeTint: template.biomeTint ? new Uint8Array(template.biomeTint) : void 0,
58
61
  atlas: 0,
@@ -64,7 +67,7 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
64
67
  const bounds = template.geometry.getAttribute("uvBounds");
65
68
  const color = template.geometry.getAttribute("color");
66
69
  const shade = template.geometry.getAttribute("shade");
67
- for (let vertex = 0; vertex < quads * 4; vertex++) {
70
+ for (let vertex = 0; vertex < quads * QUAD_VERTICES; vertex++) {
68
71
  data.positions.set([position.getX(vertex), position.getY(vertex), position.getZ(vertex)], vertex * 3);
69
72
  data.normals.set([normal.getX(vertex), normal.getY(vertex), normal.getZ(vertex)], vertex * 3);
70
73
  data.uvs.set([uv.getX(vertex), uv.getY(vertex)], vertex * 2);
@@ -77,7 +80,7 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
77
80
  data.colors.set([color?.getX(vertex) ?? 1, color?.getY(vertex) ?? 1, color?.getZ(vertex) ?? 1], vertex * 3);
78
81
  data.shade[vertex] = shade?.getX(vertex) ?? 1;
79
82
  }
80
- for (let i2 = 0; i2 < data.indices.length; i2++) data.indices[i2] = indices.getX(i2) - 4 * Math.floor(i2 / 6);
83
+ for (let i2 = 0; i2 < data.indices.length; i2++) data.indices[i2] = indices.getX(i2) - QUAD_VERTICES * Math.floor(i2 / QUAD_INDICES);
81
84
  templateCache.set(template, data);
82
85
  }
83
86
  id = input.templateData.length;
@@ -110,6 +113,54 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
110
113
  }
111
114
  return { input, atlases };
112
115
  }
116
+ function sortTransparentQuads(mesh) {
117
+ const index = mesh.geometry.getIndex();
118
+ if (index.count <= QUAD_INDICES) return;
119
+ index.setUsage(DynamicDrawUsage);
120
+ const count = index.count / QUAD_INDICES;
121
+ let source, centers, depths, order;
122
+ const view = new Matrix4(), localCamera = new Vector3(), key = new Vector3(), previous = new Vector3();
123
+ let previousOrthographic;
124
+ mesh.onBeforeRender = (_renderer, _scene, camera) => {
125
+ localCamera.setFromMatrixPosition(camera.matrixWorld).applyMatrix4(view.copy(mesh.matrixWorld).invert());
126
+ const orthographic = !!camera.isOrthographicCamera;
127
+ if (orthographic) {
128
+ view.multiplyMatrices(camera.matrixWorldInverse, mesh.matrixWorld);
129
+ key.set(view.elements[2], view.elements[6], view.elements[10]);
130
+ } else {
131
+ key.copy(localCamera).divideScalar(BLOCK_SIZE).floor();
132
+ }
133
+ if (orthographic === previousOrthographic && key.equals(previous)) return;
134
+ previous.copy(key);
135
+ previousOrthographic = orthographic;
136
+ if (!centers) {
137
+ source = new Uint32Array(index.array);
138
+ centers = new Float32Array(count * 3);
139
+ depths = new Float32Array(count);
140
+ order = Uint32Array.from({ length: count }, (_, quad) => quad);
141
+ const positions = mesh.geometry.getAttribute("position");
142
+ for (let quad = 0; quad < count; quad++) {
143
+ for (let axis = 0; axis < 3; axis++) {
144
+ let center = 0;
145
+ for (let corner = 0; corner < QUAD_VERTICES; corner++) center += positions.getComponent(quad * QUAD_VERTICES + corner, axis);
146
+ centers[quad * 3 + axis] = center / QUAD_VERTICES;
147
+ }
148
+ }
149
+ }
150
+ for (let quad = 0; quad < count; quad++) {
151
+ const x = centers[quad * 3], y = centers[quad * 3 + 1], z = centers[quad * 3 + 2];
152
+ depths[quad] = orthographic ? key.x * x + key.y * y + key.z * z : -((x - localCamera.x) ** 2 + (y - localCamera.y) ** 2 + (z - localCamera.z) ** 2);
153
+ }
154
+ order.sort((a, b) => depths[a] - depths[b] || a - b);
155
+ if (!order.some((quad, offset) => index.array[offset * QUAD_INDICES] !== source[quad * QUAD_INDICES])) return;
156
+ for (let offset = 0; offset < count; offset++) {
157
+ for (let indexOffset = 0; indexOffset < QUAD_INDICES; indexOffset++) {
158
+ index.array[offset * QUAD_INDICES + indexOffset] = source[order[offset] * QUAD_INDICES + indexOffset];
159
+ }
160
+ }
161
+ index.needsUpdate = true;
162
+ };
163
+ }
113
164
  var SectionMesh = class _SectionMesh extends Group {
114
165
  constructor() {
115
166
  super(...arguments);
@@ -119,7 +170,11 @@ var SectionMesh = class _SectionMesh extends Group {
119
170
  /** Returns the owning section cell for a triangle hit on this mesh's merged geometry. */
120
171
  getBlockIndex(hit) {
121
172
  if (hit.faceIndex === void 0 || hit.faceIndex === null) return void 0;
122
- return this.blockIndices.get(hit.object)?.[Math.floor(hit.faceIndex / 2)];
173
+ const mesh = hit.object;
174
+ const owners = this.blockIndices.get(mesh);
175
+ if (!owners) return void 0;
176
+ const vertex = mesh.geometry.getIndex().getX(hit.faceIndex * 3);
177
+ return owners[Math.floor(vertex / QUAD_VERTICES)];
123
178
  }
124
179
  /** Builds section-local meshes from visible quads. `maxAtlasSize` limits each atlas dimension in pixels. */
125
180
  static build(entries, maxAtlasSize = 2048, fluids, lighting) {
@@ -155,13 +210,14 @@ var SectionMesh = class _SectionMesh extends Group {
155
210
  const material = Materials.createShadedCanvasMaterial(canvas, transparent, false, true);
156
211
  material.side = FrontSide;
157
212
  if (page.layer === 2) {
213
+ material.depthWrite = !material.transparent;
158
214
  material.side = DoubleSide;
159
215
  material.forceSinglePass = true;
160
- material.depthWrite = !material.transparent;
161
216
  }
162
217
  material.vertexColors = true;
163
218
  const texture = material.uniforms?.map?.value ?? material.map;
164
219
  const mesh = new Mesh(geometry, material);
220
+ if (transparent) sortTransparentQuads(mesh);
165
221
  section.add(mesh);
166
222
  section.blockIndices.set(mesh, page.blockIndices);
167
223
  const unsubscribe = [];
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/world/SectionMesh.ts"],"sourcesContent":["import { BufferGeometry, Color, DoubleSide, Float32BufferAttribute, FrontSide, Group, Material, Mesh, MeshBasicMaterial, ShaderMaterial, Texture, Uint32BufferAttribute } from \"three\";\nimport { createCanvas } from \"../canvas/CanvasCompat\";\nimport { Materials } from \"../Materials\";\nimport type { MineRenderScene } from \"../renderer/MineRenderScene\";\nimport { TextureAtlas } from \"../texture/TextureAtlas\";\nimport { buildSectionGeometry, SectionGeometryInput, SectionGeometryPage, SectionTemplateData } from \"./SectionGeometry\";\nimport { SectionWorker } from \"./SectionWorker\";\nimport type { Intersection } from \"three\";\n\n/** Shared quad geometry and atlas data prepared for section merging. */\nexport interface SectionMeshTemplate {\n geometry: BufferGeometry;\n atlas: TextureAtlas;\n cullFaces: Uint8Array;\n /** Quads that accept a placement's biome tint, stored as zero or one. */\n biomeTint?: Uint8Array;\n occludes: boolean;\n /** Whether the model permits corner shading from neighboring full cubes. */\n ambientOcclusion?: boolean;\n layer: 0 | 1;\n}\n\n/** One block placement in a section, with its storage index and hidden-face mask. */\nexport interface SectionMeshEntry {\n index: number;\n template: SectionMeshTemplate;\n cullMask: number;\n /** Linear RGB replacing automatic biome colors on eligible quads. */\n biomeTint?: readonly [number, number, number];\n}\n\n/** Fluid cells for one section and the shared atlases of the kinds present. */\nexport interface SectionFluids {\n cells: Uint8Array;\n /** Linear RGB per section cell, indexed by (x + 16z + 256y) * 3. */\n waterTints?: Float32Array;\n water?: TextureAtlas;\n lava?: TextureAtlas;\n}\n\nconst templateCache = new WeakMap<SectionMeshTemplate, SectionTemplateData>();\nconst WATER_TINT = new Color(0x3f76e4).toArray() as [number, number, number];\n\nfunction toInput(entries: readonly SectionMeshEntry[], maxAtlasSize: number, fluids?: SectionFluids,\n lighting?: Pick<SectionGeometryInput, \"light\" | \"ambientLight\" | \"ambientOcclusion\">): { input: SectionGeometryInput; atlases: TextureAtlas[] } {\n if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError(\"Section atlas size must be a positive integer\");\n const atlases: TextureAtlas[] = [];\n const atlasIds = new Map<TextureAtlas, number>();\n const templateIds = new Map<SectionMeshTemplate, number>();\n const input: SectionGeometryInput = {\n count: entries.length, indices: new Uint16Array(entries.length), templates: new Uint16Array(entries.length),\n cullMasks: new Uint8Array(entries.length), templateData: [], atlases: [], maxAtlasSize, ...lighting\n };\n for (let i = 0; i < entries.length; i++) {\n const { index, template, cullMask, biomeTint } = entries[i];\n if (biomeTint) {\n input.biomeTints ??= new Float32Array(entries.length * 3).fill(NaN);\n input.biomeTints.set(biomeTint, i * 3);\n }\n let id = templateIds.get(template);\n if (id === undefined) {\n let atlas = atlasIds.get(template.atlas);\n if (atlas === undefined) {\n atlas = atlases.length;\n atlasIds.set(template.atlas, atlas);\n atlases.push(template.atlas);\n input.atlases.push({ width: template.atlas.image.width, height: template.atlas.image.height, animated: template.atlas.hasAnimation });\n }\n let data = templateCache.get(template);\n if (!data) {\n const indices = template.geometry.getIndex()!;\n const position = template.geometry.getAttribute(\"position\");\n const quads = indices.count / 6;\n if (indices.count % 6 !== 0 || position.count !== quads * 4) {\n throw new RangeError(\"Section template geometry must hold four vertices and six indices per quad\");\n }\n data = {\n quads, positions: new Float32Array(quads * 12), normals: new Float32Array(quads * 12), uvs: new Float32Array(quads * 8),\n uvBounds: new Float32Array(quads * 16), colors: new Float32Array(quads * 12), shade: new Float32Array(quads * 4), indices: new Uint16Array(quads * 6),\n cullFaces: new Uint8Array(template.cullFaces), biomeTint: template.biomeTint ? new Uint8Array(template.biomeTint) : undefined, atlas: 0, layer: template.layer, ambientOcclusion: template.ambientOcclusion\n };\n const normal = template.geometry.getAttribute(\"normal\");\n const uv = template.geometry.getAttribute(\"uv\");\n const bounds = template.geometry.getAttribute(\"uvBounds\");\n const color = template.geometry.getAttribute(\"color\");\n const shade = template.geometry.getAttribute(\"shade\");\n for (let vertex = 0; vertex < quads * 4; vertex++) {\n data.positions.set([position.getX(vertex), position.getY(vertex), position.getZ(vertex)], vertex * 3);\n data.normals.set([normal.getX(vertex), normal.getY(vertex), normal.getZ(vertex)], vertex * 3);\n data.uvs.set([uv.getX(vertex), uv.getY(vertex)], vertex * 2);\n data.uvBounds.set([bounds?.getX(vertex) ?? 0, bounds?.getY(vertex) ?? 0,\n bounds?.getZ(vertex) ?? 1, bounds?.getW(vertex) ?? 1], vertex * 4);\n data.colors.set([color?.getX(vertex) ?? 1, color?.getY(vertex) ?? 1, color?.getZ(vertex) ?? 1], vertex * 3);\n data.shade![vertex] = shade?.getX(vertex) ?? 1;\n }\n for (let i = 0; i < data.indices.length; i++) data.indices[i] = indices.getX(i) - 4 * Math.floor(i / 6);\n templateCache.set(template, data);\n }\n id = input.templateData.length;\n templateIds.set(template, id);\n // Atlas IDs belong to each build; cached buffers can be shared across concurrent builds.\n input.templateData.push({ ...data, atlas });\n }\n input.indices[i] = index;\n input.templates[i] = id;\n input.cullMasks[i] = cullMask;\n }\n if (fluids) {\n input.fluids = { cells: fluids.cells, waterTints: fluids.waterTints };\n for (const kind of [\"water\", \"lava\"] as const) {\n const atlas = fluids[kind];\n if (!atlas) continue;\n let id = atlasIds.get(atlas);\n if (id === undefined) {\n id = atlases.length;\n atlasIds.set(atlas, id);\n atlases.push(atlas);\n input.atlases.push({ width: atlas.image.width, height: atlas.image.height, animated: atlas.hasAnimation });\n }\n input.fluids[kind] = {\n atlas: id, still: [...atlas.positions.still, ...atlas.sizes.still],\n flow: [...atlas.positions.flow, ...atlas.sizes.flow], tint: kind === \"water\" ? WATER_TINT : [1, 1, 1]\n };\n }\n }\n return { input, atlases };\n}\n\n/** Merged terrain geometry and atlas pages for one 16×16×16 section. Owns its generated render resources. */\nexport class SectionMesh extends Group {\n\n private readonly ownedMeshes: { mesh: Mesh<BufferGeometry, Material>; texture?: Texture; unsubscribe: (() => void)[] }[] = [];\n private readonly blockIndices = new Map<Mesh, Uint16Array>();\n\n /** Returns the owning section cell for a triangle hit on this mesh's merged geometry. */\n public getBlockIndex(hit: Intersection): number | undefined {\n if (hit.faceIndex === undefined || hit.faceIndex === null) return undefined;\n return this.blockIndices.get(hit.object as Mesh)?.[Math.floor(hit.faceIndex / 2)];\n }\n\n /** Builds section-local meshes from visible quads. `maxAtlasSize` limits each atlas dimension in pixels. */\n public static build(entries: readonly SectionMeshEntry[], maxAtlasSize = 2048, fluids?: SectionFluids,\n lighting?: Pick<SectionGeometryInput, \"light\" | \"ambientLight\" | \"ambientOcclusion\">): SectionMesh {\n const { input, atlases } = toInput(entries, maxAtlasSize, fluids, lighting);\n return this.fromPages(buildSectionGeometry(input), atlases);\n }\n\n /** Builds section-local meshes in a browser worker when available, with synchronous fallback. */\n public static async buildAsync(entries: readonly SectionMeshEntry[], maxAtlasSize = 2048, fluids?: SectionFluids,\n lighting?: Pick<SectionGeometryInput, \"light\" | \"ambientLight\" | \"ambientOcclusion\">): Promise<SectionMesh> {\n const { input, atlases } = toInput(entries, maxAtlasSize, fluids, lighting);\n const worker = SectionWorker.shared();\n const pages = worker ? await worker.build(input).catch(() => buildSectionGeometry(input)) : buildSectionGeometry(input);\n return this.fromPages(pages, atlases);\n }\n\n private static fromPages(pages: SectionGeometryPage[], atlases: TextureAtlas[]): SectionMesh {\n const section = new SectionMesh();\n for (const page of pages) {\n const canvas = createCanvas(page.width, page.height);\n const context = canvas.getContext(\"2d\") as CanvasRenderingContext2D;\n for (const { atlas, x, y } of page.placements) {\n context.drawImage(atlases[atlas].image.canvas as CanvasImageSource, x, y);\n }\n const geometry = new BufferGeometry();\n // Passing buffers keeps Three's typed attributes from copying the builder's arrays.\n geometry.setAttribute(\"position\", new Float32BufferAttribute(page.positions.buffer, 3));\n geometry.setAttribute(\"normal\", new Float32BufferAttribute(page.normals.buffer, 3));\n geometry.setAttribute(\"uv\", new Float32BufferAttribute(page.uvs.buffer, 2));\n geometry.setAttribute(\"uvBounds\", new Float32BufferAttribute(page.uvBounds.buffer, 4));\n geometry.setAttribute(\"color\", new Float32BufferAttribute(page.colors.buffer, 3));\n geometry.setAttribute(\"shade\", new Float32BufferAttribute(page.shade.buffer, 1));\n geometry.setIndex(new Uint32BufferAttribute(page.indices.buffer, 1));\n geometry.computeBoundingBox();\n geometry.computeBoundingSphere();\n const transparent = page.layer === 2 ? atlases[page.placements[0].atlas].hasTranslucency : page.layer === 1;\n const material = Materials.createShadedCanvasMaterial(canvas as HTMLCanvasElement, transparent, false, true);\n material.side = FrontSide;\n if (page.layer === 2) {\n material.side = DoubleSide;\n material.forceSinglePass = true;\n material.depthWrite = !material.transparent;\n }\n material.vertexColors = true;\n const texture = (material as ShaderMaterial).uniforms?.map?.value ?? (material as MeshBasicMaterial).map;\n const mesh = new Mesh(geometry, material);\n section.add(mesh);\n section.blockIndices.set(mesh, page.blockIndices);\n const unsubscribe: (() => void)[] = [];\n for (const { atlas: id, x, y } of page.placements) {\n const atlas = atlases[id];\n if (!atlas.hasAnimation) continue;\n unsubscribe.push(atlas.subscribe(() => {\n context.clearRect(x, y, atlas.image.width, atlas.image.height);\n context.drawImage(atlas.image.canvas as CanvasImageSource, x, y);\n texture.needsUpdate = true;\n section.traverseAncestors(parent => {\n if ((parent as MineRenderScene).isMineRenderScene) (parent as MineRenderScene).dirty = true;\n });\n }));\n }\n section.ownedMeshes.push({ mesh, texture, unsubscribe });\n }\n return section;\n }\n\n public dispose(): void {\n for (const { mesh, texture, unsubscribe } of this.ownedMeshes.splice(0)) {\n for (const stop of unsubscribe) stop();\n mesh.removeFromParent();\n mesh.geometry.dispose();\n mesh.material.dispose();\n texture?.dispose();\n }\n this.blockIndices.clear();\n this.removeFromParent();\n super.dispose();\n }\n\n}\n"],"mappings":"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","names":["i"]}
1
+ {"version":3,"sources":["../../../../src/world/SectionMesh.ts"],"sourcesContent":["import { BufferGeometry, Color, DoubleSide, DynamicDrawUsage, Float32BufferAttribute, FrontSide, Group, Material, Matrix4, Mesh, MeshBasicMaterial, ShaderMaterial, Texture, Uint32BufferAttribute, Vector3 } from \"three\";\nimport { createCanvas } from \"../canvas/CanvasCompat\";\nimport { Materials } from \"../Materials\";\nimport type { MineRenderScene } from \"../renderer/MineRenderScene\";\nimport { TextureAtlas } from \"../texture/TextureAtlas\";\nimport { buildSectionGeometry, SectionGeometryInput, SectionGeometryPage, SectionTemplateData } from \"./SectionGeometry\";\nimport { SectionWorker } from \"./SectionWorker\";\nimport type { Camera, Intersection } from \"three\";\n\n/** Shared quad geometry and atlas data prepared for section merging. */\nexport interface SectionMeshTemplate {\n geometry: BufferGeometry;\n atlas: TextureAtlas;\n cullFaces: Uint8Array;\n /** Quads that accept a placement's biome tint, stored as zero or one. */\n biomeTint?: Uint8Array;\n occludes: boolean;\n /** Whether the model permits corner shading from neighboring full cubes. */\n ambientOcclusion?: boolean;\n layer: 0 | 1;\n}\n\n/** One block placement in a section, with its storage index and hidden-face mask. */\nexport interface SectionMeshEntry {\n index: number;\n template: SectionMeshTemplate;\n cullMask: number;\n /** Linear RGB replacing automatic biome colors on eligible quads. */\n biomeTint?: readonly [number, number, number];\n}\n\n/** Fluid cells for one section and the shared atlases of the kinds present. */\nexport interface SectionFluids {\n cells: Uint8Array;\n /** Linear RGB per section cell, indexed by (x + 16z + 256y) * 3. */\n waterTints?: Float32Array;\n water?: TextureAtlas;\n lava?: TextureAtlas;\n}\n\nconst BLOCK_SIZE = 16;\nconst QUAD_VERTICES = 4;\nconst QUAD_INDICES = 6;\n\nconst templateCache = new WeakMap<SectionMeshTemplate, SectionTemplateData>();\nconst WATER_TINT = new Color(0x3f76e4).toArray() as [number, number, number];\n\nfunction toInput(entries: readonly SectionMeshEntry[], maxAtlasSize: number, fluids?: SectionFluids,\n lighting?: Pick<SectionGeometryInput, \"light\" | \"ambientLight\" | \"ambientOcclusion\">): { input: SectionGeometryInput; atlases: TextureAtlas[] } {\n if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError(\"Section atlas size must be a positive integer\");\n const atlases: TextureAtlas[] = [];\n const atlasIds = new Map<TextureAtlas, number>();\n const templateIds = new Map<SectionMeshTemplate, number>();\n const input: SectionGeometryInput = {\n count: entries.length, indices: new Uint16Array(entries.length), templates: new Uint16Array(entries.length),\n cullMasks: new Uint8Array(entries.length), templateData: [], atlases: [], maxAtlasSize, ...lighting\n };\n for (let i = 0; i < entries.length; i++) {\n const { index, template, cullMask, biomeTint } = entries[i];\n if (biomeTint) {\n input.biomeTints ??= new Float32Array(entries.length * 3).fill(NaN);\n input.biomeTints.set(biomeTint, i * 3);\n }\n let id = templateIds.get(template);\n if (id === undefined) {\n let atlas = atlasIds.get(template.atlas);\n if (atlas === undefined) {\n atlas = atlases.length;\n atlasIds.set(template.atlas, atlas);\n atlases.push(template.atlas);\n input.atlases.push({ width: template.atlas.image.width, height: template.atlas.image.height, animated: template.atlas.hasAnimation });\n }\n let data = templateCache.get(template);\n if (!data) {\n const indices = template.geometry.getIndex()!;\n const position = template.geometry.getAttribute(\"position\");\n const quads = indices.count / QUAD_INDICES;\n if (indices.count % QUAD_INDICES !== 0 || position.count !== quads * QUAD_VERTICES) {\n throw new RangeError(\"Section template geometry must hold four vertices and six indices per quad\");\n }\n data = {\n quads, positions: new Float32Array(quads * 12), normals: new Float32Array(quads * 12), uvs: new Float32Array(quads * 8),\n uvBounds: new Float32Array(quads * 16), colors: new Float32Array(quads * 12), shade: new Float32Array(quads * QUAD_VERTICES), indices: new Uint16Array(quads * QUAD_INDICES),\n cullFaces: new Uint8Array(template.cullFaces), biomeTint: template.biomeTint ? new Uint8Array(template.biomeTint) : undefined, atlas: 0, layer: template.layer, ambientOcclusion: template.ambientOcclusion\n };\n const normal = template.geometry.getAttribute(\"normal\");\n const uv = template.geometry.getAttribute(\"uv\");\n const bounds = template.geometry.getAttribute(\"uvBounds\");\n const color = template.geometry.getAttribute(\"color\");\n const shade = template.geometry.getAttribute(\"shade\");\n for (let vertex = 0; vertex < quads * QUAD_VERTICES; vertex++) {\n data.positions.set([position.getX(vertex), position.getY(vertex), position.getZ(vertex)], vertex * 3);\n data.normals.set([normal.getX(vertex), normal.getY(vertex), normal.getZ(vertex)], vertex * 3);\n data.uvs.set([uv.getX(vertex), uv.getY(vertex)], vertex * 2);\n data.uvBounds.set([bounds?.getX(vertex) ?? 0, bounds?.getY(vertex) ?? 0,\n bounds?.getZ(vertex) ?? 1, bounds?.getW(vertex) ?? 1], vertex * 4);\n data.colors.set([color?.getX(vertex) ?? 1, color?.getY(vertex) ?? 1, color?.getZ(vertex) ?? 1], vertex * 3);\n data.shade![vertex] = shade?.getX(vertex) ?? 1;\n }\n for (let i = 0; i < data.indices.length; i++) data.indices[i] = indices.getX(i) - QUAD_VERTICES * Math.floor(i / QUAD_INDICES);\n templateCache.set(template, data);\n }\n id = input.templateData.length;\n templateIds.set(template, id);\n // Atlas IDs belong to each build; cached buffers can be shared across concurrent builds.\n input.templateData.push({ ...data, atlas });\n }\n input.indices[i] = index;\n input.templates[i] = id;\n input.cullMasks[i] = cullMask;\n }\n if (fluids) {\n input.fluids = { cells: fluids.cells, waterTints: fluids.waterTints };\n for (const kind of [\"water\", \"lava\"] as const) {\n const atlas = fluids[kind];\n if (!atlas) continue;\n let id = atlasIds.get(atlas);\n if (id === undefined) {\n id = atlases.length;\n atlasIds.set(atlas, id);\n atlases.push(atlas);\n input.atlases.push({ width: atlas.image.width, height: atlas.image.height, animated: atlas.hasAnimation });\n }\n input.fluids[kind] = {\n atlas: id, still: [...atlas.positions.still, ...atlas.sizes.still],\n flow: [...atlas.positions.flow, ...atlas.sizes.flow], tint: kind === \"water\" ? WATER_TINT : [1, 1, 1]\n };\n }\n }\n return { input, atlases };\n}\n\nfunction sortTransparentQuads(mesh: Mesh<BufferGeometry, Material>): void {\n const index = mesh.geometry.getIndex()!;\n if (index.count <= QUAD_INDICES) return;\n index.setUsage(DynamicDrawUsage);\n const count = index.count / QUAD_INDICES;\n let source: Uint32Array, centers: Float32Array, depths: Float32Array, order: Uint32Array;\n const view = new Matrix4(), localCamera = new Vector3(), key = new Vector3(), previous = new Vector3();\n let previousOrthographic: boolean | undefined;\n mesh.onBeforeRender = (_renderer, _scene, camera) => {\n localCamera.setFromMatrixPosition(camera.matrixWorld).applyMatrix4(view.copy(mesh.matrixWorld).invert());\n const orthographic = !!(camera as Camera & { isOrthographicCamera?: boolean }).isOrthographicCamera;\n if (orthographic) {\n // Three updates mesh.modelViewMatrix after this callback.\n view.multiplyMatrices(camera.matrixWorldInverse, mesh.matrixWorld);\n key.set(view.elements[2], view.elements[6], view.elements[10]);\n } else {\n key.copy(localCamera).divideScalar(BLOCK_SIZE).floor();\n }\n if (orthographic === previousOrthographic && key.equals(previous)) return;\n previous.copy(key);\n previousOrthographic = orthographic;\n if (!centers) {\n source = new Uint32Array(index.array);\n centers = new Float32Array(count * 3);\n depths = new Float32Array(count);\n order = Uint32Array.from({ length: count }, (_, quad) => quad);\n const positions = mesh.geometry.getAttribute(\"position\");\n for (let quad = 0; quad < count; quad++) {\n for (let axis = 0; axis < 3; axis++) {\n let center = 0;\n for (let corner = 0; corner < QUAD_VERTICES; corner++) center += positions.getComponent(quad * QUAD_VERTICES + corner, axis);\n centers[quad * 3 + axis] = center / QUAD_VERTICES;\n }\n }\n }\n for (let quad = 0; quad < count; quad++) {\n const x = centers[quad * 3], y = centers[quad * 3 + 1], z = centers[quad * 3 + 2];\n depths[quad] = orthographic ? key.x * x + key.y * y + key.z * z\n : -((x - localCamera.x) ** 2 + (y - localCamera.y) ** 2 + (z - localCamera.z) ** 2);\n }\n order.sort((a, b) => depths[a] - depths[b] || a - b);\n if (!order.some((quad, offset) => index.array[offset * QUAD_INDICES] !== source[quad * QUAD_INDICES])) return;\n for (let offset = 0; offset < count; offset++) {\n for (let indexOffset = 0; indexOffset < QUAD_INDICES; indexOffset++) {\n index.array[offset * QUAD_INDICES + indexOffset] = source[order[offset] * QUAD_INDICES + indexOffset];\n }\n }\n index.needsUpdate = true;\n };\n}\n\n/** Merged terrain geometry and atlas pages for one 16×16×16 section. Owns its generated render resources. */\nexport class SectionMesh extends Group {\n\n private readonly ownedMeshes: { mesh: Mesh<BufferGeometry, Material>; texture?: Texture; unsubscribe: (() => void)[] }[] = [];\n private readonly blockIndices = new Map<Mesh, Uint16Array>();\n\n /** Returns the owning section cell for a triangle hit on this mesh's merged geometry. */\n public getBlockIndex(hit: Intersection): number | undefined {\n if (hit.faceIndex === undefined || hit.faceIndex === null) return undefined;\n const mesh = hit.object as Mesh;\n const owners = this.blockIndices.get(mesh);\n if (!owners) return undefined;\n // Sorted triangles still refer to each quad's original four consecutive vertices.\n const vertex = mesh.geometry.getIndex()!.getX(hit.faceIndex * 3);\n return owners[Math.floor(vertex / QUAD_VERTICES)];\n }\n\n /** Builds section-local meshes from visible quads. `maxAtlasSize` limits each atlas dimension in pixels. */\n public static build(entries: readonly SectionMeshEntry[], maxAtlasSize = 2048, fluids?: SectionFluids,\n lighting?: Pick<SectionGeometryInput, \"light\" | \"ambientLight\" | \"ambientOcclusion\">): SectionMesh {\n const { input, atlases } = toInput(entries, maxAtlasSize, fluids, lighting);\n return this.fromPages(buildSectionGeometry(input), atlases);\n }\n\n /** Builds section-local meshes in a browser worker when available, with synchronous fallback. */\n public static async buildAsync(entries: readonly SectionMeshEntry[], maxAtlasSize = 2048, fluids?: SectionFluids,\n lighting?: Pick<SectionGeometryInput, \"light\" | \"ambientLight\" | \"ambientOcclusion\">): Promise<SectionMesh> {\n const { input, atlases } = toInput(entries, maxAtlasSize, fluids, lighting);\n const worker = SectionWorker.shared();\n const pages = worker ? await worker.build(input).catch(() => buildSectionGeometry(input)) : buildSectionGeometry(input);\n return this.fromPages(pages, atlases);\n }\n\n private static fromPages(pages: SectionGeometryPage[], atlases: TextureAtlas[]): SectionMesh {\n const section = new SectionMesh();\n for (const page of pages) {\n const canvas = createCanvas(page.width, page.height);\n const context = canvas.getContext(\"2d\") as CanvasRenderingContext2D;\n for (const { atlas, x, y } of page.placements) {\n context.drawImage(atlases[atlas].image.canvas as CanvasImageSource, x, y);\n }\n const geometry = new BufferGeometry();\n // Passing buffers keeps Three's typed attributes from copying the builder's arrays.\n geometry.setAttribute(\"position\", new Float32BufferAttribute(page.positions.buffer, 3));\n geometry.setAttribute(\"normal\", new Float32BufferAttribute(page.normals.buffer, 3));\n geometry.setAttribute(\"uv\", new Float32BufferAttribute(page.uvs.buffer, 2));\n geometry.setAttribute(\"uvBounds\", new Float32BufferAttribute(page.uvBounds.buffer, 4));\n geometry.setAttribute(\"color\", new Float32BufferAttribute(page.colors.buffer, 3));\n geometry.setAttribute(\"shade\", new Float32BufferAttribute(page.shade.buffer, 1));\n geometry.setIndex(new Uint32BufferAttribute(page.indices.buffer, 1));\n geometry.computeBoundingBox();\n geometry.computeBoundingSphere();\n const transparent = page.layer === 2 ? atlases[page.placements[0].atlas].hasTranslucency : page.layer === 1;\n const material = Materials.createShadedCanvasMaterial(canvas as HTMLCanvasElement, transparent, false, true);\n material.side = FrontSide;\n if (page.layer === 2) {\n material.depthWrite = !material.transparent;\n material.side = DoubleSide;\n material.forceSinglePass = true;\n }\n material.vertexColors = true;\n const texture = (material as ShaderMaterial).uniforms?.map?.value ?? (material as MeshBasicMaterial).map;\n const mesh = new Mesh(geometry, material);\n if (transparent) sortTransparentQuads(mesh);\n section.add(mesh);\n section.blockIndices.set(mesh, page.blockIndices);\n const unsubscribe: (() => void)[] = [];\n for (const { atlas: id, x, y } of page.placements) {\n const atlas = atlases[id];\n if (!atlas.hasAnimation) continue;\n unsubscribe.push(atlas.subscribe(() => {\n context.clearRect(x, y, atlas.image.width, atlas.image.height);\n context.drawImage(atlas.image.canvas as CanvasImageSource, x, y);\n texture.needsUpdate = true;\n section.traverseAncestors(parent => {\n if ((parent as MineRenderScene).isMineRenderScene) (parent as MineRenderScene).dirty = true;\n });\n }));\n }\n section.ownedMeshes.push({ mesh, texture, unsubscribe });\n }\n return section;\n }\n\n public dispose(): void {\n for (const { mesh, texture, unsubscribe } of this.ownedMeshes.splice(0)) {\n for (const stop of unsubscribe) stop();\n mesh.removeFromParent();\n mesh.geometry.dispose();\n mesh.material.dispose();\n texture?.dispose();\n }\n this.blockIndices.clear();\n this.removeFromParent();\n super.dispose();\n }\n\n}\n"],"mappings":"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","names":["i"]}
@@ -1,4 +1,5 @@
1
1
  // src/world/WorldStreamer.ts
2
+ var MAX_PREFETCHED_COLUMNS = 4;
2
3
  var WorldStreamer = class {
3
4
  constructor(world, source, options = {}) {
4
5
  this.world = world;
@@ -8,6 +9,7 @@ var WorldStreamer = class {
8
9
  this.missing = /* @__PURE__ */ new Map();
9
10
  this.failed = /* @__PURE__ */ new Map();
10
11
  this.desired = [];
12
+ this.reads = /* @__PURE__ */ new Map();
11
13
  this.disposed = false;
12
14
  this.loadRadius = options.loadRadius ?? 2;
13
15
  this.unloadRadius = options.unloadRadius ?? Math.min(32, this.loadRadius + 1);
@@ -52,6 +54,12 @@ var WorldStreamer = class {
52
54
  if (this.loading && !this.within(this.loading.position, this.loadRadius)) {
53
55
  this.loading.controller.abort();
54
56
  }
57
+ for (const [key, read] of this.reads) {
58
+ if (!this.within(read.position, this.loadRadius)) {
59
+ read.controller.abort();
60
+ if (read.ready) this.reads.delete(key);
61
+ }
62
+ }
55
63
  }
56
64
  return this.running ?? (this.running = Promise.resolve().then(() => this.drain()));
57
65
  }
@@ -72,8 +80,11 @@ var WorldStreamer = class {
72
80
  this.desired = [];
73
81
  this.center = void 0;
74
82
  this.loading?.controller.abort();
83
+ for (const read of this.reads.values()) read.controller.abort();
75
84
  return this.disposal ?? (this.disposal = Promise.resolve().then(async () => {
76
85
  await this.running?.catch(() => void 0);
86
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
87
+ this.reads.clear();
77
88
  try {
78
89
  await this.unloadMany([...this.owned.values()]);
79
90
  } finally {
@@ -99,6 +110,10 @@ var WorldStreamer = class {
99
110
  await this.unloadMany(obsolete);
100
111
  continue;
101
112
  }
113
+ if (this.world.placeChunks) {
114
+ if (await this.placeReadyChunks()) continue;
115
+ return;
116
+ }
102
117
  const position = this.desired.find((position2) => this.needsLoad(position2));
103
118
  if (!position) return;
104
119
  let chunk;
@@ -121,26 +136,105 @@ var WorldStreamer = class {
121
136
  continue;
122
137
  }
123
138
  this.owned.set(this.key(position), position);
139
+ await this.placeOwnedColumns([position], () => this.world.placeChunk(chunk));
140
+ }
141
+ } finally {
142
+ for (const [key, read] of this.reads) {
143
+ read.controller.abort();
144
+ if (read.ready) this.reads.delete(key);
145
+ }
146
+ this.running = void 0;
147
+ }
148
+ }
149
+ fillReadSlots() {
150
+ while (!this.disposed && this.reads.size < MAX_PREFETCHED_COLUMNS) {
151
+ const position = this.desired.find((position2) => this.needsLoad(position2) && !this.reads.has(this.key(position2)));
152
+ if (!position) return;
153
+ const key = this.key(position);
154
+ const read = {
155
+ position,
156
+ controller: new AbortController(),
157
+ promise: Promise.resolve(),
158
+ ready: false,
159
+ failed: false
160
+ };
161
+ this.reads.set(key, read);
162
+ read.promise = (async () => {
124
163
  try {
125
- await this.world.placeChunk(chunk);
126
- this.loaded.set(this.key(position), position);
164
+ read.chunk = await this.source.getChunk(position.x, position.z, read.controller.signal);
165
+ if (read.chunk) this.validateChunk(read.chunk, position);
127
166
  } catch (error) {
128
- try {
129
- await this.unloadMany([position]);
130
- } catch (cleanupError) {
131
- const failure = error instanceof Error ? error : new Error(String(error));
132
- try {
133
- Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
134
- } catch {
135
- throw Object.assign(new Error(failure.message), { cause: cleanupError });
136
- }
137
- throw failure;
167
+ read.failed = true;
168
+ read.error = error;
169
+ } finally {
170
+ read.ready = true;
171
+ if (read.controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) {
172
+ this.reads.delete(key);
138
173
  }
139
- throw error;
174
+ }
175
+ })();
176
+ }
177
+ }
178
+ /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */
179
+ async placeReadyChunks() {
180
+ this.fillReadSlots();
181
+ const position = this.desired.find((position2) => this.needsLoad(position2));
182
+ if (!position) {
183
+ if (!this.reads.size) return false;
184
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
185
+ return true;
186
+ }
187
+ const head = this.reads.get(this.key(position));
188
+ if (!head) {
189
+ for (let index = this.desired.length - 1; index >= 0; index--) {
190
+ const key = this.key(this.desired[index]);
191
+ if (this.reads.get(key)?.ready) {
192
+ this.reads.delete(key);
193
+ return true;
140
194
  }
141
195
  }
142
- } finally {
143
- this.running = void 0;
196
+ await Promise.race([...this.reads.values()].map((read) => read.promise));
197
+ return true;
198
+ }
199
+ if (!head.ready) {
200
+ await head.promise;
201
+ return true;
202
+ }
203
+ const chunks = [], positions = [];
204
+ for (const desired of this.desired) {
205
+ if (!this.needsLoad(desired)) continue;
206
+ const key = this.key(desired), read = this.reads.get(key);
207
+ if (!read?.ready) break;
208
+ this.reads.delete(key);
209
+ if (read.failed) this.failed.set(key, { ...desired, error: read.error });
210
+ else if (!read.chunk) this.missing.set(key, desired);
211
+ else {
212
+ chunks.push(read.chunk);
213
+ positions.push(desired);
214
+ }
215
+ }
216
+ if (!chunks.length) return true;
217
+ for (const position2 of positions) this.owned.set(this.key(position2), position2);
218
+ await this.placeOwnedColumns(positions, () => this.world.placeChunks(chunks));
219
+ return true;
220
+ }
221
+ async placeOwnedColumns(positions, place) {
222
+ try {
223
+ await place();
224
+ for (const position of positions) this.loaded.set(this.key(position), position);
225
+ } catch (error) {
226
+ try {
227
+ await this.unloadMany(positions);
228
+ } catch (cleanupError) {
229
+ const failure = error instanceof Error ? error : new Error(String(error));
230
+ try {
231
+ Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
232
+ } catch {
233
+ throw Object.assign(new Error(failure.message), { cause: cleanupError });
234
+ }
235
+ throw failure;
236
+ }
237
+ throw error;
144
238
  }
145
239
  }
146
240
  needsLoad(position) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/world/WorldStreamer.ts"],"sourcesContent":["import type { AnvilChunk } from \"./AnvilParser\";\nimport type { MineRenderWorld } from \"./MineRenderWorld\";\nimport type { WorldChunkSource } from \"./WorldChunkSource\";\n\nexport interface WorldStreamerOptions {\n /** Loads a square of chunk columns around the center. Integer from 0 to 32; defaults to 2. */\n loadRadius?: number;\n /** Retains loaded columns within this radius. Defaults to loadRadius + 1, capped at 32. */\n unloadRadius?: number;\n}\n\ninterface ChunkPosition {\n x: number;\n z: number;\n}\n\n/**\n * Loads nearby chunk columns in distance order and unloads distant columns.\n * Updates coalesce to the latest center and serialize world mutations. Use a dedicated world:\n * the streamer replaces columns it loads, and callers must await disposal before editing that world.\n * The source remains caller-owned. Lighting, biome tint, and LOD are not added by streaming.\n */\nexport class WorldStreamer {\n private readonly loadRadius: number;\n private readonly unloadRadius: number;\n private readonly loaded = new Map<string, ChunkPosition>();\n private readonly owned = new Map<string, ChunkPosition>();\n private readonly missing = new Map<string, ChunkPosition>();\n private readonly failed = new Map<string, ChunkPosition & { error: unknown }>();\n private desired: ChunkPosition[] = [];\n private center?: ChunkPosition;\n private loading?: { position: ChunkPosition; controller: AbortController };\n private running?: Promise<void>;\n private disposal?: Promise<void>;\n private disposed = false;\n\n constructor(\n private readonly world: Pick<MineRenderWorld<boolean>, \"placeChunk\" | \"unloadChunkColumns\">,\n private readonly source: WorldChunkSource,\n options: WorldStreamerOptions = {}\n ) {\n this.loadRadius = options.loadRadius ?? 2;\n this.unloadRadius = options.unloadRadius ?? Math.min(32, this.loadRadius + 1);\n if (![this.loadRadius, this.unloadRadius].every(value => Number.isInteger(value) && value >= 0 && value <= 32)\n || this.unloadRadius < this.loadRadius) {\n throw new RangeError(\"Chunk radii must be integers from 0 to 32, with unloadRadius at least loadRadius\");\n }\n }\n\n /** Returns absolute coordinates of successfully placed columns, including retained columns. */\n public get loadedChunks(): readonly { x: number; z: number }[] {\n return [...this.loaded.values()].map(position => ({ ...position }));\n }\n\n /** Returns source failures retained within the unload radius. Failed columns do not block other loads. */\n public get failedChunks(): readonly { x: number; z: number; error: unknown }[] {\n return [...this.failed.values()].map(failure => ({ ...failure }));\n }\n\n /** Counts desired columns still waiting for a source read or placement, including the active column. */\n public get pendingChunks(): number {\n return this.desired.filter(position => this.needsLoad(position)).length;\n }\n\n /**\n * Updates the center in absolute chunk coordinates. Repeated calls share the active drain.\n * Source failures are recorded in failedChunks; other columns continue loading. Missing and failed\n * columns are remembered within the retention radius. Call retryFailedChunks to retry source failures.\n * Placement and unload failures reject the drain; call update again to retry those operations.\n * Reads outside the latest load radius receive an abort signal; placement finishes before unloading.\n * Sources should represent an unchanged world during streaming.\n */\n public async update(x: number, z: number): Promise<void> {\n if (this.disposed) throw new Error(\"WorldStreamer is disposed\");\n if (![x, z].every(value => Number.isSafeInteger(value)\n && Number.isSafeInteger(value - this.unloadRadius) && Number.isSafeInteger(value + this.unloadRadius))) {\n throw new RangeError(\"Chunk coordinates must be safe integers within the streaming radius\");\n }\n if (!this.center || this.center.x !== x || this.center.z !== z) {\n this.center = { x, z };\n this.desired = [];\n for (let dz = -this.loadRadius; dz <= this.loadRadius; dz++) {\n for (let dx = -this.loadRadius; dx <= this.loadRadius; dx++) {\n this.desired.push({ x: x + dx, z: z + dz });\n }\n }\n this.desired.sort((a, b) => (a.x - x) ** 2 + (a.z - z) ** 2 - (b.x - x) ** 2 - (b.z - z) ** 2);\n if (this.loading && !this.within(this.loading.position, this.loadRadius)) {\n this.loading.controller.abort();\n }\n }\n return this.running ??= Promise.resolve().then(() => this.drain());\n }\n\n /** Updates from a camera or view-center position in scene units. One chunk spans 256 scene units. */\n public async updatePosition(position: { x: number; z: number }): Promise<void> {\n if (![position.x, position.z].every(Number.isFinite)) throw new RangeError(\"Scene coordinates must be finite\");\n return this.update(Math.floor(position.x / 256), Math.floor(position.z / 256));\n }\n\n /** Forgets recorded source failures and retries desired columns at the latest center. */\n public async retryFailedChunks(): Promise<void> {\n if (this.disposed) throw new Error(\"WorldStreamer is disposed\");\n this.failed.clear();\n if (this.center) await this.update(this.center.x, this.center.z);\n }\n\n /** Aborts source reads, waits for active work, then unloads owned columns. The world and source remain caller-owned. */\n public dispose(): Promise<void> {\n this.disposed = true;\n this.desired = [];\n this.center = undefined;\n this.loading?.controller.abort();\n return this.disposal ??= Promise.resolve().then(async () => {\n await this.running?.catch(() => undefined);\n try {\n await this.unloadMany([...this.owned.values()]);\n } finally {\n this.missing.clear();\n this.failed.clear();\n }\n }).catch(error => {\n this.disposal = undefined;\n throw error;\n });\n }\n\n private async drain(): Promise<void> {\n try {\n while (!this.disposed) {\n for (const [key, position] of this.missing) {\n if (!this.within(position, this.unloadRadius)) this.missing.delete(key);\n }\n for (const [key, position] of this.failed) {\n if (!this.within(position, this.unloadRadius)) this.failed.delete(key);\n }\n // Failed placements remain owned until cleanup succeeds, even inside the retained area.\n const obsolete = [...this.owned.values()].filter(position =>\n !this.loaded.has(this.key(position)) || !this.within(position, this.unloadRadius));\n if (obsolete.length) {\n await this.unloadMany(obsolete);\n continue;\n }\n const position = this.desired.find(position => this.needsLoad(position));\n if (!position) return;\n let chunk: AnvilChunk | undefined;\n const controller = new AbortController();\n this.loading = { position, controller };\n try {\n chunk = await this.source.getChunk(position.x, position.z, controller.signal);\n if (chunk) this.validateChunk(chunk, position);\n } catch (error) {\n if (!controller.signal.aborted && !this.disposed && this.within(position, this.loadRadius)) {\n this.failed.set(this.key(position), { ...position, error });\n }\n continue;\n } finally {\n this.loading = undefined;\n }\n if (controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) continue;\n if (!chunk) {\n this.missing.set(this.key(position), position);\n continue;\n }\n this.owned.set(this.key(position), position);\n try {\n await this.world.placeChunk(chunk);\n this.loaded.set(this.key(position), position);\n } catch (error) {\n try {\n await this.unloadMany([position]);\n } catch (cleanupError) {\n const failure = error instanceof Error ? error : new Error(String(error));\n try {\n Object.defineProperty(failure, \"cause\", { value: cleanupError, configurable: true });\n } catch {\n throw Object.assign(new Error(failure.message), { cause: cleanupError });\n }\n throw failure;\n }\n throw error;\n }\n }\n } finally {\n this.running = undefined;\n }\n }\n\n private needsLoad(position: ChunkPosition): boolean {\n const key = this.key(position);\n return !this.loaded.has(key) && !this.missing.has(key) && !this.failed.has(key);\n }\n\n private async unloadMany(positions: readonly ChunkPosition[]): Promise<void> {\n if (!positions.length) return;\n try {\n await this.world.unloadChunkColumns(positions.map(({ x, z }) => ({ x, z })));\n } catch (error) {\n // A rejected batch may have removed any subset; retry every owner before reloading it.\n for (const position of positions) this.loaded.delete(this.key(position));\n throw error;\n }\n for (const position of positions) {\n this.owned.delete(this.key(position));\n this.loaded.delete(this.key(position));\n }\n }\n\n private validateChunk(chunk: AnvilChunk, position: ChunkPosition): void {\n if (chunk.x !== position.x || chunk.z !== position.z) {\n throw new Error(`World source returned chunk ${chunk.x},${chunk.z} for ${position.x},${position.z}`);\n }\n }\n\n private within(position: ChunkPosition, radius: number): boolean {\n return !!this.center && Math.abs(position.x - this.center.x) <= radius && Math.abs(position.z - this.center.z) <= radius;\n }\n\n private key(position: ChunkPosition): string {\n return `${position.x},${position.z}`;\n }\n}\n"],"mappings":"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","names":["position"]}
1
+ {"version":3,"sources":["../../../../src/world/WorldStreamer.ts"],"sourcesContent":["import type { AnvilChunk } from \"./AnvilParser\";\nimport type { MineRenderWorld } from \"./MineRenderWorld\";\nimport type { WorldChunkSource } from \"./WorldChunkSource\";\n\nexport interface WorldStreamerOptions {\n /** Loads a square of chunk columns around the center. Integer from 0 to 32; defaults to 2. */\n loadRadius?: number;\n /** Retains loaded columns within this radius. Defaults to loadRadius + 1, capped at 32. */\n unloadRadius?: number;\n}\n\ninterface ChunkPosition {\n x: number;\n z: number;\n}\n\ninterface ChunkRead {\n position: ChunkPosition;\n controller: AbortController;\n promise: Promise<void>;\n ready: boolean;\n chunk?: AnvilChunk;\n failed: boolean;\n error?: unknown;\n}\n\nconst MAX_PREFETCHED_COLUMNS = 4;\n\n/**\n * Loads nearby chunk columns in distance order and unloads distant columns.\n * Updates coalesce to the latest center and serialize world mutations. Use a dedicated world:\n * the streamer replaces columns it loads, and callers must await disposal before editing that world.\n * The source remains caller-owned. Lighting, biome tint, and LOD are not added by streaming.\n */\nexport class WorldStreamer {\n private readonly loadRadius: number;\n private readonly unloadRadius: number;\n private readonly loaded = new Map<string, ChunkPosition>();\n private readonly owned = new Map<string, ChunkPosition>();\n private readonly missing = new Map<string, ChunkPosition>();\n private readonly failed = new Map<string, ChunkPosition & { error: unknown }>();\n private desired: ChunkPosition[] = [];\n private center?: ChunkPosition;\n private loading?: { position: ChunkPosition; controller: AbortController };\n private readonly reads = new Map<string, ChunkRead>();\n private running?: Promise<void>;\n private disposal?: Promise<void>;\n private disposed = false;\n\n constructor(\n private readonly world: Pick<MineRenderWorld<boolean>, \"placeChunk\" | \"unloadChunkColumns\">\n & Partial<Pick<MineRenderWorld<boolean>, \"placeChunks\">>,\n private readonly source: WorldChunkSource,\n options: WorldStreamerOptions = {}\n ) {\n this.loadRadius = options.loadRadius ?? 2;\n this.unloadRadius = options.unloadRadius ?? Math.min(32, this.loadRadius + 1);\n if (![this.loadRadius, this.unloadRadius].every(value => Number.isInteger(value) && value >= 0 && value <= 32)\n || this.unloadRadius < this.loadRadius) {\n throw new RangeError(\"Chunk radii must be integers from 0 to 32, with unloadRadius at least loadRadius\");\n }\n }\n\n /** Returns absolute coordinates of successfully placed columns, including retained columns. */\n public get loadedChunks(): readonly { x: number; z: number }[] {\n return [...this.loaded.values()].map(position => ({ ...position }));\n }\n\n /** Returns source failures retained within the unload radius. Failed columns do not block other loads. */\n public get failedChunks(): readonly { x: number; z: number; error: unknown }[] {\n return [...this.failed.values()].map(failure => ({ ...failure }));\n }\n\n /** Counts desired columns still waiting for a source read or placement, including the active column. */\n public get pendingChunks(): number {\n return this.desired.filter(position => this.needsLoad(position)).length;\n }\n\n /**\n * Updates the center in absolute chunk coordinates. Repeated calls share the active drain.\n * Source failures are recorded in failedChunks; other columns continue loading. Missing and failed\n * columns are remembered within the retention radius. Call retryFailedChunks to retry source failures.\n * Placement and unload failures reject the drain; call update again to retry those operations.\n * Reads outside the latest load radius receive an abort signal; placement finishes before unloading.\n * Sources should represent an unchanged world during streaming.\n */\n public async update(x: number, z: number): Promise<void> {\n if (this.disposed) throw new Error(\"WorldStreamer is disposed\");\n if (![x, z].every(value => Number.isSafeInteger(value)\n && Number.isSafeInteger(value - this.unloadRadius) && Number.isSafeInteger(value + this.unloadRadius))) {\n throw new RangeError(\"Chunk coordinates must be safe integers within the streaming radius\");\n }\n if (!this.center || this.center.x !== x || this.center.z !== z) {\n this.center = { x, z };\n this.desired = [];\n for (let dz = -this.loadRadius; dz <= this.loadRadius; dz++) {\n for (let dx = -this.loadRadius; dx <= this.loadRadius; dx++) {\n this.desired.push({ x: x + dx, z: z + dz });\n }\n }\n this.desired.sort((a, b) => (a.x - x) ** 2 + (a.z - z) ** 2 - (b.x - x) ** 2 - (b.z - z) ** 2);\n if (this.loading && !this.within(this.loading.position, this.loadRadius)) {\n this.loading.controller.abort();\n }\n for (const [key, read] of this.reads) {\n if (!this.within(read.position, this.loadRadius)) {\n read.controller.abort();\n if (read.ready) this.reads.delete(key);\n }\n }\n }\n return this.running ??= Promise.resolve().then(() => this.drain());\n }\n\n /** Updates from a camera or view-center position in scene units. One chunk spans 256 scene units. */\n public async updatePosition(position: { x: number; z: number }): Promise<void> {\n if (![position.x, position.z].every(Number.isFinite)) throw new RangeError(\"Scene coordinates must be finite\");\n return this.update(Math.floor(position.x / 256), Math.floor(position.z / 256));\n }\n\n /** Forgets recorded source failures and retries desired columns at the latest center. */\n public async retryFailedChunks(): Promise<void> {\n if (this.disposed) throw new Error(\"WorldStreamer is disposed\");\n this.failed.clear();\n if (this.center) await this.update(this.center.x, this.center.z);\n }\n\n /** Aborts source reads, waits for active work, then unloads owned columns. The world and source remain caller-owned. */\n public dispose(): Promise<void> {\n this.disposed = true;\n this.desired = [];\n this.center = undefined;\n this.loading?.controller.abort();\n for (const read of this.reads.values()) read.controller.abort();\n return this.disposal ??= Promise.resolve().then(async () => {\n await this.running?.catch(() => undefined);\n await Promise.all([...this.reads.values()].map(read => read.promise));\n this.reads.clear();\n try {\n await this.unloadMany([...this.owned.values()]);\n } finally {\n this.missing.clear();\n this.failed.clear();\n }\n }).catch(error => {\n this.disposal = undefined;\n throw error;\n });\n }\n\n private async drain(): Promise<void> {\n try {\n while (!this.disposed) {\n for (const [key, position] of this.missing) {\n if (!this.within(position, this.unloadRadius)) this.missing.delete(key);\n }\n for (const [key, position] of this.failed) {\n if (!this.within(position, this.unloadRadius)) this.failed.delete(key);\n }\n // Failed placements remain owned until cleanup succeeds, even inside the retained area.\n const obsolete = [...this.owned.values()].filter(position =>\n !this.loaded.has(this.key(position)) || !this.within(position, this.unloadRadius));\n if (obsolete.length) {\n await this.unloadMany(obsolete);\n continue;\n }\n if (this.world.placeChunks) {\n if (await this.placeReadyChunks()) continue;\n return;\n }\n const position = this.desired.find(position => this.needsLoad(position));\n if (!position) return;\n let chunk: AnvilChunk | undefined;\n const controller = new AbortController();\n this.loading = { position, controller };\n try {\n chunk = await this.source.getChunk(position.x, position.z, controller.signal);\n if (chunk) this.validateChunk(chunk, position);\n } catch (error) {\n if (!controller.signal.aborted && !this.disposed && this.within(position, this.loadRadius)) {\n this.failed.set(this.key(position), { ...position, error });\n }\n continue;\n } finally {\n this.loading = undefined;\n }\n if (controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) continue;\n if (!chunk) {\n this.missing.set(this.key(position), position);\n continue;\n }\n this.owned.set(this.key(position), position);\n await this.placeOwnedColumns([position], () => this.world.placeChunk(chunk!));\n }\n } finally {\n for (const [key, read] of this.reads) {\n read.controller.abort();\n if (read.ready) this.reads.delete(key);\n }\n this.running = undefined;\n }\n }\n\n private fillReadSlots(): void {\n while (!this.disposed && this.reads.size < MAX_PREFETCHED_COLUMNS) {\n const position = this.desired.find(position => this.needsLoad(position) && !this.reads.has(this.key(position)));\n if (!position) return;\n const key = this.key(position);\n const read: ChunkRead = {\n position, controller: new AbortController(), promise: Promise.resolve(), ready: false, failed: false\n };\n this.reads.set(key, read);\n read.promise = (async () => {\n try {\n read.chunk = await this.source.getChunk(position.x, position.z, read.controller.signal);\n if (read.chunk) this.validateChunk(read.chunk, position);\n } catch (error) {\n read.failed = true;\n read.error = error;\n } finally {\n read.ready = true;\n if (read.controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) {\n this.reads.delete(key);\n }\n }\n })();\n }\n }\n\n /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */\n private async placeReadyChunks(): Promise<boolean> {\n this.fillReadSlots();\n const position = this.desired.find(position => this.needsLoad(position));\n if (!position) {\n if (!this.reads.size) return false;\n await Promise.all([...this.reads.values()].map(read => read.promise));\n return true;\n }\n const head = this.reads.get(this.key(position));\n if (!head) {\n for (let index = this.desired.length - 1; index >= 0; index--) {\n const key = this.key(this.desired[index]);\n if (this.reads.get(key)?.ready) {\n this.reads.delete(key);\n return true;\n }\n }\n // Aborted reads still occupy slots until their source work settles.\n await Promise.race([...this.reads.values()].map(read => read.promise));\n return true;\n }\n if (!head.ready) {\n await head.promise;\n return true;\n }\n const chunks: AnvilChunk[] = [], positions: ChunkPosition[] = [];\n for (const desired of this.desired) {\n if (!this.needsLoad(desired)) continue;\n const key = this.key(desired), read = this.reads.get(key);\n if (!read?.ready) break;\n this.reads.delete(key);\n if (read.failed) this.failed.set(key, { ...desired, error: read.error });\n else if (!read.chunk) this.missing.set(key, desired);\n else {\n chunks.push(read.chunk);\n positions.push(desired);\n }\n }\n if (!chunks.length) return true;\n for (const position of positions) this.owned.set(this.key(position), position);\n await this.placeOwnedColumns(positions, () => this.world.placeChunks!(chunks));\n return true;\n }\n\n private async placeOwnedColumns(positions: readonly ChunkPosition[], place: () => Promise<void>): Promise<void> {\n try {\n await place();\n for (const position of positions) this.loaded.set(this.key(position), position);\n } catch (error) {\n try {\n await this.unloadMany(positions);\n } catch (cleanupError) {\n const failure = error instanceof Error ? error : new Error(String(error));\n try {\n Object.defineProperty(failure, \"cause\", { value: cleanupError, configurable: true });\n } catch {\n throw Object.assign(new Error(failure.message), { cause: cleanupError });\n }\n throw failure;\n }\n throw error;\n }\n }\n\n private needsLoad(position: ChunkPosition): boolean {\n const key = this.key(position);\n return !this.loaded.has(key) && !this.missing.has(key) && !this.failed.has(key);\n }\n\n private async unloadMany(positions: readonly ChunkPosition[]): Promise<void> {\n if (!positions.length) return;\n try {\n await this.world.unloadChunkColumns(positions.map(({ x, z }) => ({ x, z })));\n } catch (error) {\n // A rejected batch may have removed any subset; retry every owner before reloading it.\n for (const position of positions) this.loaded.delete(this.key(position));\n throw error;\n }\n for (const position of positions) {\n this.owned.delete(this.key(position));\n this.loaded.delete(this.key(position));\n }\n }\n\n private validateChunk(chunk: AnvilChunk, position: ChunkPosition): void {\n if (chunk.x !== position.x || chunk.z !== position.z) {\n throw new Error(`World source returned chunk ${chunk.x},${chunk.z} for ${position.x},${position.z}`);\n }\n }\n\n private within(position: ChunkPosition, radius: number): boolean {\n return !!this.center && Math.abs(position.x - this.center.x) <= radius && Math.abs(position.z - this.center.z) <= radius;\n }\n\n private key(position: ChunkPosition): string {\n return `${position.x},${position.z}`;\n }\n}\n"],"mappings":"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","names":["position"]}
@@ -783,7 +783,10 @@ declare function toRadians(degrees: number): number;
783
783
  declare function toDegrees(radians: number): number;
784
784
  /** Resolves after a delay in milliseconds. */
785
785
  declare function sleep(timeout: number): Promise<void>;
786
- /** Yields to the next macrotask without timer clamping when the platform supports it. */
786
+ /**
787
+ * Yields to the next macrotask without timer clamping when the platform supports it.
788
+ * Not `scheduler.yield()`: Chrome runs its continuations ahead of rendering, so chained yields delay frames.
789
+ */
787
790
  declare function yieldToEventLoop(): Promise<void>;
788
791
  /** Reduces an angle modulo 360, retaining a negative sign for negative inputs. */
789
792
  declare function clampRotationDegrees(deg: number): number;
@@ -4282,6 +4285,12 @@ declare class MineRenderWorld<SectionMeshing extends boolean = false> {
4282
4285
  private placeChunkGroups;
4283
4286
  /** Replaces a chunk column's blocks, biomes, and saved light and, with `renderEntities`, its supported saved mobs. */
4284
4287
  placeChunk(chunk: AnvilChunk, executor?: BatchedExecutor): Promise<void>;
4288
+ /**
4289
+ * Replaces parsed columns in input order, then refreshes neighboring geometry once.
4290
+ * Duplicate coordinates retain their order. A failed column stops placement; queued refreshes finish before rejection.
4291
+ */
4292
+ placeChunks(chunks: readonly AnvilChunk[]): Promise<void>;
4293
+ private placeColumn;
4285
4294
  /**
4286
4295
  * Removes blocks and owned entities at chunk-column coordinates, then refreshes neighboring faces and fluids.
4287
4296
  * Coordinates match AnvilChunk.x/z, not block positions. Missing columns are ignored.
@@ -4369,10 +4378,11 @@ declare class WorldStreamer {
4369
4378
  private desired;
4370
4379
  private center?;
4371
4380
  private loading?;
4381
+ private readonly reads;
4372
4382
  private running?;
4373
4383
  private disposal?;
4374
4384
  private disposed;
4375
- constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumns">, source: WorldChunkSource, options?: WorldStreamerOptions);
4385
+ constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumns"> & Partial<Pick<MineRenderWorld<boolean>, "placeChunks">>, source: WorldChunkSource, options?: WorldStreamerOptions);
4376
4386
  /** Returns absolute coordinates of successfully placed columns, including retained columns. */
4377
4387
  get loadedChunks(): readonly {
4378
4388
  x: number;
@@ -4405,6 +4415,10 @@ declare class WorldStreamer {
4405
4415
  /** Aborts source reads, waits for active work, then unloads owned columns. The world and source remain caller-owned. */
4406
4416
  dispose(): Promise<void>;
4407
4417
  private drain;
4418
+ private fillReadSlots;
4419
+ /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */
4420
+ private placeReadyChunks;
4421
+ private placeOwnedColumns;
4408
4422
  private needsLoad;
4409
4423
  private unloadMany;
4410
4424
  private validateChunk;