MikaFil commited on
Commit
5c49f23
·
verified ·
1 Parent(s): 3b9791b

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +226 -263
viewer.js CHANGED
@@ -1,283 +1,246 @@
1
  // viewer.js
2
- // ==============================
 
3
 
4
- let pc;
5
  export let app = null;
6
  let cameraEntity = null;
7
  let modelEntity = null;
8
  let viewerInitialized = false;
9
  let resizeObserver = null;
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 sogsJsonUrl;
15
 
16
  export async function initializeViewer(config, instanceId) {
17
- if (viewerInitialized) return;
18
-
19
- const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
20
- const isMobile = isIOS || /Android/i.test(navigator.userAgent);
21
-
22
- sogsJsonUrl = config.sogs_json_url;
23
- if (!sogsJsonUrl) {
24
- alert('Missing config.sogs_json_url for SOGS/webp loading.');
25
- return;
26
- }
27
-
28
- minZoom = parseFloat(config.minZoom || "1");
29
- maxZoom = parseFloat(config.maxZoom || "20");
30
- minAngle = parseFloat(config.minAngle || "-45");
31
- maxAngle = parseFloat(config.maxAngle || "90");
32
- minAzimuth = (config.minAzimuth !== undefined) ? parseFloat(config.minAzimuth) : -360;
33
- maxAzimuth = (config.maxAzimuth !== undefined) ? parseFloat(config.maxAzimuth) : 360;
34
- minPivotY = parseFloat(config.minPivotY || "0");
35
- minY = (config.minY !== undefined) ? parseFloat(config.minY) : 0;
36
-
37
- modelX = (config.modelX !== undefined) ? parseFloat(config.modelX) : 0;
38
- modelY = (config.modelY !== undefined) ? parseFloat(config.modelY) : 0;
39
- modelZ = (config.modelZ !== undefined) ? parseFloat(config.modelZ) : 0;
40
- modelScale = (config.modelScale !== undefined) ? parseFloat(config.modelScale) : 1;
41
- modelRotationX = (config.modelRotationX !== undefined) ? parseFloat(config.modelRotationX) : 0;
42
- modelRotationY = (config.modelRotationY !== undefined) ? parseFloat(config.modelRotationY) : 0;
43
- modelRotationZ = (config.modelRotationZ !== undefined) ? parseFloat(config.modelRotationZ) : 0;
44
-
45
- const cameraX = (config.cameraX !== undefined) ? parseFloat(config.cameraX) : 0;
46
- const cameraY = (config.cameraY !== undefined) ? parseFloat(config.cameraY) : 2;
47
- const cameraZ = (config.cameraZ !== undefined) ? parseFloat(config.cameraZ) : 5;
48
- const cameraXPhone = (config.cameraXPhone !== undefined) ? parseFloat(config.cameraXPhone) : cameraX;
49
- const cameraYPhone = (config.cameraYPhone !== undefined) ? parseFloat(config.cameraYPhone) : cameraY;
50
- const cameraZPhone = (config.cameraZPhone !== undefined) ? parseFloat(config.cameraZPhone) : (cameraZ * 1.5);
51
-
52
- chosenCameraX = isMobile ? cameraXPhone : cameraX;
53
- chosenCameraY = isMobile ? cameraYPhone : cameraY;
54
- chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
55
-
56
- const canvasId = 'canvas-' + instanceId;
57
- const progressDialog = document.getElementById('progress-dialog-' + instanceId);
58
- const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
59
- const viewerContainer = document.getElementById('viewer-container-' + instanceId);
60
-
61
- let oldCanvas = document.getElementById(canvasId);
62
- if (oldCanvas) oldCanvas.remove();
63
-
64
- const canvas = document.createElement('canvas');
65
- canvas.id = canvasId;
66
- canvas.className = 'ply-canvas';
67
- canvas.style.width = "100%";
68
- canvas.style.height = "100%";
69
- canvas.setAttribute('tabindex', '0');
70
- viewerContainer.insertBefore(canvas, progressDialog);
71
-
72
- canvas.style.touchAction = "none";
73
- canvas.style.webkitTouchCallout = "none";
74
- canvas.addEventListener('gesturestart', e => e.preventDefault());
75
- canvas.addEventListener('gesturechange', e => e.preventDefault());
76
- canvas.addEventListener('gestureend', e => e.preventDefault());
77
- canvas.addEventListener('dblclick', e => e.preventDefault());
78
- canvas.addEventListener('touchstart', e => { if (e.touches.length > 1) e.preventDefault(); }, { passive: false });
79
- canvas.addEventListener('wheel', (e) => { e.preventDefault(); }, { passive: false });
80
-
81
- progressDialog.style.display = 'block';
82
-
83
- if (!pc) {
84
- pc = await import("https://cdn.jsdelivr.net/npm/playcanvas/build/playcanvas.module.min.js");
85
- window.pc = pc;
86
- }
87
-
88
- // Create PlayCanvas app
89
- const device = await pc.createGraphicsDevice(canvas, {
90
- deviceTypes: ["webgl2"],
91
- glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
92
- twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
93
- antialias: false
94
- });
95
- device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
96
-
97
- const opts = new pc.AppOptions();
98
- opts.graphicsDevice = device;
99
- opts.mouse = new pc.Mouse(canvas);
100
- opts.touch = new pc.TouchDevice(canvas);
101
- opts.componentSystems = [
102
- pc.RenderComponentSystem,
103
- pc.CameraComponentSystem,
104
- pc.LightComponentSystem,
105
- pc.ScriptComponentSystem,
106
- pc.CollisionComponentSystem,
107
- pc.RigidbodyComponentSystem
108
- ];
109
- opts.resourceHandlers = [
110
- pc.TextureHandler,
111
- pc.ContainerHandler,
112
- pc.ScriptHandler
113
- ];
114
-
115
- app = new pc.Application(canvas, opts);
116
- app.setCanvasFillMode(pc.FILLMODE_NONE);
117
- app.setCanvasResolution(pc.RESOLUTION_AUTO);
118
-
119
- resizeObserver = new ResizeObserver(entries => {
120
- entries.forEach(entry => {
121
- app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
122
- });
123
- });
124
- resizeObserver.observe(viewerContainer);
125
-
126
- window.addEventListener('resize', () => app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight));
127
- app.on('destroy', () => resizeObserver.disconnect());
128
-
129
- // Load orbit camera scripts and gsplat script
130
- const orbitAsset = new pc.Asset('orbit', 'script', { url: "https://mikafil-viewer-sgos.static.hf.space/orbit-camera.js" });
131
- app.assets.add(orbitAsset);
132
- const gsplatAsset = new pc.Asset('gsplat', 'script', { url: "https://mikafil-viewer-sgos.static.hf.space/gsplat.js" }); // If using relative URL
133
-
134
- app.assets.add(gsplatAsset);
135
-
136
- // Fetch SOGS meta.json and webp textures
137
- let sogsData, sogsTextures = {};
138
- try {
139
- const jsonResp = await fetch(sogsJsonUrl);
140
- sogsData = await jsonResp.json();
141
-
142
- // Parse meta to get file list
143
- let meta = sogsData;
144
- let filesSet = new Set();
145
- if (meta.means && meta.means.files) meta.means.files.forEach(f => filesSet.add(f));
146
- if (meta.scales && meta.scales.files) meta.scales.files.forEach(f => filesSet.add(f));
147
- if (meta.quats && meta.quats.files) meta.quats.files.forEach(f => filesSet.add(f));
148
- if (meta.sh0 && meta.sh0.files) meta.sh0.files.forEach(f => filesSet.add(f));
149
-
150
- // For each, fetch as a Texture
151
- let baseUrl = sogsJsonUrl.split('/').slice(0, -1).join('/');
152
- for (let fname of filesSet) {
153
- let url = fname.startsWith('http') ? fname : `${baseUrl}/${fname}`;
154
- // Use PlayCanvas Texture Asset
155
- sogsTextures[fname] = await new Promise((resolve, reject) => {
156
- const asset = new pc.Asset(fname, 'texture', { url });
157
- app.assets.add(asset);
158
- asset.ready(() => resolve(asset.resource));
159
- asset.on('error', reject);
160
- app.assets.load(asset);
161
- });
162
  }
163
- } catch (e) {
164
- progressDialog.style.display = 'none';
165
- alert("Failed to load SOGS JSON/images: " + e.message);
166
- throw e;
167
- }
168
-
169
- // Continue after both scripts loaded
170
- orbitAsset.ready(() => {
171
- gsplatAsset.ready(() => {
172
- app.start();
173
- progressDialog.style.display = 'none';
174
-
175
- // Add GSplat entity and script
176
- modelEntity = new pc.Entity('model');
177
- modelEntity.addComponent('script');
178
- modelEntity.script.create('gsplat', {
179
- attributes: {
180
- sogsData: sogsData,
181
- textures: sogsTextures
182
- }
183
- });
184
-
185
- modelEntity.setLocalPosition(modelX, modelY, modelZ);
186
- modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
187
- modelEntity.setLocalScale(modelScale, modelScale, modelScale);
188
- app.root.addChild(modelEntity);
189
-
190
- // Camera entity
191
- cameraEntity = new pc.Entity('camera');
192
- cameraEntity.addComponent('camera', { clearColor: new pc.Color(1, 1, 1, 1) });
193
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
194
- cameraEntity.lookAt(modelEntity.getPosition());
195
- cameraEntity.addComponent('script');
196
- cameraEntity.script.create('orbitCamera', {
197
- attributes: {
198
- focusEntity: modelEntity,
199
- inertiaFactor: 0.2,
200
- distanceMax: maxZoom,
201
- distanceMin: minZoom,
202
- pitchAngleMax: maxAngle,
203
- pitchAngleMin: minAngle,
204
- yawAngleMax: maxAzimuth,
205
- yawAngleMin: minAzimuth,
206
- minY: minY,
207
- frameOnStart: false
208
- }
209
- });
210
- cameraEntity.script.create('orbitCameraInputMouse');
211
- cameraEntity.script.create('orbitCameraInputTouch');
212
- app.root.addChild(cameraEntity);
213
-
214
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
215
- app.once('update', () => resetViewerCamera());
216
-
217
- // Optional: tooltips
218
- try {
219
- if (config.tooltips_url) {
220
- import('https://mikafil-viewer-sgos.static.hf.space/tooltips.js').then(tooltipsModule => {
221
- tooltipsModule.initializeTooltips({
222
- app,
223
- cameraEntity,
224
- modelEntity,
225
- tooltipsUrl: config.tooltips_url,
226
- defaultVisible: !!config.showTooltipsDefault,
227
- moveDuration: config.tooltipMoveDuration || 0.6
228
- });
229
- }).catch(e => {});
230
- }
231
- } catch (e) {}
232
-
233
- viewerInitialized = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  });
235
- app.assets.load(gsplatAsset);
236
- });
237
- app.assets.load(orbitAsset);
238
  }
