diff --git a/package-lock.json b/package-lock.json index 6f10add..7853309 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tippyjs/react": "^4.2.6", "flatbuffers": "^23.5.26", + "pako": "^2.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.13.0", @@ -22,6 +23,7 @@ "devDependencies": { "@types/mdast": "^3.0.11", "@types/node": "^20.3.3", + "@types/pako": "^2.0.0", "@types/react": "^18.0.37", "@types/react-dom": "^18.0.11", "@types/three": "^0.152.1", @@ -801,6 +803,12 @@ "integrity": "sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==", "dev": true }, + "node_modules/@types/pako": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.0.tgz", + "integrity": "sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==", + "dev": true + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -3537,6 +3545,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/package.json b/package.json index 1a67fac..e0be715 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@tippyjs/react": "^4.2.6", "flatbuffers": "^23.5.26", + "pako": "^2.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.13.0", @@ -24,6 +25,7 @@ "devDependencies": { "@types/mdast": "^3.0.11", "@types/node": "^20.3.3", + "@types/pako": "^2.0.0", "@types/react": "^18.0.37", "@types/react-dom": "^18.0.11", "@types/three": "^0.152.1", diff --git a/src/GuideLoader.tsx b/src/GuideLoader.tsx index 5e1724b..b15b9be 100644 --- a/src/GuideLoader.tsx +++ b/src/GuideLoader.tsx @@ -3,6 +3,7 @@ import { PropsWithChildren, useMemo } from "react"; import useLoadEffect from "./data/useLoadEffect.ts"; import { Guide, GuideProvider } from "./data/Guide.ts"; import LoadState from "./components/LoadState.tsx"; +import decompress from "./decompress.ts"; export type GuideLoaderProps = PropsWithChildren<{ version: GuideVersion; @@ -26,18 +27,16 @@ async function loadGuideFromResponseV1( ): Promise { const guideDataUrl = new URL(metadata.guideDataPath, metadataUrl); - const response = await fetch(guideDataUrl, { + let response = await fetch(guideDataUrl, { cache: "force-cache", }); // Decompress - const blob = await response.blob(); - const ds = new DecompressionStream("gzip"); - const decompressedStream = blob.stream().pipeThrough(ds); - const jsonBody = await new Response(decompressedStream).json(); + response = await decompress(response); + const jsonBody = await response.json(); // Use the directory we loaded the guide from to load further assets - const baseUrl = new URL("./", response.url).toString(); + const baseUrl = new URL("./", guideDataUrl).toString(); console.info("Deducing base URL %s for guide data %s", baseUrl, response.url); return new Guide( baseUrl, @@ -78,7 +77,7 @@ function GuideLoader({ version, children }: GuideLoaderProps) { } else { return ( ); diff --git a/src/components/model-viewer/loadScene.ts b/src/components/model-viewer/loadScene.ts index 1bb0c62..b71cca6 100644 --- a/src/components/model-viewer/loadScene.ts +++ b/src/components/model-viewer/loadScene.ts @@ -4,6 +4,7 @@ import { Group, Mesh } from "three"; import TextureManager from "./TextureManager.ts"; import loadGeometry from "./loadGeometry.ts"; import loadMaterial from "./loadMaterial.ts"; +import decompress from "../../decompress.ts"; type LoadedScene = { group: Group; @@ -19,9 +20,8 @@ export type CameraProps = { async function decompressResponse(response: Response) { const blob = await response.blob(); - const ds = new DecompressionStream("gzip"); - const decompressedStream = blob.stream().pipeThrough(ds); - const sceneContent = await new Response(decompressedStream).arrayBuffer(); + response = await decompress(blob); + const sceneContent = await response.arrayBuffer(); console.debug( "Loaded %s, %d byte compressed, %d byte uncompressed", @@ -79,5 +79,6 @@ export default async function loadScene( roll: expCamera.roll(), zoom: expCamera.zoom(), }; + return { cameraProps, group }; } diff --git a/src/decompress.ts b/src/decompress.ts new file mode 100644 index 0000000..13ac447 --- /dev/null +++ b/src/decompress.ts @@ -0,0 +1,23 @@ +export default async function decompress(response: Response): Promise; +export default async function decompress(blob: Blob): Promise; +export default async function decompress( + response: Response | Blob +): Promise { + let blob: Blob; + if (response instanceof Blob) { + blob = response; + } else { + blob = await response.blob(); + } + + // Fallback for browsers without support for DecompressionStream + if (typeof DecompressionStream === "undefined") { + const fallbackDecompress = await import("./decompressFallback.ts"); + const data = await fallbackDecompress.default(blob); + return new Response(data); + } + + const ds = new DecompressionStream("gzip"); + const decompressedStream = blob.stream().pipeThrough(ds); + return new Response(decompressedStream); +} diff --git a/src/decompressFallback.ts b/src/decompressFallback.ts new file mode 100644 index 0000000..879c7b8 --- /dev/null +++ b/src/decompressFallback.ts @@ -0,0 +1,8 @@ +import { ungzip } from "pako"; + +export default async function decompressFallback( + blob: Blob +): Promise { + const data = await blob.arrayBuffer(); + return ungzip(data); +}