littlejsengine 1.24.1 → 1.24.2

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/EDITOR.md +2 -0
  3. package/FAQ.md +13 -2
  4. package/README.md +1 -1
  5. package/REFERENCE.md +20 -6
  6. package/dist/littlejs.d.ts +77 -13
  7. package/dist/littlejs.esm.js +2395 -1939
  8. package/dist/littlejs.esm.min.js +3 -3
  9. package/dist/littlejs.js +2392 -1939
  10. package/dist/littlejs.min.js +2 -2
  11. package/dist/littlejs.release.js +2356 -1952
  12. package/package.json +1 -1
  13. package/plugins/gltf.js +62 -9
  14. package/plugins/lightSystem.js +1 -1
  15. package/plugins/math3d.js +1014 -1013
  16. package/plugins/medalSystem.js +7 -11
  17. package/plugins/particleEffects.js +2 -2
  18. package/plugins/pathFinder.js +2 -2
  19. package/plugins/pluginExport.js +1 -0
  20. package/plugins/render3d.js +131 -55
  21. package/plugins/render3dEditor.js +24 -10
  22. package/plugins/render3dExtras.js +29 -17
  23. package/plugins/render3dLevel.js +21 -12
  24. package/plugins/render3dVoxels.js +2 -2
  25. package/plugins/textureSheet.js +97 -3
  26. package/plugins/tweakables.js +1 -1
  27. package/plugins/tweenSystem.js +2 -2
  28. package/plugins/uiSystem.js +6 -2
  29. package/src/engine.js +34 -11
  30. package/src/engineAudio.js +13 -3
  31. package/src/engineDebug.js +4 -1
  32. package/src/engineDraw.js +29 -16
  33. package/src/engineEditor.js +82 -50
  34. package/src/engineExport.js +2 -0
  35. package/src/engineInput.js +17 -3
  36. package/src/engineMath.js +6 -2
  37. package/src/engineObject.js +675 -675
  38. package/src/engineParticles.js +1 -1
  39. package/src/engineSettings.js +16 -0
  40. package/src/engineTileLayer.js +85 -22
  41. package/src/engineUtilities.js +20 -8
  42. package/src/engineWebGL.js +12 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.24.1",
3
+ "version": "1.24.2",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
package/plugins/gltf.js CHANGED
@@ -371,11 +371,24 @@ async function loadGLTF(url)
371
371
  }
372
372
 
373
373
  /** Parse a model from GLB bytes or glTF JSON, fetching the buffers and images it refers to
374
+ * - A .gltf names its .bin and image files, which are fetched from baseUrl, or found among files: what a game
375
+ * has in hand, like the files dropped on the page, by their paths in the drop; with files, baseUrl is the
376
+ * .gltf's own folder among them ('' or 'models/house/'), its names are read from there, and a name found nowhere
377
+ * there is taken by its file name alone only when one file of the drop has it; only an http, https or blob uri
378
+ * is fetched then
379
+ * - A file the model needs that is not found is named in the error, and an image that can not be read is named in
380
+ * a warning and left out
374
381
  * @param {ArrayBuffer|Object|string} data - GLB bytes, or the glTF JSON as bytes, text or an object
375
382
  * @param {string} [baseUrl] - Where the .bin and image files are, with its trailing slash; loadGLTF passes the file's folder
383
+ * @param {Map<string, Blob>} [files] - The files it refers to, by their paths, in place of fetching them
376
384
  * @return {Promise<GLTFModel>}
385
+ * @example
386
+ * // the files of a drop, a .gltf with its .bin and textures, by their names
387
+ * const files = new Map([...dataTransfer.files].map((file)=> [file.name, file]));
388
+ * const gltf = [...files.values()].find((file)=> file.name.endsWith('.gltf'));
389
+ * const model = await parseGLTF(await gltf.text(), '', files);
377
390
  * @memberof GLTF */
