bilca commited on
Commit
85f79d1
·
verified ·
1 Parent(s): 7f3416a

Update js_scripts/index.js

Browse files
Files changed (1) hide show
  1. js_scripts/index.js +281 -502
js_scripts/index.js CHANGED
@@ -1,36 +1,6 @@
1
- // Store the current script reference before the async function runs
2
- const currentScriptTag = document.currentScript;
3
-
4
  (async function() {
5
- // Import PlayCanvas
6
- const pc = await import("https://cdn.skypack.dev/[email protected]");
7
- window.pc = pc;
8
-
9
- // Find the script tag using a more reliable method
10
- let scriptTag = currentScriptTag;
11
-
12
- // Fallback method if currentScriptTag is null
13
- if (!scriptTag) {
14
- const scripts = document.getElementsByTagName('script');
15
- for (let i = 0; i < scripts.length; i++) {
16
- if (scripts[i].src.includes('index.js') && scripts[i].hasAttribute('data-config')) {
17
- scriptTag = scripts[i];
18
- break;
19
- }
20
- }
21
-
22
- // If still not found, try the last script on the page
23
- if (!scriptTag && scripts.length > 0) {
24
- scriptTag = scripts[scripts.length - 1];
25
- }
26
- }
27
-
28
- // Check if we found a script tag
29
- if (!scriptTag) {
30
- console.error("Could not find the script tag with data-config attribute.");
31
- return;
32
- }
33
-
34
  const configUrl = scriptTag.getAttribute("data-config");
35
  let config = {};
36
  if (configUrl) {
@@ -54,14 +24,9 @@ const currentScriptTag = document.currentScript;
54
  document.head.appendChild(linkEl);
55
  }
56
 
57
- // --- Outer scope variables ---
58
- let cameraEntity = null;
59
- let app = null;
60
- let modelEntity = null;
61
- let viewerInitialized = false;
62
- let wheelHandlers = [];
63
- let resizeHandler = null;
64
- let progressChecker = null;
65
 
66
  // Generate a unique identifier for this widget instance.
67
  const instanceId = Math.random().toString(36).substr(2, 8);
@@ -69,45 +34,31 @@ const currentScriptTag = document.currentScript;
69
  // Read configuration values from the JSON file.
70
  const gifUrl = config.gif_url;
71
  const plyUrl = config.ply_url;
72
-
73
- // Camera constraint parameters
74
- const minZoom = parseFloat(config.minZoom || "1");
75
  const maxZoom = parseFloat(config.maxZoom || "20");
76
- const minAngle = parseFloat(config.minAngle || "-45");
77
- const maxAngle = parseFloat(config.maxAngle || "90");
78
- const minAzimuth = config.minAzimuth !== undefined ? parseFloat(config.minAzimuth) : -360;
79
- const maxAzimuth = config.maxAzimuth !== undefined ? parseFloat(config.maxAzimuth) : 360;
80
-
81
- // Model position, scale, and rotation parameters
82
- const modelX = config.modelX !== undefined ? parseFloat(config.modelX) : 0;
83
- const modelY = config.modelY !== undefined ? parseFloat(config.modelY) : 0;
84
- const modelZ = config.modelZ !== undefined ? parseFloat(config.modelZ) : 0;
85
-
86
- const modelScale = config.modelScale !== undefined ? parseFloat(config.modelScale) : 1;
87
-
88
- const modelRotationX = config.modelRotationX !== undefined ? parseFloat(config.modelRotationX) : 0;
89
- const modelRotationY = config.modelRotationY !== undefined ? parseFloat(config.modelRotationY) : 0;
90
- const modelRotationZ = config.modelRotationZ !== undefined ? parseFloat(config.modelRotationZ) : 0;
91
-
92
- // Direct camera coordinates
93
- const cameraX = config.cameraX !== undefined ? parseFloat(config.cameraX) : 0;
94
- const cameraY = config.cameraY !== undefined ? parseFloat(config.cameraY) : 2;
95
- const cameraZ = config.cameraZ !== undefined ? parseFloat(config.cameraZ) : 5;
96
-
97
- // Camera coordinates for mobile devices
98
- const cameraXPhone = config.cameraXPhone !== undefined ? parseFloat(config.cameraXPhone) : cameraX;
99
- const cameraYPhone = config.cameraYPhone !== undefined ? parseFloat(config.cameraYPhone) : cameraY;
100
- const cameraZPhone = config.cameraZPhone !== undefined ? parseFloat(config.cameraZPhone) : cameraZ * 1.5;
101
 
102
  // Detect if the device is iOS.
103
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
104
  // Also detect Android devices.
105
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
106
 
107
- // Choose the appropriate coordinates based on device type
108
- const chosenCameraX = isMobile ? cameraXPhone : cameraX;
109
- const chosenCameraY = isMobile ? cameraYPhone : cameraY;
110
- const chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
111
 
112
  // Determine the aspect ratio.
113
  let aspectPercent = "100%";
@@ -181,18 +132,6 @@ const currentScriptTag = document.currentScript;
181
  const progressDialog = document.getElementById('progress-dialog-' + instanceId);
182
  const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
183
 
184
- // Flag to track if mouse is over the viewer
185
- let isMouseOverViewer = false;
186
-
187
- // Add mouse hover tracking for the viewer container
188
- viewerContainer.addEventListener('mouseenter', function() {
189
- isMouseOverViewer = true;
190
- });
191
-
192
- viewerContainer.addEventListener('mouseleave', function() {
193
- isMouseOverViewer = false;
194
- });
195
-
196
  // Set help instructions based on device type.
197
  if (isMobile) {
198
  menuContent.innerHTML = `
@@ -208,7 +147,9 @@ const currentScriptTag = document.currentScript;
208
  `;
209
  }
210
 
211
- // Handle GIF configuration
 
 
212
  if (gifUrl) {
213
  previewImage.src = gifUrl;
214
  gifPreview.style.display = 'block';
@@ -217,83 +158,22 @@ const currentScriptTag = document.currentScript;
217
  gifPreview.style.display = 'none';
218
  viewerContainer.style.display = 'block';
219
  closeBtn.style.display = 'none';
220
- // Initialize the viewer only when needed
221
  setTimeout(() => {
222
- initializeViewer();
223
  }, 100);
224
  }
225
 
226
  // --- Button Event Handlers ---
227
  if (gifUrl) {
228
- // Add click event to the GIF container
229
  gifPreview.addEventListener('click', function() {
230
  console.log("GIF preview clicked, showing 3D viewer");
231
  gifPreview.style.display = 'none';
232
  viewerContainer.style.display = 'block';
233
-
234
- // Make sure we're not initializing again if already done
235
- if (!viewerInitialized) {
236
- initializeViewer();
237
- }
238
  });
239
  }
240
 
241
- // Function to clean up the viewer completely
242
- function cleanupViewer() {
243
- console.log("Starting viewer cleanup...");
244
-
245
- // Clear any running intervals
246
- if (progressChecker) {
247
- clearInterval(progressChecker);
248
- progressChecker = null;
249
- }
250
-
251
- // Remove wheel event listeners
252
- for (const handler of wheelHandlers) {
253
- const [element, func] = handler;
254
- element.removeEventListener('wheel', func, { passive: false });
255
- }
256
- wheelHandlers = [];
257
-
258
- // Remove resize handler if it exists
259
- if (resizeHandler) {
260
- window.removeEventListener('resize', resizeHandler);
261
- resizeHandler = null;
262
- }
263
-
264
- // Reset the canvas context
265
- if (canvas) {
266
- const ctx = canvas.getContext('webgl2') || canvas.getContext('webgl');
267
- if (ctx && ctx.getExtension('WEBGL_lose_context')) {
268
- try {
269
- ctx.getExtension('WEBGL_lose_context').loseContext();
270
- } catch (e) {
271
- console.error("Error releasing WebGL context:", e);
272
- }
273
- }
274
- }
275
-
276
- // Destroy PlayCanvas app if it exists
277
- if (app) {
278
- try {
279
- app.destroy();
280
- } catch (e) {
281
- console.error("Error destroying PlayCanvas app:", e);
282
- }
283
- app = null;
284
- }
285
-
286
- // Reset entities
287
- cameraEntity = null;
288
- modelEntity = null;
289
-
290
- // Mark the viewer as not initialized
291
- viewerInitialized = false;
292
-
293
- console.log("Viewer cleanup complete");
294
- }
295
-
296
- // Close button event handler
297
  closeBtn.addEventListener('click', function() {
298
  console.log("Close button clicked");
299
 
@@ -308,22 +188,20 @@ const currentScriptTag = document.currentScript;
308
  fullscreenToggle.textContent = '⇱';
309
  }
310
 
311
- // Clean up the viewer
312
- cleanupViewer();
313
 
314
- // Hide viewer and show GIF
315
  viewerContainer.style.display = 'none';
316
  gifPreview.style.display = 'block';
317
  });
318
 
319
- // Fullscreen toggle handler
320
  fullscreenToggle.addEventListener('click', function() {
321
  if (isIOS) {
322
  if (!widgetContainer.classList.contains('fake-fullscreen')) {
323
  widgetContainer.classList.add('fake-fullscreen');
324
  } else {
325
  widgetContainer.classList.remove('fake-fullscreen');
326
- resetCamera();
327
  }
328
  fullscreenToggle.textContent = widgetContainer.classList.contains('fake-fullscreen') ? '⇲' : '⇱';
329
  } else {
@@ -351,102 +229,25 @@ const currentScriptTag = document.currentScript;
351
  fullscreenToggle.textContent = '⇲';
352
  widgetContainer.style.height = '100%';
353
  widgetContainer.style.paddingBottom = '0';
354
- resetCamera();
355
  } else {
356
  fullscreenToggle.textContent = '⇱';
357
  widgetContainer.style.height = '0';
358
  widgetContainer.style.paddingBottom = aspectPercent;
359
- resetCamera();
360
  }
361
  });
362
 
363
- // Help toggle button
364
  helpToggle.addEventListener('click', function(e) {
365
  e.stopPropagation();
366
  menuContent.style.display = (menuContent.style.display === 'block') ? 'none' : 'block';
367
  });
368
 
369
- // --- Camera Reset Function ---
370
- function resetCamera() {
371
- if (!cameraEntity || !modelEntity || !app) {
372
- console.log("Cannot reset camera - missing entities or app");
373
- return;
374
- }
375
-
376
- try {
377
- // Get the orbit camera script
378
- const orbitCam = cameraEntity.script.orbitCamera;
379
- if (!orbitCam) {
380
- console.log("Cannot reset camera - missing orbit camera script");
381
- return;
382
- }
383
-
384
- // Store model position
385
- const modelPos = modelEntity.getPosition();
386
-
387
- // 1. Create a temporary entity to help calculate new values
388
- const tempEntity = new pc.Entity();
389
- tempEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
390
- tempEntity.lookAt(modelPos);
391
-
392
- // 2. Calculate the distance between camera and model
393
- const distance = new pc.Vec3().sub2(
394
- new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
395
- modelPos
396
- ).length();
397
-
398
- // 3. Set camera position
399
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
400
- cameraEntity.lookAt(modelPos);
401
-
402
- // 4. Update the orbit camera's pivot point
403
- orbitCam.pivotPoint = new pc.Vec3(modelPos.x, modelPos.y, modelPos.z);
404
-
405
- // 5. Set the distance
406
- orbitCam._targetDistance = distance;
407
- orbitCam._distance = distance;
408
-
409
- // 6. Calculate and set yaw and pitch from the camera's rotation
410
- const rotation = tempEntity.getRotation();
411
- const tempForward = new pc.Vec3();
412
- rotation.transformVector(pc.Vec3.FORWARD, tempForward);
413
-
414
- const yaw = Math.atan2(-tempForward.x, -tempForward.z) * pc.math.RAD_TO_DEG;
415
-
416
- const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
417
- const rotWithoutYaw = new pc.Quat().mul2(yawQuat, rotation);
418
- const forwardWithoutYaw = new pc.Vec3();
419
- rotWithoutYaw.transformVector(pc.Vec3.FORWARD, forwardWithoutYaw);
420
- const pitch = Math.atan2(forwardWithoutYaw.y, -forwardWithoutYaw.z) * pc.math.RAD_TO_DEG;
421
-
422
- // Set yaw and pitch directly on internal variables
423
- orbitCam._targetYaw = yaw;
424
- orbitCam._yaw = yaw;
425
- orbitCam._targetPitch = pitch;
426
- orbitCam._pitch = pitch;
427
-
428
- // Force update
429
- if (typeof orbitCam._updatePosition === 'function') {
430
- orbitCam._updatePosition();
431
- }
432
-
433
- // Clean up
434
- tempEntity.destroy();
435
-
436
- console.log("Camera reset complete");
437
-
438
- } catch (error) {
439
- console.error("Error resetting camera:", error);
440
- }
441
- }
442
-
443
- // Reset camera button event handler
444
  resetCameraBtn.addEventListener('click', function() {
445
- console.log("Reset camera button clicked");
446
- resetCamera();
 
 
447
  });
448
 
449
- // Escape key handler for fullscreen exit
450
  document.addEventListener('keydown', function(e) {
451
  if (e.key === 'Escape' || e.key === 'Esc') {
452
  let wasFullscreen = false;
@@ -461,64 +262,14 @@ const currentScriptTag = document.currentScript;
461
  widgetContainer.classList.remove('fake-fullscreen');
462
  fullscreenToggle.textContent = '⇱';
463
  }
464
- if (wasFullscreen) {
465
- resetCamera();
466
- }
467
  }
468
  });