239
 
240
  // Reset camera helper
241
  export function resetViewerCamera() {
242
- try {
243
- if (!cameraEntity || !modelEntity || !app) return;
244
- const orbitCam = cameraEntity.script.orbitCamera;
245
- if (!orbitCam) return;
246
-
247
- const modelPos = modelEntity.getPosition();
248
- const tempEnt = new pc.Entity();
249
- tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
250
- tempEnt.lookAt(modelPos);
251
-
252
- const dist = new pc.Vec3().sub2(
253
- new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
254
- modelPos
255
- ).length();
256
-
257
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
258
- cameraEntity.lookAt(modelPos);
259
-
260
- orbitCam.pivotPoint = modelPos.clone();
261
- orbitCam._targetDistance = dist;
262
- orbitCam._distance = dist;
263
-
264
- const rot = tempEnt.getRotation();
265
- const fwd = new pc.Vec3();
266
- rot.transformVector(pc.Vec3.FORWARD, fwd);
267
-
268
- const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
269
- const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
270
- const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
271
- const fNoYaw = new pc.Vec3();
272
- rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
273
- const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
274
-
275
- orbitCam._targetYaw = yaw;
276
- orbitCam._yaw = yaw;
277
- orbitCam._targetPitch = pitch;
278
- orbitCam._pitch = pitch;
279
- if (orbitCam._updatePosition) orbitCam._updatePosition();
280
-
281
- tempEnt.destroy();
282
- } catch (e) {}
 
 
283
  }
 
