@needle-tools/gltf-progressive 3.6.0-alpha.3 → 3.6.0-beta.1

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 (37) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +42 -8
  3. package/examples/modelviewer-multiple.html +4 -4
  4. package/examples/modelviewer.html +4 -4
  5. package/examples/offscreen/index.html +15 -0
  6. package/examples/offscreen/main.js +98 -0
  7. package/examples/react-three-fiber/index.html +2 -2
  8. package/examples/react-three-fiber/package-lock.json +482 -484
  9. package/examples/react-three-fiber/package.json +4 -4
  10. package/examples/react-three-fiber/src/App.tsx +76 -21
  11. package/examples/react-three-fiber/src/styles.css +2 -4
  12. package/examples/react-three-fiber/vite.config.js +2 -2
  13. package/examples/shared/example-utils.js +297 -0
  14. package/examples/shared/example.css +44 -0
  15. package/examples/shared/runtime.js +34 -0
  16. package/examples/threejs/index.html +6 -10
  17. package/examples/threejs/main.js +5 -23
  18. package/examples/webgpu/index.html +15 -0
  19. package/examples/webgpu/main.js +105 -0
  20. package/examples/worker-rendering/index.html +15 -0
  21. package/examples/worker-rendering/main.js +109 -0
  22. package/examples/worker-rendering/worker.js +166 -0
  23. package/gltf-progressive.js +429 -355
  24. package/gltf-progressive.min.js +9 -9
  25. package/gltf-progressive.umd.cjs +9 -9
  26. package/lib/extension.js +5 -7
  27. package/lib/loaders.d.ts +1 -8
  28. package/lib/loaders.js +15 -2
  29. package/lib/lods.debug.js +1 -1
  30. package/lib/lods.manager.d.ts +3 -0
  31. package/lib/lods.manager.js +62 -18
  32. package/lib/utils.d.ts +1 -1
  33. package/lib/utils.internal.d.ts +27 -0
  34. package/lib/utils.internal.js +68 -25
  35. package/lib/version.js +1 -1
  36. package/lib/worker/loader.mainthread.js +6 -4
  37. package/package.json +8 -3
package/lib/loaders.d.ts CHANGED
@@ -28,14 +28,7 @@ export declare function setKTX2TranscoderLocation(location: string): void;
28
28
  export declare function createLoaders(renderer: WebGLRenderer | null): {
29
29
  dracoLoader: DRACOLoader;
30
30
  ktx2Loader: KTX2Loader;
31
- meshoptDecoder: {
32
- supported: boolean;
33
- ready: Promise<void>;
34
- decodeVertexBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array, filter?: string) => void;
35
- decodeIndexBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array) => void;
36
- decodeIndexSequence: (target: Uint8Array, count: number, size: number, source: Uint8Array) => void;
37
- decodeGltfBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array, mode: string, filter?: string) => void;
38
- };
31
+ meshoptDecoder: any;
39
32
  };
40
33
  export declare function addDracoAndKTX2Loaders(loader: GLTFLoader): void;
