@plasius/gpu-shared 0.1.11 → 0.1.13

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 (41) hide show
  1. package/CHANGELOG.md +36 -3
  2. package/README.md +55 -1
  3. package/assets/brigantine.gltf +549 -24
  4. package/assets/cutter.gltf +538 -0
  5. package/assets/harbor-dock.gltf +680 -0
  6. package/assets/lighthouse.gltf +604 -0
  7. package/dist/chunk-2FIFSBB4.js +74 -0
  8. package/dist/chunk-2FIFSBB4.js.map +1 -0
  9. package/dist/chunk-DABW627O.js +113 -0
  10. package/dist/chunk-DABW627O.js.map +1 -0
  11. package/dist/chunk-DQX4DXBR.js +369 -0
  12. package/dist/chunk-DQX4DXBR.js.map +1 -0
  13. package/dist/chunk-NCPJWLX3.js +17 -0
  14. package/dist/chunk-NCPJWLX3.js.map +1 -0
  15. package/dist/gltf-loader-WAM23F37.js +9 -0
  16. package/dist/index.cjs +1255 -279
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.js +19 -6
  19. package/dist/index.js.map +1 -1
  20. package/dist/showcase-inline-assets-B7U7VX5H.js +7 -0
  21. package/dist/{showcase-runtime-2ZNPKD7D.js → showcase-runtime-PN7N3FZY.js} +808 -237
  22. package/dist/showcase-runtime-PN7N3FZY.js.map +1 -0
  23. package/package.json +15 -1
  24. package/src/asset-url.js +62 -11
  25. package/src/feature-flags.js +1 -0
  26. package/src/gltf-loader.js +322 -32
  27. package/src/i18n.js +71 -0
  28. package/src/index.d.ts +115 -1
  29. package/src/index.js +9 -1
  30. package/src/showcase-inline-assets.js +3 -0
  31. package/src/showcase-runtime.js +912 -188
  32. package/src/translations/en-GB.js +55 -0
  33. package/dist/chunk-DGUM43GV.js +0 -11
  34. package/dist/chunk-OTCJ3VOK.js +0 -35
  35. package/dist/chunk-OTCJ3VOK.js.map +0 -1
  36. package/dist/chunk-QBMXJ3V2.js +0 -142
  37. package/dist/chunk-QBMXJ3V2.js.map +0 -1
  38. package/dist/gltf-loader-LKALCZAV.js +0 -8
  39. package/dist/showcase-runtime-2ZNPKD7D.js.map +0 -1
  40. /package/dist/{chunk-DGUM43GV.js.map → gltf-loader-WAM23F37.js.map} +0 -0
  41. /package/dist/{gltf-loader-LKALCZAV.js.map → showcase-inline-assets-B7U7VX5H.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/gpu-shared",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Shared browser-safe demo runtime and asset helpers for the Plasius gpu-* package family.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -23,12 +23,16 @@
23
23
  "require": "./dist/index.cjs"
24
24
  },
25
25
  "./assets/brigantine.gltf": "./assets/brigantine.gltf",
26
+ "./assets/cutter.gltf": "./assets/cutter.gltf",
27
+ "./assets/lighthouse.gltf": "./assets/lighthouse.gltf",
28
+ "./assets/harbor-dock.gltf": "./assets/harbor-dock.gltf",
26
29
  "./package.json": "./package.json"
27
30
  },
28
31
  "scripts": {
29
32
  "build": "tsup",
30
33
  "demo": "python3 -m http.server --directory ..",
31
34
  "demo:example": "node demo/example.js",
35
+ "generate:assets": "node scripts/generate-showcase-assets.mjs",
32
36
  "typecheck": "node --check src/index.js && node --check src/showcase-runtime.js && node --check src/gltf-loader.js && node --check demo/main.js",
33
37
  "audit:eslint": "eslint . --max-warnings=0",
34
38
  "audit:deps": "npm ls --all --omit=optional --omit=peer > /dev/null 2>&1 || true",
@@ -60,11 +64,21 @@
60
64
  "@plasius/gpu-performance": "^0.1.6",
61
65
  "@plasius/gpu-physics": "^0.1.13"
62
66
  },
67
+ "peerDependencies": {
68
+ "@plasius/translations": "^1.0.17"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "@plasius/translations": {
72
+ "optional": true
73
+ }
74
+ },
63
75
  "devDependencies": {
64
76
  "@eslint/js": "^10.0.1",
77
+ "@plasius/translations": "^1.0.17",
65
78
  "c8": "^11.0.0",
66
79
  "eslint": "^10.3.0",
67
80
  "globals": "^17.6.0",
81
+ "react": "^19.2.6",
68
82
  "tsup": "^8.5.1",
69
83
  "typescript": "^6.0.3"
70
84
  },