378
- async function parseGLTF(data, baseUrl='')
391
+ async function parseGLTF(data, baseUrl='', files)
379
392
  {
380
393
  let json = data, glbBuffer;
381
394
  if (data instanceof ArrayBuffer)
@@ -419,7 +432,7 @@ async function parseGLTF(data, baseUrl='')
419
432
  ASSERT(glbBuffer && !i, 'a buffer without a uri is the GLB chunk, and only the first can be');
420
433
  return glbBuffer;
421
434
  }
422
- return gltfFetch(buffer.uri, baseUrl).then(r=> r.arrayBuffer());
435
+ return gltfFetch(buffer.uri, baseUrl, files).then(r=> r.arrayBuffer());
423
436
  }));
424
437
 
425
438
  // the textures, decoded together first; none without WebGL, and a failed image only logs; the base color, normal
@@ -438,7 +451,7 @@ async function parseGLTF(data, baseUrl='')
438
451
  const image = json.images[source], sampler = json.samplers?.[texture.sampler] || {};
439
452
  let blob;
440
453
  if (image.uri)
441
- blob = await gltfFetch(image.uri, baseUrl).then(r=> r.blob());
454
+ blob = await gltfFetch(image.uri, baseUrl, files).then(r=> r.blob());
442
455
  else
443
456
  {
444
457
  const view = json.bufferViews[image.bufferView];
@@ -455,14 +468,18 @@ async function parseGLTF(data, baseUrl='')
455
468
  // REPEAT by default, and hard edged only when its sampler says NEAREST, not as the game's tiles are
456
469
  return new TextureInfo(bitmap, true, [sampler.wrapS ?? 10497, sampler.wrapT ?? 10497], sampler.magFilter === 9728);
457
470
  }
458
- catch (e) { LOG('glTF image not loaded', e); }
471
+ catch (e) { console.warn('glTF image not loaded, left out: ' + (e?.message || e)); }
459
472
  }));
460
473
 
461
474
  // the parts: the scene's nodes walked with their transforms, every primitive of a node's mesh placed by it;
462
475
  // each node's parent and resting place are kept, so an animation can move a part from where it rests
463
- const parts = [], parents = [], restInverse = [], restPose = [];
476
+ const parts = [], parents = [], restInverse = [], restPose = [], visited = new Set;
464
477
  const visit = (index, parentMatrix, parentIndex, parentRest)=>
465
478
  {
479
+ // nodes are trees, so one reached again is a cycle or a node with two parents, a file problem
480
+ if (visited.has(index))
481
+ throw new Error('glTF node ' + index + ' is reached twice, nodes must form trees');
482
+ visited.add(index);
466
483
  const node = json.nodes[index], local = gltfNodeMatrix(node);
467
484
  let matrix = parentMatrix ? parentMatrix.copy().multiply(local) : local;
468
485
  let rest = parentRest && parentRest.copy().multiply(local); // where it rests, when that is not where it is baked
@@ -645,8 +662,37 @@ function gltfSample(channel, time, out)
645
662
  }
646
663
 
647
664
  // fetch a uri beside the model, or decode a data uri without going out