41
34
  /**
package/lib/loaders.js CHANGED
@@ -1,3 +1,4 @@
1
+ // @ts-ignore @types/three re-exports from meshoptimizer which may not resolve in all configs
1
2
  import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
2
3
  import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
3
4
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
@@ -142,8 +143,8 @@ const originalLoadFunction = GLTFLoader.prototype.load;
142
143
  function onLoad(...args) {
143
144
  const config = gltfLoaderConfigurations.get(this);
144
145
  let url_str = args[0];
145
- const url = new URL(url_str, window.location.href);
146
- if (url.hostname.endsWith("needle.tools")) {
146
+ const url = tryResolveUrl(url_str);
147
+ if (url?.hostname.endsWith("needle.tools")) {
147
148
  const progressive = config?.progressive !== undefined ? config.progressive : true;
148
149
  const usecase = config?.usecase ? config.usecase : "default";
149
150
  if (progressive) {
@@ -159,3 +160,15 @@ function onLoad(...args) {
159
160
  return res;
160
161
  }
161
162
  GLTFLoader.prototype.load = onLoad;
163
+ function tryResolveUrl(url) {
164
+ try {
165
+ if (url instanceof URL) {
166
+ return url;
167
+ }
168
+ const base = globalThis.location?.href;
169
+ return base ? new URL(url, base) : new URL(url);
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ }
package/lib/lods.debug.js CHANGED
@@ -2,7 +2,7 @@ import { getParam } from "./utils.internal.js";
2
2
  export const debug = getParam("debugprogressive");
3
3
  let debug_RenderWireframe = undefined;
4
4
  export let debug_OverrideLodLevel = -1; // -1 is automatic
5
- if (debug) {
5
+ if (debug && typeof window !== "undefined") {
6
6
  const maxLevel = 6;
7
7
  function debugToggleProgressive() {
8
8
  debug_OverrideLodLevel += 1;
@@ -75,6 +75,8 @@ export declare class LODsManager {
75
75
  static getObjectLODState(object: Object3D): LOD_state | undefined;
76
76
  static addPlugin(plugin: NEEDLE_progressive_plugin): void;
77
77
  static removePlugin(plugin: NEEDLE_progressive_plugin): void;
78
+ /** Read-only snapshot of the currently registered plugins, for inspection. Use {@link addPlugin} / {@link removePlugin} to modify the registry. */
79
+ static getPlugins(): readonly NEEDLE_progressive_plugin[];
78
80
  /**
79
81
  * Gets the LODsManager for the given renderer. If the LODsManager does not exist yet, it will be created.
80
82
  * @param renderer The renderer to get the LODsManager for.
@@ -207,6 +209,7 @@ export declare class LODsManager {
207
209
  * Update LODs in a scene
208
210
  */
209
211
  private internalUpdate;
212
+ private getRenderList;
210
213
  /** Update the LOD levels for the renderer. */
211
214
  private updateLODs;