package/src/asset-url.js CHANGED
@@ -1,12 +1,23 @@
1
- const INLINE_BRIGANTINE_GLTF_URL =
2
- "data:application/json;base64,ewogICJhc3NldCI6IHsKICAgICJ2ZXJzaW9uIjogIjIuMCIsCiAgICAiZ2VuZXJhdG9yIjogIlBsYXNpdXMgZGVtbyBhc3NldCBnZW5lcmF0b3IiCiAgfSwKICAic2NlbmUiOiAwLAogICJzY2VuZXMiOiBbCiAgICB7CiAgICAgICJub2RlcyI6IFswXQogICAgfQogIF0sCiAgIm5vZGVzIjogWwogICAgewogICAgICAibWVzaCI6IDAsCiAgICAgICJuYW1lIjogImJyaWdhbnRpbmUiLAogICAgICAiZXh0cmFzIjogewogICAgICAgICJwaHlzaWNzIjogewogICAgICAgICAgInNoYXBlIjogImJveCIsCiAgICAgICAgICAiaGFsZkV4dGVudHMiOiBbMS4zNSwgMC45NSwgMy45XSwKICAgICAgICAgICJtYXNzIjogMzIwMCwKICAgICAgICAgICJyZXN0aXR1dGlvbiI6IDAuMjIsCiAgICAgICAgICAibGluZWFyRGFtcGluZyI6IDAuMDQsCiAgICAgICAgICAiYW5ndWxhckRhbXBpbmciOiAwLjA4LAogICAgICAgICAgIndhdGVybGluZSI6IDAuNDIKICAgICAgICB9CiAgICAgIH0KICAgIH0KICBdLAogICJtZXNoZXMiOiBbCiAgICB7CiAgICAgICJuYW1lIjogImJyaWdhbnRpbmUtaHVsbCIsCiAgICAgICJwcmltaXRpdmVzIjogWwogICAgICAgIHsKICAgICAgICAgICJhdHRyaWJ1dGVzIjogewogICAgICAgICAgICAiUE9TSVRJT04iOiAwCiAgICAgICAgICB9LAogICAgICAgICAgImluZGljZXMiOiAxLAogICAgICAgICAgIm1hdGVyaWFsIjogMAogICAgICAgIH0KICAgICAgXQogICAgfQogIF0sCiAgIm1hdGVyaWFscyI6IFsKICAgIHsKICAgICAgIm5hbWUiOiAicGFpbnRlZC1odWxsIiwKICAgICAgInBick1ldGFsbGljUm91Z2huZXNzIjogewogICAgICAgICJiYXNlQ29sb3JGYWN0b3IiOiBbMC41NiwgMC4zMywgMC4yMiwgMV0sCiAgICAgICAgIm1ldGFsbGljRmFjdG9yIjogMC4wOCwKICAgICAgICAicm91Z2huZXNzRmFjdG9yIjogMC45MgogICAgICB9CiAgICB9CiAgXSwKICAiYnVmZmVycyI6IFsKICAgIHsKICAgICAgInVyaSI6ICJkYXRhOmFwcGxpY2F0aW9uL29jdGV0LXN0cmVhbTtiYXNlNjQsbXBtWnZ3QUFBTC9OekV6QW1wbVpQd0FBQUwvTnpFekF6Y3lzdnpNenM3NHpNN08vemN5c1B6TXpzNzR6TTdPL0FBQ2d2ODNNVEwzTnpNdy9BQUNnUDgzTVRMM056TXcvQUFBQUFPeFJPTDR6TTROQUFBQUFBR1ptNWo0QUFIQkFNek56dnpNenN6NmFtUm5BTXpOelB6TXpzejZhbVJuQXpjeE12ejBLMXo3TnpFdy96Y3hNUHowSzF6N056RXcvQUFBQUFETXpjejltWm1hL0FBQUNBQU1BQUFBREFBRUFBZ0FFQUFVQUFnQUZBQU1BQkFBSEFBVUFCQUFHQUFjQUJRQUhBQVlBQUFBQkFBa0FBQUFKQUFnQUNBQUpBQXdBQWdBSUFBd0FBd0FNQUFrQUFnQU1BQW9BQXdBTEFBd0FBZ0FLQUFRQUF3QUZBQXNBQ2dBTUFBc0FBQUFJQUFJQUFRQURBQWtBQkFBS0FBWUFCUUFHQUFzQUFnQUtBQXNBQWdBTEFBTUEiLAogICAgICAiYnl0ZUxlbmd0aCI6IDI5NAogICAgfQogIF0sCiAgImJ1ZmZlclZpZXdzIjogWwogICAgewogICAgICAiYnVmZmVyIjogMCwKICAgICAgImJ5dGVPZmZzZXQiOiAwLAogICAgICAiYnl0ZUxlbmd0aCI6IDE1NiwKICAgICAgInRhcmdldCI6IDM0OTYyCiAgICB9LAogICAgewogICAgICAiYnVmZmVyIjogMCwKICAgICAgImJ5dGVPZmZzZXQiOiAxNTYsCiAgICAgICJieXRlTGVuZ3RoIjogMTM4LAogICAgICAidGFyZ2V0IjogMzQ5NjMKICAgIH0KICBdLAogICJhY2Nlc3NvcnMiOiBbCiAgICB7CiAgICAgICJidWZmZXJWaWV3IjogMCwKICAgICAgImJ5dGVPZmZzZXQiOiAwLAogICAgICAiY29tcG9uZW50VHlwZSI6IDUxMjYsCiAgICAgICJjb3VudCI6IDEzLAogICAgICAidHlwZSI6ICJWRUMzIiwKICAgICAgIm1pbiI6IFstMS4zNSwgLTAuNSwgLTMuMl0sCiAgICAgICJtYXgiOiBbMS4zNSwgMC45NSwgNC4xXQogICAgfSwKICAgIHsKICAgICAgImJ1ZmZlclZpZXciOiAxLAogICAgICAiYnl0ZU9mZnNldCI6IDAsCiAgICAgICJjb21wb25lbnRUeXBlIjogNTEyMywKICAgICAgImNvdW50IjogNjksCiAgICAgICJ0eXBlIjogIlNDQUxBUiIsCiAgICAgICJtYXgiOiBbMTJdLAogICAgICAibWluIjogWzBdCiAgICB9CiAgXQp9Cg==";
1
+ import { INLINE_SHOWCASE_ASSET_URLS } from "./showcase-inline-assets.js";
3
2
 
