MikaFil commited on
Commit
9ed7e85
·
verified ·
1 Parent(s): 23062ca

Update viewer.js

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