MikaFil commited on
Commit
6c5f286
·
verified ·
1 Parent(s): 43cde96

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +165 -184
viewer.js CHANGED
@@ -1,52 +1,48 @@
1
  // viewer.js
2
  // ==============================
3
- // viewer.js
4
- // ==============================
5
 
6
- // -- Module/global state --
7
- let pc; // PlayCanvas module
8
  export let app = null;
9
- let cameraEntity = null;
10
- let modelEntity = null;
11
  let viewerInitialized = false;
12
- let resizeObserver = null;
13
 
14
  let chosenCameraX, chosenCameraY, chosenCameraZ;
15
  let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, minY;
16
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
17
  let plyUrl, glbUrl;
18
 
19
- /**
20
- * initializeViewer(config, instanceId)
21
- */
22
  export async function initializeViewer(config, instanceId) {
23
  if (viewerInitialized) return;
24
 
25
- // Device detection (match Space A)
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 (match Space A variable names/behavior)
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
- modelX = (config.modelX !== undefined) ? parseFloat(config.modelX) : 0;
41
- modelY = (config.modelY !== undefined) ? parseFloat(config.modelY) : 0;
42
- modelZ = (config.modelZ !== undefined) ? parseFloat(config.modelZ) : 0;
43
- modelScale = (config.modelScale !== undefined) ? parseFloat(config.modelScale) : 1;
 
44
  modelRotationX = (config.modelRotationX !== undefined) ? parseFloat(config.modelRotationX) : 0;
45
  modelRotationY = (config.modelRotationY !== undefined) ? parseFloat(config.modelRotationY) : 0;
46
  modelRotationZ = (config.modelRotationZ !== undefined) ? parseFloat(config.modelRotationZ) : 0;
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);
@@ -55,27 +51,23 @@ export async function initializeViewer(config, instanceId) {
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
- // --- Space A style: block, 100% size for iOS layout bugs ---
72
- canvas.style.display = "block";
73
- canvas.style.width = "100%";
74
- canvas.style.height = "100%";
75
  viewerContainer.insertBefore(canvas, progressDialog);
76
 
77
- // 4. Wheel listener
78
- canvas.addEventListener('wheel', e => {
79
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
80
  const orbitCam = cameraEntity.script.orbitCamera;
81
  const sens = (cameraEntity.script.orbitCameraInputMouse?.distanceSensitivity) || 0.4;
@@ -91,28 +83,31 @@ export async function initializeViewer(config, instanceId) {
91
 
92
  progressDialog.style.display = 'block';
93
 
94
- // 5. Import PlayCanvas
95
  if (!pc) {
96
  pc = await import("https://esm.run/playcanvas");
97
  window.pc = pc;
98
  }
99
 
100
- // -- Defer creation of app until all assets are loaded (Space A pattern) --
101
  try {
102
- // 6. Robust WebGL2 → WebGL1 fallback (for iPhone)
103
  async function getGraphicsDevice(preferWebgl2 = true) {
104
  const baseOpts = {
105
- glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
106
- twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
107
- antialias: false
108
  };
109
  try {
110
- return await pc.createGraphicsDevice(canvas, {
111
  ...baseOpts,
112
  deviceTypes: preferWebgl2 ? ["webgl2"] : ["webgl1"]
113
  });
 
 
 
114
  } catch (err) {
115
  if (preferWebgl2) {
 
116
  return getGraphicsDevice(false);
117
  }
118
  throw err;
@@ -122,7 +117,42 @@ export async function initializeViewer(config, instanceId) {
122
  const device = await getGraphicsDevice(true);
123
  device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
124
 
125
- // -- Asset declaration: match structure/order to Space A --
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  const assets = {
127
  model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
128
  orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
@@ -132,17 +162,18 @@ export async function initializeViewer(config, instanceId) {
132
  }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
133
  };
134
 
135
- const loader = new pc.AssetListLoader(Object.values(assets), pc.app && pc.app.assets ? pc.app.assets : undefined);
136
  let lastProg = 0;
137
 
138
- assets.model.on('load', () => {
139
  progressDialog.style.display = 'none';
140
  });
141
- assets.model.on('error', err => {
142
  progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;
 
143
  });
144
 
145
- const progCheck = setInterval(() => {
146
  if (assets.model.resource) {
147
  progressIndicator.value = 100;
148
  clearInterval(progCheck);
@@ -153,56 +184,12 @@ export async function initializeViewer(config, instanceId) {
153
  }
154
  }, 100);
155
 
156
- // -- Load all assets, then initialize the App (Space A sequence) --
157
- loader.load(async () => {
158
- // -- AppOptions: match Space A (do not add extra physics/collision if not used) --
159
- const opts = new pc.AppOptions();
160
- opts.graphicsDevice = device;
161
- opts.mouse = new pc.Mouse(canvas);
162
- opts.touch = new pc.TouchDevice(canvas);
163
- opts.componentSystems = [
164
- pc.RenderComponentSystem,
165
- pc.CameraComponentSystem,
166
- pc.LightComponentSystem,
167
- pc.ScriptComponentSystem,
168
- pc.GSplatComponentSystem
169
- ];
170
- opts.resourceHandlers = [
171
- pc.TextureHandler,
172
- pc.ContainerHandler,
173
- pc.ScriptHandler,
174
- pc.GSplatHandler
175
- ];
176
-
177
- // -- Actually create and start the App --
178
- app = new pc.Application(canvas, opts);
179
- app.setCanvasFillMode(pc.FILLMODE_NONE);
180
- app.setCanvasResolution(pc.RESOLUTION_AUTO);
181
- app.scene.exposure = 0.5;
182
- app.scene.toneMapping = pc.TONEMAP_ACES;
183
-
184
- // -- Resize observer (Space A logic) --
185
- resizeObserver = new ResizeObserver(entries => {
186
- for (const entry of entries) {
187
- const { width, height } = entry.contentRect;
188
- if (app) {
189
- app.resizeCanvas(width, height);
190
- }
191
- }
192
- });
193
- resizeObserver.observe(viewerContainer);
194
-
195
- window.addEventListener('resize', () => {
196
- if (app) app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
197
- });
198
- app.on('destroy', () => {
199
- window.removeEventListener('resize', resizeCanvas);
200
- });
201
 
202
- // -- Load HDR environment --
203
  app.scene.envAtlas = assets.hdr.resource;
204
 
205
- // -- Build the scene graph --
206
  modelEntity = new pc.Entity('model');
207
  modelEntity.addComponent('gsplat', { asset: assets.model });
208
  modelEntity.setLocalPosition(modelX, modelY, modelZ);
@@ -210,7 +197,7 @@ export async function initializeViewer(config, instanceId) {
210
  modelEntity.setLocalScale(modelScale, modelScale, modelScale);
211
  app.root.addChild(modelEntity);
212
 
213
- // Directional light (same as Space A)
214
  const dirLight = new pc.Entity('Cascaded Light');
215
  dirLight.addComponent('light', {
216
  type: 'directional',
@@ -228,30 +215,23 @@ export async function initializeViewer(config, instanceId) {
228
  shadowType: pc.SHADOW_PCSS_32F,
229
  shadowDistance: 1000
230
  });
231
- dirLight.setLocalEulerAngles(0, 0, 0);
232
  app.root.addChild(dirLight);
 
233
 
234
- // Gallery container (GLB)
235
- if (assets.galerie && assets.galerie.resource && assets.galerie.resource.instantiateRenderEntity) {
236
- const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
237
- app.root.addChild(galleryEntity);
238
- }
239
 
240
- // Camera entity (Space A logic, with true color init)
241
  cameraEntity = new pc.Entity('camera');
242
- // Color: set to black by default unless config.canvas_background is hex
243
- let clearColor = new pc.Color(0, 0, 0);
244
- if (config.canvas_background && config.canvas_background[0] === "#") {
245
- // #RRGGBB
246
- const hex = config.canvas_background;
247
- clearColor = new pc.Color(
248
- parseInt(hex.substr(1, 2), 16) / 255,
249
- parseInt(hex.substr(3, 2), 16) / 255,
250
- parseInt(hex.substr(5, 2), 16) / 255
251
- );
252
- }
253
  cameraEntity.addComponent('camera', {
254
- clearColor: clearColor,
 
 
 
 
 
 
255
  toneMapping: pc.TONEMAP_ACES
256
  });
257
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
@@ -289,8 +269,24 @@ export async function initializeViewer(config, instanceId) {
289
  });
290
  app.root.addChild(cameraEntity);
291
 
292
- // -- Per-frame update guard for minY (same as Space A) --
293
- app.on('update', dt => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  if (cameraEntity) {
295
  const pos = cameraEntity.getPosition();
296
  if (pos.y < minY) {
@@ -299,92 +295,82 @@ export async function initializeViewer(config, instanceId) {
299
  }
300
  });
301
 
302
- // -- Camera reset on first frame --
303
- app.once('update', () => resetViewerCamera());
304
-
305
- // -- Insert iOS-only first-draw debug popup (one-shot) --
306
- if (isIOS) {
307
- let iosAlertShown = false;
308
- app.on('frameend', () => {
309
- if (!iosAlertShown) {
310
- iosAlertShown = true;
311
- alert('First draw call reached (iOS)');
312
- }
 
 
 
 
 
313
  });
314
  }
315
 
316
- // -- Start PlayCanvas app last --
317
- app.start();
318
 
319
- // -- Tooltips support, try/catch required (unchanged) --
320
- try {
321
- const tooltipsModule = await import('./tooltips.js');
322
- tooltipsModule.initializeTooltips({
323
- app,
324
- cameraEntity,
325
- modelEntity,
326
- tooltipsUrl: config.tooltips_url,
327
- defaultVisible: !!config.showTooltipsDefault,
328
- moveDuration: config.tooltipMoveDuration || 0.6
329
  });
330
- } catch (e) {
331
- // Silent fail: tooltips are optional
332
  }
333
-
334
- progressDialog.style.display = 'none';
335
- viewerInitialized = true;
336
  });
337
 
338
  } catch (error) {
339
  progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error.message}</p>`;
 
340
  }
341
  }
342
 
343
  // -----------------------------------------------------------------------------
344
- // resetViewerCamera / cleanupViewer remain UNCHANGED
345
- // -----------------------------------------------------------------------------
346
  export function resetViewerCamera() {
347
  try {
348
  if (!cameraEntity || !modelEntity || !app) return;
349
  const orbitCam = cameraEntity.script.orbitCamera;
350
  if (!orbitCam) return;
351
-
352
  const modelPos = modelEntity.getPosition();
353
- const tempEnt = new pc.Entity();
354
- tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
355
- tempEnt.lookAt(modelPos);
356
-
357
- const dist = new pc.Vec3().sub2(
358
- new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
359
- modelPos
360
- ).length();
361
-
362
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
363
  cameraEntity.lookAt(modelPos);
364
-
365
  orbitCam.pivotPoint = modelPos.clone();
366
- orbitCam._targetDistance = dist;
367
- orbitCam._distance = dist;
368
-
369
- const rot = tempEnt.getRotation();
370
- const fwd = new pc.Vec3();
371
- rot.transformVector(pc.Vec3.FORWARD, fwd);
372
-
373
- const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
374
  const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
375
- const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
376
- const fNoYaw = new pc.Vec3();
377
- rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
378
- const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
379
-
380
  orbitCam._targetYaw = yaw;
381
  orbitCam._yaw = yaw;
382
  orbitCam._targetPitch = pitch;
383
  orbitCam._pitch = pitch;
384
- orbitCam._updatePosition && orbitCam._updatePosition();
385
-
386
- tempEnt.destroy();
387
- } catch (e) {}
 
 
 
388
  }
389
 
390
  export function cleanupViewer() {
@@ -395,11 +381,6 @@ export function cleanupViewer() {
395
  app = null;
396
  }
397
  cameraEntity = null;
398
- modelEntity = null;
399
  viewerInitialized = false;
400
-
401
- if (resizeObserver) {
402
- resizeObserver.disconnect();
403
- resizeObserver = null;
404
- }
405
  }
 
1
  // viewer.js
2
  // ==============================
3
+ // (Space B: iPhone/desktop/Android compatible, matches Space A + minimal iOS fix)
 
4
 
5
+ let pc; // PlayCanvas module
 
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
  export async function initializeViewer(config, instanceId) {
17
  if (viewerInitialized) return;
18
 
19
+ // Detect iOS/mobile for camera parameters and debug alert
20
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
21
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
22
 
23
+ // 1. Read config
24
  plyUrl = config.ply_url;
25
  glbUrl = config.glb_url;
26
+ minZoom = parseFloat(config.minZoom || "1");
27
+ maxZoom = parseFloat(config.maxZoom || "20");
28
+ minAngle = parseFloat(config.minAngle || "-45");
29
+ maxAngle = parseFloat(config.maxAngle || "90");
30
+ minAzimuth = (config.minAzimuth !== undefined) ? parseFloat(config.minAzimuth) : -360;
31
+ maxAzimuth = (config.maxAzimuth !== undefined) ? parseFloat(config.maxAzimuth) : 360;
32
+ minPivotY = parseFloat(config.minPivotY || "0");
33
+ minY = (config.minY !== undefined) ? parseFloat(config.minY) : 0;
34
+
35
+ modelX = (config.modelX !== undefined) ? parseFloat(config.modelX) : 0;
36
+ modelY = (config.modelY !== undefined) ? parseFloat(config.modelY) : 0;
37
+ modelZ = (config.modelZ !== undefined) ? parseFloat(config.modelZ) : 0;
38
+ modelScale = (config.modelScale !== undefined) ? parseFloat(config.modelScale) : 1;
39
  modelRotationX = (config.modelRotationX !== undefined) ? parseFloat(config.modelRotationX) : 0;
40
  modelRotationY = (config.modelRotationY !== undefined) ? parseFloat(config.modelRotationY) : 0;
41
  modelRotationZ = (config.modelRotationZ !== undefined) ? parseFloat(config.modelRotationZ) : 0;
42
+
43
+ const cameraX = (config.cameraX !== undefined) ? parseFloat(config.cameraX) : 0;
44
+ const cameraY = (config.cameraY !== undefined) ? parseFloat(config.cameraY) : 2;
45
+ const cameraZ = (config.cameraZ !== undefined) ? parseFloat(config.cameraZ) : 5;
46
  const cameraXPhone = (config.cameraXPhone !== undefined) ? parseFloat(config.cameraXPhone) : cameraX;
47
  const cameraYPhone = (config.cameraYPhone !== undefined) ? parseFloat(config.cameraYPhone) : cameraY;
48
  const cameraZPhone = (config.cameraZPhone !== undefined) ? parseFloat(config.cameraZPhone) : (cameraZ * 1.5);
 
51
  chosenCameraY = isMobile ? cameraYPhone : cameraY;
52
  chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
53
 
54
+ // 2. DOM references
55
+ const canvasId = 'canvas-' + instanceId;
56
+ const progressDialog = document.getElementById('progress-dialog-' + instanceId);
57
  const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
58
+ const viewerContainer = document.getElementById('viewer-container-' + instanceId);
59
 
60
+ // 3. Canvas creation (replace old if present)
61
  let oldCanvas = document.getElementById(canvasId);
62
  if (oldCanvas) oldCanvas.remove();
63
  const canvas = document.createElement('canvas');
64
  canvas.id = canvasId;
65
  canvas.className = 'ply-canvas';
66
  canvas.style.zIndex = "1";
 
 
 
 
67
  viewerContainer.insertBefore(canvas, progressDialog);
68
 
69
+ // 4. Mouse wheel (zoom) handler
70
+ canvas.addEventListener('wheel', function(e) {
71
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
72
  const orbitCam = cameraEntity.script.orbitCamera;
73
  const sens = (cameraEntity.script.orbitCameraInputMouse?.distanceSensitivity) || 0.4;
 
83
 
84
  progressDialog.style.display = 'block';
85
 
86
+ // 5. Load PlayCanvas module if needed
87
  if (!pc) {
88
  pc = await import("https://esm.run/playcanvas");
89
  window.pc = pc;
90
  }
91
 
 
92
  try {
93
+ // 6. Setup graphics device (WebGL2 → WebGL1 fallback for iPhone)
94
  async function getGraphicsDevice(preferWebgl2 = true) {
95
  const baseOpts = {
96
+ glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
97
+ twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
98
+ antialias: false
99
  };
100
  try {
101
+ const dev = await pc.createGraphicsDevice(canvas, {
102
  ...baseOpts,
103
  deviceTypes: preferWebgl2 ? ["webgl2"] : ["webgl1"]
104
  });
105
+ // Optionally alert for iPhone debug
106
+ // if (isIOS) alert(preferWebgl2 ? "WebGL2 context OK" : "WebGL1 context OK");
107
+ return dev;
108
  } catch (err) {
109
  if (preferWebgl2) {
110
+ // if (isIOS) alert("WebGL2 unavailable; falling back to WebGL1");
111
  return getGraphicsDevice(false);
112
  }
113
  throw err;
 
117
  const device = await getGraphicsDevice(true);
118
  device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
119
 
120
+ // Use the *Space A* method: AppBase + .init(options)
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
+ ];
132
+ opts.resourceHandlers = [
133
+ pc.TextureHandler,
134
+ pc.ContainerHandler,
135
+ pc.ScriptHandler,
136
+ pc.GSplatHandler
137
+ ];
138
+
139
+ app = new pc.AppBase(canvas);
140
+ app.init(opts);
141
+ app.setCanvasFillMode(pc.FILLMODE_NONE);
142
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
143
+ app.scene.exposure = 0.5;
144
+ app.scene.toneMapping = pc.TONEMAP_ACES;
145
+
146
+ // 7. Resize logic (like Space A)
147
+ function resize() {
148
+ if (app) {
149
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
150
+ }
151
+ }
152
+ window.addEventListener('resize', resize);
153
+ app.on('destroy', () => window.removeEventListener('resize', resize));
154
+
155
+ // 8. Assets
156
  const assets = {
157
  model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
158
  orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
 
162
  }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
163
  };
164
 
165
+ const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
166
  let lastProg = 0;
167
 
168
+ assets.model.on('load', function() {
169
  progressDialog.style.display = 'none';
170
  });
171
+ assets.model.on('error', function(err) {
172
  progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;
173
+ console.error("Error loading PLY file:", err);
174
  });
175
 
176
+ const progCheck = setInterval(function() {
177
  if (assets.model.resource) {
178
  progressIndicator.value = 100;
179
  clearInterval(progCheck);
 
184
  }
185
  }, 100);
186
 
187
+ loader.load(function() {
188
+ app.start();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
 
190
  app.scene.envAtlas = assets.hdr.resource;
191
 
192
+ // Model
193
  modelEntity = new pc.Entity('model');
194
  modelEntity.addComponent('gsplat', { asset: assets.model });
195
  modelEntity.setLocalPosition(modelX, modelY, modelZ);
 
197
  modelEntity.setLocalScale(modelScale, modelScale, modelScale);
198
  app.root.addChild(modelEntity);
199
 
200
+ // Light
201
  const dirLight = new pc.Entity('Cascaded Light');
202
  dirLight.addComponent('light', {
203
  type: 'directional',
 
215
  shadowType: pc.SHADOW_PCSS_32F,
216
  shadowDistance: 1000
217
  });
 
218
  app.root.addChild(dirLight);
219
+ dirLight.setLocalEulerAngles(0, 0, 0);
220
 
221
+ // Gallery (GLB)
222
+ const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
223
+ app.root.addChild(galleryEntity);
 
 
224
 
225
+ // Camera
226
  cameraEntity = new pc.Entity('camera');
 
 
 
 
 
 
 
 
 
 
 
227
  cameraEntity.addComponent('camera', {
228
+ clearColor: config.canvas_background
229
+ ? new pc.Color(
230
+ parseInt(config.canvas_background.substr(1, 2), 16) / 255,
231
+ parseInt(config.canvas_background.substr(3, 2), 16) / 255,
232
+ parseInt(config.canvas_background.substr(5, 2), 16) / 255
233
+ )
234
+ : new pc.Color(0, 0, 0),
235
  toneMapping: pc.TONEMAP_ACES
236
  });
237
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
 
269
  });
270
  app.root.addChild(cameraEntity);
271
 
272
+ // Initial camera reset
273
+ setTimeout(function() {
274
+ if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
275
+ const modelPos = modelEntity.getPosition();
276
+ const camPos = cameraEntity.getPosition();
277
+ const distanceVec = new pc.Vec3();
278
+ distanceVec.sub2(camPos, modelPos);
279
+ const distance = distanceVec.length();
280
+ cameraEntity.script.orbitCamera.pivotPoint.copy(modelPos);
281
+ cameraEntity.script.orbitCamera.distance = distance;
282
+ if (typeof cameraEntity.script.orbitCamera._removeInertia === 'function') {
283
+ cameraEntity.script.orbitCamera._removeInertia();
284
+ }
285
+ }
286
+ }, 100);
287
+
288
+ // Per-frame Y guard (for camera)
289
+ app.on('update', function(dt) {
290
  if (cameraEntity) {
291
  const pos = cameraEntity.getPosition();
292
  if (pos.y < minY) {
 
295
  }
296
  });
297
 
298
+ // Initial resize
299
+ resize();
300
+
301
+ // Tooltips support (optional)
302
+ if (config.tooltips_url) {
303
+ import('./tooltips.js').then(tooltipsModule => {
304
+ tooltipsModule.initializeTooltips({
305
+ app,
306
+ cameraEntity,
307
+ modelEntity,
308
+ tooltipsUrl: config.tooltips_url,
309
+ defaultVisible: !!config.showTooltipsDefault,
310
+ moveDuration: config.tooltipMoveDuration || 0.6
311
+ });
312
+ }).catch(e => {
313
+ console.error("Error loading tooltips.js:", e);
314
  });
315
  }
316
 
317
+ viewerInitialized = true;
 
318
 
319
+ // Debug: show an alert on iPhone only on first frame rendered
320
+ if (isIOS) {
321
+ let firstDraw = true;
322
+ app.on('frameend', function() {
323
+ if (firstDraw) {
324
+ alert("First draw call reached (iOS)");
325
+ firstDraw = false;
326
+ }
 
 
327
  });
 
 
328
  }
 
 
 
329
  });
330
 
331
  } catch (error) {
332
  progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error.message}</p>`;
333
+ console.error("Error initializing PlayCanvas viewer:", error);
334
  }
335
  }
336
 
337
  // -----------------------------------------------------------------------------
338
+ // resetViewerCamera: (unchanged; matches Space A)
 
339
  export function resetViewerCamera() {
340
  try {
341
  if (!cameraEntity || !modelEntity || !app) return;
342
  const orbitCam = cameraEntity.script.orbitCamera;
343
  if (!orbitCam) return;
 
344
  const modelPos = modelEntity.getPosition();
345
+ const tempEntity = new pc.Entity();
346
+ tempEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
347
+ tempEntity.lookAt(modelPos);
348
+ const distance = new pc.Vec3().sub2(new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ), modelPos).length();
 
 
 
 
 
349
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
350
  cameraEntity.lookAt(modelPos);
 
351
  orbitCam.pivotPoint = modelPos.clone();
352
+ orbitCam._targetDistance = distance;
353
+ orbitCam._distance = distance;
354
+ const rotation = tempEntity.getRotation();
355
+ const tempForward = new pc.Vec3();
356
+ rotation.transformVector(pc.Vec3.FORWARD, tempForward);
357
+ const yaw = Math.atan2(-tempForward.x, -tempForward.z) * pc.math.RAD_TO_DEG;
 
 
358
  const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
359
+ const rotWithoutYaw = new pc.Quat().mul2(yawQuat, rotation);
360
+ const forwardWithoutYaw = new pc.Vec3();
361
+ rotWithoutYaw.transformVector(pc.Vec3.FORWARD, forwardWithoutYaw);
362
+ const pitch = Math.atan2(forwardWithoutYaw.y, -forwardWithoutYaw.z) * pc.math.RAD_TO_DEG;
 
363
  orbitCam._targetYaw = yaw;
364
  orbitCam._yaw = yaw;
365
  orbitCam._targetPitch = pitch;
366
  orbitCam._pitch = pitch;
367
+ if (typeof orbitCam._updatePosition === 'function') {
368
+ orbitCam._updatePosition();
369
+ }
370
+ tempEntity.destroy();
371
+ } catch (e) {
372
+ console.error("Error resetting camera:", e);
373
+ }
374
  }
375
 
376
  export function cleanupViewer() {
 
381
  app = null;
382
  }
383
  cameraEntity = null;
384
+ modelEntity = null;
385
  viewerInitialized = false;
 
 
 
 
 
386
  }