648
- function gltfFetch(uri, baseUrl)
665
+ function gltfFetch(uri, baseUrl, files)
649
666
  {
667
+ // among the files given, by its path from the model's folder, or by its name when only one file has it; a uri
668
+ // of the web is still fetched, and no other kind is
669
+ if (files && !/^(data|https?|blob):/i.test(uri))
670
+ {
671
+ let name = uri.replace(/[?#].*$/, '');
672
+ try { name = decodeURIComponent(name); } catch {}
673
+ // a path with its . and .. steps taken, the same for the keys, which may start with ./ or / or use \
674
+ const clean = (text)=>
675
+ {
676
+ const parts = [];
677
+ for (const part of text.replace(/\\/g, '/').split('/'))
678
+ if (part === '..' ? !parts.pop() : part && part !== '.' && !parts.push(part))
679
+ return; // above the top of the files
680
+ return parts.join('/');
681
+ };
682
+ const path = clean(baseUrl + name);
683
+ if (path === undefined)
684
+ return Promise.reject(new Error('glTF needs ' + name + ', which is above the files given'));
685
+ const lower = path.toLowerCase(), file = path.slice(path.lastIndexOf('/') + 1);
686
+ const byPath = new Map([...files].map(([key, value])=> [clean(key)?.toLowerCase(), value]));
687
+ const found = files.get(path) ?? byPath.get(lower), keys = [...byPath.keys()];
688
+ if (found)
689
+ return Promise.resolve(new Response(found));
690
+ const named = new Set(keys.filter((k)=> k?.slice(k.lastIndexOf('/') + 1) === file).map((k)=> byPath.get(k)));
691
+ if (named.size === 1)
692
+ return Promise.resolve(new Response([...named][0]));
693
+ return Promise.reject(new Error('glTF needs ' + path + (named.size ? ', and more than one file has its name' :
694
+ ', which is not among the files given')));
695
+ }
650
696
  if (uri.startsWith('data:'))
651
697
  {
652
698
  const comma = uri.indexOf(','), bytes = atob(uri.slice(comma + 1)), data = new Uint8Array(bytes.length);
@@ -661,7 +707,7 @@ function gltfFetch(uri, baseUrl)
661
707
  const url = base ? new URL(uri, base).href : baseUrl + uri;
662
708
  return fetch(url).then(r=>
663
709
  {
664
- if (!r.ok) throw new Error('glTF file not found: ' + url);
710
+ if (!r.ok) throw new Error('glTF needs ' + uri + ', not found at ' + url);
665
711
  return r;
666
712
  });
667
713
  }
@@ -692,11 +738,18 @@ function gltfAccessor(json, buffers, index)
692
738
  const size = Type.BYTES_PER_ELEMENT;
693
739
  const scales = /** @type {Array<[Object, number]>} */ ([[Int8Array, 127], [Uint8Array, 255], [Int16Array, 32767], [Uint16Array, 65535]]);
694
740
  const scale = a.normalized ? new Map(scales).get(Type) || 1 : 1;
741
+ // with a buffer under it the count is bounded by the buffer, without one it is all zeros and a sparse few,
742
+ // which no real model makes millions of
743
+ if (!view && !(a.count <= 1 << 20))
744
+ throw new Error('glTF accessor ' + index + ' has no buffer and a count of ' + a.count);
745
+ // its elements must lie in its buffer before so many floats are made for them
746
+ const viewEnd = view && (buffers[view.buffer]?.byteLength ?? 0), stride = view?.byteStride || components * size;
747
+ if (view && !((view.byteOffset || 0) + (a.byteOffset || 0) + (a.count - 1) * stride + components * size <= viewEnd))
748
+ throw new Error('glTF accessor ' + index + ' reaches past its buffer');
695
749
  const out = new Float32Array(a.count * components);
696
750
  if (view)
697
751
  {
698
752
  const buffer = buffers[view.buffer], offset = (view.byteOffset || 0) + (a.byteOffset || 0);
699
- const stride = view.byteStride || components * size;
700
753
  if (stride === components * size)
701
754
  out.set(new Type(buffer, offset, a.count * components)); // packed, one view over all of it
702
755
  else
@@ -795,7 +848,7 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
795
848
  let gltfWhiteTextureInfo;
796
849
  function gltfWhiteTexture()
797
850
  {
798
- if (gltfWhiteTextureInfo || !glContext || typeof OffscreenCanvas == 'undefined') return gltfWhiteTextureInfo;
851
+ if (gltfWhiteTextureInfo || !glContext || !canvasAvailable()) return gltfWhiteTextureInfo;
799
852
  const context = createCanvasContext(1);
800
853
  context.fillStyle = '#fff';
801
854
  context.fillRect(0, 0, 1, 1);
@@ -151,7 +151,7 @@ class LightSystemPlugin
151
151
  }
152
152
 
153
153
  // where Canvas2D draws go during the shadow and emissive passes
154
- lightSystem.shadowContext ||= new OffscreenCanvas(1, 1).getContext('2d');
154
+ lightSystem.shadowContext ||= createCanvasContext(1);
155
155
 
156
156
  // resolve texture size default at init time (mainCanvasSize may
157
157
  // not be set yet at the moment the constructor first ran), and