212
215
  /** Load progressive textures for the given material
@@ -1,4 +1,5 @@
1
- import { Box3, Clock, Color, Matrix4, Mesh, Sphere, Vector3 } from "three";
1
+ import * as THREE from "three";
2
+ import { Box3, Color, Matrix4, Mesh, Sphere, Vector3 } from "three";
2
3
  import { NEEDLE_progressive } from "./extension.js";
3
4
  import { createLoaders } from "./loaders.js";
4
5
  import { getParam, isDevelopmentServer, isMobileDevice } from "./utils.internal.js";
@@ -12,6 +13,7 @@ const suppressProgressiveLoading = getParam("noprogressive");
12
13
  const $lodsManager = Symbol("Needle:LODSManager");
13
14
  const $lodstate = Symbol("Needle:LODState");
14
15
  const $currentLOD = Symbol("Needle:CurrentLOD");
16
+ const ThreeRuntime = THREE;
15
17
  const levels = { mesh_lod: -1, texture_lod: -1 };
16
18
  const debugLODColor = new Color();
17
19
  export const lodDebugColors = [
@@ -48,6 +50,10 @@ export const lodDebugColors = [
48
50
  0x4d908e,
49
51
  0x555555,
50
52
  ];
53
+ function createLODTimer() {
54
+ const Timer = ThreeRuntime.Timer || ThreeRuntime.Clock;
55
+ return new Timer();
56
+ }
51
57
  const _meshLODWorldBox = new Box3();
52
58
  const _meshLODProjectedBox = new Box3();
53
59
  const _meshLODCameraSpaceBox = new Box3();
@@ -82,8 +88,10 @@ export function calculateMeshLODLevel(options) {
82
88
  result.screenCoverage = 0;
83
89
  result.screenspaceVolume.set(0, 0, 0);
84
90
  result.centrality = 1;
85
- if (!meshLods?.length)
86
- return result;
91
+ // Note: we intentionally do NOT early-return when there are no mesh LODs.
92
+ // The screen coverage / screenspace volume computed below is also consumed by
93
+ // the texture LOD selection, which must keep working for meshes that only have
94
+ // texture LODs (no mesh LODs). Only the mesh LOD-level selection loop is skipped.
87
95
  let boundingBox = options.boundingBox ?? geometry.boundingBox;
88
96
  if (!boundingBox) {
89
97
  geometry.computeBoundingBox();
@@ -159,16 +167,18 @@ export function calculateMeshLODLevel(options) {
159
167
  debugDrawLine(_meshLODCorner1, _meshLODCorner3, 0x0000ff);
160
168
  debugDrawLine(_meshLODCorner2, _meshLODCorner3, 0x0000ff);
161
169
  }
162
- for (let i = 0; i < meshLods.length; i++) {
163
- const lod = meshLods[i];
164
- const density = lod.densities?.[primitiveIndex] || lod.density || .00001;
165
- if (primitiveIndex > 0 && warnMissingPrimitiveDensities && isDevelopmentServer() && !lod.densities && !globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"]) {
166
- globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"] = true;
167
- console.warn(`[Needle Progressive] Detected usage of mesh without primitive densities. This might cause incorrect LOD level selection: Consider re-optimizing your model by updating your Needle Integration, Needle glTF Pipeline or running optimization again on Needle Cloud.`);
168
- }
169
- if (density / screenCoverage < desiredDensity) {
170
- result.level = i;
171
- break;
170
+ if (meshLods?.length) {
171
+ for (let i = 0; i < meshLods.length; i++) {
172
+ const lod = meshLods[i];
173
+ const density = lod.densities?.[primitiveIndex] || lod.density || .00001;
174
+ if (primitiveIndex > 0 && warnMissingPrimitiveDensities && isDevelopmentServer() && !lod.densities && !globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"]) {
175
+ globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"] = true;
176
+ console.warn(`[Needle Progressive] Detected usage of mesh without primitive densities. This might cause incorrect LOD level selection: Consider re-optimizing your model by updating your Needle Integration, Needle glTF Pipeline or running optimization again on Needle Cloud.`);
177
+ }
178
+ if (density / screenCoverage < desiredDensity) {
179
+ result.level = i;
180
+ break;
181
+ }
172
182
  }
173
183
  }
174
184
  return result;
@@ -219,6 +229,10 @@ export class LODsManager {
219
229
  if (index >= 0)
220
230
  plugins.splice(index, 1);
221
231
  }
232
+ /** Read-only snapshot of the currently registered plugins, for inspection. Use {@link addPlugin} / {@link removePlugin} to modify the registry. */
233
+ static getPlugins() {
234
+ return plugins;
235
+ }
222
236
  /**
223
237
  * Gets the LODsManager for the given renderer. If the LODsManager does not exist yet, it will be created.
224
238
  * @param renderer The renderer to get the LODsManager for.
@@ -390,11 +404,11 @@ export class LODsManager {
390
404
  // })
391
405
  }
392
406
  #originalRender;
393
- #clock = new Clock();
394
407
  #frame = 0;
395
408
  #delta = 0;
396
409
  #time = 0;
397
410
  #fps = 0;
411
+ #clock = createLODTimer();
398
412
  _fpsBuffer = [60, 60, 60, 60, 60];
399
413
  /**
400
414
  * Enable the LODsManager. This will replace the render method of the renderer with a method that updates the LODs.
@@ -416,7 +430,8 @@ export class LODsManager {
416
430
  if (renderTarget == null || ("isXRRenderTarget" in renderTarget && renderTarget.isXRRenderTarget)) {
417
431
  stack = 0;
418
432
  self.#frame += 1;
419
- self.#delta = self.#clock.getDelta();
433
+ self.#clock.update?.();
434
+ self.#delta = Math.max(self.#clock.getDelta(), 1 / 1000);
420
435
  self.#time += self.#delta;
421
436
  self._fpsBuffer.shift();
422
437
  self._fpsBuffer.push(1 / self.#delta);
@@ -457,7 +472,9 @@ export class LODsManager {
457
472
  onAfterRender(scene, camera, _stack) {
458
473
  if (this.pause)
459
474
  return;
460
- const renderList = this.renderer.renderLists.get(scene, 0);
475
+ const renderList = this.getRenderList(scene, camera, _stack);
476
+ if (!renderList)
477
+ return;
461
478
  const opaque = renderList.opaque;
462
479
  let updateLODs = true;
463
480
  // check if we're rendering a postprocessing pass
@@ -514,7 +531,9 @@ export class LODsManager {
514
531
  * Update LODs in a scene
515
532
  */