469
 
470
- // --- Prevent app from hijacking all wheel events ---
471
- function setupWheelHandlers() {
472
- // First remove any existing handlers
473
- for (const handler of wheelHandlers) {
474
- const [element, func] = handler;
475
- element.removeEventListener('wheel', func, { passive: false });
476
- }
477
- wheelHandlers = [];
478
-
479
- // Create new wheel handler
480
- const handleWheel = function(event) {
481
- // Check if mouse is over the viewer
482
- if (!isMouseOverViewer) {
483
- // Allow normal page scrolling
484
- return true;
485
- }
486
-
487
- // Otherwise apply zooming, but prevent default only for viewer area
488
- event.stopPropagation();
489
-
490
- if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
491
- const camera = cameraEntity.camera;
492
- const orbitCamera = cameraEntity.script.orbitCamera;
493
- const sensitivity = cameraEntity.script.orbitCameraInputMouse ?
494
- cameraEntity.script.orbitCameraInputMouse.distanceSensitivity || 0.4 : 0.4;
495
-
496
- if (camera.projection === pc.PROJECTION_PERSPECTIVE) {
497
- orbitCamera.distance -= event.deltaY * 0.01 * sensitivity * (orbitCamera.distance * 0.1);
498
- } else {
499
- orbitCamera.orthoHeight -= event.deltaY * 0.01 * sensitivity * (orbitCamera.orthoHeight * 0.1);
500
- }
501
-
502
- event.preventDefault();
503
- }
504
- };
505
-
506
- // Add wheel handlers and store references for cleanup
507
- viewerContainer.addEventListener('wheel', handleWheel, { passive: false });
508
- canvas.addEventListener('wheel', handleWheel, { passive: false });
509
-
510
- // Store handlers for later cleanup
511
- wheelHandlers.push([viewerContainer, handleWheel]);
512
- wheelHandlers.push([canvas, handleWheel]);
513
-
514
- console.log("Wheel handlers set up");
515
- }
516
-
517
- // --- Initialize the 3D PLY Viewer using PlayCanvas ---
518
- async function initializeViewer() {
519
- // Skip initialization if already initialized
520
- if (viewerInitialized || app) {
521
- console.log("Viewer already initialized or app exists, skipping initialization");
522
  return;
523
  }
524
 
@@ -526,232 +277,260 @@ const currentScriptTag = document.currentScript;
526
  progressDialog.style.display = 'block';
527
  progressIndicator.value = 0;
528
 
529
- // Initialize PlayCanvas
530
- const deviceType = "webgl2";
531
- const gfxOptions = {
532
- deviceTypes: [deviceType],
533
- glslangUrl: `https://playcanvas.vercel.app/static/lib/glslang/glslang.js`,
534
- twgslUrl: `https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js`,
535
- antialias: false
536
- };
537
-
538
  try {
539
- // Create graphics device
540
- const device = await pc.createGraphicsDevice(canvas, gfxOptions);
541
- device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
542
-
543
- // Create app
544
- const createOptions = new pc.AppOptions();
545
- createOptions.graphicsDevice = device;
546
- createOptions.mouse = new pc.Mouse(canvas);
547
- createOptions.touch = new pc.TouchDevice(canvas);
548
- createOptions.componentSystems = [
549
- pc.RenderComponentSystem,
550
- pc.CameraComponentSystem,
551
- pc.LightComponentSystem,
552
- pc.ScriptComponentSystem,
553
- pc.GSplatComponentSystem
554
- ];
555
- createOptions.resourceHandlers = [
556
- pc.TextureHandler,
557
- pc.ContainerHandler,
558
- pc.ScriptHandler,
559
- pc.GSplatHandler
560
- ];
561
-
562
- app = new pc.AppBase(canvas);
563
- app.init(createOptions);
564
-
565
- // Set canvas fill mode to match the container
566
- app.setCanvasFillMode(pc.FILLMODE_NONE);
567
- app.setCanvasResolution(pc.RESOLUTION_AUTO);
568
-
569
- // Set scene options
570
- app.scene.exposure = 0.8;
571
- app.scene.toneMapping = pc.TONEMAP_ACES;
572
-
573
- // Handle window resizing
574
- const resize = () => {
575
- if (app) {
576
- app.resizeCanvas(canvas.clientWidth, canvas.clientHeight);
577
- }
578
- };
579
-
580
- // Store resize handler for cleanup
581
- resizeHandler = resize;
582
- window.addEventListener('resize', resizeHandler);
583
-
584
- // Add cleanup when app is destroyed
585
- app.on('destroy', () => {
586
- if (resizeHandler) {
587
- window.removeEventListener('resize', resizeHandler);
588
- resizeHandler = null;
589
- }
590
- if (progressChecker) {
591
- clearInterval(progressChecker);
592
- progressChecker = null;
593
- }
594
- });
595
-
596
- // Load required assets
597
- const assets = {
598
- model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
599
- orbit: new pc.Asset('script', 'script', { url: `https://bilca-visionneur-play-canva-2.static.hf.space/orbit-camera.js` })
600
- };
601
-
602
- // Create asset loader with progress tracking
603
- const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
604
-
605
- // Handle asset loading progress
606
- let lastProgress = 0;
607
- assets.model.on('load', (asset) => {
608
- progressDialog.style.display = 'none';
609
- console.log("Model loaded successfully");
610
- });
611
-
612
- assets.model.on('error', (err) => {
613
- console.error("Error loading PLY file:", err);
614
- progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;
615
- viewerInitialized = false;
616
- });
617
-
618
- // Set up progress monitoring
619
- const checkProgress = () => {
620
- if (!app) {
621
- clearInterval(progressChecker);
622
- progressChecker = null;
623
- return;
624
- }
625
-
626
- if (app && assets.model.resource) {
627
- progressIndicator.value = 100;
628
- clearInterval(progressChecker);
629
- progressChecker = null;
630
- progressDialog.style.display = 'none';
631
- } else if (assets.model.loading) {
632
- // Increment progress for visual feedback
633
- lastProgress += 2;
634
- if (lastProgress > 90) lastProgress = 90; // Cap at 90% until fully loaded
635
- progressIndicator.value = lastProgress;
636
- }
637
- };
638
-
639
- progressChecker = setInterval(checkProgress, 100);
640
 
641
- // Load assets and set up scene
642
- assetListLoader.load(() => {
643
- if (!app) {
644
- console.log("App was destroyed during asset loading");
645
- return;
646
- }
647
-
648
- app.start();
649
-
650
- // Create model entity
651
- modelEntity = new pc.Entity('model');
652
- modelEntity.addComponent('gsplat', {
653
- asset: assets.model
654
- });
655
-
656
- // Position the model using JSON parameters
657
- modelEntity.setLocalPosition(modelX, modelY, modelZ);
658
- modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
659
- modelEntity.setLocalScale(modelScale, modelScale, modelScale);
660
-
661
- app.root.addChild(modelEntity);
662
-
663
- // Create camera entity
664
- cameraEntity = new pc.Entity('camera');
665
- cameraEntity.addComponent('camera', {
666
- clearColor: new pc.Color(
667
- config.canvas_background ? parseInt(config.canvas_background.substr(1, 2), 16) / 255 : 0,
668
- config.canvas_background ? parseInt(config.canvas_background.substr(3, 2), 16) / 255 : 0,
669
- config.canvas_background ? parseInt(config.canvas_background.substr(5, 2), 16) / 255 : 0
670
- ),
671
- toneMapping: pc.TONEMAP_ACES
672
- });
673
-
674
- // Set camera position directly using X, Y, Z coordinates from config
675
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
676
- cameraEntity.lookAt(modelEntity.getPosition());
677
 
678
- // Add orbit camera script for interactive navigation
679
- cameraEntity.addComponent('script');
680
- cameraEntity.script.create('orbitCamera', {
681
- attributes: {
682
- inertiaFactor: 0.2,
683
- focusEntity: modelEntity,
684
- distanceMax: maxZoom,
685
- distanceMin: minZoom,
686
- pitchAngleMax: maxAngle,
687
- pitchAngleMin: minAngle,
688
- yawAngleMax: maxAzimuth,
689
- yawAngleMin: minAzimuth,
690
- frameOnStart: false // Don't auto-frame since we're setting position directly
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
  }
692
- });
 
 
 
 
693
 
694
- // Create input controllers but don't add mouse wheel handling - we handle that separately
695
- cameraEntity.script.create('orbitCameraInputMouse', {
696
- attributes: {
697
- orbitSensitivity: isMobile ? 0.6 : 0.3,
698
- distanceSensitivity: isMobile ? 0.5 : 0.4
699
  }
700
- });
701
 
702
- // Disable wheel event in the orbit camera input
703
- if (cameraEntity.script.orbitCameraInputMouse) {
704
- // Override mouse wheel to do nothing - we handle wheel events separately
705
- cameraEntity.script.orbitCameraInputMouse.onMouseWheel = function() {};
706
- }
 
 
 
 
 
 
 
 
707
 
708
- // Add touch input controller
709
- cameraEntity.script.create('orbitCameraInputTouch', {
710
- attributes: {
711
- orbitSensitivity: 0.6,
712
- distanceSensitivity: 0.5
713
  }
714
- });
715
-
716
- // Initialize the orbit controller
717
- setTimeout(() => {
718
- if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
719
- // Calculate distance from camera to model
720
- const modelPos = modelEntity.getPosition();
721
- const camPos = cameraEntity.getPosition();
722
- const distanceVec = new pc.Vec3();
723
- distanceVec.sub2(camPos, modelPos);
724
- const distance = distanceVec.length();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
725
 
726
- // Set up the orbit controller
727
- cameraEntity.script.orbitCamera.pivotPoint.copy(modelPos);
728
- cameraEntity.script.orbitCamera.distance = distance;
729
- cameraEntity.script.orbitCamera._removeInertia();
 
 
730
  }
731
- }, 100);
732
-
733
- app.root.addChild(cameraEntity);
734
-
735
- // Initial resize to match container
736
- resize();
737
-
738
- // Set up wheel handlers
739
- setupWheelHandlers();
740
-
741
- // Hide progress dialog when everything is set up
742
- progressDialog.style.display = 'none';
743
 
744
- // Mark viewer as initialized
745
- viewerInitialized = true;
746
-
747
- console.log("PLY viewer initialization complete");
748
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
 
750
  } catch (error) {
751
- console.error("Error initializing PlayCanvas viewer:", error);
752
- progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error.message}</p>`;
753
- viewerInitialized = false;
754
- app = null;
 
 
 
 
 
 
 
755
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
756
  }
757
  })();
 
 
 
 
1
  (async function() {
2
+ // Retrieve the current script tag and load the JSON configuration file from the data-config attribute.
3
+ const scriptTag = document.currentScript;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  const configUrl = scriptTag.getAttribute("data-config");
5
  let config = {};
6
  if (configUrl) {
 
24
  document.head.appendChild(linkEl);
25
  }
26
 
27
+ // --- Outer scope variables for viewer state ---
28
+ let SPLAT = null;
29
+ let viewerActive = false;
 
 
 
 
 
30
 
31
  // Generate a unique identifier for this widget instance.
32
  const instanceId = Math.random().toString(36).substr(2, 8);
 
34
  // Read configuration values from the JSON file.
35
  const gifUrl = config.gif_url;
36
  const plyUrl = config.ply_url;
37
+ const minZoom = parseFloat(config.minZoom || "0");
 
 
38
  const maxZoom = parseFloat(config.maxZoom || "20");
39
+ const minAngle = parseFloat(config.minAngle || "0");
40
+ const maxAngle = parseFloat(config.maxAngle || "360");
41
+ const minAzimuth = config.minAzimuth !== undefined ? parseFloat(config.minAzimuth) : -Infinity;
42
+ const maxAzimuth = config.maxAzimuth !== undefined ? parseFloat(config.maxAzimuth) : Infinity;
43
+
44
+ // Read initial orbit parameters for desktop.
45
+ const initAlphaDesktop = config.initAlpha !== undefined ? parseFloat(config.initAlpha) : 0.5;
46
+ const initBetaDesktop = config.initBeta !== undefined ? parseFloat(config.initBeta) : 0.5;
47
+ const initRadiusDesktop = config.initRadius !== undefined ? parseFloat(config.initRadius) : 5;
48
+ // Read initial orbit parameters for phone.
49
+ const initAlphaPhone = config.initAlphaPhone !== undefined ? parseFloat(config.initAlphaPhone) : initAlphaDesktop;
50
+ const initBetaPhone = config.initBetaPhone !== undefined ? parseFloat(config.initBetaPhone) : initBetaDesktop;
51
+ const initRadiusPhone = config.initRadiusPhone !== undefined ? parseFloat(config.initRadiusPhone) : initRadiusDesktop;
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  // Detect if the device is iOS.
54
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
55
  // Also detect Android devices.
56
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
57
 
58
+ // Choose the appropriate initial orbit values based on device type.
59
+ const chosenInitAlpha = isMobile ? initAlphaPhone : initAlphaDesktop;
60
+ const chosenInitBeta = isMobile ? initBetaPhone : initBetaDesktop;
61
+ const chosenInitRadius = isMobile ? initRadiusPhone : initRadiusDesktop;
62
 
63
  // Determine the aspect ratio.
64
  let aspectPercent = "100%";
 
132
  const progressDialog = document.getElementById('progress-dialog-' + instanceId);
133
  const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
134
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  // Set help instructions based on device type.
136
  if (isMobile) {
137
  menuContent.innerHTML = `
 
147
  `;
148
  }
149
 
150
+ // If a gif_url is provided, set the preview image.
151
+ // Otherwise, hide the preview container, show the viewer immediately,
152
+ // and hide the "close" button since there's no preview to return to.
153
  if (gifUrl) {
154
  previewImage.src = gifUrl;
155
  gifPreview.style.display = 'block';
 
158
  gifPreview.style.display = 'none';
159
  viewerContainer.style.display = 'block';
160
  closeBtn.style.display = 'none';
161
+ // Need to load the PLY without a GIF preview
162
  setTimeout(() => {
163
+ loadAndStartViewer();
164
  }, 100);
165
  }
166
 
167
  // --- Button Event Handlers ---
168
  if (gifUrl) {
 
169
  gifPreview.addEventListener('click', function() {
170
  console.log("GIF preview clicked, showing 3D viewer");
171
  gifPreview.style.display = 'none';
172
  viewerContainer.style.display = 'block';
173
+ loadAndStartViewer();
 
 
 
 
174
  });
175
  }
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  closeBtn.addEventListener('click', function() {
178
  console.log("Close button clicked");
179
 
 
188
  fullscreenToggle.textContent = '⇱';
189
  }
190
 
191
+ // Stop the viewer
192
+ stopViewer();
193
 
194
+ // Show GIF and hide viewer
195
  viewerContainer.style.display = 'none';
196
  gifPreview.style.display = 'block';
197
  });
198
 
 
199
  fullscreenToggle.addEventListener('click', function() {
200
  if (isIOS) {
201
  if (!widgetContainer.classList.contains('fake-fullscreen')) {
202
  widgetContainer.classList.add('fake-fullscreen');
203
  } else {
204
  widgetContainer.classList.remove('fake-fullscreen');
 
205
  }
206
  fullscreenToggle.textContent = widgetContainer.classList.contains('fake-fullscreen') ? '⇲' : '⇱';
207
  } else {
 
229
  fullscreenToggle.textContent = '⇲';
230
  widgetContainer.style.height = '100%';
231
  widgetContainer.style.paddingBottom = '0';
 
232
  } else {
233
  fullscreenToggle.textContent = '⇱';
234
  widgetContainer.style.height = '0';
235
  widgetContainer.style.paddingBottom = aspectPercent;
 
236
  }
237
  });
238
 
 
239
  helpToggle.addEventListener('click', function(e) {
240
  e.stopPropagation();
241
  menuContent.style.display = (menuContent.style.display === 'block') ? 'none' : 'block';
242
  });
243
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  resetCameraBtn.addEventListener('click', function() {
245
+ console.log("Reset camera button clicked.");
246
+ if (window.viewer && typeof window.viewer.resetCamera === 'function') {
247
+ window.viewer.resetCamera();
248
+ }
249
  });
250
 
 
251
  document.addEventListener('keydown', function(e) {
252
  if (e.key === 'Escape' || e.key === 'Esc') {
253
  let wasFullscreen = false;
 
262
  widgetContainer.classList.remove('fake-fullscreen');
263
  fullscreenToggle.textContent = '⇱';
264
  }
 
 
 
265
  }
266
  });
267
 
268
+ // --- Load SPLAT Library and Initialize the 3D PLY Viewer ---
269
+ async function loadAndStartViewer() {
270
+ // Don't reinitialize if already active
271
+ if (viewerActive) {
272
+ console.log("Viewer already active, skipping initialization");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  return;
274
  }
275
 
 
277
  progressDialog.style.display = 'block';
278
  progressIndicator.value = 0;
279
 
 
 
 
 
 
 
 
 
 
280
  try {
281
+ // Only load the SPLAT library once
282
+ if (!SPLAT) {
283
+ SPLAT = await import("https://bilca-gsplat-library.static.hf.space/dist/index.js");
284
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
+ // Create a fresh viewer instance
287
+ window.viewer = {
288
+ // Properties
289
+ renderer: null,
290
+ scene: null,
291
+ camera: null,
292
+ controls: null,
293
+ animFrameId: null,
294
+ resizeObserver: null,
295
+ initialCameraPosition: null,
296
+ initialCameraRotation: null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
 
298
+ // Initialize the viewer
299
+ async init() {
300
+ // Create renderer and scene
301
+ this.renderer = new SPLAT.WebGLRenderer(canvas);
302
+ this.scene = new SPLAT.Scene();
303
+
304
+ // Create camera
305
+ this.camera = new SPLAT.Camera();
306
+
307
+ // Store initial camera state
308
+ this.initialCameraPosition = this.camera.position.clone();
309
+ this.initialCameraRotation = this.camera.rotation.clone();
310
+
311
+ // Create controls
312
+ this.controls = new SPLAT.OrbitControls(
313
+ this.camera,
314
+ canvas,
315
+ 0.5,
316
+ 0.5,
317
+ 5,
318
+ true,
319
+ new SPLAT.Vector3(),
320
+ chosenInitAlpha,
321
+ chosenInitBeta,
322
+ chosenInitRadius
323
+ );
324
+
325
+ // Set control constraints
326
+ this.controls.maxZoom = maxZoom;
327
+ this.controls.minZoom = minZoom;
328
+ this.controls.minAngle = minAngle;
329
+ this.controls.maxAngle = maxAngle;
330
+ this.controls.minAzimuth = minAzimuth;
331
+ this.controls.maxAzimuth = maxAzimuth;
332
+ this.controls.panSpeed = isMobile ? 0.5 : 1.2;
333
+
334
+ // Set canvas background
335
+ canvas.style.background = config.canvas_background || "#FEFEFD";
336
+
337
+ // Handle resize
338
+ this.handleResize();
339
+
340
+ // Set up resize observer
341
+ this.resizeObserver = new ResizeObserver(() => {
342
+ this.handleResize();
343
+ });
344
+ this.resizeObserver.observe(canvas);
345
+
346
+ // Update controls
347
+ this.controls.update();
348
+
349
+ // Load the PLY file
350
+ try {
351
+ await SPLAT.PLYLoader.LoadAsync(
352
+ plyUrl,
353
+ this.scene,
354
+ (progress) => {
355
+ progressIndicator.value = progress * 100;
356
+ }
357
+ );
358
+ progressDialog.style.display = 'none';
359
+ } catch (error) {
360
+ console.error("Error loading PLY file:", error);
361
+ progressDialog.innerHTML = `<p style="color: red">Error loading model: ${error.message}</p>`;
362
+ return false;
363
  }
364
+
365
+ // Start rendering
366
+ this.startRenderLoop();
367
+ return true;
368
+ },
369
 
370
+ // Handle resize
371
+ handleResize() {
372
+ if (this.renderer) {
373
+ this.renderer.setSize(canvas.clientWidth, canvas.clientHeight);
 
374
  }
375
+ },
376
 
377
+ // Start the render loop
378
+ startRenderLoop() {
379
+ // Animation frame loop
380
+ const renderFrame = () => {
381
+ if (this.controls && this.scene && this.camera && this.renderer) {
382
+ this.controls.update();
383
+ this.renderer.render(this.scene, this.camera);
384
+ this.animFrameId = requestAnimationFrame(renderFrame);
385
+ }
386
+ };
387
+
388
+ this.animFrameId = requestAnimationFrame(renderFrame);
389
+ },
390
 
391
+ // Reset camera to initial position
392
+ resetCamera() {
393
+ if (!this.camera || !this.initialCameraPosition || !this.initialCameraRotation) {
394
+ console.log("Cannot reset camera - missing camera instance or initial positions");
395
+ return;
396
  }
397
+
398
+ try {
399
+ // Dispose previous controls if they exist
400
+ if (this.controls && typeof this.controls.dispose === 'function') {
401
+ this.controls.dispose();
402
+ }
403
+
404
+ // Set camera back to initial position
405
+ this.camera.position = this.initialCameraPosition.clone();
406
+ this.camera.rotation = this.initialCameraRotation.clone();
407
+
408
+ // Create new controls
409
+ this.controls = new SPLAT.OrbitControls(
410
+ this.camera,
411
+ canvas,
412
+ 0.5,
413
+ 0.5,
414
+ 5,
415
+ true,
416
+ new SPLAT.Vector3(),
417
+ chosenInitAlpha,
418
+ chosenInitBeta,
419
+ chosenInitRadius
420
+ );
421
+
422
+ // Set control constraints
423
+ this.controls.maxZoom = maxZoom;
424
+ this.controls.minZoom = minZoom;
425
+ this.controls.minAngle = minAngle;
426
+ this.controls.maxAngle = maxAngle;
427
+ this.controls.minAzimuth = minAzimuth;
428
+ this.controls.maxAzimuth = maxAzimuth;
429
+ this.controls.panSpeed = isMobile ? 0.5 : 1.2;
430
 
431
+ // Update controls
432
+ this.controls.update();
433
+
434
+ console.log("Camera reset successful");
435
+ } catch (error) {
436
+ console.error("Error resetting camera:", error);
437
  }
438
+ },
 
 
 
 
 
 
 
 
 
 
 
439
 
440
+ // Clean up resources
441
+ dispose() {
442
+ // Cancel animation frame
443
+ if (this.animFrameId) {
444
+ cancelAnimationFrame(this.animFrameId);
445
+ this.animFrameId = null;
446
+ }
447
+
448
+ // Remove resize observer
449
+ if (this.resizeObserver) {
450
+ this.resizeObserver.disconnect();
451
+ this.resizeObserver = null;
452
+ }
453
+
454
+ // Dispose controls
455
+ if (this.controls && typeof this.controls.dispose === 'function') {
456
+ try {
457
+ this.controls.dispose();
458
+ } catch (e) {
459
+ console.warn("Error disposing controls:", e);
460
+ }
461
+ this.controls = null;
462
+ }
463
+
464
+ // Dispose renderer
465
+ if (this.renderer && typeof this.renderer.dispose === 'function') {
466
+ try {
467
+ this.renderer.dispose();
468
+ } catch (e) {
469
+ console.warn("Error disposing renderer:", e);
470
+ }
471
+ this.renderer = null;
472
+ }
473
+
474
+ // Clear scene
475
+ if (this.scene) {
476
+ this.scene = null;
477
+ }
478
+
479
+ // Clear camera
480
+ this.camera = null;
481
+ this.initialCameraPosition = null;
482
+ this.initialCameraRotation = null;
483
+
484
+ console.log("Viewer resources disposed");
485
+ }
486
+ };
487
+
488
+ // Initialize the viewer
489
+ const success = await window.viewer.init();
490
+ if (success) {
491
+ viewerActive = true;
492
+ console.log("PLY viewer started successfully");
493
+ } else {
494
+ viewerActive = false;
495
+ console.error("Failed to initialize PLY viewer");
496
+ }
497
 
498
  } catch (error) {
499
+ console.error("Error in loadAndStartViewer:", error);
500
+ progressDialog.innerHTML = `<p style="color: red">Error initializing viewer: ${error.message}</p>`;
501
+ viewerActive = false;
502
+ }
503
+ }
504
+
505
+ // Stop the viewer and clean up resources
506
+ function stopViewer() {
507
+ if (!viewerActive) {
508
+ console.log("Viewer not active, nothing to stop");
509
+ return;
510
  }
511
+
512
+ console.log("Stopping viewer...");
513
+
514
+ // Dispose viewer resources
515
+ if (window.viewer && typeof window.viewer.dispose === 'function') {
516
+ window.viewer.dispose();
517
+ }
518
+
519
+ // Reset WebGL context if possible
520
+ if (canvas) {
521
+ const ctx = canvas.getContext('webgl') || canvas.getContext('webgl2');
522
+ if (ctx && ctx.getExtension('WEBGL_lose_context')) {
523
+ try {
524
+ ctx.getExtension('WEBGL_lose_context').loseContext();
525
+ } catch (e) {
526
+ console.warn("Error releasing WebGL context:", e);
527
+ }
528
+ }
529
+ }
530
+
531
+ // Mark viewer as not active
532
+ viewerActive = false;
533
+
534
+ console.log("Viewer stopped");
535
  }
536
  })();