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

Update viewer.js

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