4
- function createInlineShowcaseAssetUrl() {
5
- return new URL(INLINE_BRIGANTINE_GLTF_URL);
3
+ const SHOWCASE_ASSET_FILES = Object.freeze({
4
+ brigantine: "brigantine.gltf",
5
+ cutter: "cutter.gltf",
6
+ lighthouse: "lighthouse.gltf",
7
+ "harbor-dock": "harbor-dock.gltf",
8
+ });
9
+
10
+ function createInlineShowcaseAssetUrl(assetName) {
11
+ const inlineUrl = INLINE_SHOWCASE_ASSET_URLS[assetName];
12
+ return inlineUrl ? new URL(inlineUrl) : null;
6
13
  }
7
14
 
8
15
  function getBrowserBaseUrl() {
9
- if (typeof document !== "undefined" && typeof document.baseURI === "string" && document.baseURI.length > 0) {
16
+ if (
17
+ typeof document !== "undefined" &&
18
+ typeof document.baseURI === "string" &&
19
+ document.baseURI.length > 0
20
+ ) {
10
21
  return document.baseURI;
11
22
  }
12
23
  if (
@@ -19,19 +30,59 @@ function getBrowserBaseUrl() {
19
30
  return null;
20
31
  }
21
32
 
22
- export function resolveShowcaseAssetUrl(baseUrl = import.meta.url) {
33
+ function normalizeAssetName(assetName) {
34
+ return typeof assetName === "string" && assetName in SHOWCASE_ASSET_FILES
35
+ ? assetName
36
+ : "brigantine";
37
+ }
38
+
39
+ function parseResolveArgs(baseUrlOrAssetName, assetName) {
40
+ if (
41
+ typeof baseUrlOrAssetName === "string" &&
42
+ baseUrlOrAssetName in SHOWCASE_ASSET_FILES &&
43
+ typeof assetName === "undefined"
44
+ ) {
45
+ return {
46
+ baseUrl: import.meta.url,
47
+ assetName: baseUrlOrAssetName,
48
+ };
49
+ }
50
+
51
+ return {
52
+ baseUrl: baseUrlOrAssetName ?? import.meta.url,
53
+ assetName: normalizeAssetName(assetName),
54
+ };
55
+ }
56
+
57
+ export function resolveShowcaseAssetUrl(baseUrlOrAssetName, assetName) {
58
+ const resolved = parseResolveArgs(baseUrlOrAssetName, assetName);
59
+ const fileName = SHOWCASE_ASSET_FILES[resolved.assetName];
60
+
23
61
  try {
24
- return new URL("../assets/brigantine.gltf", baseUrl);
62
+ return new URL(`../assets/${fileName}`, resolved.baseUrl);
25
63
  } catch {
26
64
  const browserBaseUrl = getBrowserBaseUrl();
27
65
  if (browserBaseUrl) {
28
66
  try {
29
- const normalizedBaseUrl = new URL(baseUrl, browserBaseUrl);
30
- return new URL("../assets/brigantine.gltf", normalizedBaseUrl);
67
+ const normalizedBaseUrl = new URL(resolved.baseUrl, browserBaseUrl);
68
+ return new URL(`../assets/${fileName}`, normalizedBaseUrl);
31
69
  } catch {
32
- return createInlineShowcaseAssetUrl();
70
+ const inlineAsset = createInlineShowcaseAssetUrl(resolved.assetName);
71
+ if (inlineAsset) {
72
+ return inlineAsset;
73
+ }
33
74
  }
34
75
  }
35
- return createInlineShowcaseAssetUrl();
76
+
77
+ try {
78
+ return new URL(`../assets/${fileName}`, import.meta.url);
79
+ } catch {
80
+ return new URL(`assets/${fileName}`, "file:///");
81
+ }
36
82
  }
37
83
  }
84
+
85
+ export function shouldUseInlineShowcaseFallback(url) {
86
+ const href = url instanceof URL ? url.href : String(url ?? "");
87
+ return href.includes("/assets/");
88
+ }
@@ -0,0 +1 @@
1
+ export const GPU_SHOWCASE_REALISTIC_MODELS_FEATURE = "gpu_showcase_realistic_models_v1";
@@ -1,3 +1,5 @@
1
+ import { shouldUseInlineShowcaseFallback } from "./asset-url.js";
2
+
1
3
  function decodeDataUri(uri) {
2
4
  const match = /^data:.*?;base64,(.+)$/i.exec(uri);
3
5
  if (!match) {
@@ -14,6 +16,8 @@ function decodeDataUri(uri) {
14
16
 
15
17
  function getComponentArray(componentType, buffer, byteOffset, count) {
16
18
  switch (componentType) {
19
+ case 5121:
20
+ return new Uint8Array(buffer, byteOffset, count);
17
21
  case 5123:
18
22
  return new Uint16Array(buffer, byteOffset, count);
19
23
  case 5125:
@@ -25,6 +29,17 @@ function getComponentArray(componentType, buffer, byteOffset, count) {
25
29
  }
26
30
  }
27
31
 
32
+ function getNormalizationScale(componentType) {
33
+ switch (componentType) {
34
+ case 5121:
35
+ return 255;
36
+ case 5123:
37
+ return 65535;
38
+ default:
39
+ return 1;
40
+ }
41
+ }
42
+
28
43
  function getTypeSize(type) {
29
44
  switch (type) {
30
45
  case "SCALAR":
@@ -41,30 +56,74 @@ function getTypeSize(type) {
41
56
  }
42
57
 
43
58
  function readAccessor(document, accessorIndex, buffers) {
44
- const accessor = document.accessors[accessorIndex];
45
- const bufferView = document.bufferViews[accessor.bufferView];
59
+ const accessor = document.accessors?.[accessorIndex];
60
+ if (!accessor) {
61
+ throw new Error(`glTF accessor ${accessorIndex} is missing.`);
62
+ }
63
+
64
+ const bufferView = document.bufferViews?.[accessor.bufferView];
65
+ if (!bufferView) {
66
+ throw new Error(`glTF bufferView ${accessor.bufferView} is missing.`);
67
+ }
68
+
46
69
  const buffer = buffers[bufferView.buffer];
47
70
  const componentCount = getTypeSize(accessor.type);
48
71
  const byteOffset = (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
49
72
  const valueCount = accessor.count * componentCount;
50
- return getComponentArray(accessor.componentType, buffer, byteOffset, valueCount);
73
+ const values = Array.from(
74
+ getComponentArray(accessor.componentType, buffer, byteOffset, valueCount)
75
+ );
76
+
77
+ if (accessor.normalized) {
78
+ const scale = getNormalizationScale(accessor.componentType);
79
+ return values.map((value) => value / scale);
80
+ }
81
+
82
+ return values;
51
83
  }
52
84
 
53
- function getMaterialColor(document, primitive) {
85
+ function getMaterialInfo(document, primitive) {
54
86
  const material = document.materials?.[primitive.material] ?? null;
55
87
  const factor =
56
88
  material?.pbrMetallicRoughness?.baseColorFactor ?? [0.56, 0.33, 0.22, 1];
57
- return {
58
- r: factor[0],
59
- g: factor[1],
60
- b: factor[2],
61
- a: factor[3] ?? 1,
62
- };
89
+ const emissive = material?.emissiveFactor ?? [0, 0, 0];
90
+
91
+ return Object.freeze({
92
+ name: material?.name ?? "default-material",
93
+ color: Object.freeze({
94
+ r: factor[0],
95
+ g: factor[1],
96
+ b: factor[2],
97
+ a: factor[3] ?? 1,
98
+ }),
99
+ roughness:
100
+ typeof material?.pbrMetallicRoughness?.roughnessFactor === "number"
101
+ ? material.pbrMetallicRoughness.roughnessFactor
102
+ : 0.92,
103
+ metallic:
104
+ typeof material?.pbrMetallicRoughness?.metallicFactor === "number"
105
+ ? material.pbrMetallicRoughness.metallicFactor
106
+ : 0.08,
107
+ emissive: Object.freeze({
108
+ r: emissive[0] ?? 0,
109
+ g: emissive[1] ?? 0,
110
+ b: emissive[2] ?? 0,
111
+ }),
112
+ });
63
113
  }
64
114
 
65
115
  function computeBounds(positions) {
66
- const min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
67
- const max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
116
+ const min = [
117
+ Number.POSITIVE_INFINITY,
118
+ Number.POSITIVE_INFINITY,
119
+ Number.POSITIVE_INFINITY,
120
+ ];
121
+ const max = [
122
+ Number.NEGATIVE_INFINITY,
123
+ Number.NEGATIVE_INFINITY,
124
+ Number.NEGATIVE_INFINITY,
125
+ ];
126
+
68
127
  for (let index = 0; index < positions.length; index += 3) {
69
128
  min[0] = Math.min(min[0], positions[index]);
70
129
  min[1] = Math.min(min[1], positions[index + 1]);
@@ -73,11 +132,19 @@ function computeBounds(positions) {
73
132
  max[1] = Math.max(max[1], positions[index + 1]);
74
133
  max[2] = Math.max(max[2], positions[index + 2]);
75
134
  }
76
- return { min, max };
135
+
136
+ return Object.freeze({
137
+ min: Object.freeze([min[0], min[1], min[2]]),
138
+ max: Object.freeze([max[0], max[1], max[2]]),
139
+ });
77
140
  }
78
141
 
79
142
  function resolveBrowserRequestBaseUrl() {
80
- if (typeof document !== "undefined" && typeof document.baseURI === "string" && document.baseURI.length > 0) {
143
+ if (
144
+ typeof document !== "undefined" &&
145
+ typeof document.baseURI === "string" &&
146
+ document.baseURI.length > 0
147
+ ) {
81
148
  return document.baseURI;
82
149
  }
83
150
  if (
@@ -106,18 +173,217 @@ function resolveFetchBaseUrl(requestUrl, responseUrl) {
106
173
  if (browserBaseUrl) {
107
174
  return new URL(requestUrl, browserBaseUrl);
108
175
  }
109
- throw new Error(`Unable to resolve a stable base URL for glTF asset loading: ${String(requestUrl)}`);
176
+ throw new Error(
177
+ `Unable to resolve a stable base URL for glTF asset loading: ${String(requestUrl)}`
178
+ );
110
179
  }
111
180
  }
112
181
 
113
- export async function loadGltfModel(url) {
182
+ function createIdentityMatrix() {
183
+ return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
184
+ }
185
+
186
+ function multiplyMatrices(a, b) {
187
+ const out = new Array(16).fill(0);
188
+ for (let column = 0; column < 4; column += 1) {
189
+ for (let row = 0; row < 4; row += 1) {
190
+ out[column * 4 + row] =
191
+ a[0 * 4 + row] * b[column * 4 + 0] +
192
+ a[1 * 4 + row] * b[column * 4 + 1] +
193
+ a[2 * 4 + row] * b[column * 4 + 2] +
194
+ a[3 * 4 + row] * b[column * 4 + 3];
195
+ }
196
+ }
197
+ return out;
198
+ }
199
+
200
+ function composeNodeMatrix(node) {
201
+ if (Array.isArray(node.matrix) && node.matrix.length === 16) {
202
+ return [...node.matrix];
203
+ }
204
+
205
+ const translation = Array.isArray(node.translation) ? node.translation : [0, 0, 0];
206
+ const rotation = Array.isArray(node.rotation) ? node.rotation : [0, 0, 0, 1];
207
+ const scale = Array.isArray(node.scale) ? node.scale : [1, 1, 1];
208
+ const [x, y, z, w] = rotation;
209
+ const x2 = x + x;
210
+ const y2 = y + y;
211
+ const z2 = z + z;
212
+ const xx = x * x2;
213
+ const xy = x * y2;
214
+ const xz = x * z2;
215
+ const yy = y * y2;
216
+ const yz = y * z2;
217
+ const zz = z * z2;
218
+ const wx = w * x2;
219
+ const wy = w * y2;
220
+ const wz = w * z2;
221
+
222
+ return [
223
+ (1 - (yy + zz)) * scale[0],
224
+ (xy + wz) * scale[0],
225
+ (xz - wy) * scale[0],
226
+ 0,
227
+ (xy - wz) * scale[1],
228
+ (1 - (xx + zz)) * scale[1],
229
+ (yz + wx) * scale[1],
230
+ 0,
231
+ (xz + wy) * scale[2],
232
+ (yz - wx) * scale[2],
233
+ (1 - (xx + yy)) * scale[2],
234
+ 0,
235
+ translation[0],
236
+ translation[1],
237
+ translation[2],
238
+ 1,
239
+ ];
240
+ }
241
+
242
+ function transformPosition(position, matrix) {
243
+ return [
244
+ matrix[0] * position[0] + matrix[4] * position[1] + matrix[8] * position[2] + matrix[12],
245
+ matrix[1] * position[0] + matrix[5] * position[1] + matrix[9] * position[2] + matrix[13],
246
+ matrix[2] * position[0] + matrix[6] * position[1] + matrix[10] * position[2] + matrix[14],
247
+ ];
248
+ }
249
+
250
+ function transformNormal(normal, matrix) {
251
+ const transformed = [
252
+ matrix[0] * normal[0] + matrix[4] * normal[1] + matrix[8] * normal[2],
253
+ matrix[1] * normal[0] + matrix[5] * normal[1] + matrix[9] * normal[2],
254
+ matrix[2] * normal[0] + matrix[6] * normal[1] + matrix[10] * normal[2],
255
+ ];
256
+ const length = Math.hypot(transformed[0], transformed[1], transformed[2]) || 1;
257
+ return [transformed[0] / length, transformed[1] / length, transformed[2] / length];
258
+ }
259
+
260
+ function collectScenePrimitives(document, buffers) {
261
+ const scene = document.scenes?.[document.scene ?? 0];
262
+ if (!scene || !Array.isArray(scene.nodes) || scene.nodes.length === 0) {
263
+ throw new Error("glTF demo asset must expose a default scene with at least one node.");
264
+ }
265
+
266
+ const results = [];
267
+ let modelName = null;
268
+ let physics = null;
269
+
270
+ function visit(nodeIndex, parentMatrix) {
271
+ const node = document.nodes?.[nodeIndex];
272
+ if (!node) {
273
+ throw new Error(`glTF node ${nodeIndex} is missing.`);
274
+ }
275
+
276
+ const localMatrix = composeNodeMatrix(node);
277
+ const worldMatrix = multiplyMatrices(parentMatrix, localMatrix);
278
+
279
+ if (!modelName && typeof node.name === "string" && node.name.length > 0) {
280
+ modelName = node.name;
281
+ }
282
+
283
+ if (!physics && node.extras?.physics && typeof node.extras.physics === "object") {
284
+ physics = Object.freeze({ ...node.extras.physics });
285
+ }
286
+
287
+ if (typeof node.mesh === "number") {
288
+ const mesh = document.meshes?.[node.mesh];
289
+ if (!mesh || !Array.isArray(mesh.primitives)) {
290
+ throw new Error(`glTF mesh ${node.mesh} is missing primitives.`);
291
+ }
292
+
293
+ mesh.primitives.forEach((primitive, primitiveIndex) => {
294
+ const positions = readAccessor(document, primitive.attributes.POSITION, buffers);
295
+ const normals =
296
+ typeof primitive.attributes.NORMAL === "number"
297
+ ? readAccessor(document, primitive.attributes.NORMAL, buffers)
298
+ : null;
299
+ const colors =
300
+ typeof primitive.attributes.COLOR_0 === "number"
301
+ ? readAccessor(document, primitive.attributes.COLOR_0, buffers)
302
+ : null;
303
+ const transformedPositions = [];
304
+ const transformedNormals = [];
305
+
306
+ for (let index = 0; index < positions.length; index += 3) {
307
+ const point = transformPosition(
308
+ [positions[index], positions[index + 1], positions[index + 2]],
309
+ worldMatrix
310
+ );
311
+ transformedPositions.push(point[0], point[1], point[2]);
312
+
313
+ if (normals) {
314
+ const normal = transformNormal(
315
+ [normals[index], normals[index + 1], normals[index + 2]],
316
+ worldMatrix
317
+ );
318
+ transformedNormals.push(normal[0], normal[1], normal[2]);
319
+ }
320
+ }
321
+
322
+ const indices =
323
+ typeof primitive.indices === "number"
324
+ ? readAccessor(document, primitive.indices, buffers).map((value) => Number(value))
325
+ : Array.from({ length: transformedPositions.length / 3 }, (_, index) => index);
326
+ const material = getMaterialInfo(document, primitive);
327
+ const primitiveName =
328
+ `${node.name ?? mesh.name ?? "mesh"}-${primitiveIndex}`;
329
+
330
+ results.push(
331
+ Object.freeze({
332
+ name: primitiveName,
333
+ positions: Object.freeze(transformedPositions),
334
+ indices: Object.freeze(indices),
335
+ normals:
336
+ transformedNormals.length > 0
337
+ ? Object.freeze(transformedNormals)
338
+ : null,
339
+ colors: colors ? Object.freeze(colors) : null,
340
+ material,
341
+ bounds: computeBounds(transformedPositions),
342
+ })
343
+ );
344
+ });
345
+ }
346
+
347
+ if (Array.isArray(node.children)) {
348
+ for (const childIndex of node.children) {
349
+ visit(childIndex, worldMatrix);
350
+ }
351
+ }
352
+ }
353
+
354
+ for (const rootNodeIndex of scene.nodes) {
355
+ visit(rootNodeIndex, createIdentityMatrix());
356
+ }
357
+
358
+ if (results.length === 0) {
359
+ throw new Error("glTF demo asset must contain at least one mesh primitive.");
360
+ }
361
+
362
+ return {
363
+ name: modelName ?? "gltf-model",
364
+ physics: physics ?? Object.freeze({}),
365
+ primitives: results,
366
+ };
367
+ }
368
+
369
+ async function loadGltfDocument(url) {
114
370
  const response = await fetch(url);
115
371
  if (!response.ok) {
116
372
  throw new Error(`Failed to load glTF asset: ${response.status} ${response.statusText}`);
117
373
  }
118
374
 
119
- const document = await response.json();
120
- const baseUrl = resolveFetchBaseUrl(url, response.url);
375
+ return {
376
+ document: await response.json(),
377
+ baseUrl: resolveFetchBaseUrl(url, response.url),
378
+ };
379
+ }
380
+
381
+ async function loadInlineShowcaseDocument() {
382
+ const module = await import("./showcase-inline-assets.js");
383
+ return loadGltfDocument(new URL(module.INLINE_SHOWCASE_ASSET_URLS.brigantine));
384
+ }
385
+
386
+ async function buildGltfModel(document, baseUrl) {
121
387
  const buffers = await Promise.all(
122
388
  (document.buffers ?? []).map(async (buffer) => {
123
389
  if (typeof buffer.uri !== "string") {
@@ -134,23 +400,47 @@ export async function loadGltfModel(url) {
134
400
  })
135
401
  );
136
402
 
137
- const scene = document.scenes?.[document.scene ?? 0];
138
- if (!scene || !Array.isArray(scene.nodes) || scene.nodes.length === 0) {
139
- throw new Error("glTF demo asset must expose a default scene with at least one node.");
403
+ const scene = collectScenePrimitives(document, buffers);
404
+ const aggregatePositions = [];
405
+ const aggregateIndices = [];
406
+
407
+ for (const primitive of scene.primitives) {
408
+ const vertexOffset = aggregatePositions.length / 3;
409
+ aggregatePositions.push(...primitive.positions);
410
+ aggregateIndices.push(...primitive.indices.map((index) => index + vertexOffset));
140
411
  }
141
412
 
142
- const node = document.nodes[scene.nodes[0]];
143
- const mesh = document.meshes[node.mesh];
144
- const primitive = mesh.primitives[0];
145
- const positions = Array.from(readAccessor(document, primitive.attributes.POSITION, buffers));
146
- const indices = Array.from(readAccessor(document, primitive.indices, buffers));
413
+ const color = scene.primitives[0]?.material?.color ?? { r: 0.56, g: 0.33, b: 0.22, a: 1 };
147
414
 
148
415
  return Object.freeze({
149
- name: node.name ?? mesh.name ?? "gltf-model",
150
- positions,
151
- indices,
152
- bounds: computeBounds(positions),
153
- color: getMaterialColor(document, primitive),
154
- physics: Object.freeze({ ...(node.extras?.physics ?? {}) }),
416
+ name: scene.name,
417
+ positions: Object.freeze(aggregatePositions),
418
+ indices: Object.freeze(aggregateIndices),
419
+ bounds: computeBounds(aggregatePositions),
420
+ color: Object.freeze({ ...color }),
421
+ physics: scene.physics,
422
+ primitives: Object.freeze(scene.primitives),
155
423
  });
156
424
  }
425
+
426
+ function shouldRetryWithInlineShowcaseFallback(url, error) {
427
+ if (!shouldUseInlineShowcaseFallback(url)) {
428
+ return false;
429
+ }
430
+
431
+ return error instanceof TypeError || /^Failed to load glTF asset:/u.test(error.message);
432
+ }
433
+
434
+ export async function loadGltfModel(url) {
435
+ try {
436
+ const { document, baseUrl } = await loadGltfDocument(url);
437
+ return buildGltfModel(document, baseUrl);
438
+ } catch (error) {
439
+ if (!shouldRetryWithInlineShowcaseFallback(url, error)) {
440
+ throw error;
441
+ }
442
+
443
+ const { document, baseUrl } = await loadInlineShowcaseDocument();
444
+ return buildGltfModel(document, baseUrl);
445
+ }
446
+ }
package/src/i18n.js ADDED
@@ -0,0 +1,71 @@
1
+ import { gpuSharedEnGbTranslations } from "./translations/en-GB.js";
2
+
3
+ export const gpuSharedTranslationKeys = Object.freeze({
4
+ showcaseTitle: "gpuShared.showcase.title",
5
+ showcaseSubtitle: "gpuShared.showcase.subtitle",
6
+ statusBooting: "gpuShared.showcase.status.booting",
7
+ statusLive: "gpuShared.showcase.status.live",
8
+ detailsBooting: "gpuShared.showcase.details.booting",
9
+ detailsPhysics: "gpuShared.showcase.details.physics",
10
+ detailsRealistic: "gpuShared.showcase.details.realistic",
11
+ detailsLegacy: "gpuShared.showcase.details.legacy",
12
+ pause: "gpuShared.showcase.action.pause",
13
+ resume: "gpuShared.showcase.action.resume",
14
+ stressMode: "gpuShared.showcase.control.stressMode",
15
+ focus: "gpuShared.showcase.control.focus",
16
+ focusIntegrated: "gpuShared.showcase.focus.integrated",
17
+ focusLighting: "gpuShared.showcase.focus.lighting",
18
+ focusCloth: "gpuShared.showcase.focus.cloth",
19
+ focusFluid: "gpuShared.showcase.focus.fluid",
20
+ focusPhysics: "gpuShared.showcase.focus.physics",
21
+ focusPerformance: "gpuShared.showcase.focus.performance",
22
+ focusDebug: "gpuShared.showcase.focus.debug",
23
+ legendTitle: "gpuShared.showcase.legend.title",
24
+ legendShipMetadata: "gpuShared.showcase.legend.shipMetadata",
25
+ legendLighting: "gpuShared.showcase.legend.lighting",
26
+ legendCollisions: "gpuShared.showcase.legend.collisions",
27
+ sceneState: "gpuShared.showcase.section.sceneState",
28
+ qualityBudgets: "gpuShared.showcase.section.qualityBudgets",
29
+ debugTelemetry: "gpuShared.showcase.section.debugTelemetry",
30
+ notes: "gpuShared.showcase.section.notes",
31
+ noteAssetLoading: "gpuShared.showcase.note.assetLoading",
32
+ noteMoonlight: "gpuShared.showcase.note.moonlight",
33
+ noteContinuity: "gpuShared.showcase.note.continuity",
34
+ notePerformance: "gpuShared.showcase.note.performance",
35
+ notePhysicsSnapshots: "gpuShared.showcase.note.physicsSnapshots",
36
+ notePhysicsCollisions: "gpuShared.showcase.note.physicsCollisions",
37
+ notePhysicsLighting: "gpuShared.showcase.note.physicsLighting",
38
+ debugAdapterShowcase: "gpuShared.debug.adapter.showcase",
39
+ debugMainColorBuffer: "gpuShared.debug.allocation.mainColorBuffer",
40
+ debugShadowImpressionAtlas: "gpuShared.debug.allocation.shadowImpressionAtlas",
41
+ });
42
+
43
+ export const gpuSharedTranslations = Object.freeze({
44
+ "en-GB": gpuSharedEnGbTranslations,
45
+ });
46
+
47
+ function formatTranslation(template, args = {}) {
48
+ return template.replace(/\{([A-Za-z0-9_]+)\}/g, (match, name) => {
49
+ if (!Object.prototype.hasOwnProperty.call(args, name)) {
50
+ return match;
51
+ }
52
+
53
+ const value = args[name];
54
+ return value == null ? "" : String(value);
55
+ });
56
+ }
57
+
58
+ export function translateGpuSharedText(key, args, translate) {
59
+ const translated = translate?.(key, args);
60
+ if (translated && translated !== key) {
61
+ return translated;
62
+ }
63
+
64
+ const fallback = gpuSharedEnGbTranslations[key];
65
+ return fallback ? formatTranslation(fallback, args) : key;
66
+ }
67
+
68
+ export function createGpuSharedTranslator(translate) {
69
+ return (key, args) => translateGpuSharedText(key, args, translate);
70
+ }
71
+