@plasius/gpu-shared 0.1.13 → 0.1.14

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 +22 -0
  2. package/README.md +55 -3
  3. package/dist/{chunk-NCPJWLX3.js → chunk-2GM64LB6.js} +1 -9
  4. package/dist/{chunk-NCPJWLX3.js.map → chunk-2GM64LB6.js.map} +1 -1
  5. package/dist/{chunk-DABW627O.js → chunk-3ARPGHCQ.js} +8 -2
  6. package/dist/chunk-3ARPGHCQ.js.map +1 -0
  7. package/dist/chunk-4ZJ24VRS.js +402 -0
  8. package/dist/chunk-4ZJ24VRS.js.map +1 -0
  9. package/dist/{chunk-DQX4DXBR.js → chunk-W5GA3VA6.js} +79 -6
  10. package/dist/chunk-W5GA3VA6.js.map +1 -0
  11. package/dist/gltf-loader-YDPLZS5Q.js +8 -0
  12. package/dist/index.cjs +1230 -6198
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.js +31 -5
  15. package/dist/index.js.map +1 -1
  16. package/dist/product-studio-runtime-HDAUDWYO.js +11 -0
  17. package/dist/showcase-inline-assets-WT4PSNKI.js +7 -0
  18. package/dist/showcase-inline-assets-WT4PSNKI.js.map +1 -0
  19. package/dist/showcase-runtime-SNCUFSSC.js +3785 -0
  20. package/dist/showcase-runtime-SNCUFSSC.js.map +1 -0
  21. package/package.json +6 -8
  22. package/src/feature-flags.js +1 -0
  23. package/src/gltf-loader.js +10 -2
  24. package/src/index.d.ts +72 -1
  25. package/src/index.js +33 -0
  26. package/src/product-studio-runtime.js +465 -0
  27. package/src/showcase-runtime.js +875 -72
  28. package/dist/chunk-2FIFSBB4.js +0 -74
  29. package/dist/chunk-2FIFSBB4.js.map +0 -1
  30. package/dist/chunk-DABW627O.js.map +0 -1
  31. package/dist/chunk-DQX4DXBR.js.map +0 -1
  32. package/dist/gltf-loader-WAM23F37.js +0 -9
  33. package/dist/showcase-inline-assets-B7U7VX5H.js +0 -7
  34. package/dist/showcase-runtime-PN7N3FZY.js +0 -9164
  35. package/dist/showcase-runtime-PN7N3FZY.js.map +0 -1
  36. /package/dist/{gltf-loader-WAM23F37.js.map → gltf-loader-YDPLZS5Q.js.map} +0 -0
  37. /package/dist/{showcase-inline-assets-B7U7VX5H.js.map → product-studio-runtime-HDAUDWYO.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/gpu-shared",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
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,
@@ -33,7 +33,7 @@
33
33
  "demo": "python3 -m http.server --directory ..",
34
34
  "demo:example": "node demo/example.js",
35
35
  "generate:assets": "node scripts/generate-showcase-assets.mjs",
36
- "typecheck": "node --check src/index.js && node --check src/showcase-runtime.js && node --check src/gltf-loader.js && node --check demo/main.js",
36
+ "typecheck": "node --check src/index.js && node --check src/showcase-runtime.js && node --check src/gltf-loader.js && node --check src/product-studio-runtime.js && node --check demo/main.js",
37
37
  "audit:eslint": "eslint . --max-warnings=0",
38
38
  "audit:deps": "npm ls --all --omit=optional --omit=peer > /dev/null 2>&1 || true",
39
39
  "audit:npm": "npm audit --audit-level=high --omit=dev",
@@ -57,17 +57,15 @@
57
57
  "author": "Plasius LTD <development@plasius.co.uk>",
58
58
  "license": "Apache-2.0",
59
59
  "dependencies": {
60
- "@plasius/gpu-cloth": "^0.1.4",
61
- "@plasius/gpu-debug": "^0.1.5",
62
- "@plasius/gpu-fluid": "^0.1.4",
63
- "@plasius/gpu-lighting": "^0.1.16",
64
- "@plasius/gpu-performance": "^0.1.6",
65
- "@plasius/gpu-physics": "^0.1.13"
66
60
  },
67
61
  "peerDependencies": {
62
+ "@plasius/gpu-renderer": "^0.1.14",
68
63
  "@plasius/translations": "^1.0.17"
69
64
  },
70
65
  "peerDependenciesMeta": {
66
+ "@plasius/gpu-renderer": {
67
+ "optional": true
68
+ },
71
69
  "@plasius/translations": {
72
70
  "optional": true
73
71
  }
@@ -1 +1,2 @@
1
1
  export const GPU_SHOWCASE_REALISTIC_MODELS_FEATURE = "gpu_showcase_realistic_models_v1";
2
+ export const GPU_SHOWCASE_PRODUCT_STUDIO_FEATURE = "gpu_showcase_product_studio_wavefront_v1";
@@ -139,6 +139,12 @@ function computeBounds(positions) {
139
139
  });
140
140
  }
