MikaFil commited on
Commit
f550d72
·
verified ·
1 Parent(s): e6c5b4a

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +329 -354
viewer.js CHANGED
@@ -19,372 +19,347 @@ let plyUrl, glbUrl;
19
  * initializeViewer(config, instanceId)
20
  */
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
-
29
- // 1. Read config -------------------------------------------------------------
30
- plyUrl = config.ply_url;
31
- glbUrl = config.glb_url;
32
- minZoom = parseFloat(config.minZoom || "1");
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;
58
- chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
59
-
60
- // 2. Grab DOM ---------------------------------------------------------------
61
- const canvasId = 'canvas-' + instanceId;
62
- const progressDialog = document.getElementById('progress-dialog-' + instanceId);
63
- const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
64
- const viewerContainer = document.getElementById('viewer-container-' + instanceId);
65
-
66
- // 3. Create <canvas> --------------------------------------------------------
67
- let oldCanvas = document.getElementById(canvasId);
68
- if (oldCanvas) oldCanvas.remove();
69
- const canvas = document.createElement('canvas');
70
- canvas.id = canvasId;
71
- canvas.className = 'ply-canvas';
72
- canvas.style.zIndex = "1";
73
- viewerContainer.insertBefore(canvas, progressDialog);
74
-
75
- // 4. Wheel listener ---------------------------------------------------------
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 {
83
- orbitCam.orthoHeight -= e.deltaY * 0.01 * sens * (orbitCam.orthoHeight * 0.1);
84
- }
85
- e.preventDefault();
86
- e.stopPropagation();
87
- }
88
- }, { passive: false });
89
-
90
- progressDialog.style.display = 'block';
91
-
92
- // 5. Import PlayCanvas -------------------------------------------------------
93
- if (!pc) {
94
- pc = await import("https://esm.run/playcanvas");
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;
389
- }
390
  }
 
19
  * initializeViewer(config, instanceId)
20
  */
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
+
27
+ // 1. Read config -------------------------------------------------------------
28
+ plyUrl = config.ply_url;
29
+ glbUrl = config.glb_url;
30
+ minZoom = parseFloat(config.minZoom || "1");
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;
56
+ chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
57
+
58
+ // 2. Grab DOM ---------------------------------------------------------------
59
+ const canvasId = 'canvas-' + instanceId;
60
+ const progressDialog = document.getElementById('progress-dialog-' + instanceId);
61
+ const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
62
+ const viewerContainer = document.getElementById('viewer-container-' + instanceId);
63
+
64
+ // 3. Create <canvas> --------------------------------------------------------
65
+ let oldCanvas = document.getElementById(canvasId);
66
+ if (oldCanvas) oldCanvas.remove();
67
+ const canvas = document.createElement('canvas');
68
+ canvas.id = canvasId;
69
+ canvas.className = 'ply-canvas';
70
+ canvas.style.zIndex = "1";
71
+ viewerContainer.insertBefore(canvas, progressDialog);
72
+
73
+ // 4. Wheel listener ---------------------------------------------------------
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 {
81
+ orbitCam.orthoHeight -= e.deltaY * 0.01 * sens * (orbitCam.orthoHeight * 0.1);
82
+ }
83
+ e.preventDefault();
84
+ e.stopPropagation();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  }
86
+ }, { passive: false });
87
+
88
+ progressDialog.style.display = 'block';
89
+
90
+ // 5. Import PlayCanvas -------------------------------------------------------
91
+ if (!pc) {
92
+ pc = await import("https://esm.run/playcanvas");
93
+ window.pc = pc;
94
  }
95
 
