minerender 2.1.0-alpha.142 → 2.1.0-alpha.144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MineRender V2
2
2
 
3
- A TypeScript library for interactive Minecraft skins, models, blocks, entities, and worlds.
3
+ A TypeScript library for interactive Minecraft skins, models, blocks, entities, GUIs, and worlds.
4
4
  V2 is in beta. See the [roadmap](https://github.com/InventivetalentDev/MineRender/blob/main/ROADMAP.md) for feature parity and remaining work.
5
5
 
6
6
  Live examples and usage docs: [beta.minerender.org](https://beta.minerender.org/). API reference: [beta-docs.minerender.org](https://beta-docs.minerender.org/).
@@ -29,6 +29,11 @@ an injected WebGL 2 context or the optional native `gl` backend. See
29
29
 
30
30
  Requires Node.js 22.12+ or a browser with WebGL 2, Fetch, `AbortSignal.any()`, and `AbortSignal.timeout()`.
31
31
 
32
+ The library prints only warnings and errors. To see debug output, call
33
+ `Logging.configure({ level: "debug" })`, or set the `minerender:log` localStorage key
34
+ in a browser or the `MINERENDER_LOG` environment variable in Node. See
35
+ [Logging](https://beta-docs.minerender.org/concepts#logging).
36
+
32
37
  ## Resource-pack ZIPs
33
38
 
34
39
  Register a browser `File` or `Blob` before loading models or textures:
@@ -123,3 +128,7 @@ Supported object types are `skin`, `block`, `item`, `model`, `entity`, and `gui`
123
128
  Positions use model units (16 per block); JSON rotations use radians.
124
129
 
125
130
  Callers apply the saved camera settings and select the matching asset version.
131
+
132
+ `encodeSceneDocument` and `decodeSceneDocument` convert a document to and from the compact
133
+ link format used by the editor. To show a scene on another page without bundling the library,
134
+ use the [iframe embed page](https://beta-docs.minerender.org/embeds).
@@ -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;
@@ -4019,6 +4022,8 @@ declare class SectionModels {
4019
4022
  pick(position?: Readonly<TripleArray>): SectionMeshTemplate[];
4020
4023
  biomeTint?: boolean;
4021
4024
  } | undefined>;
4025
+ /** Starts loading the state's models and atlases so a later `prepareState` with the same properties finds them loaded. */
4026
+ preload(blockState: BlockState, properties?: BlockStateProperties): Promise<void>;
4022
4027
  private state;
4023
4028
  /** Returns the shared still/flow atlas used by per-block fluid objects. */
4024
4029
  fluidAtlas(kind: FluidKind, root?: string): Promise<TextureAtlas>;
@@ -4133,6 +4138,7 @@ declare class Chunk<SectionMeshing extends boolean = false> {
4133
4138
  /** Replaces every cell from parsed section data. The caller refreshes neighbor culling. */
4134
4139
  placeSection(data: ChunkData): Promise<void>;
4135
4140
  private replaceSection;
4141
+ private createBlockObject;
4136
4142
  private positionAt;
4137
4143
  /** Refreshes saved lighting after adjacent columns load or unload, including columns with only light data. */
4138
4144
  refreshLight(indices?: Iterable<number>): Promise<void>;
@@ -4282,6 +4288,12 @@ declare class MineRenderWorld<SectionMeshing extends boolean = false> {
4282
4288
  private placeChunkGroups;
4283
4289
  /** Replaces a chunk column's blocks, biomes, and saved light and, with `renderEntities`, its supported saved mobs. */
4284
4290
  placeChunk(chunk: AnvilChunk, executor?: BatchedExecutor): Promise<void>;
4291
+ /**
4292
+ * Replaces parsed columns in input order, then refreshes neighboring geometry once.
4293
+ * Duplicate coordinates retain their order. A failed column stops placement; queued refreshes finish before rejection.
4294
+ */
4295
+ placeChunks(chunks: readonly AnvilChunk[]): Promise<void>;
4296
+ private placeColumn;
4285
4297
  /**
4286
4298
  * Removes blocks and owned entities at chunk-column coordinates, then refreshes neighboring faces and fluids.
4287
4299
  * Coordinates match AnvilChunk.x/z, not block positions. Missing columns are ignored.
@@ -4314,7 +4326,7 @@ declare class MineRenderWorld<SectionMeshing extends boolean = false> {
4314
4326
  interface MineRenderWorldOptions<SectionMeshing extends boolean = boolean> {
4315
4327
  /** Render supported saved mobs at their position and yaw, including supported appearance fields. Defaults to false. */
4316
4328
  renderEntities?: boolean;
4317
- /** Merge static opaque cubes into section meshes. Merged blocks have no individual object. */
4329
+ /** Merge block models of any shape and fluid surfaces into per-section meshes; block entities keep individual objects. Merged blocks have no individual object. */
4318
4330
  sectionMeshing?: SectionMeshing;
4319
4331
  /** Shade blocks and fluids with saved sky/block light. Missing or invalid data keeps preview brightness. Defaults to false. Requires `sectionMeshing`. */
4320
4332
  savedLighting?: boolean;
@@ -4324,7 +4336,7 @@ interface MineRenderWorldOptions<SectionMeshing extends boolean = boolean> {
4324
4336
  ambientOcclusion?: boolean;
4325
4337
  /** Average neighboring biome colors within this horizontal radius, from 0 to 7 blocks. Defaults to 0 (off). Requires `sectionMeshing`. */
4326
4338
  biomeBlendRadius?: number;
4327
- /** Maximum width and height of each section atlas page, in pixels. */
4339
+ /** Maximum width and height of each section atlas page, in pixels. Defaults to 2048. */
4328
4340
  maxAtlasSize?: number;
4329
4341
  }
4330
4342
 
@@ -4369,10 +4381,11 @@ declare class WorldStreamer {
4369
4381
  private desired;
4370
4382
  private center?;
4371
4383
  private loading?;
4384
+ private readonly reads;
4372
4385
  private running?;
4373
4386
  private disposal?;
4374
4387
  private disposed;
4375
- constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumns">, source: WorldChunkSource, options?: WorldStreamerOptions);
4388
+ constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumns"> & Partial<Pick<MineRenderWorld<boolean>, "placeChunks">>, source: WorldChunkSource, options?: WorldStreamerOptions);
4376
4389
  /** Returns absolute coordinates of successfully placed columns, including retained columns. */
4377
4390
  get loadedChunks(): readonly {
4378
4391
  x: number;
@@ -4405,6 +4418,10 @@ declare class WorldStreamer {
4405
4418
  /** Aborts source reads, waits for active work, then unloads owned columns. The world and source remain caller-owned. */
4406
4419
  dispose(): Promise<void>;
4407
4420
  private drain;
4421
+ private fillReadSlots;
4422
+ /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */
4423
+ private placeReadyChunks;
4424
+ private placeOwnedColumns;
4408
4425
  private needsLoad;
4409
4426
  private unloadMany;
4410
4427
  private validateChunk;
@@ -28,8 +28,6 @@ async function sleep(timeout) {
28
28
  });
29
29
  }
30
30
  function yieldToEventLoop() {
31
- const scheduler = globalThis.scheduler;
32
- if (scheduler?.yield) return scheduler.yield();
33
31
  return new Promise((resolve) => {
34
32
  if (typeof MessageChannel !== "undefined") {
35
33
  const channel = new MessageChannel();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/util/util.ts"],"sourcesContent":["// Node ESM needs the explicit .js extension for the CommonJS hash module.\nimport MD5 from \"crypto-js/md5.js\";\nimport { Vector3 } from \"three\";\n\nexport const changeEvent = { type: 'change' as const };\n\nexport type Maybe<T> = T | undefined;\n\n// MD5 runs in both environments without pulling Node's crypto into the browser graph.\nexport function md5(str: string): string {\n return MD5(str).toString();\n}\n\nexport function base64encode(str: string): string {\n if (typeof btoa !== \"undefined\") {\n return btoa(str);\n }\n return Buffer.from(str).toString(\"base64\");\n}\n\nexport function base64decode(str: string): string {\n if (typeof atob !== \"undefined\") {\n return atob(str);\n }\n return Buffer.from(str, \"base64\").toString(\"ascii\");\n}\n\nexport function toRadians(degrees: number): number {\n return degrees * Math.PI / 180;\n}\n\nexport function toDegrees(radians: number): number {\n return radians * 180 / Math.PI;\n}\n\n/** Resolves after a delay in milliseconds. */\nexport async function sleep(timeout: number): Promise<void> {\n return new Promise(resolve => {\n setTimeout(() => resolve(), timeout);\n });\n}\n\n/** Yields to the next macrotask without timer clamping when the platform supports it. */\nexport function yieldToEventLoop(): Promise<void> {\n const scheduler = (globalThis as typeof globalThis & { scheduler?: { yield?: () => Promise<void> } }).scheduler;\n if (scheduler?.yield) return scheduler.yield();\n return new Promise(resolve => {\n if (typeof MessageChannel !== \"undefined\") {\n const channel = new MessageChannel();\n channel.port1.onmessage = () => {\n channel.port1.close();\n channel.port2.close();\n resolve();\n };\n channel.port2.postMessage(undefined);\n } else {\n setTimeout(resolve, 0);\n }\n });\n}\n\n/** Reduces an angle modulo 360, retaining a negative sign for negative inputs. */\nexport function clampRotationDegrees(deg: number): number {\n return deg % 360;\n}\n\nexport function isVector3(obj: any): obj is Vector3 {\n return (<Vector3>obj).isVector3;\n}\n\nexport type DeepPartial<T> = {\n [P in keyof T]?: DeepPartial<T[P]>;\n};\n"],"mappings":";AACA,OAAO,SAAS;AAGT,IAAM,cAAc,EAAE,MAAM,SAAkB;AAK9C,SAAS,IAAI,KAAqB;AACrC,SAAO,IAAI,GAAG,EAAE,SAAS;AAC7B;AAEO,SAAS,aAAa,KAAqB;AAC9C,MAAI,OAAO,SAAS,aAAa;AAC7B,WAAO,KAAK,GAAG;AAAA,EACnB;AACA,SAAO,OAAO,KAAK,GAAG,EAAE,SAAS,QAAQ;AAC7C;AAEO,SAAS,aAAa,KAAqB;AAC9C,MAAI,OAAO,SAAS,aAAa;AAC7B,WAAO,KAAK,GAAG;AAAA,EACnB;AACA,SAAO,OAAO,KAAK,KAAK,QAAQ,EAAE,SAAS,OAAO;AACtD;AAEO,SAAS,UAAU,SAAyB;AAC/C,SAAO,UAAU,KAAK,KAAK;AAC/B;AAEO,SAAS,UAAU,SAAyB;AAC/C,SAAO,UAAU,MAAM,KAAK;AAChC;AAGA,eAAsB,MAAM,SAAgC;AACxD,SAAO,IAAI,QAAQ,aAAW;AAC1B,eAAW,MAAM,QAAQ,GAAG,OAAO;AAAA,EACvC,CAAC;AACL;AAGO,SAAS,mBAAkC;AAC9C,QAAM,YAAa,WAAmF;AACtG,MAAI,WAAW,MAAO,QAAO,UAAU,MAAM;AAC7C,SAAO,IAAI,QAAQ,aAAW;AAC1B,QAAI,OAAO,mBAAmB,aAAa;AACvC,YAAM,UAAU,IAAI,eAAe;AACnC,cAAQ,MAAM,YAAY,MAAM;AAC5B,gBAAQ,MAAM,MAAM;AACpB,gBAAQ,MAAM,MAAM;AACpB,gBAAQ;AAAA,MACZ;AACA,cAAQ,MAAM,YAAY,MAAS;AAAA,IACvC,OAAO;AACH,iBAAW,SAAS,CAAC;AAAA,IACzB;AAAA,EACJ,CAAC;AACL;AAGO,SAAS,qBAAqB,KAAqB;AACtD,SAAO,MAAM;AACjB;AAEO,SAAS,UAAU,KAA0B;AAChD,SAAiB,IAAK;AAC1B;","names":[]}
1
+ {"version":3,"sources":["../../../../src/util/util.ts"],"sourcesContent":["// Node ESM needs the explicit .js extension for the CommonJS hash module.\nimport MD5 from \"crypto-js/md5.js\";\nimport { Vector3 } from \"three\";\n\nexport const changeEvent = { type: 'change' as const };\n\nexport type Maybe<T> = T | undefined;\n\n// MD5 runs in both environments without pulling Node's crypto into the browser graph.\nexport function md5(str: string): string {\n return MD5(str).toString();\n}\n\nexport function base64encode(str: string): string {\n if (typeof btoa !== \"undefined\") {\n return btoa(str);\n }\n return Buffer.from(str).toString(\"base64\");\n}\n\nexport function base64decode(str: string): string {\n if (typeof atob !== \"undefined\") {\n return atob(str);\n }\n return Buffer.from(str, \"base64\").toString(\"ascii\");\n}\n\nexport function toRadians(degrees: number): number {\n return degrees * Math.PI / 180;\n}\n\nexport function toDegrees(radians: number): number {\n return radians * 180 / Math.PI;\n}\n\n/** Resolves after a delay in milliseconds. */\nexport async function sleep(timeout: number): Promise<void> {\n return new Promise(resolve => {\n setTimeout(() => resolve(), timeout);\n });\n}\n\n/**\n * Yields to the next macrotask without timer clamping when the platform supports it.\n * Not `scheduler.yield()`: Chrome runs its continuations ahead of rendering, so chained yields delay frames.\n */\nexport function yieldToEventLoop(): Promise<void> {\n return new Promise(resolve => {\n if (typeof MessageChannel !== \"undefined\") {\n const channel = new MessageChannel();\n channel.port1.onmessage = () => {\n channel.port1.close();\n channel.port2.close();\n resolve();\n };\n channel.port2.postMessage(undefined);\n } else {\n setTimeout(resolve, 0);\n }\n });\n}\n\n/** Reduces an angle modulo 360, retaining a negative sign for negative inputs. */\nexport function clampRotationDegrees(deg: number): number {\n return deg % 360;\n}\n\nexport function isVector3(obj: any): obj is Vector3 {\n return (<Vector3>obj).isVector3;\n}\n\nexport type DeepPartial<T> = {\n [P in keyof T]?: DeepPartial<T[P]>;\n};\n"],"mappings":";AACA,OAAO,SAAS;AAGT,IAAM,cAAc,EAAE,MAAM,SAAkB;AAK9C,SAAS,IAAI,KAAqB;AACrC,SAAO,IAAI,GAAG,EAAE,SAAS;AAC7B;AAEO,SAAS,aAAa,KAAqB;AAC9C,MAAI,OAAO,SAAS,aAAa;AAC7B,WAAO,KAAK,GAAG;AAAA,EACnB;AACA,SAAO,OAAO,KAAK,GAAG,EAAE,SAAS,QAAQ;AAC7C;AAEO,SAAS,aAAa,KAAqB;AAC9C,MAAI,OAAO,SAAS,aAAa;AAC7B,WAAO,KAAK,GAAG;AAAA,EACnB;AACA,SAAO,OAAO,KAAK,KAAK,QAAQ,EAAE,SAAS,OAAO;AACtD;AAEO,SAAS,UAAU,SAAyB;AAC/C,SAAO,UAAU,KAAK,KAAK;AAC/B;AAEO,SAAS,UAAU,SAAyB;AAC/C,SAAO,UAAU,MAAM,KAAK;AAChC;AAGA,eAAsB,MAAM,SAAgC;AACxD,SAAO,IAAI,QAAQ,aAAW;AAC1B,eAAW,MAAM,QAAQ,GAAG,OAAO;AAAA,EACvC,CAAC;AACL;AAMO,SAAS,mBAAkC;AAC9C,SAAO,IAAI,QAAQ,aAAW;AAC1B,QAAI,OAAO,mBAAmB,aAAa;AACvC,YAAM,UAAU,IAAI,eAAe;AACnC,cAAQ,MAAM,YAAY,MAAM;AAC5B,gBAAQ,MAAM,MAAM;AACpB,gBAAQ,MAAM,MAAM;AACpB,gBAAQ;AAAA,MACZ;AACA,cAAQ,MAAM,YAAY,MAAS;AAAA,IACvC,OAAO;AACH,iBAAW,SAAS,CAAC;AAAA,IACzB;AAAA,EACJ,CAAC;AACL;AAGO,SAAS,qBAAqB,KAAqB;AACtD,SAAO,MAAM;AACjB;AAEO,SAAS,UAAU,KAA0B;AAChD,SAAiB,IAAK;AAC1B;","names":[]}
@@ -1,4 +1,5 @@
1
1
  // src/world/Chunk.ts
2
+ import { BlockObject } from "../model/block/scene/BlockObject.mjs";
2
3
  import { Color, Vector3 } from "three";
3
4
  import { ChunkData } from "./ChunkData.mjs";
4
5
  import { BlockStates } from "../assets/BlockStates.mjs";
@@ -269,8 +270,10 @@ var Chunk = class _Chunk {
269
270
  if (rejected?.status === "rejected") throw rejected.reason;
270
271
  }
271
272
  const positions = [];
272
- let failed = false, failure;
273
- for (const { index, key, block } of placements) {
273
+ const jobs = /* @__PURE__ */ new Map();
274
+ let failureOrder = Infinity, failure;
275
+ for (const [order, { index, key, block }] of placements.entries()) {
276
+ jobs.delete(index);
274
277
  this.hiddenBlocks.delete(index);
275
278
  this.data.assign(index, this.data.intern(block), block?.nbt);
276
279
  const readBlock = this.data.snapshot(index);
@@ -286,7 +289,6 @@ var Chunk = class _Chunk {
286
289
  this.z * 16 + Math.floor(index / 16) % 16
287
290
  );
288
291
  positions.push(worldPos);
289
- let object;
290
292
  try {
291
293
  if (!readBlock) continue;
292
294
  const resolved = resolutions.get(key);
@@ -308,7 +310,7 @@ var Chunk = class _Chunk {
308
310
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0, biomeTint: context?.tint })));
309
311
  this.meshDirty = true;
310
312
  } else {
311
- object = await this.scene.addBlock(blockState, {
313
+ jobs.set(index, { order, readBlock, blockState, position: MineRenderWorld.worldToScenePosition(worldPos), options: {
312
314
  mergeMeshes: true,
313
315
  instanceMeshes: true,
314
316
  maxInstanceCount: 2e3,
@@ -316,13 +318,12 @@ var Chunk = class _Chunk {
316
318
  variantPosition,
317
319
  biome: context?.biome,
318
320
  biomePosition: this.biomePosition(index, blockState, stored)
319
- });
320
- object.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
321
+ } });
322
+ continue;
321
323
  }
322
324
  this.renderedBlocks.set(index, { get block() {
323
325
  return readBlock();
324
- }, object });
325
- if (object) this.objectBlocks.add(index);
326
+ }, object: void 0 });
326
327
  } catch (error) {
327
328
  if (preserveDataOnFailure && readBlock) {
328
329
  this.renderedBlocks.set(index, { get block() {
@@ -330,12 +331,35 @@ var Chunk = class _Chunk {
330
331
  }, object: void 0 });
331
332
  } else this.data.assign(index, 0);
332
333
  this.fluidCells[index] = 0;
333
- object?.removeFromScene();
334
- if (!failed) failure = error;
335
- failed = true;
334
+ if (order < failureOrder) {
335
+ failureOrder = order;
336
+ failure = error;
337
+ }
336
338
  }
337
339
  }
338
- if (failed) throw failure;
340
+ const pending = [...jobs];
341
+ const objects = await Promise.allSettled(pending.map(([, job]) => this.createBlockObject(job.blockState, job.position, job.options)));
342
+ for (const [i, [index, { order, readBlock }]] of pending.entries()) {
343
+ const result = objects[i];
344
+ if (result.status === "fulfilled") {
345
+ this.renderedBlocks.set(index, { get block() {
346
+ return readBlock();
347
+ }, object: result.value });
348
+ this.objectBlocks.add(index);
349
+ } else {
350
+ if (preserveDataOnFailure) {
351
+ this.renderedBlocks.set(index, { get block() {
352
+ return readBlock();
353
+ }, object: void 0 });
354
+ } else this.data.assign(index, 0);
355
+ this.fluidCells[index] = 0;
356
+ if (order < failureOrder) {
357
+ failureOrder = order;
358
+ failure = result.reason;
359
+ }
360
+ }
361
+ }
362
+ if (failureOrder < Infinity) throw failure;
339
363
  return positions;
340
364
  }
341
365
  /** Replaces every cell from parsed section data. The caller refreshes neighbor culling. */
@@ -406,37 +430,42 @@ var Chunk = class _Chunk {
406
430
  failed = true;
407
431
  }
408
432
  }
409
- for (const { index, blockState, biome } of perBlocks) {
410
- let object;
411
- try {
412
- object = await this.scene.addBlock(blockState, {
413
- mergeMeshes: true,
414
- instanceMeshes: true,
415
- maxInstanceCount: 2e3,
416
- initialState: this.data.stateAt(this.data.idAt(index)).properties,
417
- variantPosition: this.positionAt(index),
418
- biome,
419
- biomePosition: this.biomePosition(index, blockState, this.data.stateAt(this.data.idAt(index)))
420
- });
421
- object.setPosition(new Vector3(
422
- this.x * 256 + index % 16 * 16,
423
- this.y * 256 + Math.floor(index / 256) * 16,
424
- this.z * 256 + Math.floor(index / 16) % 16 * 16
425
- ));
426
- this.renderedBlocks.get(index).object = object;
433
+ const objects = await Promise.allSettled(perBlocks.map(({ index, blockState, biome }) => this.createBlockObject(blockState, new Vector3(
434
+ this.x * 256 + index % 16 * 16,
435
+ this.y * 256 + Math.floor(index / 256) * 16,
436
+ this.z * 256 + Math.floor(index / 16) % 16 * 16
437
+ ), {
438
+ mergeMeshes: true,
439
+ instanceMeshes: true,
440
+ maxInstanceCount: 2e3,
441
+ initialState: this.data.stateAt(this.data.idAt(index)).properties,
442
+ variantPosition: this.positionAt(index),
443
+ biome,
444
+ biomePosition: this.biomePosition(index, blockState, this.data.stateAt(this.data.idAt(index)))
445
+ })));
446
+ for (const [i, { index }] of perBlocks.entries()) {
447
+ const result = objects[i];
448
+ if (result.status === "fulfilled") {
449
+ this.renderedBlocks.get(index).object = result.value;
427
450
  this.objectBlocks.add(index);
428
- } catch (error) {
451
+ } else {
429
452
  this.data.assign(index, 0);
430
453
  this.fluidCells[index] = 0;
431
454
  this.renderedBlocks.delete(index);
432
455
  this.objectBlocks.delete(index);
433
- object?.removeFromScene();
434
- if (!failed) failure = error;
456
+ if (!failed) failure = result.reason;
435
457
  failed = true;
436
458
  }
437
459
  }
438
460
  if (failed) throw failure;
439
461
  }
462
+ createBlockObject(blockState, position, options) {
463
+ return this.scene.addSceneObject(blockState, () => {
464
+ const object = new BlockObject(blockState, options);
465
+ object.position.copy(position);
466
+ return object;
467
+ }, options);
468
+ }
440
469
  positionAt(index) {
441
470
  return [this.x * 16 + index % 16, this.y * 16 + Math.floor(index / 256), this.z * 16 + Math.floor(index / 16) % 16];
442
471
  }