516
533
  internalUpdate(scene, camera) {
517
- const renderList = this.renderer.renderLists.get(scene, 0);
534
+ const renderList = this.getRenderList(scene, camera, 0);
535
+ if (!renderList)
536
+ return;
518
537
  const opaque = renderList.opaque;
519
538
  this.projectionScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
520
539
  const desiredDensity = this.targetTriangleDensity;
@@ -560,6 +579,30 @@ export class LODsManager {
560
579
  }
561
580
  }
562
581
  }
582
+ getRenderList(scene, camera, stack) {
583
+ const renderer = this.renderer;
584
+ let renderList = null;
585
+ if (renderer.isWebGPURenderer === true) {
586
+ const renderLists = renderer._renderLists;
587
+ if (!renderLists)
588
+ return null;
589
+ renderList = renderLists.get(scene, camera);
590
+ }
591
+ else if (renderer.isWebGLRenderer === true) {
592
+ const renderLists = renderer.renderLists;
593
+ if (!renderLists)
594
+ return null;
595
+ renderList = renderLists.get(scene, stack);
596
+ }
597
+ if (!renderList)
598
+ return null;
599
+ return {
600
+ opaque: renderList.opaque || [],
601
+ transparent: renderList.transparent || [],
602
+ transmissive: renderList.transmissive || renderList.transparentDoublePass || [],
603
+ transparentDoublePass: renderList.transparentDoublePass || [],
604
+ };
605
+ }
563
606
  /** Update the LOD levels for the renderer. */