96
+ try {
97
+ // 6. Setup device & app ----------------------------------------------------
98
+ async function getGraphicsDevice(preferWebgl2 = true) {
99
+ const baseOpts = {
100
+ glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
101
+ 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, retrying with WebGL1 …", err);
112
+ return await getGraphicsDevice(false);
113
+ }
114
+ throw err;
115
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  }
117
+
118
+ const device = await getGraphicsDevice(true);
119
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
120
+
121
+ const opts = new pc.AppOptions();
122
+ opts.graphicsDevice = device;
123
+ opts.mouse = new pc.Mouse(canvas);
124
+ opts.touch = new pc.TouchDevice(canvas);
125
+ opts.componentSystems = [
126
+ pc.RenderComponentSystem,
127
+ pc.CameraComponentSystem,
128
+ pc.LightComponentSystem,
129
+ pc.ScriptComponentSystem,
130
+ pc.GSplatComponentSystem,
131
+ pc.CollisionComponentSystem,
132
+ pc.RigidbodyComponentSystem
133
+ ];
134
+ opts.resourceHandlers = [
135
+ pc.TextureHandler,
136
+ pc.ContainerHandler,
137
+ pc.ScriptHandler,
138
+ pc.GSplatHandler
139
+ ];
140
+
141
+ app = new pc.Application(canvas, opts);
142
+ app.setCanvasFillMode(pc.FILLMODE_NONE);
143
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
144
+
145
+ // 7. Resize observer -------------------------------------------------------
146
+ resizeObserver = new ResizeObserver(entries => {
147
+ for (const entry of entries) {
148
+ const { width, height } = entry.contentRect;
149
+ if (app) {
150
+ app.resizeCanvas(width, height);
151
+ }
152
+ }
153
+ });
154
+ resizeObserver.observe(viewerContainer);
155
+
156
+ window.addEventListener('resize', () => {
157
+ if (app) app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
158
+ });
159
+ app.on('destroy', () => {
160
+ window.removeEventListener('resize', resizeCanvas);
161
  });
162
+
163
+ // 8. Assets ----------------------------------------------------------------
164
+ const assets = {
165
+ model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
166
+ orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
167
+ galerie: new pc.Asset('galerie', 'container', { url: glbUrl }),
168
+ hdr: new pc.Asset('hdr', 'texture', {
169
+ url: `https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png`
170
+ }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
171
+ };
172
+
173
+ const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
174
+ let lastProg = 0;
175
+
176
+ assets.model.on('load', () => {
177
+ progressDialog.style.display = 'none';
178
+ });
179
+ assets.model.on('error', err => {
180
+ console.error("Error loading PLY file:", err);
181
+ progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;
182
+ });
183
+
184
+ const progCheck = setInterval(() => {
185
+ if (assets.model.resource) {
186
+ progressIndicator.value = 100;
187
+ clearInterval(progCheck);
188
+ progressDialog.style.display = 'none';
189
+ } else if (assets.model.loading) {
190
+ lastProg = Math.min(lastProg + 2, 90);
191
+ progressIndicator.value = lastProg;
192
+ }
193
+ }, 100);
194
+
195
+ loader.load(async () => {
196
+ app.start();
197
+ app.scene.envAtlas = assets.hdr.resource;
198
+
199
+ // ** Insert render-loop confirmation alert for iOS **
200
+ if (isIOS) {
201
+ app.once('update', function firstFrame() {
202
+ alert('Render loop started on iOS');
203
+ });
204
+ }
205
+
206
+ // 9. Scene ----------------------------------------------------------------
207
+ modelEntity = new pc.Entity('model');
208
+ modelEntity.addComponent('gsplat', { asset: assets.model });
209
+ modelEntity.setLocalPosition(modelX, modelY, modelZ);
210
+ modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
211
+ modelEntity.setLocalScale(modelScale, modelScale, modelScale);
212
+ app.root.addChild(modelEntity);
213
+
214
+ const dirLight = new pc.Entity('Cascaded Light');
215
+ dirLight.addComponent('light', {
216
+ type: 'directional',
217
+ color: pc.Color.WHITE,
218
+ shadowBias: 0.3,
219
+ normalOffsetBias: 0.2,
220
+ intensity: 1.0,
221
+ soft: true,
222
+ shadowResolution: 4096,
223
+ penumbraSize: 7,
224
+ penumbraFalloff: 1.5,
225
+ shadowSamples: 128,
226
+ shadowBlockerSamples: 16,
227
+ castShadows: true,
228
+ shadowType: pc.SHADOW_PCSS_32F,
229
+ shadowDistance: 1000
230
+ });
231
+ dirLight.setLocalEulerAngles(0, 0, 0);
232
+ app.root.addChild(dirLight);
233
+
234
+ const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
235
+ app.root.addChild(galleryEntity);
236
+
237
+ // 10. Camera --------------------------------------------------------------
238
+ cameraEntity = new pc.Entity('camera');
239
+ cameraEntity.addComponent('camera', {
240
+ clearColor: config.canvas_background
241
+ ? parseInt(config.canvas_background.substr(1, 2), 16) / 255
242
+ : 0,
243
+ });
244
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
245
+ cameraEntity.lookAt(modelEntity.getPosition());
246
+
247
+ cameraEntity.addComponent('script');
248
+ cameraEntity.script.create('orbitCamera', {
249
+ attributes: {
250
+ inertiaFactor: 0.2,
251
+ focusEntity: modelEntity,
252
+ distanceMax: maxZoom,
253
+ distanceMin: minZoom,
254
+ pitchAngleMax: maxAngle,
255
+ pitchAngleMin: minAngle,
256
+ yawAngleMax: maxAzimuth,
257
+ yawAngleMin: minAzimuth,
258
+ minPivotY: minPivotY,
259
+ frameOnStart: false
260
+ }
261
+ });
262
+ cameraEntity.script.create('orbitCameraInputMouse', {
263
+ attributes: {
264
+ orbitSensitivity: isMobile ? 0.6 : 0.3,
265
+ distanceSensitivity: isMobile ? 0.5 : 0.4
266
+ }
267
+ });
268
+ if (cameraEntity.script.orbitCameraInputMouse) {
269
+ cameraEntity.script.orbitCameraInputMouse.onMouseWheel = function() {};
270
+ }
271
+ cameraEntity.script.create('orbitCameraInputTouch', {
272
+ attributes: {
273
+ orbitSensitivity: 0.6,
274
+ distanceSensitivity: 0.5
275
+ }
276
+ });
277
+ app.root.addChild(cameraEntity);
278
+
279
+ // 11. Guard & resize ------------------------------------------------------
280
+ app.once('update', () => resetViewerCamera());
281
+ app.on('update', dt => {
282
+ if (cameraEntity) {
283
+ const pos = cameraEntity.getPosition();
284
+ if (pos.y < minY) {
285
+ cameraEntity.setPosition(pos.x, minY, pos.z);
286
+ }
287
+ }
288
+ });
289
+
290
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
291
+
292
+ viewerInitialized = true;
293
+ });
294
+
295
+ } catch (error) {
296
+ console.error("Error initializing PlayCanvas viewer:", error);
297
+ progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error.message}</p>`;
298
+ }
299
  }
300
 
301
  // -----------------------------------------------------------------------------
302
  // resetViewerCamera / cleanupViewer remain UNCHANGED
303
  // -----------------------------------------------------------------------------
304
  export function resetViewerCamera() {
305
+ try {
306
+ if (!cameraEntity || !modelEntity || !app) return;
307
+ const orbitCam = cameraEntity.script.orbitCamera;
308
+ if (!orbitCam) return;
309
+
310
+ const modelPos = modelEntity.getPosition();
311
+ const tempEnt = new pc.Entity();
312
+ tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
313
+ tempEnt.lookAt(modelPos);
314
+
315
+ const dist = new pc.Vec3().sub2(
316
+ new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
317
+ modelPos
318
+ ).length();
319
+
320
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
321
+ cameraEntity.lookAt(modelPos);
322
+
323
+ orbitCam.pivotPoint = modelPos.clone();
324
+ orbitCam._targetDistance = dist;
325
+ orbitCam._distance = dist;
326
+
327
+ const rot = tempEnt.getRotation();
328
+ const fwd = new pc.Vec3();
329
+ rot.transformVector(pc.Vec3.FORWARD, fwd);
330
+
331
+ const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
332
+ const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
333
+ const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
334
+ const fNoYaw = new pc.Vec3();
335
+ rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
336
+ const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
337
+
338
+ orbitCam._targetYaw = yaw;
339
+ orbitCam._yaw = yaw;
340
+ orbitCam._targetPitch = pitch;
341
+ orbitCam._pitch = pitch;
342
+ orbitCam._updatePosition && orbitCam._updatePosition();
343
+
344
+ tempEnt.destroy();
345
+ } catch (e) {
346
+ console.error("Error resetting camera:", e);
347
+ }
348
  }
349
 
350
  export function cleanupViewer() {
351
+ if (app) {
352
+ try {
353
+ app.destroy();
354
+ } catch {}
355
+ app = null;
356
+ }
357
+ cameraEntity = null;
358
+ modelEntity = null;
359
+ viewerInitialized = false;
360
+
361
+ if (resizeObserver) {
362
+ resizeObserver.disconnect();
363
+ resizeObserver = null;
364
+ }
365
  }