MikaFil commited on
Commit
c7c8c1d
·
verified ·
1 Parent(s): 48b3594

Create viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +410 -0
viewer.js ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ==============================
2
+ // viewer.js
3
+ // ==============================
4
+
5
+ let pc; // will hold the PlayCanvas module once imported
6
+ export let app = null;
7
+ let cameraEntity = null;
8
+ let modelEntity = null;
9
+ let viewerInitialized = false;
10
+
11
+ let chosenCameraX, chosenCameraY, chosenCameraZ;
12
+ let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, minY;
13
+ let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
14
+ let plyUrl, glbUrl;
15
+
16
+ /**
17
+ * initializeViewer(config, instanceId)
18
+ *
19
+ * - Dynamically imports PlayCanvas (once).
20
+ * - Creates a new <canvas id="canvas-<instanceId>"> inside the viewer container.
21
+ * - Sets up lights, model loading, camera + orbit controls, HDR, etc.
22
+ * - Exposes app, cameraEntity, modelEntity, so that interface.js can resize or reset.
23
+ * - Imports & initializes tooltip hotspots from tooltips.js.
24
+ */
25
+ export async function initializeViewer(config, instanceId) {
26
+ if (viewerInitialized) return;
27
+
28
+ // Detect device type again (for camera coordinates & input sensitivity)
29
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
30
+ const isMobile = isIOS || /Android/i.test(navigator.userAgent);
31
+
32
+ // ─── 1. Read all relevant config values ───────────────────────────────────────
33
+ plyUrl = config.ply_url;
34
+ glbUrl = config.glb_url;
35
+
36
+ minZoom = parseFloat(config.minZoom || "1");
37
+ maxZoom = parseFloat(config.maxZoom || "20");
38
+ minAngle = parseFloat(config.minAngle || "-45");
39
+ maxAngle = parseFloat(config.maxAngle || "90");
40
+ minAzimuth = (config.minAzimuth !== undefined) ? parseFloat(config.minAzimuth) : -360;
41
+ maxAzimuth = (config.maxAzimuth !== undefined) ? parseFloat(config.maxAzimuth) : 360;
42
+ minPivotY = parseFloat(config.minPivotY || "0");
43
+ minY = (config.minY !== undefined) ? parseFloat(config.minY) : 0;
44
+
45
+ modelX = (config.modelX !== undefined) ? parseFloat(config.modelX) : 0;
46
+ modelY = (config.modelY !== undefined) ? parseFloat(config.modelY) : 0;
47
+ modelZ = (config.modelZ !== undefined) ? parseFloat(config.modelZ) : 0;
48
+ modelScale = (config.modelScale !== undefined) ? parseFloat(config.modelScale) : 1;
49
+ modelRotationX = (config.modelRotationX !== undefined) ? parseFloat(config.modelRotationX) : 0;
50
+ modelRotationY = (config.modelRotationY !== undefined) ? parseFloat(config.modelRotationY) : 0;
51
+ modelRotationZ = (config.modelRotationZ !== undefined) ? parseFloat(config.modelRotationZ) : 0;
52
+
53
+ const cameraX = (config.cameraX !== undefined) ? parseFloat(config.cameraX) : 0;
54
+ const cameraY = (config.cameraY !== undefined) ? parseFloat(config.cameraY) : 2;
55
+ const cameraZ = (config.cameraZ !== undefined) ? parseFloat(config.cameraZ) : 5;
56
+ const cameraXPhone = (config.cameraXPhone !== undefined) ? parseFloat(config.cameraXPhone) : cameraX;
57
+ const cameraYPhone = (config.cameraYPhone !== undefined) ? parseFloat(config.cameraYPhone) : cameraY;
58
+ const cameraZPhone = (config.cameraZPhone !== undefined) ? parseFloat(config.cameraZPhone) : (cameraZ * 1.5);
59
+
60
+ chosenCameraX = isMobile ? cameraXPhone : cameraX;
61
+ chosenCameraY = isMobile ? cameraYPhone : cameraY;
62
+ chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
63
+
64
+ // ─── 2. Grab DOM elements inside interface.js’s container ─────────────────────
65
+ const canvasId = 'canvas-' + instanceId;
66
+ const progressDialog = document.getElementById('progress-dialog-' + instanceId);
67
+ const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
68
+ const viewerContainerElem = document.getElementById('viewer-container-' + instanceId);
69
+
70
+ // ─── 3. Create a fresh <canvas> (removing any old one) ────────────────────────
71
+ let oldCanvas = document.getElementById(canvasId);
72
+ if (oldCanvas) {
73
+ oldCanvas.remove();
74
+ }
75
+ const newCanvas = document.createElement('canvas');
76
+ newCanvas.id = canvasId;
77
+ newCanvas.className = 'ply-canvas';
78
+ newCanvas.style.zIndex = "1";
79
+ viewerContainerElem.insertBefore(newCanvas, progressDialog);
80
+ const canvas = newCanvas;
81
+
82
+ // ─── 4. Add mouse‐wheel listener for zoom adjustments ────────────────────────
83
+ canvas.addEventListener('wheel', function(e) {
84
+ if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
85
+ const orbitCam = cameraEntity.script.orbitCamera;
86
+ const sensitivity = (cameraEntity.script.orbitCameraInputMouse && cameraEntity.script.orbitCameraInputMouse.distanceSensitivity) || 0.4;
87
+
88
+ if (cameraEntity.camera.projection === pc.PROJECTION_PERSPECTIVE) {
89
+ orbitCam.distance -= e.deltaY * 0.01 * sensitivity * (orbitCam.distance * 0.1);
90
+ } else {
91
+ orbitCam.orthoHeight -= e.deltaY * 0.01 * sensitivity * (orbitCam.orthoHeight * 0.1);
92
+ }
93
+ e.preventDefault();
94
+ e.stopPropagation();
95
+ }
96
+ }, { passive: false });
97
+
98
+ progressDialog.style.display = 'block';
99
+
100
+ // ─── 5. Import PlayCanvas (only once) ─────────────────────────────────────────
101
+ if (!pc) {
102
+ pc = await import("https://esm.run/playcanvas");
103
+ window.pc = pc;
104
+ }
105
+
106
+ try {
107
+ // ─── 6. Create a GraphicsDevice + AppOptions ────────────────────────────────
108
+ const deviceType = "webgl2";
109
+ const gfxOptions = {
110
+ deviceTypes: [deviceType],
111
+ glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
112
+ twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
113
+ antialias: false
114
+ };
115
+ const device = await pc.createGraphicsDevice(canvas, gfxOptions);
116
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
117
+
118
+ const createOptions = new pc.AppOptions();
119
+ createOptions.graphicsDevice = device;
120
+ createOptions.mouse = new pc.Mouse(canvas);
121
+ createOptions.touch = new pc.TouchDevice(canvas);
122
+
123
+ // ─── ADD: include physics systems for collision-based picking ───────────────
124
+ createOptions.componentSystems = [
125
+ pc.RenderComponentSystem,
126
+ pc.CameraComponentSystem,
127
+ pc.LightComponentSystem,
128
+ pc.ScriptComponentSystem,
129
+ pc.GSplatComponentSystem,
130
+ pc.CollisionComponentSystem,
131
+ pc.RigidbodyComponentSystem
132
+ ];
133
+ createOptions.resourceHandlers = [
134
+ pc.TextureHandler,
135
+ pc.ContainerHandler,
136
+ pc.ScriptHandler,
137
+ pc.GSplatHandler
138
+ ];
139
+
140
+ // ─── REPLACED: use pc.Application (instead of deprecated / removed AppBase) ──
141
+ app = new pc.Application(canvas, createOptions);
142
+
143
+ // Configure application
144
+ app.setCanvasFillMode(pc.FILLMODE_NONE);
145
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
146
+ app.scene.exposure = 0.5;
147
+ app.scene.toneMapping = pc.TONEMAP_ACES;
148
+
149
+ // ─── 7. Handle window‐resize for the PlayCanvas canvas ─────────────────────
150
+ const resizeCanvas = () => {
151
+ if (app) {
152
+ app.resizeCanvas(canvas.clientWidth, canvas.clientHeight);
153
+ }
154
+ };
155
+ window.addEventListener('resize', resizeCanvas);
156
+ app.on('destroy', () => {
157
+ window.removeEventListener('resize', resizeCanvas);
158
+ });
159
+
160
+ // ─── 8. Declare assets: PLY + orbit‐camera.js + GLB + HDR ───────────────────
161
+ const assets = {
162
+ model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
163
+ orbit: new pc.Asset('script', 'script', { url: "https://bilca-visionneur-play-canva-2.static.hf.space/orbit-camera.js" }),
164
+ galerie: new pc.Asset('galerie', 'container', { url: glbUrl }),
165
+ hdr: new pc.Asset(
166
+ 'hdr',
167
+ 'texture',
168
+ { url: `https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png` },
169
+ { type: pc.TEXTURETYPE_RGBP, mipmaps: false }
170
+ )
171
+ };
172
+
173
+ const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
174
+ let lastProgress = 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 checkProgress = () => {
185
+ if (assets.model.resource) {
186
+ progressIndicator.value = 100;
187
+ clearInterval(progressChecker);
188
+ progressDialog.style.display = 'none';
189
+ } else if (assets.model.loading) {
190
+ lastProgress += 2;
191
+ if (lastProgress > 90) lastProgress = 90;
192
+ progressIndicator.value = lastProgress;
193
+ }
194
+ };
195
+ const progressChecker = setInterval(checkProgress, 100);
196
+
197
+ // ─── 9. Once assets are loaded, build the scene ────────────────────────────
198
+ assetListLoader.load(async () => {
199
+ app.start();
200
+ app.scene.envAtlas = assets.hdr.resource;
201
+
202
+ // 9a. Create & position the model
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
+ // 9b. Add a directional 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
+ app.root.addChild(dirLight);
229
+ dirLight.setLocalEulerAngles(0, 0, 0);
230
+
231
+ // 9c. Instantiate the GLB “gallery” if any
232
+ const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
233
+ app.root.addChild(galleryEntity);
234
+
235
+ // 9d. Create & configure the camera + orbit‐camera scripts
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
+ toneMapping: pc.TONEMAP_ACES
242
+ });
243
+ // *** Set initial position/orientation from config immediately ***
244
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
245
+ cameraEntity.lookAt(modelEntity.getPosition());
246
+
247
+ // Warn if camera is very far
248
+ const distanceCheck = new pc.Vec3().sub2(
249
+ cameraEntity.getPosition(),
250
+ modelEntity.getPosition()
251
+ ).length();
252
+ if (distanceCheck > 100) {
253
+ console.warn("Warning: Camera is far from model; it might not be visible");
254
+ }
255
+
256
+ cameraEntity.addComponent('script');
257
+ cameraEntity.script.create('orbitCamera', {
258
+ attributes: {
259
+ inertiaFactor: 0.2,
260
+ focusEntity: modelEntity,
261
+ distanceMax: maxZoom,
262
+ distanceMin: minZoom,
263
+ pitchAngleMax: maxAngle,
264
+ pitchAngleMin: minAngle,
265
+ yawAngleMax: maxAzimuth,
266
+ yawAngleMin: minAzimuth,
267
+ minPivotY: minPivotY,
268
+ frameOnStart: false
269
+ }
270
+ });
271
+ cameraEntity.script.create('orbitCameraInputMouse', {
272
+ attributes: {
273
+ orbitSensitivity: isMobile ? 0.6 : 0.3,
274
+ distanceSensitivity: isMobile ? 0.5 : 0.4
275
+ }
276
+ });
277
+ // Disable default onMouseWheel in orbitCameraInputMouse so we handle it ourselves
278
+ if (cameraEntity.script.orbitCameraInputMouse) {
279
+ cameraEntity.script.orbitCameraInputMouse.onMouseWheel = function() {};
280
+ }
281
+ cameraEntity.script.create('orbitCameraInputTouch', {
282
+ attributes: {
283
+ orbitSensitivity: 0.6,
284
+ distanceSensitivity: 0.5
285
+ }
286
+ });
287
+
288
+ app.root.addChild(cameraEntity);
289
+
290
+ // ─── NEW: Ensure camera is reset on the first frame ───────────────────────
291
+ app.once('update', function() {
292
+ // Call existing resetViewerCamera() so that pivot, yaw/pitch, and distance get
293
+ // recalculated correctly before the first render.
294
+ resetViewerCamera();
295
+ });
296
+
297
+ // 9e. Constrain camera’s Y so it never dips below minY
298
+ app.on('update', function(dt) {
299
+ if (cameraEntity) {
300
+ const pos = cameraEntity.getPosition();
301
+ if (pos.y < minY) {
302
+ cameraEntity.setPosition(pos.x, minY, pos.z);
303
+ }
304
+ }
305
+ });
306
+
307
+ // 9f. Resize canvas one last time to fit container
308
+ app.resizeCanvas(viewerContainerElem.clientWidth, viewerContainerElem.clientHeight);
309
+
310
+ // ─── NEW: Load & initialize the tooltips module ──────────────────────
311
+ try {
312
+ const tooltipsModule = await import('./tooltips.js');
313
+ tooltipsModule.initializeTooltips({
314
+ app,
315
+ cameraEntity,
316
+ modelEntity,
317
+ tooltipsUrl: config.tooltips_url,
318
+ defaultVisible: !!config.showTooltipsDefault,
319
+ moveDuration: config.tooltipMoveDuration || 0.6 // optional smooth‐move duration
320
+ });
321
+ } catch (e) {
322
+ console.error("Error loading tooltips.js:", e);
323
+ }
324
+
325
+ progressDialog.style.display = 'none';
326
+ viewerInitialized = true;
327
+ });
328
+
329
+ } catch (error) {
330
+ console.error("Error initializing PlayCanvas viewer:", error);
331
+ progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error.message}</p>`;
332
+ }
333
+ }
334
+
335
+ /**
336
+ * resetViewerCamera()
337
+ *
338
+ * Re‐centers the camera on the model, using the chosenCameraXYZ from config.
339
+ * Exactly the same math as in the old index.js‘s resetCamera().
340
+ */
341
+ export function resetViewerCamera() {
342
+ try {
343
+ if (!cameraEntity || !modelEntity || !app) return;
344
+ const orbitCam = cameraEntity.script.orbitCamera;
345
+ if (!orbitCam) return;
346
+
347
+ const modelPos = modelEntity.getPosition();
348
+ // Create a temporary entity at chosenCameraX,Y,Z to compute yaw/pitch
349
+ const tempEntity = new pc.Entity();
350
+ tempEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
351
+ tempEntity.lookAt(modelPos);
352
+
353
+ // Compute distance
354
+ const distance = new pc.Vec3().sub2(
355
+ new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
356
+ modelPos
357
+ ).length();
358
+
359
+ // Reposition camera
360
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
361
+ cameraEntity.lookAt(modelPos);
362
+
363
+ orbitCam.pivotPoint = new pc.Vec3(modelPos.x, modelPos.y, modelPos.z);
364
+ orbitCam._targetDistance = distance;
365
+ orbitCam._distance = distance;
366
+
367
+ // Compute yaw & pitch from the “tempEntity”
368
+ const rotation = tempEntity.getRotation();
369
+ const tempForward = new pc.Vec3();
370
+ rotation.transformVector(pc.Vec3.FORWARD, tempForward);
371
+
372
+ const yaw = Math.atan2(-tempForward.x, -tempForward.z) * pc.math.RAD_TO_DEG;
373
+ const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
374
+ const rotWithoutYaw = new pc.Quat().mul2(yawQuat, rotation);
375
+ const forwardWithoutYaw = new pc.Vec3();
376
+ rotWithoutYaw.transformVector(pc.Vec3.FORWARD, forwardWithoutYaw);
377
+ const pitch = Math.atan2(forwardWithoutYaw.y, -forwardWithoutYaw.z) * pc.math.RAD_TO_DEG;
378
+
379
+ orbitCam._targetYaw = yaw;
380
+ orbitCam._yaw = yaw;
381
+ orbitCam._targetPitch = pitch;
382
+ orbitCam._pitch = pitch;
383
+ if (typeof orbitCam._updatePosition === 'function') {
384
+ orbitCam._updatePosition();
385
+ }
386
+
387
+ tempEntity.destroy();
388
+ } catch (error) {
389
+ console.error("Error resetting camera:", error);
390
+ }
391
+ }
392
+
393
+ /**
394
+ * cleanupViewer()
395
+ *
396
+ * Destroys the PlayCanvas `app` (if it exists) and clears references. Called when the user clicks “X”.
397
+ */
398
+ export function cleanupViewer() {
399
+ if (app) {
400
+ try {
401
+ app.destroy();
402
+ } catch (e) {
403
+ console.error("Error destroying PlayCanvas app:", e);
404
+ }
405
+ app = null;
406
+ }
407
+ cameraEntity = null;
408
+ modelEntity = null;
409
+ viewerInitialized = false;
410
+ }