141
141
 
142
+ function appendValues(target, values) {
143
+ for (let index = 0; index < values.length; index += 1) {
144
+ target.push(values[index]);
145
+ }
146
+ }
147
+
142
148
  function resolveBrowserRequestBaseUrl() {
143
149
  if (
144
150
  typeof document !== "undefined" &&
@@ -406,8 +412,10 @@ async function buildGltfModel(document, baseUrl) {
406
412
 
407
413
  for (const primitive of scene.primitives) {
408
414
  const vertexOffset = aggregatePositions.length / 3;
409
- aggregatePositions.push(...primitive.positions);
410
- aggregateIndices.push(...primitive.indices.map((index) => index + vertexOffset));
415
+ appendValues(aggregatePositions, primitive.positions);
416
+ for (const index of primitive.indices) {
417
+ aggregateIndices.push(index + vertexOffset);
418
+ }
411
419
  }
412
420
 
413
421
  const color = scene.primitives[0]?.material?.color ?? { r: 0.56, g: 0.33, b: 0.22, a: 1 };
package/src/index.d.ts CHANGED
@@ -57,6 +57,23 @@ export type ShowcaseFocusMode =
57
57
  | "performance"
58
58
  | "debug";
59
59
 
60
+ export type ShowcaseDemoMode = "harbor" | "product-studio" | "product" | "studio" | "eames";
61
+
62
+ export interface ProductStudioMesh {
63
+ readonly id: number;
64
+ readonly positions: readonly number[];
65
+ readonly indices: readonly number[];
66
+ readonly normals?: readonly number[] | null;
67
+ readonly uvs?: readonly number[] | null;
68
+ readonly color: readonly number[];
69
+ readonly emission?: readonly number[];
70
+ readonly materialKind: string | number;
71
+ readonly materialRefId?: number;
72
+ readonly roughness?: number;
73
+ readonly metallic?: number;
74
+ readonly opacity?: number;
75
+ }
76
+
60
77
  export type GpuSharedTranslationValue =
61
78
  | string
62
79
  | number
@@ -137,7 +154,17 @@ export function createGpuSharedTranslator(
137
154
  ): (key: GpuSharedTranslationKey, args?: GpuSharedTranslationArgs) => string;
138
155
 
139
156
  export interface MountGpuShowcaseOptions {
157
+ __showcaseFeatureLoaders?: {
158
+ cloth?: () => Promise<unknown>;
159
+ fluid?: () => Promise<unknown>;
160
+ lighting?: () => Promise<unknown>;
161
+ performance?: () => Promise<unknown>;
162
+ debug?: () => Promise<unknown>;
163
+ physics?: () => Promise<unknown>;
164
+ };
140
165
  root?: HTMLElement;
166
+ demoMode?: ShowcaseDemoMode;
167
+ mode?: ShowcaseDemoMode;
141
168
  focus?: ShowcaseFocusMode | string;
142
169
  packageName?: string;
143
170
  title?: string;
@@ -145,6 +172,16 @@ export interface MountGpuShowcaseOptions {
145
172
  translate?: GpuSharedTranslate;
146
173
  captureMode?: boolean;
147
174
  renderScale?: number;
175
+ productAssetUrl?: string | URL;
176
+ assetUrl?: string | URL;
177
+ width?: number;
178
+ height?: number;
179
+ maxDepth?: number;
180
+ tileSize?: number;
181
+ samplesPerPixel?: number;
182
+ denoise?: boolean;
183
+ lightingPreset?: string;
184
+ lightingIntensity?: number;
148
185
  createState?: () => unknown;
149
186
  updateState?: (state: unknown, scene: Record<string, unknown>, dt: number) => unknown;
150
187
  describeState?: (state: unknown, scene: Record<string, unknown>) => Record<string, unknown> | null;
@@ -158,7 +195,29 @@ export interface MountGpuShowcaseResult {
158
195
  destroy(): void;
159
196
  }
160
197
 
198
+ export interface MountGpuProductStudioResult {
199
+ readonly state: Readonly<{
200
+ featureFlags: unknown;
201
+ modelName: string;
202
+ sourceTriangleCount: number;
203
+ meshCount: number;
204
+ geometryMode: string;
205
+ requiresTriangleMeshRenderer: boolean;
206
+ displayQuality: boolean;
207
+ requiresMeshBvhForDisplayQuality: boolean;
208
+ rendererStats: Record<string, unknown>;
209
+ }>;
210
+ readonly model: GltfModel;
211
+ readonly canvas: HTMLCanvasElement;
212
+ readonly renderer: unknown;
213
+ readonly meshes: readonly ProductStudioMesh[];
214
+ destroy(): void;
215
+ }
216
+
161
217
  export const showcaseFocusModes: readonly ShowcaseFocusMode[];
218
+ export const showcaseDemoModes: readonly ShowcaseDemoMode[];
219
+ export const GPU_SHOWCASE_REALISTIC_MODELS_FEATURE: "gpu_showcase_realistic_models_v1";
220
+ export const GPU_SHOWCASE_PRODUCT_STUDIO_FEATURE: "gpu_showcase_product_studio_wavefront_v1";
162
221
 
163
222
  export function resolveShowcaseAssetUrl(
164
223
  baseUrlOrAssetName?: string | URL | ShowcaseAssetName,
@@ -167,6 +226,18 @@ export function resolveShowcaseAssetUrl(
167
226
 
168
227
  export function loadGltfModel(url: string | URL): Promise<GltfModel>;
169
228
 
229
+ export function createProductStudioMeshes(
230
+ model: GltfModel,
231
+ options?: {
232
+ targetCenter?: readonly number[];
233
+ targetSize?: number;
234
+ }
235
+ ): readonly ProductStudioMesh[];
236
+
237
+ export function mountGpuProductStudio(
238
+ options?: MountGpuShowcaseOptions
239
+ ): Promise<MountGpuProductStudioResult>;
240
+
170
241
  export function mountGpuShowcase(
171
242
  options?: MountGpuShowcaseOptions
172
- ): Promise<MountGpuShowcaseResult>;
243
+ ): Promise<MountGpuShowcaseResult | MountGpuProductStudioResult>;
package/src/index.js CHANGED
@@ -6,6 +6,14 @@ export {
6
6
  translateGpuSharedText,
7
7
  } from "./i18n.js";
8
8
  export { gpuSharedEnGbTranslations } from "./translations/en-GB.js";
9
+ export {
10
+ GPU_SHOWCASE_PRODUCT_STUDIO_FEATURE,
11
+ GPU_SHOWCASE_REALISTIC_MODELS_FEATURE,
12
+ } from "./feature-flags.js";
13
+ export {
14
+ createProductStudioMeshes,
15
+ mountGpuProductStudio,
16
+ } from "./product-studio-runtime.js";
9
17
 
10
18
  export const showcaseFocusModes = Object.freeze([
11
19
  "integrated",
@@ -16,6 +24,7 @@ export const showcaseFocusModes = Object.freeze([
16
24
  "performance",
17
25
  "debug",
18
26
  ]);
27
+ export const showcaseDemoModes = Object.freeze(["harbor", "product-studio"]);
19
28
 
20
29
  export async function loadGltfModel(url) {
21
30
  const module = await import("./gltf-loader.js");
@@ -23,6 +32,29 @@ export async function loadGltfModel(url) {
23
32
  }
24
33
 
25
34
  export async function mountGpuShowcase(options = {}) {
35
+ const demoMode = options.demoMode ?? options.mode;
36
+ if (
37
+ demoMode === "product-studio" ||
38
+ demoMode === "product" ||
39
+ demoMode === "studio" ||
40
+ demoMode === "eames"
41
+ ) {
42
+ const productRuntimeLoader =
43
+ typeof options.__productRuntimeLoader === "function"
44
+ ? options.__productRuntimeLoader
45
+ : () => import("./product-studio-runtime.js");
46
+ const productModule = await productRuntimeLoader();
47
+ if (typeof productModule.mountGpuProductStudio !== "function") {
48
+ throw new Error("product runtime loader must provide mountGpuProductStudio.");
49
+ }
50
+
51
+ const productOptions = { ...options, demoMode };
52
+ delete productOptions.__runtimeLoader;
53
+ delete productOptions.__productRuntimeLoader;
54
+ delete productOptions.__featureFlags;
55
+ return productModule.mountGpuProductStudio(productOptions, options.__featureFlags);
56
+ }
57
+
26
58
  const runtimeLoader =
27
59
  typeof options.__runtimeLoader === "function"
28
60
  ? options.__runtimeLoader
@@ -34,6 +66,7 @@ export async function mountGpuShowcase(options = {}) {
34
66
 
35
67
  const publicOptions = { ...options };
36
68
  delete publicOptions.__runtimeLoader;
69
+ delete publicOptions.__productRuntimeLoader;
37
70
  delete publicOptions.__featureFlags;
38
71
  return module.mountGpuShowcase(publicOptions, options.__featureFlags);
39
72
  }
@@ -0,0 +1,465 @@
1
+ import { loadGltfModel } from "./gltf-loader.js";
2
+
3
+ const STYLE_ID = "plasius-product-studio-wavefront-style";
4
+ const DEFAULT_PRODUCT_ASSET_URL =
5
+ "/data/models/eames-lounge-chair-ottoman/Eames_Lounge_Chair_Ottoman.gltf";
6
+ const DEFAULT_TARGET_CENTER = Object.freeze([0, 0.74, 0]);
7
+ const DEFAULT_TARGET_SIZE = 2.25;
8
+
9
+ function clamp(value, min, max) {
10
+ return Math.max(min, Math.min(max, value));
11
+ }
12
+
13
+ function isFiniteVector(value) {
14
+ return (
15
+ Array.isArray(value) &&
16
+ value.length >= 3 &&
17
+ Number.isFinite(value[0]) &&
18
+ Number.isFinite(value[1]) &&
19
+ Number.isFinite(value[2])
20
+ );
21
+ }
22
+
23
+ function createEmptyBounds() {
24
+ return {
25
+ min: [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
26
+ max: [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
27
+ };
28
+ }
29
+
30
+ function expandBounds(bounds, point) {
31
+ bounds.min[0] = Math.min(bounds.min[0], point[0]);
32
+ bounds.min[1] = Math.min(bounds.min[1], point[1]);
33
+ bounds.min[2] = Math.min(bounds.min[2], point[2]);
34
+ bounds.max[0] = Math.max(bounds.max[0], point[0]);
35
+ bounds.max[1] = Math.max(bounds.max[1], point[1]);
36
+ bounds.max[2] = Math.max(bounds.max[2], point[2]);
37
+ }
38
+
39
+ function getBoundsSize(bounds) {
40
+ return [
41
+ bounds.max[0] - bounds.min[0],
42
+ bounds.max[1] - bounds.min[1],
43
+ bounds.max[2] - bounds.min[2],
44
+ ];
45
+ }
46
+
47
+ function getBoundsCenter(bounds) {
48
+ return [
49
+ (bounds.min[0] + bounds.max[0]) * 0.5,
50
+ (bounds.min[1] + bounds.max[1]) * 0.5,
51
+ (bounds.min[2] + bounds.max[2]) * 0.5,
52
+ ];
53
+ }
54
+
55
+ function getModelBounds(model) {
56
+ if (isFiniteVector(model?.bounds?.min) && isFiniteVector(model?.bounds?.max)) {
57
+ return {
58
+ min: [...model.bounds.min],
59
+ max: [...model.bounds.max],
60
+ };
61
+ }
62
+
63
+ const bounds = createEmptyBounds();
64
+ for (const primitive of model?.primitives ?? []) {
65
+ for (let index = 0; index < primitive.positions.length; index += 3) {
66
+ expandBounds(bounds, [
67
+ primitive.positions[index],
68
+ primitive.positions[index + 1],
69
+ primitive.positions[index + 2],
70
+ ]);
71
+ }
72
+ }
73
+ return bounds;
74
+ }
75
+
76
+ function transformPoint(point, modelCenter, scale, targetCenter) {
77
+ return [
78
+ (point[0] - modelCenter[0]) * scale + targetCenter[0],
79
+ (point[1] - modelCenter[1]) * scale + targetCenter[1],
80
+ (point[2] - modelCenter[2]) * scale + targetCenter[2],
81
+ ];
82
+ }
83
+
84
+ function readMaterialColor(material) {
85
+ const color = material?.color ?? {};
86
+ return [
87
+ Number.isFinite(color.r) ? color.r : 0.62,
88
+ Number.isFinite(color.g) ? color.g : 0.56,
89
+ Number.isFinite(color.b) ? color.b : 0.48,
90
+ Number.isFinite(color.a) ? color.a : 1,
91
+ ];
92
+ }
93
+
94
+ function readMaterialKind(material) {
95
+ const emissive = material?.emissive ?? {};
96
+ if ((emissive.r ?? 0) + (emissive.g ?? 0) + (emissive.b ?? 0) > 0.001) {
97
+ return "emissive";
98
+ }
99
+ if ((material?.metallic ?? 0) >= 0.5) {
100
+ return "metal";
101
+ }
102
+ const alpha = readMaterialColor(material)[3];
103
+ if (alpha < 0.9) {
104
+ return "transparent";
105
+ }
106
+ return "diffuse";
107
+ }
108
+
109
+ function readEmission(material) {
110
+ const emissive = material?.emissive ?? {};
111
+ return [
112
+ Number.isFinite(emissive.r) ? emissive.r : 0,
113
+ Number.isFinite(emissive.g) ? emissive.g : 0,
114
+ Number.isFinite(emissive.b) ? emissive.b : 0,
115
+ 1,
116
+ ];
117
+ }
118
+
119
+ function createQuadMesh({
120
+ id,
121
+ corners,
122
+ color,
123
+ emission = [0, 0, 0, 1],
124
+ materialKind = "diffuse",
125
+ roughness = 0.72,
126
+ metallic = 0,
127
+ opacity = color[3] ?? 1,
128
+ }) {
129
+ const [a, b, c] = corners;
130
+ const edge1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
131
+ const edge2 = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
132
+ const normal = [
133
+ edge1[1] * edge2[2] - edge1[2] * edge2[1],
134
+ edge1[2] * edge2[0] - edge1[0] * edge2[2],
135
+ edge1[0] * edge2[1] - edge1[1] * edge2[0],
136
+ ];
137
+ const length = Math.hypot(normal[0], normal[1], normal[2]) || 1;
138
+ const unitNormal = normal.map((value) => value / length);
139
+
140
+ return Object.freeze({
141
+ id,
142
+ positions: Object.freeze(corners.flat()),
143
+ indices: Object.freeze([0, 1, 2, 0, 2, 3]),
144
+ normals: Object.freeze([unitNormal, unitNormal, unitNormal, unitNormal].flat()),
145
+ color: Object.freeze([...color]),
146
+ emission: Object.freeze([...emission]),
147
+ materialKind,
148
+ roughness,
149
+ metallic,
150
+ opacity,
151
+ });
152
+ }
153
+
154
+ function createProductStudioEnvironmentMeshes() {
155
+ return [
156
+ createQuadMesh({
157
+ id: 1,
158
+ corners: [
159
+ [-3.2, -0.08, 2.4],
160
+ [3.2, -0.08, 2.4],
161
+ [3.2, -0.08, -3.1],
162
+ [-3.2, -0.08, -3.1],
163
+ ],
164
+ color: [0.48, 0.55, 0.55, 1],
165
+ roughness: 0.82,
166
+ }),
167
+ createQuadMesh({
168
+ id: 2,
169
+ corners: [
170
+ [-3.2, -0.08, -2.45],
171
+ [3.2, -0.08, -2.45],
172
+ [3.2, 2.65, -2.45],
173
+ [-3.2, 2.65, -2.45],
174
+ ],
175
+ color: [0.43, 0.42, 0.38, 1],
176
+ roughness: 0.86,
177
+ }),
178
+ createQuadMesh({
179
+ id: 3,
180
+ corners: [
181
+ [-2.85, -0.08, -2.45],
182
+ [-2.85, 2.55, -2.45],
183
+ [-2.85, 2.55, 2.15],
184
+ [-2.85, -0.08, 2.15],
185
+ ],
186
+ color: [0.36, 0.42, 0.45, 1],
187
+ roughness: 0.8,
188
+ }),
189
+ createQuadMesh({
190
+ id: 4,
191
+ corners: [
192
+ [0.78, 2.55, -0.82],
193
+ [-0.78, 2.55, -0.82],
194
+ [-0.78, 2.55, -1.78],
195
+ [0.78, 2.55, -1.78],
196
+ ],
197
+ color: [1, 0.94, 0.78, 1],
198
+ emission: [8.5, 7.2, 4.8, 1],
199
+ materialKind: "emissive",
200
+ roughness: 0,
201
+ }),
202
+ ];
203
+ }
204
+
205
+ function createProductStudioMeshFromPrimitive(primitive, primitiveIndex, transform) {
206
+ if (!Array.isArray(primitive?.positions) || primitive.positions.length < 9) {
207
+ return null;
208
+ }
209
+
210
+ const positions = [];
211
+ for (let index = 0; index < primitive.positions.length; index += 3) {
212
+ const point = transform([
213
+ primitive.positions[index],
214
+ primitive.positions[index + 1],
215
+ primitive.positions[index + 2],
216
+ ]);
217
+ positions.push(point[0], point[1], point[2]);
218
+ }
219
+
220
+ const indices =
221
+ Array.isArray(primitive.indices) && primitive.indices.length >= 3
222
+ ? [...primitive.indices]
223
+ : Array.from({ length: positions.length / 3 }, (_, index) => index);
224
+ const material = primitive.material ?? {};
225
+ const color = readMaterialColor(material);
226
+
227
+ return Object.freeze({
228
+ id: 1000 + primitiveIndex,
229
+ positions: Object.freeze(positions),
230
+ indices: Object.freeze(indices),
231
+ normals: Array.isArray(primitive.normals) ? Object.freeze([...primitive.normals]) : null,
232
+ color: Object.freeze(color),
233
+ emission: Object.freeze(readEmission(material)),
234
+ materialKind: readMaterialKind(material),
235
+ materialRefId: 1000 + primitiveIndex,
236
+ roughness: Number.isFinite(material.roughness) ? material.roughness : 0.72,
237
+ metallic: Number.isFinite(material.metallic) ? material.metallic : 0,
238
+ opacity: color[3],
239
+ });
240
+ }
241
+
242
+ export function createProductStudioMeshes(model, options = {}) {
243
+ const primitives = Array.isArray(model?.primitives) ? model.primitives : [];
244
+ if (primitives.length === 0) {
245
+ throw new Error("Product Studio model must contain at least one renderable primitive.");
246
+ }
247
+
248
+ const targetCenter = isFiniteVector(options.targetCenter)
249
+ ? [...options.targetCenter]
250
+ : [...DEFAULT_TARGET_CENTER];
251
+ const targetSize = Number.isFinite(options.targetSize)
252
+ ? Math.max(options.targetSize, 0.25)
253
+ : DEFAULT_TARGET_SIZE;
254
+ const modelBounds = getModelBounds(model);
255
+ const modelSize = getBoundsSize(modelBounds);
256
+ const modelCenter = getBoundsCenter(modelBounds);
257
+ const scale = targetSize / Math.max(modelSize[0], modelSize[1], modelSize[2], 0.000001);
258
+ const transform = (point) => transformPoint(point, modelCenter, scale, targetCenter);
259
+ const modelMeshes = primitives
260
+ .map((primitive, index) => createProductStudioMeshFromPrimitive(primitive, index, transform))
261
+ .filter(Boolean);
262
+
263
+ return Object.freeze([...createProductStudioEnvironmentMeshes(), ...modelMeshes]);
264
+ }
265
+
266
+ function ensureStyles(documentRef) {
267
+ if (documentRef.getElementById?.(STYLE_ID)) {
268
+ return;
269
+ }
270
+ const style = documentRef.createElement("style");
271
+ style.id = STYLE_ID;
272
+ style.textContent = `
273
+ .plasius-product-studio-wavefront {
274
+ position: relative;
275
+ width: 100%;
276
+ min-height: 420px;
277
+ overflow: hidden;
278
+ background: #0f1418;
279
+ display: grid;
280
+ place-items: center;
281
+ }
282
+
283
+ .plasius-product-studio-wavefront canvas {
284
+ display: block;
285
+ width: 100%;
286
+ height: auto;
287
+ max-height: 100%;
288
+ aspect-ratio: 16 / 9;
289
+ min-height: 420px;
290
+ object-fit: contain;
291
+ }
292
+ `;
293
+ documentRef.head?.appendChild?.(style);
294
+ }
295
+
296
+ function resolveRoot(options) {
297
+ const documentRef = options.document ?? globalThis.document;
298
+ if (options.root) {
299
+ return options.root;
300
+ }
301
+ const root =
302
+ documentRef?.querySelector?.("[data-plasius-gpu-product-studio]") ?? documentRef?.body;
303
+ if (!root) {
304
+ throw new Error("Product Studio requires a DOM root.");
305
+ }
306
+ return root;
307
+ }
308
+
309
+ function resolveRenderSize(root, options) {
310
+ const rect = root.getBoundingClientRect?.() ?? { width: 1280, height: 720 };
311
+ const devicePixelRatio =
312
+ Number.isFinite(options.devicePixelRatio)
313
+ ? options.devicePixelRatio
314
+ : Number.isFinite(globalThis.window?.devicePixelRatio)
315
+ ? globalThis.window.devicePixelRatio
316
+ : 1;
317
+ const cssWidth = Number.isFinite(rect.width) && rect.width > 0 ? rect.width : 1280;
318
+ const cssHeight =
319
+ Number.isFinite(rect.height) && rect.height > 0 ? rect.height : cssWidth * (9 / 16);
320
+ const width = Number.isFinite(options.width)
321
+ ? Math.trunc(options.width)
322
+ : clamp(Math.round(cssWidth * devicePixelRatio), 640, 1920);
323
+ const height = Number.isFinite(options.height)
324
+ ? Math.trunc(options.height)
325
+ : clamp(Math.round(cssHeight * devicePixelRatio), 360, 1080);
326
+
327
+ return {
328
+ width,
329
+ height,
330
+ };
331
+ }
332
+
333
+ function installSnapshotHook(state) {
334
+ if (typeof globalThis.window === "undefined") {
335
+ return () => {};
336
+ }
337
+ const previous = globalThis.window.render_game_to_text;
338
+ globalThis.window.render_game_to_text = () =>
339
+ JSON.stringify({
340
+ surface: "gpu-product-studio-wavefront",
341
+ model: state.modelName,
342
+ sourceTriangles: state.sourceTriangleCount,
343
+ meshCount: state.meshCount,
344
+ geometryMode: state.geometryMode,
345
+ requiresTriangleMeshRenderer: state.requiresTriangleMeshRenderer,
346
+ displayQuality: state.displayQuality,
347
+ requiresMeshBvhForDisplayQuality: state.requiresMeshBvhForDisplayQuality,
348
+ renderer: state.rendererStats,
349
+ });
350
+ return () => {
351
+ if (previous === undefined) {
352
+ delete globalThis.window.render_game_to_text;
353
+ } else {
354
+ globalThis.window.render_game_to_text = previous;
355
+ }
356
+ };
357
+ }
358
+
359
+ function countSourceTriangles(model) {
360
+ return (model.primitives ?? []).reduce(
361
+ (total, primitive) => total + Math.floor((primitive.indices?.length ?? 0) / 3),
362
+ 0
363
+ );
364
+ }
365
+
366
+ async function resolveWavefrontLightingOptions(options) {
367
+ const fallback = {
368
+ environmentColor: [0.35, 0.43, 0.49, 1],
369
+ ambientColor: [0.02, 0.024, 0.028, 1],
370
+ };
371
+ const lightingLoader =
372
+ typeof options.__lightingLoader === "function"
373
+ ? options.__lightingLoader
374
+ : () => import("@plasius/gpu-lighting").catch(() => null);
375
+ const lightingModule = await lightingLoader();
376
+
377
+ if (
378
+ typeof lightingModule?.createWavefrontEnvironmentLightingOptions !== "function"
379
+ ) {
380
+ return fallback;
381
+ }
382
+
383
+ return lightingModule.createWavefrontEnvironmentLightingOptions({
384
+ preset: options.lightingPreset ?? "product-studio",
385
+ intensity: options.lightingIntensity,
386
+ });
387
+ }
388
+
389
+ export async function mountGpuProductStudio(options = {}, featureFlags = null) {
390
+ const root = resolveRoot(options);
391
+ const documentRef = options.document ?? root.ownerDocument ?? globalThis.document;
392
+ ensureStyles(documentRef);
393
+ const previousMarkup = root.innerHTML;
394
+ root.innerHTML = "";
395
+ root.classList?.add?.("plasius-product-studio-wavefront");
396
+
397
+ const canvas = documentRef.createElement("canvas");
398
+ canvas.dataset.plasiusGpuProductStudio = "wavefront";
399
+ root.appendChild(canvas);
400
+
401
+ const modelLoader =
402
+ typeof options.__modelLoader === "function" ? options.__modelLoader : loadGltfModel;
403
+ const rendererLoader =
404
+ typeof options.__rendererLoader === "function"
405
+ ? options.__rendererLoader
406
+ : () => import("@plasius/gpu-renderer");
407
+ const assetUrl = options.productAssetUrl ?? options.assetUrl ?? DEFAULT_PRODUCT_ASSET_URL;
408
+ const model = await modelLoader(assetUrl);
409
+ const meshes = createProductStudioMeshes(model, {
410
+ targetCenter: options.targetCenter,
411
+ targetSize: options.targetSize,
412
+ });
413
+ const rendererModule = await rendererLoader();
414
+ if (typeof rendererModule.createWavefrontPathTracingComputeRenderer !== "function") {
415
+ throw new Error("Product Studio renderer loader must provide createWavefrontPathTracingComputeRenderer.");
416
+ }
417
+
418
+ const size = resolveRenderSize(root, options);
419
+ const lightingOptions = await resolveWavefrontLightingOptions(options);
420
+ const renderer = await rendererModule.createWavefrontPathTracingComputeRenderer({
421
+ canvas,
422
+ width: size.width,
423
+ height: size.height,
424
+ maxDepth: Number.isFinite(options.maxDepth) ? options.maxDepth : 6,
425
+ tileSize: Number.isFinite(options.tileSize) ? options.tileSize : 128,
426
+ samplesPerPixel: Number.isFinite(options.samplesPerPixel) ? options.samplesPerPixel : 8,
427
+ denoise: options.denoise !== false,
428
+ displayQuality: true,
429
+ meshes,
430
+ camera: {
431
+ position: [0, 1.12, 5.05],
432
+ target: [0, 0.72, 0],
433
+ up: [0, 1, 0],
434
+ fovYDegrees: 43,
435
+ },
436
+ ...lightingOptions,
437
+ });
438
+ const rendererStats = renderer.renderOnce();
439
+ const state = Object.freeze({
440
+ featureFlags,
441
+ modelName: model.name,
442
+ sourceTriangleCount: countSourceTriangles(model),
443
+ meshCount: meshes.length,
444
+ geometryMode: "mesh-bvh-display-quality",
445
+ requiresTriangleMeshRenderer: true,
446
+ displayQuality: true,
447
+ requiresMeshBvhForDisplayQuality: true,
448
+ rendererStats,
449
+ });
450
+ const restoreSnapshotHook = installSnapshotHook(state);
451
+
452
+ return Object.freeze({
453
+ state,
454
+ model,
455
+ canvas,
456
+ renderer,
457
+ meshes,
458
+ destroy() {
459
+ restoreSnapshotHook();
460
+ renderer.destroy();
461
+ root.classList?.remove?.("plasius-product-studio-wavefront");
462
+ root.innerHTML = previousMarkup;
463
+ },
464
+ });
465
+ }