1
  // viewer.js
2
+ // Modern PlayCanvas 2.8.1+ with SOGS/GSplat
3
+ import * as pc from 'https://cdn.jsdelivr.net/npm/playcanvas@2.8.1/build/playcanvas.module.js';
4
 
 
5
  export let app = null;
6
  let cameraEntity = null;
7
  let modelEntity = null;
8
  let viewerInitialized = false;
9
  let resizeObserver = null;
10
 
11
+ // Camera and model params
12
  let chosenCameraX, chosenCameraY, chosenCameraZ;
13
  let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, minY;
14
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
 
15
 
16
  export async function initializeViewer(config, instanceId) {
17
+ if (viewerInitialized) return;
18
+
19
+ // Params
20
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
21
+ const isMobile = isIOS || /Android/i.test(navigator.userAgent);
22
+
23
+ // URLs and config
24
+ const sogsUrl = config.sogs_json_url;
25
+ if (!sogsUrl) {
26
+ alert('Missing SOGS (meta.json) URL in config.sogs_json_url.');
27
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
29
+
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
+ // DOM Elements
59
+ const canvasId = 'canvas-' + instanceId;
60
+ const progressDialog = document.getElementById('progress-dialog-' + instanceId);
61
+ const viewerContainer = document.getElementById('viewer-container-' + instanceId);
62
+
63
+ // Remove old canvas if any
64
+ let oldCanvas = document.getElementById(canvasId);
65
+ if (oldCanvas) oldCanvas.remove();
66
+
67
+ // Create and add canvas
68
+ const canvas = document.createElement('canvas');
69
+ canvas.id = canvasId;
70
+ canvas.className = 'ply-canvas';
71
+ canvas.style.width = "100%";
72
+ canvas.style.height = "100%";
73
+ canvas.setAttribute('tabindex', '0');
74
+ viewerContainer.insertBefore(canvas, progressDialog);
75
+ canvas.style.touchAction = "none";
76
+ canvas.style.webkitTouchCallout = "none";
77
+ canvas.addEventListener('gesturestart', e => e.preventDefault());
78
+ canvas.addEventListener('gesturechange', e => e.preventDefault());
79
+ canvas.addEventListener('gestureend', e => e.preventDefault());
80
+ canvas.addEventListener('dblclick', e => e.preventDefault());
81
+ canvas.addEventListener('touchstart', e => { if (e.touches.length > 1) e.preventDefault(); }, { passive: false });
82
+ canvas.addEventListener('wheel', (e) => { e.preventDefault(); }, { passive: false });
83
+
84
+ progressDialog.style.display = 'block';
85
+
86
+ // --- PlayCanvas Graphics Device ---
87
+ const device = await pc.createGraphicsDevice(canvas, {
88
+ deviceTypes: ["webgl2"],
89
+ antialias: false,
90
+ glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
91
+ twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js"
92
+ });
93
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
94
+
95
+ // --- PlayCanvas AppBase (modern, 2.8.1+) ---
96
+ const appOpts = new pc.AppOptions();
97
+ appOpts.graphicsDevice = device;
98
+ appOpts.mouse = new pc.Mouse(canvas);
99
+ appOpts.touch = new pc.TouchDevice(canvas);
100
+ appOpts.componentSystems = [
101
+ pc.RenderComponentSystem,
102
+ pc.CameraComponentSystem,
103
+ pc.LightComponentSystem,
104
+ pc.ScriptComponentSystem,
105
+ pc.GSplatComponentSystem
106
+ ];
107
+ appOpts.resourceHandlers = [
108
+ pc.TextureHandler,
109
+ pc.ContainerHandler,
110
+ pc.ScriptHandler,
111
+ pc.GSplatHandler
112
+ ];
113
+
114
+ app = new pc.AppBase(canvas);
115
+ app.init(appOpts);
116
+
117
+ // Fill/resize
118
+ app.setCanvasFillMode(pc.FILLMODE_NONE);
119
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
120
+
121
+ resizeObserver = new ResizeObserver(entries => {
122
+ entries.forEach(entry => {
123
+ app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
124
+ });
125
+ });
126
+ resizeObserver.observe(viewerContainer);
127
+
128
+ window.addEventListener('resize', () => app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight));
129
+ app.on('destroy', () => resizeObserver.disconnect());
130
+
131
+ // --- ASSETS (GSplat + Orbit camera) ---
132
+ const assets = {
133
+ model: new pc.Asset('gsplat', 'gsplat', { url: sogsUrl }),
134
+ orbit: new pc.Asset('orbit', 'script', { url: "https://mikafil-viewer-sgos.static.hf.space/orbit-camera.js" }),
135
+ // Optionally add other assets here if you want
136
+ };
137
+ for (const key in assets) app.assets.add(assets[key]);
138
+
139
+ const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
140
+ loader.load(() => {
141
+ app.start();
142
+ progressDialog.style.display = 'none';
143
+
144
+ // --- Add GSplat Model ---
145
+ modelEntity = new pc.Entity('model');
146
+ modelEntity.addComponent('gsplat', { asset: assets.model });
147
+ modelEntity.setLocalPosition(modelX, modelY, modelZ);
148
+ modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
149
+ modelEntity.setLocalScale(modelScale, modelScale, modelScale);
150
+ app.root.addChild(modelEntity);
151
+
152
+ // --- Camera ---
153
+ cameraEntity = new pc.Entity('camera');
154
+ cameraEntity.addComponent('camera', {
155
+ clearColor: new pc.Color(1, 1, 1, 1)
156
+ });
157
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
158
+ cameraEntity.lookAt(modelEntity.getPosition());
159
+ cameraEntity.addComponent('script');
160
+ cameraEntity.script.create('orbitCamera', {
161
+ attributes: {
162
+ focusEntity: modelEntity,
163
+ inertiaFactor: 0.2,
164
+ distanceMax: maxZoom,
165
+ distanceMin: minZoom,
166
+ pitchAngleMax: maxAngle,
167
+ pitchAngleMin: minAngle,
168
+ yawAngleMax: maxAzimuth,
169
+ yawAngleMin: minAzimuth,
170
+ minY: minY,
171
+ frameOnStart: false
172
+ }
173
+ });
174
+ cameraEntity.script.create('orbitCameraInputMouse');
175
+ cameraEntity.script.create('orbitCameraInputTouch');
176
+ app.root.addChild(cameraEntity);
177
+
178
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
179
+ app.once('update', () => resetViewerCamera());
180
+
181
+ // --- Tooltips ---
182
+ try {
183
+ if (config.tooltips_url) {
184
+ import('./tooltips.js').then(tooltipsModule => {
185
+ tooltipsModule.initializeTooltips({
186
+ app,
187
+ cameraEntity,
188
+ modelEntity,
189
+ tooltipsUrl: config.tooltips_url,
190
+ defaultVisible: !!config.showTooltipsDefault,
191
+ moveDuration: config.tooltipMoveDuration || 0.6
192
+ });
193
+ }).catch(e => {});
194
+ }
195
+ } catch (e) {}
196
+
197
+ viewerInitialized = true;
198
  });
 
 
 
199
  }
