MikaFil commited on
Commit
b42c09b
·
verified ·
1 Parent(s): f7e19c7

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +197 -285
viewer.js CHANGED
@@ -1,6 +1,6 @@
1
  // viewer.js
2
  // ==============================
3
- // viewer.js
4
  // ==============================
5
 
6
  let pc; // will hold the PlayCanvas module once imported
@@ -21,8 +21,6 @@ let plyUrl, glbUrl;
21
  export async function initializeViewer(config, instanceId) {
22
  if (viewerInitialized) return;
23
 
24
- alert("Viewer init starting...");
25
-
26
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
27
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
28
 
@@ -33,25 +31,25 @@ export async function initializeViewer(config, instanceId) {
33
  maxZoom = parseFloat(config.maxZoom || "20");
34
  minAngle = parseFloat(config.minAngle || "-45");
35
  maxAngle = parseFloat(config.maxAngle || "90");
36
- minAzimuth = (config.minAzimuth !== undefined) ? parseFloat(config.minAzimuth) : -360;
37
- maxAzimuth = (config.maxAzimuth !== undefined) ? parseFloat(config.maxAzimuth) : 360;
38
  minPivotY = parseFloat(config.minPivotY || "0");
39
- minY = (config.minY !== undefined) ? parseFloat(config.minY) : 0;
40
-
41
- modelX = (config.modelX !== undefined) ? parseFloat(config.modelX) : 0;
42
- modelY = (config.modelY !== undefined) ? parseFloat(config.modelY) : 0;
43
- modelZ = (config.modelZ !== undefined) ? parseFloat(config.modelZ) : 0;
44
- modelScale = (config.modelScale !== undefined) ? parseFloat(config.modelScale) : 1;
45
- modelRotationX = (config.modelRotationX !== undefined) ? parseFloat(config.modelRotationX) : 0;
46
- modelRotationY = (config.modelRotationY !== undefined) ? parseFloat(config.modelRotationY) : 0;
47
- modelRotationZ = (config.modelRotationZ !== undefined) ? parseFloat(config.modelRotationZ) : 0;
48
-
49
- const cameraX = (config.cameraX !== undefined) ? parseFloat(config.cameraX) : 0;
50
- const cameraY = (config.cameraY !== undefined) ? parseFloat(config.cameraY) : 2;
51
- const cameraZ = (config.cameraZ !== undefined) ? parseFloat(config.cameraZ) : 5;
52
- const cameraXPhone = (config.cameraXPhone !== undefined) ? parseFloat(config.cameraXPhone) : cameraX;
53
- const cameraYPhone = (config.cameraYPhone !== undefined) ? parseFloat(config.cameraYPhone) : cameraY;
54
- const cameraZPhone = (config.cameraZPhone !== undefined) ? parseFloat(config.cameraZPhone) : (cameraZ * 1.5);
55
 
56
  chosenCameraX = isMobile ? cameraXPhone : cameraX;
57
  chosenCameraY = isMobile ? cameraYPhone : cameraY;
@@ -76,7 +74,7 @@ export async function initializeViewer(config, instanceId) {
76
  canvas.addEventListener('wheel', e => {
77
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
78
  const orbitCam = cameraEntity.script.orbitCamera;
79
- const sens = (cameraEntity.script.orbitCameraInputMouse?.distanceSensitivity) || 0.4;
80
  if (cameraEntity.camera.projection === pc.PROJECTION_PERSPECTIVE) {
81
  orbitCam.distance -= e.deltaY * 0.01 * sens * (orbitCam.distance * 0.1);
82
  } else {
@@ -95,294 +93,208 @@ export async function initializeViewer(config, instanceId) {
95
  window.pc = pc;
96
  }
97
 
98
- try {
99
- // 6. Setup device & app ----------------------------------------------------
100
- // --- NEW: robust WebGL-2 → WebGL-1 fallback (fixes iPhone blank screen) ---
101
- async function getGraphicsDevice(preferWebgl2 = true) {
102
- const baseOpts = {
103
- glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
104
- twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
105
- antialias: false
106
- };
107
- try {
108
- const dev = await pc.createGraphicsDevice(canvas, {
109
- ...baseOpts,
110
- deviceTypes: preferWebgl2 ? ["webgl2"] : ["webgl1"]
111
- });
112
- alert(preferWebgl2 ? "WebGL2 context OK" : "WebGL1 context OK");
113
- return dev;
114
- } catch (err) {
115
- if (preferWebgl2) {
116
- console.warn("[viewer] WebGL2 unavailable, retrying with WebGL1 …", err);
117
- alert("WebGL2 unavailable on this device; falling back to WebGL1");
118
- return getGraphicsDevice(false);
119
- }
120
- throw err;
121
  }
 
122
  }
 
123
 
124
- const device = await getGraphicsDevice(true); // first try WebGL2
125
- device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
126
-
127
- alert("Graphics device initialized; creating App…");
128
-
129
- const opts = new pc.AppOptions();
130
- opts.graphicsDevice = device;
131
- opts.mouse = new pc.Mouse(canvas);
132
- opts.touch = new pc.TouchDevice(canvas);
133
- opts.componentSystems = [
134
- pc.RenderComponentSystem,
135
- pc.CameraComponentSystem,
136
- pc.LightComponentSystem,
137
- pc.ScriptComponentSystem,
138
- pc.GSplatComponentSystem,
139
- pc.CollisionComponentSystem,
140
- pc.RigidbodyComponentSystem
141
- ];
142
- opts.resourceHandlers = [
143
- pc.TextureHandler,
144
- pc.ContainerHandler,
145
- pc.ScriptHandler,
146
- pc.GSplatHandler
147
- ];
148
-
149
- app = new pc.Application(canvas, opts);
150
- app.setCanvasFillMode(pc.FILLMODE_NONE);
151
- app.setCanvasResolution(pc.RESOLUTION_AUTO);
152
-
153
- // 7. Resize observer -------------------------------------------------------
154
- resizeObserver = new ResizeObserver(entries => {
155
- for (const entry of entries) {
156
- const { width, height } = entry.contentRect;
157
- if (app) {
158
- app.resizeCanvas(width, height);
159
- }
160
- }
161
- });
162
- resizeObserver.observe(viewerContainer);
163
-
164
- window.addEventListener('resize', () => {
165
- if (app) app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
166
- });
167
- app.on('destroy', () => {
168
- window.removeEventListener('resize', resizeCanvas);
169
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
- // 8. Assets ----------------------------------------------------------------
172
- const assets = {
173
- model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
174
- orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
175
- galerie: new pc.Asset('galerie', 'container', { url: glbUrl }),
176
- hdr: new pc.Asset('hdr', 'texture', {
177
- url: `https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png`
178
- }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
179
- };
180
 
181
- const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
182
- let lastProg = 0;
 
 
183
 
184
- assets.model.on('load', () => {
185
- alert("PLY model asset loaded successfully");
186
- progressDialog.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  });
188
- assets.model.on('error', err => {
189
- alert("Error loading PLY file: " + err);
190
- console.error("Error loading PLY file:", err);
191
- progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;
 
 
 
 
 
 
 
 
 
192
  });
193
-
194
- const progCheck = setInterval(() => {
195
- if (assets.model.resource) {
196
- progressIndicator.value = 100;
197
- clearInterval(progCheck);
198
- progressDialog.style.display = 'none';
199
- } else if (assets.model.loading) {
200
- lastProg = Math.min(lastProg + 2, 90);
201
- progressIndicator.value = lastProg;
 
 
 
 
 
 
 
202
  }
203
- }, 100);
204
-
205
- alert("Starting asset loader…");
206
- loader.load(async () => {
207
- alert("All assets loaded; starting app…");
208
- app.start();
209
- app.scene.envAtlas = assets.hdr.resource;
210
-
211
- // 9. Scene ----------------------------------------------------------------
212
- modelEntity = new pc.Entity('model');
213
- modelEntity.addComponent('gsplat', { asset: assets.model });
214
- modelEntity.setLocalPosition(modelX, modelY, modelZ);
215
- modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
216
- modelEntity.setLocalScale(modelScale, modelScale, modelScale);
217
- app.root.addChild(modelEntity);
218
-
219
- const dirLight = new pc.Entity('Cascaded Light');
220
- dirLight.addComponent('light', {
221
- type: 'directional',
222
- color: pc.Color.WHITE,
223
- shadowBias: 0.3,
224
- normalOffsetBias: 0.2,
225
- intensity: 1.0,
226
- soft: true,
227
- shadowResolution: 4096,
228
- penumbraSize: 7,
229
- penumbraFalloff: 1.5,
230
- shadowSamples: 128,
231
- shadowBlockerSamples: 16,
232
- castShadows: true,
233
- shadowType: pc.SHADOW_PCSS_32F,
234
- shadowDistance: 1000
235
- });
236
- dirLight.setLocalEulerAngles(0, 0, 0);
237
- app.root.addChild(dirLight);
238
-
239
- const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
240
- app.root.addChild(galleryEntity);
241
-
242
- // 10. Camera --------------------------------------------------------------
243
- cameraEntity = new pc.Entity('camera');
244
- cameraEntity.addComponent('camera', {
245
- clearColor: config.canvas_background
246
- ? parseInt(config.canvas_background.substr(1, 2), 16) / 255
247
- : 0,
248
- });
249
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
250
- cameraEntity.lookAt(modelEntity.getPosition());
251
-
252
- cameraEntity.addComponent('script');
253
- cameraEntity.script.create('orbitCamera', {
254
- attributes: {
255
- inertiaFactor: 0.2,
256
- focusEntity: modelEntity,
257
- distanceMax: maxZoom,
258
- distanceMin: minZoom,
259
- pitchAngleMax: maxAngle,
260
- pitchAngleMin: minAngle,
261
- yawAngleMax: maxAzimuth,
262
- yawAngleMin: minAzimuth,
263
- minPivotY: minPivotY,
264
- frameOnStart: false
265
- }
266
- });
267
- cameraEntity.script.create('orbitCameraInputMouse', {
268
- attributes: {
269
- orbitSensitivity: isMobile ? 0.6 : 0.3,
270
- distanceSensitivity: isMobile ? 0.5 : 0.4
271
- }
272
- });
273
- if (cameraEntity.script.orbitCameraInputMouse) {
274
- cameraEntity.script.orbitCameraInputMouse.onMouseWheel = function() {};
275
  }
276
- cameraEntity.script.create('orbitCameraInputTouch', {
277
- attributes: {
278
- orbitSensitivity: 0.6,
279
- distanceSensitivity: 0.5
280
- }
281
- });
282
- app.root.addChild(cameraEntity);
283
-
284
- alert("Camera and controls set up; rendering should now appear.");
285
-
286
- // 11. Misc per-frame guard & resize --------------------------------------
287
- app.once('update', () => resetViewerCamera());
288
- app.on('update', dt => {
289
- if (cameraEntity) {
290
- const pos = cameraEntity.getPosition();
291
- if (pos.y < minY) {
292
- cameraEntity.setPosition(pos.x, minY, pos.z);
293
- }
294
- }
295
- });
296
-
297
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
298
-
299
- // 12. Tooltips ------------------------------------------------------------
300
- try {
301
- const tooltipsModule = await import('./tooltips.js');
302
- tooltipsModule.initializeTooltips({
303
- app,
304
- cameraEntity,
305
- modelEntity,
306
- tooltipsUrl: config.tooltips_url,
307
- defaultVisible: !!config.showTooltipsDefault,
308
- moveDuration: config.tooltipMoveDuration || 0.6
309
- });
310
- } catch (e) {
311
- console.error("Error loading tooltips.js:", e);
312
  }
 
 
313
 
314
- progressDialog.style.display = 'none';
315
- viewerInitialized = true;
316
- alert("Viewer initialization complete.");
 
317
  });
318
 
319
- } catch (error) {
320
- alert("Error initializing PlayCanvas viewer: " + error);
321
- console.error("Error initializing PlayCanvas viewer:", error);
322
- progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error.message}</p>`;
323
- }
324
  }
325
 
326
- // -----------------------------------------------------------------------------
327
- // resetViewerCamera / cleanupViewer remain UNCHANGED
328
- // -----------------------------------------------------------------------------
329
- export function resetViewerCamera() {
330
- try {
331
- if (!cameraEntity || !modelEntity || !app) return;
332
- const orbitCam = cameraEntity.script.orbitCamera;
333
- if (!orbitCam) return;
334
-
335
- const modelPos = modelEntity.getPosition();
336
- const tempEnt = new pc.Entity();
337
- tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
338
- tempEnt.lookAt(modelPos);
339
-
340
- const dist = new pc.Vec3().sub2(
341
- new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
342
- modelPos
343
- ).length();
344
 
345
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
346
- cameraEntity.lookAt(modelPos);
347
-
348
- orbitCam.pivotPoint = modelPos.clone();
349
- orbitCam._targetDistance = dist;
350
- orbitCam._distance = dist;
351
-
352
- const rot = tempEnt.getRotation();
353
- const fwd = new pc.Vec3();
354
- rot.transformVector(pc.Vec3.FORWARD, fwd);
355
-
356
- const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
357
- const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
358
- const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
359
- const fNoYaw = new pc.Vec3();
360
- rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
361
- const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
362
-
363
- orbitCam._targetYaw = yaw;
364
- orbitCam._yaw = yaw;
365
- orbitCam._targetPitch = pitch;
366
- orbitCam._pitch = pitch;
367
- orbitCam._updatePosition && orbitCam._updatePosition();
368
-
369
- tempEnt.destroy();
370
- } catch (e) {
371
- console.error("Error resetting camera:", e);
372
- }
373
  }
374
 
375
  export function cleanupViewer() {
376
  if (app) {
377
- try {
378
- app.destroy();
379
- } catch {}
380
  app = null;
381
  }
382
  cameraEntity = null;
383
- modelEntity = null;
384
  viewerInitialized = false;
385
-
386
  if (resizeObserver) {
387
  resizeObserver.disconnect();
388
  resizeObserver = null;
 
1
  // viewer.js
2
  // ==============================
3
+ // Complete patched viewer.js for Space B
4
  // ==============================
5
 
6
  let pc; // will hold the PlayCanvas module once imported
 
21
  export async function initializeViewer(config, instanceId) {
22
  if (viewerInitialized) return;
23
 
 
 
24
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
25
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
26
 
 
31
  maxZoom = parseFloat(config.maxZoom || "20");
32
  minAngle = parseFloat(config.minAngle || "-45");
33
  maxAngle = parseFloat(config.maxAngle || "90");
34
+ minAzimuth = config.minAzimuth !== undefined ? parseFloat(config.minAzimuth) : -360;
35
+ maxAzimuth = config.maxAzimuth !== undefined ? parseFloat(config.maxAzimuth) : 360;
36
  minPivotY = parseFloat(config.minPivotY || "0");
37
+ minY = config.minY !== undefined ? parseFloat(config.minY) : 0;
38
+
39
+ modelX = config.modelX !== undefined ? parseFloat(config.modelX) : 0;
40
+ modelY = config.modelY !== undefined ? parseFloat(config.modelY) : 0;
41
+ modelZ = config.modelZ !== undefined ? parseFloat(config.modelZ) : 0;
42
+ modelScale = config.modelScale !== undefined ? parseFloat(config.modelScale) : 1;
43
+ modelRotationX = config.modelRotationX !== undefined ? parseFloat(config.modelRotationX) : 0;
44
+ modelRotationY = config.modelRotationY !== undefined ? parseFloat(config.modelRotationY) : 0;
45
+ modelRotationZ = config.modelRotationZ !== undefined ? parseFloat(config.modelRotationZ) : 0;
46
+
47
+ const cameraX = config.cameraX !== undefined ? parseFloat(config.cameraX) : 0;
48
+ const cameraY = config.cameraY !== undefined ? parseFloat(config.cameraY) : 2;
49
+ const cameraZ = config.cameraZ !== undefined ? parseFloat(config.cameraZ) : 5;
50
+ const cameraXPhone = config.cameraXPhone !== undefined ? parseFloat(config.cameraXPhone) : cameraX;
51
+ const cameraYPhone = config.cameraYPhone !== undefined ? parseFloat(config.cameraYPhone) : cameraY;
52
+ const cameraZPhone = config.cameraZPhone !== undefined ? parseFloat(config.cameraZPhone) : cameraZ * 1.5;
53
 
54
  chosenCameraX = isMobile ? cameraXPhone : cameraX;
55
  chosenCameraY = isMobile ? cameraYPhone : cameraY;
 
74
  canvas.addEventListener('wheel', e => {
75
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
76
  const orbitCam = cameraEntity.script.orbitCamera;
77
+ const sens = cameraEntity.script.orbitCameraInputMouse?.distanceSensitivity || 0.4;
78
  if (cameraEntity.camera.projection === pc.PROJECTION_PERSPECTIVE) {
79
  orbitCam.distance -= e.deltaY * 0.01 * sens * (orbitCam.distance * 0.1);
80
  } else {
 
93
  window.pc = pc;
94
  }
95
 
96
+ // 6. Setup device & app ----------------------------------------------------
97
+ // --- NEW: WebGL2 WebGL1 fallback (Space A style) ---
98
+ async function getGraphicsDevice(preferWebgl2 = true) {
99
+ const baseOpts = {
100
+ glslangUrl: config.glslangUrl || "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
101
+ twgslUrl: config.twgslUrl || "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
102
+ antialias: false
103
+ };
104
+ try {
105
+ return await pc.createGraphicsDevice(canvas, {
106
+ ...baseOpts,
107
+ deviceTypes: preferWebgl2 ? ["webgl2"] : ["webgl1"]
108
+ });
109
+ } catch (err) {
110
+ if (preferWebgl2) {
111
+ console.warn("[viewer] WebGL2 unavailable, falling back to WebGL1…", err);
112
+ return await getGraphicsDevice(false);
 
 
 
 
 
 
113
  }
114
+ throw err;
115
  }
116
+ }
117
 
118
+ let device;
119
+ try {
120
+ device = await getGraphicsDevice(true);
121
+ } catch (e) {
122
+ console.error("Failed to create WebGL context:", e);
123
+ progressDialog.innerHTML = `<p style="color:red">WebGL init failed: ${e.message}</p>`;
124
+ return;
125
+ }
126
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
127
+
128
+ const opts = new pc.AppOptions();
129
+ opts.graphicsDevice = device;
130
+ opts.mouse = new pc.Mouse(canvas);
131
+ opts.touch = new pc.TouchDevice(canvas);
132
+ opts.componentSystems = [
133
+ pc.RenderComponentSystem,
134
+ pc.CameraComponentSystem,
135
+ pc.LightComponentSystem,
136
+ pc.ScriptComponentSystem,
137
+ pc.GSplatComponentSystem,
138
+ pc.CollisionComponentSystem,
139
+ pc.RigidbodyComponentSystem
140
+ ];
141
+ opts.resourceHandlers = [
142
+ pc.TextureHandler,
143
+ pc.ContainerHandler,
144
+ pc.ScriptHandler,
145
+ pc.GSplatHandler
146
+ ];
147
+
148
+ app = new pc.Application(canvas, opts);
149
+ app.setCanvasFillMode(pc.FILLMODE_NONE);
150
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
151
+
152
+ // Resize observer to keep canvas in sync
153
+ resizeObserver = new ResizeObserver(entries => {
154
+ for (const entry of entries) {
155
+ const { width, height } = entry.contentRect;
156
+ if (app) app.resizeCanvas(width, height);
157
+ }
158
+ });
159
+ resizeObserver.observe(viewerContainer);
160
+ window.addEventListener('resize', () => {
161
+ if (app) app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
162
+ });
163
+
164
+ // 7. Assets ----------------------------------------------------------------
165
+ const assets = {
166
+ model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
167
+ orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
168
+ galerie: new pc.Asset('galerie', 'container', { url: glbUrl }),
169
+ hdr: new pc.Asset('hdr', 'texture', { url: config.hdrUrl || "https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png" }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
170
+ };
171
+
172
+ const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
173
+ let lastProg = 0;
174
+ assets.model.on('load', () => progressDialog.style.display = 'none');
175
+ assets.model.on('error', err => {
176
+ console.error("Error loading PLY file:", err);
177
+ progressDialog.innerHTML = `<p style="color:red">Model load error: ${err}</p>`;
178
+ });
179
+
180
+ const progCheck = setInterval(() => {
181
+ if (assets.model.resource) {
182
+ progressIndicator.value = 100;
183
+ clearInterval(progCheck);
184
+ progressDialog.style.display = 'none';
185
+ } else if (assets.model.loading) {
186
+ lastProg = Math.min(lastProg + 2, 90);
187
+ progressIndicator.value = lastProg;
188
+ }
189
+ }, 100);
190
 
191
+ // 8. Start loading
192
+ loader.load(() => {
193
+ // 8a. Start the app
194
+ app.start();
195
+ app.scene.envAtlas = assets.hdr.resource;
 
 
 
 
196
 
197
+ // 8b. **iOS-only** confirm first draw call
198
+ if (isIOS) {
199
+ app.once('update', () => alert("✅ First frame rendered on iOS"));
200
+ }
201
 
202
+ // 9. Scene ----------------------------------------------------------------
203
+ modelEntity = new pc.Entity('model');
204
+ modelEntity.addComponent('gsplat', { asset: assets.model });
205
+ modelEntity.setLocalPosition(modelX, modelY, modelZ);
206
+ modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
207
+ modelEntity.setLocalScale(modelScale, modelScale, modelScale);
208
+ app.root.addChild(modelEntity);
209
+
210
+ // Light
211
+ const dirLight = new pc.Entity('Cascaded Light');
212
+ dirLight.addComponent('light', {
213
+ type: 'directional',
214
+ color: pc.Color.WHITE,
215
+ shadowBias: 0.3,
216
+ normalOffsetBias: 0.2,
217
+ intensity: 1.0,
218
+ soft: true,
219
+ shadowResolution: 4096,
220
+ penumbraSize: 7,
221
+ penumbraFalloff: 1.5,
222
+ shadowSamples: 128,
223
+ shadowBlockerSamples: 16,
224
+ castShadows: true,
225
+ shadowType: pc.SHADOW_PCSS_32F,
226
+ shadowDistance: 1000
227
  });
228
+ dirLight.setLocalEulerAngles(0, 0, 0);
229
+ app.root.addChild(dirLight);
230
+
231
+ // GLB gallery
232
+ const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
233
+ app.root.addChild(galleryEntity);
234
+
235
+ // 10. Camera --------------------------------------------------------------
236
+ cameraEntity = new pc.Entity('camera');
237
+ cameraEntity.addComponent('camera', {
238
+ clearColor: config.canvas_background
239
+ ? parseInt(config.canvas_background.substr(1, 2), 16) / 255
240
+ : 0
241
  });
242
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
243
+ cameraEntity.lookAt(modelEntity.getPosition());
244
+
245
+ cameraEntity.addComponent('script');
246
+ cameraEntity.script.create('orbitCamera', {
247
+ attributes: {
248
+ inertiaFactor: 0.2,
249
+ focusEntity: modelEntity,
250
+ distanceMax: maxZoom,
251
+ distanceMin: minZoom,
252
+ pitchAngleMax:maxAngle,
253
+ pitchAngleMin:minAngle,
254
+ yawAngleMax: maxAzimuth,
255
+ yawAngleMin: minAzimuth,
256
+ minPivotY: minPivotY,
257
+ frameOnStart: false
258
  }
259
+ });
260
+ cameraEntity.script.create('orbitCameraInputMouse', {
261
+ attributes: {
262
+ orbitSensitivity: isMobile ? 0.6 : 0.3,
263
+ distanceSensitivity: isMobile ? 0.5 : 0.4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  }
265
+ });
266
+ cameraEntity.script.create('orbitCameraInputTouch', {
267
+ attributes: {
268
+ orbitSensitivity: 0.6,
269
+ distanceSensitivity: 0.5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  }
271
+ });
272
+ app.root.addChild(cameraEntity);
273
 
274
+ // 11. Clamp camera y every frame ----------------------------------------
275
+ app.on('update', () => {
276
+ const p = cameraEntity.getPosition();
277
+ if (p.y < minY) cameraEntity.setPosition(p.x, minY, p.z);
278
  });
279
 
280
+ viewerInitialized = true;
281
+ });
 
 
 
282
  }
283
 
284
+ // Optional: resetViewerCamera() and cleanupViewer() remain unchanged below…
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
+ export function resetViewerCamera() {
287
+ // … your existing reset logic …
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  }
289
 
290
  export function cleanupViewer() {
291
  if (app) {
292
+ try { app.destroy(); } catch {}
 
 
293
  app = null;
294
  }
295
  cameraEntity = null;
296
+ modelEntity = null;
297
  viewerInitialized = false;
 
298
  if (resizeObserver) {
299
  resizeObserver.disconnect();
300
  resizeObserver = null;