564
607
  updateLODs(scene, camera, object, desiredDensity) {
565
608
  if (!object.userData) {
@@ -820,7 +863,8 @@ export class LODsManager {
820
863
  if (this.context?.engine === "model-viewer") {
821
864
  factor *= 1.5;
822
865
  }
823
- const screenSize = canvasHeight / window.devicePixelRatio;
866
+ const devicePixelRatio = this.renderer.getPixelRatio?.() || globalThis.devicePixelRatio || 1;
867
+ const screenSize = canvasHeight / devicePixelRatio;
824
868
  const pixelSizeOnScreen = screenSize * factor;
825
869
  let foundLod = false;
826
870
  for (let i = texture_lods_minmax.lods.length - 1; i >= 0; i--) {
package/lib/utils.d.ts CHANGED
@@ -5,7 +5,7 @@ export declare const isSSR: boolean;
5
5
  * @param obj the object to get the raycast mesh from
6
6
  * @returns the raycast mesh or null if not set
7
7
  */
8
- export declare function getRaycastMesh(obj: Object3D): BufferGeometry<any> | null;
8
+ export declare function getRaycastMesh(obj: Object3D): BufferGeometry<any, any> | null;
9
9
  /**
10
10
  * Set the raycast mesh for an object.
11
11
  * The raycast mesh is a low poly version of the mesh used for raycasting. It is set when a mesh that has LOD level with more vertices is discovered for the first time
@@ -1,4 +1,31 @@
1
1
  import { Texture } from "three";
2
+ /** Represents the possible shapes of texture image/source data in three.js.
3
+ * Source.data is typed as `{}` in r183 but at runtime can be ImageBitmap, HTMLImageElement, etc. */
4
+ export type TextureImageData = {
5
+ width?: number;
6
+ height?: number;
7
+ depth?: number;
8
+ data?: ArrayBufferView | null;
9
+ };
10
+ /** Check if a value has image-like dimensions (width/height) */
11
+ export declare function hasImageDimensions(value: unknown): value is {
12
+ width: number;
13
+ height: number;
14
+ };
15
+ /** Check if a value has pixel data (e.g. typed array from a DataTexture) */
16
+ export declare function hasPixelData(value: unknown): value is {
17
+ data: ArrayBufferView;
18
+ };
19
+ /** Get the source data of a texture, typed for dimension/data access */
20
+ export declare function getSourceData(tex: Texture): TextureImageData | null;
21
+ /** Get the image of a texture, typed for dimension/data access.
22
+ * In r183, Texture.image is typed as `{}` but at runtime is an ImageBitmap, HTMLImageElement, etc. */
23
+ export declare function getTextureImage(tex: Texture): TextureImageData | null;
24
+ /** Get width/height of a texture from image or source data */
25
+ export declare function getTextureDimensions(tex: Texture): {
26
+ width: number;
27
+ height: number;
28
+ };
2
29
  export declare function isDebugMode(): string | boolean;
3
30
  export declare function getParam(name: string): boolean | string;
4
31
  export declare function resolveUrl(source: string | undefined, uri: string): string;
@@ -1,11 +1,41 @@
1
+ import { RedFormat, RedIntegerFormat, RGFormat, RGIntegerFormat, RGBFormat, RGBAFormat, RGBAIntegerFormat } from "three";
2
+ /** Check if a value has image-like dimensions (width/height) */
3
+ export function hasImageDimensions(value) {
4
+ return value != null && typeof value.width === 'number' && typeof value.height === 'number';
5
+ }
6
+ /** Check if a value has pixel data (e.g. typed array from a DataTexture) */
7
+ export function hasPixelData(value) {
8
+ return value != null && value.data != null;
9
+ }
10
+ /** Get the source data of a texture, typed for dimension/data access */
11
+ export function getSourceData(tex) {
12
+ const data = tex.source?.data;
13
+ return data != null && typeof data === 'object' ? data : null;
14
+ }
15
+ /** Get the image of a texture, typed for dimension/data access.
16
+ * In r183, Texture.image is typed as `{}` but at runtime is an ImageBitmap, HTMLImageElement, etc. */
17
+ export function getTextureImage(tex) {
18
+ const img = tex.image;
19
+ return img != null && typeof img === 'object' ? img : null;
20
+ }
21
+ /** Get width/height of a texture from image or source data */
22
+ export function getTextureDimensions(tex) {
23
+ const img = getTextureImage(tex);
24
+ const src = getSourceData(tex);
25
+ return {
26
+ width: img?.width || src?.width || 0,
27
+ height: img?.height || src?.height || 0,
28
+ };
29
+ }
1
30
  const debug = getParam("debugprogressive");
2
31
  export function isDebugMode() {
3
32
  return debug;
4
33
  }
5
34
  export function getParam(name) {
6
- if (typeof window === "undefined")
35
+ const href = globalThis.location?.href;
36
+ if (!href)
7
37
  return false;
8
- const url = new URL(window.location.href);
38
+ const url = new URL(href);
9
39
  const param = url.searchParams.get(name);
10
40
  if (param == null || param === "0" || param === "false")
11
41
  return false;
@@ -44,7 +74,8 @@ export function resolveUrl(source, uri) {
44
74
  export function isMobileDevice() {
45
75
  if (_ismobile !== undefined)
46
76
  return _ismobile;
47
- _ismobile = /iPhone|iPad|iPod|Android|IEMobile/i.test(navigator.userAgent);
77
+ const userAgent = globalThis.navigator?.userAgent || "";
78
+ _ismobile = /iPhone|iPad|iPod|Android|IEMobile/i.test(userAgent);
48
79
  if (getParam("debugprogressive"))
49
80
  console.log("[glTF Progressive]: isMobileDevice", _ismobile);
50
81
  return _ismobile;
@@ -55,9 +86,10 @@ let _ismobile;
55
86
  * @returns `true` if we are running in a development server (localhost or ip address).
56
87
  */
57
88
  export function isDevelopmentServer() {
58
- if (typeof window === "undefined")
89
+ const href = globalThis.location?.href;
90
+ if (!href)
59
91
  return false;
60
- const url = new URL(window.location.href);
92
+ const url = new URL(href);
61
93
  const isLocalhostOrIpAddress = url.hostname === "localhost" || /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(url.hostname);
62
94
  const isDevelopment = url.hostname === "127.0.0.1" || isLocalhostOrIpAddress;
63
95
  return isDevelopment;
@@ -74,8 +106,14 @@ export class PromiseQueue {
74
106
  constructor(maxConcurrent, opts = {}) {
75
107
  this.maxConcurrent = maxConcurrent;
76
108
  this.debug = opts.debug ?? false;
77
- if (typeof window !== "undefined")
78
- window.requestAnimationFrame(this.tick);
109
+ // Dedicated workers can have requestAnimationFrame when they are owned by a window.
110
+ // Other worker-like scopes do not, so keep the frame-based tick when available and fall back to timers otherwise.
111
+ if (typeof globalThis.requestAnimationFrame === "function") {
112
+ globalThis.requestAnimationFrame(this.tick);
113
+ }
114
+ else {
115
+ setTimeout(this.tick, 0);
116
+ }
79
117
  }
80
118
  tick = () => {
81
119
  this.internalUpdate();
@@ -118,9 +156,10 @@ export class PromiseQueue {
118
156
  }
119
157
  // #region Texture Memory
120
158
  export function determineTextureMemoryInBytes(texture) {
121
- const width = texture.image?.width ?? 0;
122
- const height = texture.image?.height ?? 0;
123
- const depth = texture.image?.depth ?? 1;
159
+ const img = texture.image;
160
+ const width = img?.width ?? 0;
161
+ const height = img?.height ?? 0;
162
+ const depth = img?.depth ?? 1;
124
163
  const mipLevels = Math.floor(Math.log2(Math.max(width, height, depth))) + 1;
125
164
  const bytesPerPixel = getBytesPerPixel(texture);
126
165
  const totalBytes = (width * height * depth * bytesPerPixel * (1 - Math.pow(0.25, mipLevels))) / (1 - 0.25);
@@ -130,22 +169,22 @@ function getBytesPerPixel(texture) {
130
169
  // Determine channel count from format
131
170
  let channels = 4; // Default RGBA
132
171
  const format = texture.format;
133
- if (format === 1024)
134
- channels = 1; // RedFormat
135
- else if (format === 1025)
136
- channels = 1; // RedIntegerFormat
137
- else if (format === 1026)
138
- channels = 2; // RGFormat
139
- else if (format === 1027)
140
- channels = 2; // RGIntegerFormat
141
- else if (format === 1022)
142
- channels = 3; // RGBFormat
172
+ if (format === RedFormat)
173
+ channels = 1;
174
+ else if (format === RedIntegerFormat)
175
+ channels = 1;
176
+ else if (format === RGFormat)
177
+ channels = 2;
178
+ else if (format === RGIntegerFormat)
179
+ channels = 2;
180
+ else if (format === RGBFormat)
181
+ channels = 3;
143
182
  else if (format === 1029)
144
- channels = 3; // RGBIntegerFormat
145
- else if (format === 1023)
146
- channels = 4; // RGBAFormat
147
- else if (format === 1033)
148
- channels = 4; // RGBAIntegerFormat
183
+ channels = 3; // RGBIntegerFormat (not exported in r183)
184
+ else if (format === RGBAFormat)
185
+ channels = 4;
186
+ else if (format === RGBAIntegerFormat)
187
+ channels = 4;
149
188
  // Determine bytes per channel from type
150
189
  let bytesPerChannel = 1; // UnsignedByteType default
151
190
  const type = texture.type;
@@ -177,6 +216,10 @@ export function detectGPUMemory() {
177
216
  if (rendererInfo !== undefined) {
178
217
  return rendererInfo?.estimatedMemory;
179
218
  }
219
+ if (typeof document === "undefined") {
220
+ rendererInfo = null;
221
+ return undefined;
222
+ }
180
223
  const canvas = document.createElement('canvas');
181
224
  const powerPreference = "high-performance";
182
225
  const gl = canvas.getContext('webgl', { powerPreference }) || canvas.getContext('experimental-webgl', { powerPreference });
package/lib/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // replaced at build time
2
- export const version = "3.6.0-alpha.3";
2
+ export const version = "3.6.0-beta.1";
3
3
  globalThis["GLTF_PROGRESSIVE_VERSION"] = version;
4
4
  console.debug(`[gltf-progressive] version ${version || "-"}`);
@@ -1,6 +1,6 @@
1
1
  import { Box3, BufferAttribute, BufferGeometry, CompressedTexture, InterleavedBuffer, InterleavedBufferAttribute, Matrix3, Sphere, Texture, Vector3 } from "three";
2
2
  import { createLoaders, GET_LOADER_LOCATION_CONFIG } from "../loaders.js";
3
- import { isMobileDevice } from "../utils.internal.js";
3
+ import { getTextureDimensions, isMobileDevice } from "../utils.internal.js";
4
4
  import { debug } from "../lods.debug.js";
5
5
  const workers = new Array();
6
6
  let getWorkerId = 0;
@@ -51,7 +51,10 @@ class GLTFLoaderWorker {
51
51
  url = URL.createObjectURL(new Blob([url]));
52
52
  }
53
53
  else if (!url.startsWith("blob:") && !url.startsWith("http:") && !url.startsWith("https:")) {
54
- url = new URL(url, window.location.href).toString();
54
+ const base = globalThis.location?.href;
55
+ if (base) {
56
+ url = new URL(url, base).toString();
57
+ }
55
58
  }
56
59
  const options = {
57
60
  type: "load",
@@ -152,8 +155,7 @@ function processReceivedData(data) {
152
155
  let newTexture = null;
153
156
  if (texture.isCompressedTexture) {
154
157
  const mipmaps = texture.mipmaps;
155
- const width = texture.image?.width || texture.source?.data?.width || -1;
156
- const height = texture.image?.height || texture.source?.data?.height || -1;
158
+ const { width, height } = getTextureDimensions(texture);
157
159
  newTexture = new CompressedTexture(mipmaps, width, height, texture.format, texture.type, texture.mapping, texture.wrapS, texture.wrapT, texture.magFilter, texture.minFilter, texture.anisotropy, texture.colorSpace);
158
160
  }
159
161
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-progressive",
3
- "version": "3.6.0-alpha.3",
3
+ "version": "3.6.0-beta.1",
4
4
  "description": "three.js support for loading glTF or GLB files that contain progressive loading data",
5
5
  "homepage": "https://needle.tools",
6
6
  "author": {
@@ -59,10 +59,13 @@
59
59
  "three": ">= 0.160.0"
60
60
  },
61
61
  "devDependencies": {
62
+ "@needle-tools/engine": "^5.1.0-alpha.6",
63
+ "@needle-tools/three-test-matrix": "^0.1.0",
62
64
  "@stylistic/eslint-plugin-ts": "^1.5.4",
63
- "@types/three": "0.169.0",
65
+ "@types/three": "0.183.1",
64
66
  "@typescript-eslint/eslint-plugin": "^6.2.0",
65
67
  "@typescript-eslint/parser": "^6.2.0",
68
+ "@vitest/browser-playwright": "^4.1.8",
66
69
  "eslint": "^8.56.0",
67
70
  "eslint-plugin-import": "^2.29.1",
68
71
  "eslint-plugin-no-secrets": "^0.8.9",
@@ -71,8 +74,10 @@
71
74
  "eslint-plugin-xss": "^0.1.12",
72
75
  "nodemon": "^3.1.4",
73
76
  "npm-watch": "^0.13.0",
77
+ "playwright": "^1.60.0",
74
78
  "three": ">= 0.160.0",
75
- "vite": "7"
79
+ "vite": "7",
80
+ "vitest": "^4.1.8"
76
81
  },
77
82
  "types": "./lib/index.d.ts"
78
83
  }