200
 
201
  // Reset camera helper
202
  export function resetViewerCamera() {
203
+ try {
204
+ if (!cameraEntity || !modelEntity || !app) return;
205
+ const orbitCam = cameraEntity.script.orbitCamera;
206
+ if (!orbitCam) return;
207
+
208
+ const modelPos = modelEntity.getPosition();
209
+ const tempEnt = new pc.Entity();
210
+ tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
211
+ tempEnt.lookAt(modelPos);
212
+
213
+ const dist = new pc.Vec3().sub2(
214
+ new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
215
+ modelPos
216
+ ).length();
217
+
218
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
219
+ cameraEntity.lookAt(modelPos);
220
+
221
+ orbitCam.pivotPoint = modelPos.clone();
222
+ orbitCam._targetDistance = dist;
223
+ orbitCam._distance = dist;
224
+
225
+ const rot = tempEnt.getRotation();
226
+ const fwd = new pc.Vec3();
227
+ rot.transformVector(pc.Vec3.FORWARD, fwd);
228
+
229
+ const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
230
+ const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
231
+ const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
232
+ const fNoYaw = new pc.Vec3();
233
+ rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
234
+ const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
235
+
236
+ orbitCam._targetYaw = yaw;
237
+ orbitCam._yaw = yaw;
238
+ orbitCam._targetPitch = pitch;
239
+ orbitCam._pitch = pitch;
240
+ if (orbitCam._updatePosition) orbitCam._updatePosition();
241
+
242
+ tempEnt.destroy();
243
+ } catch (e) {
244
+ // Silent fail
245
+ }
246
  }