MikaFil commited on
Commit
1303672
·
verified ·
1 Parent(s): 6fa5b91

Create viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +499 -0
viewer.js ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // viewer.js
2
+ // ==============================
3
+
4
+ /* -------------------------------------------
5
+ Utils
6
+ (les helpers image ne sont plus nécessaires pour .sog,
7
+ mais on les garde sans effet de bord pour compat ascendante)
8
+ -------------------------------------------- */
9
+
10
+ async function loadImageAsTexture(url, app) {
11
+ return new Promise((resolve, reject) => {
12
+ const img = new window.Image();
13
+ img.crossOrigin = "anonymous";
14
+ img.onload = function () {
15
+ const tex = new pc.Texture(app.graphicsDevice, {
16
+ width: img.width,
17
+ height: img.height,
18
+ format: pc.PIXELFORMAT_R8_G8_B8_A8
19
+ });
20
+ tex.setSource(img);
21
+ resolve(tex);
22
+ };
23
+ img.onerror = reject;
24
+ img.src = url;
25
+ });
26
+ }
27
+
28
+ // Patch global Image -> force CORS (sans incidence pour .sog)
29
+ (function () {
30
+ const OriginalImage = window.Image;
31
+ window.Image = function (...args) {
32
+ const img = new OriginalImage(...args);
33
+ img.crossOrigin = "anonymous";
34
+ return img;
35
+ };
36
+ })();
37
+
38
+ function hexToRgbaArray(hex) {
39
+ try {
40
+ hex = String(hex || "").replace("#", "");
41
+ if (hex.length === 6) hex += "FF";
42
+ if (hex.length !== 8) return [1, 1, 1, 1];
43
+ const num = parseInt(hex, 16);
44
+ return [
45
+ ((num >> 24) & 0xff) / 255,
46
+ ((num >> 16) & 0xff) / 255,
47
+ ((num >> 8) & 0xff) / 255,
48
+ (num & 0xff) / 255
49
+ ];
50
+ } catch (e) {
51
+ console.warn("hexToRgbaArray error:", e);
52
+ return [1, 1, 1, 1];
53
+ }
54
+ }
55
+
56
+ // Parcours récursif d'une hiérarchie d'entités
57
+ function traverse(entity, callback) {
58
+ callback(entity);
59
+ if (entity.children) {
60
+ entity.children.forEach((child) => traverse(child, callback));
61
+ }
62
+ }
63
+
64
+ /* -------------------------------------------
65
+ Chargement unique de orbit-camera.js
66
+ -------------------------------------------- */
67
+
68
+ async function ensureOrbitScriptsLoaded() {
69
+ if (window.__PLY_ORBIT_LOADED__) return;
70
+ if (window.__PLY_ORBIT_LOADING__) {
71
+ await window.__PLY_ORBIT_LOADING__;
72
+ return;
73
+ }
74
+
75
+ window.__PLY_ORBIT_LOADING__ = new Promise((resolve, reject) => {
76
+ const s = document.createElement("script");
77
+ s.src = "https://mikafil-sog-viewer.static.hf.space/orbit-camera.js";
78
+ s.async = true;
79
+ s.onload = () => {
80
+ window.__PLY_ORBIT_LOADED__ = true;
81
+ resolve();
82
+ };
83
+ s.onerror = (e) => {
84
+ console.error("[viewer.js] Failed to load orbit-camera.js", e);
85
+ reject(e);
86
+ };
87
+ document.head.appendChild(s);
88
+ });
89
+
90
+ await window.__PLY_ORBIT_LOADING__;
91
+ }
92
+
93
+ /* -------------------------------------------
94
+ State (par module = par instance importée)
95
+ -------------------------------------------- */
96
+
97
+ let pc;
98
+ export let app = null;
99
+ let cameraEntity = null;
100
+ let modelEntity = null;
101
+ let viewerInitialized = false;
102
+ let resizeObserver = null;
103
+
104
+ // paramètres courants de l'instance
105
+ let chosenCameraX, chosenCameraY, chosenCameraZ;
106
+ let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minY;
107
+ let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
108
+ let presentoirScaleX, presentoirScaleY, presentoirScaleZ;
109
+ let sogUrl, glbUrl, presentoirUrl;
110
+ let color_bg_hex, color_bg, espace_expo_bool;
111
+
112
+ /* -------------------------------------------
113
+ Initialisation
114
+ -------------------------------------------- */
115
+
116
+ export async function initializeViewer(config, instanceId) {
117
+ // ce module ES est importé avec un param unique ?inst=..., donc 1 instance par import
118
+ if (viewerInitialized) return;
119
+
120
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
121
+ const isMobile = isIOS || /Android/i.test(navigator.userAgent);
122
+
123
+ // --- Configuration ---
124
+ // Nouveau : utiliser un .sog "bundled" (format SOG PlayCanvas)
125
+ // Compat ascendante : on accepte encore sogs_json_url si sog_url absent
126
+ sogUrl = config.sog_url || config.sogs_json_url;
127
+
128
+ glbUrl =
129
+ config.glb_url !== undefined
130
+ ? config.glb_url
131
+ : "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/ressources/espace_expo/sol_blanc_2.glb";
132
+
133
+ presentoirUrl =
134
+ config.presentoir_url !== undefined
135
+ ? config.presentoir_url
136
+ : "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/ressources/espace_expo/sol_blanc_2.glb";
137
+
138
+ minZoom = parseFloat(config.minZoom || "1");
139
+ maxZoom = parseFloat(config.maxZoom || "20");
140
+ minAngle = parseFloat(config.minAngle || "-2000");
141
+ maxAngle = parseFloat(config.maxAngle || "2000");
142
+ minAzimuth = config.minAzimuth !== undefined ? parseFloat(config.minAzimuth) : -360;
143
+ maxAzimuth = config.maxAzimuth !== undefined ? parseFloat(config.maxAzimuth) : 360;
144
+ minY = config.minY !== undefined ? parseFloat(config.minY) : 0;
145
+
146
+ modelX = config.modelX !== undefined ? parseFloat(config.modelX) : 0;
147
+ modelY = config.modelY !== undefined ? parseFloat(config.modelY) : 0;
148
+ modelZ = config.modelZ !== undefined ? parseFloat(config.modelZ) : 0;
149
+ modelScale = config.modelScale !== undefined ? parseFloat(config.modelScale) : 1;
150
+ modelRotationX = config.modelRotationX !== undefined ? parseFloat(config.modelRotationX) : 0;
151
+ modelRotationY = config.modelRotationY !== undefined ? parseFloat(config.modelRotationY) : 0;
152
+ modelRotationZ = config.modelRotationZ !== undefined ? parseFloat(config.modelRotationZ) : 0;
153
+
154
+ presentoirScaleX = config.presentoirScaleX !== undefined ? parseFloat(config.presentoirScaleX) : 0;
155
+ presentoirScaleY = config.presentoirScaleY !== undefined ? parseFloat(config.presentoirScaleY) : 0;
156
+ presentoirScaleZ = config.presentoirScaleZ !== undefined ? parseFloat(config.presentoirScaleZ) : 0;
157
+
158
+ const cameraX = config.cameraX !== undefined ? parseFloat(config.cameraX) : 0;
159
+ const cameraY = config.cameraY !== undefined ? parseFloat(config.cameraY) : 2;
160
+ const cameraZ = config.cameraZ !== undefined ? parseFloat(config.cameraZ) : 5;
161
+
162
+ const cameraXPhone = config.cameraXPhone !== undefined ? parseFloat(config.cameraXPhone) : cameraX;
163
+ const cameraYPhone = config.cameraYPhone !== undefined ? parseFloat(config.cameraYPhone) : cameraY;
164
+ const cameraZPhone = config.cameraZPhone !== undefined ? parseFloat(config.cameraZPhone) : cameraZ * 1.5;
165
+
166
+ color_bg_hex = config.canvas_background !== undefined ? config.canvas_background : "#FFFFFF";
167
+ espace_expo_bool = config.espace_expo_bool !== undefined ? config.espace_expo_bool : false;
168
+ color_bg = hexToRgbaArray(color_bg_hex);
169
+
170
+ chosenCameraX = isMobile ? cameraXPhone : cameraX;
171
+ chosenCameraY = isMobile ? cameraYPhone : cameraY;
172
+ chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
173
+
174
+ // --- Prépare le canvas unique à cette instance ---
175
+ const canvasId = "canvas-" + instanceId;
176
+ const progressDialog = document.getElementById("progress-dialog-" + instanceId);
177
+ const viewerContainer = document.getElementById("viewer-container-" + instanceId);
178
+
179
+ const old = document.getElementById(canvasId);
180
+ if (old) old.remove();
181
+
182
+ const canvas = document.createElement("canvas");
183
+ canvas.id = canvasId;
184
+ canvas.className = "ply-canvas";
185
+ canvas.style.width = "100%";
186
+ canvas.style.height = "100%";
187
+ canvas.setAttribute("tabindex", "0");
188
+ viewerContainer.insertBefore(canvas, progressDialog);
189
+
190
+ // interactions de base
191
+ canvas.style.touchAction = "none";
192
+ canvas.style.webkitTouchCallout = "none";
193
+ canvas.addEventListener("gesturestart", (e) => e.preventDefault());
194
+ canvas.addEventListener("gesturechange", (e) => e.preventDefault());
195
+ canvas.addEventListener("gestureend", (e) => e.preventDefault());
196
+ canvas.addEventListener("dblclick", (e) => e.preventDefault());
197
+ canvas.addEventListener(
198
+ "touchstart",
199
+ (e) => {
200
+ if (e.touches.length > 1) e.preventDefault();
201
+ },
202
+ { passive: false }
203
+ );
204
+ canvas.addEventListener(
205
+ "wheel",
206
+ (e) => {
207
+ e.preventDefault();
208
+ },
209
+ { passive: false }
210
+ );
211
+
212
+ // Bloque le scroll page uniquement quand le pointeur est sur le canvas
213
+ const scrollKeys = new Set([
214
+ "ArrowUp",
215
+ "ArrowDown",
216
+ "ArrowLeft",
217
+ "ArrowRight",
218
+ "PageUp",
219
+ "PageDown",
220
+ "Home",
221
+ "End",
222
+ " ",
223
+ "Space",
224
+ "Spacebar"
225
+ ]);
226
+ let isPointerOverCanvas = false;
227
+ const focusCanvas = () => canvas.focus({ preventScroll: true });
228
+
229
+ const onPointerEnter = () => {
230
+ isPointerOverCanvas = true;
231
+ focusCanvas();
232
+ };
233
+ const onPointerLeave = () => {
234
+ isPointerOverCanvas = false;
235
+ if (document.activeElement === canvas) canvas.blur();
236
+ };
237
+ const onCanvasBlur = () => {
238
+ isPointerOverCanvas = false;
239
+ };
240
+
241
+ canvas.addEventListener("pointerenter", onPointerEnter);
242
+ canvas.addEventListener("pointerleave", onPointerLeave);
243
+ canvas.addEventListener("mouseenter", onPointerEnter);
244
+ canvas.addEventListener("mouseleave", onPointerLeave);
245
+ canvas.addEventListener("mousedown", focusCanvas);
246
+ canvas.addEventListener(
247
+ "touchstart",
248
+ () => {
249
+ focusCanvas();
250
+ },
251
+ { passive: false }
252
+ );
253
+ canvas.addEventListener("blur", onCanvasBlur);
254
+
255
+ const onKeyDownCapture = (e) => {
256
+ if (!isPointerOverCanvas) return;
257
+ if (scrollKeys.has(e.key) || scrollKeys.has(e.code)) {
258
+ e.preventDefault();
259
+ }
260
+ };
261
+ window.addEventListener("keydown", onKeyDownCapture, true);
262
+
263
+ progressDialog.style.display = "block";
264
+
265
+ // --- Charge PlayCanvas lib ESM (une par module/instance) ---
266
+ if (!pc) {
267
+ pc = await import("https://esm.run/playcanvas");
268
+ window.pc = pc; // utiles pour tooltips.js
269
+ }
270
+
271
+ // --- Crée l'Application ---
272
+ const device = await pc.createGraphicsDevice(canvas, {
273
+ deviceTypes: ["webgl2"],
274
+ glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
275
+ twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
276
+ antialias: false
277
+ });
278
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
279
+
280
+ const opts = new pc.AppOptions();
281
+ opts.graphicsDevice = device;
282
+ opts.mouse = new pc.Mouse(canvas);
283
+ opts.touch = new pc.TouchDevice(canvas);
284
+ opts.keyboard = new pc.Keyboard(canvas); // clavier scoping canvas
285
+ opts.componentSystems = [
286
+ pc.RenderComponentSystem,
287
+ pc.CameraComponentSystem,
288
+ pc.LightComponentSystem,
289
+ pc.ScriptComponentSystem,
290
+ pc.GSplatComponentSystem,
291
+ pc.CollisionComponentSystem,
292
+ pc.RigidbodyComponentSystem
293
+ ];
294
+ // GSplatHandler gère nativement les .sog (bundled SOG)
295
+ opts.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler, pc.ScriptHandler, pc.GSplatHandler];
296
+
297
+ app = new pc.Application(canvas, opts);
298
+ app.setCanvasFillMode(pc.FILLMODE_NONE);
299
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
300
+
301
+ resizeObserver = new ResizeObserver((entries) => {
302
+ entries.forEach((entry) => {
303
+ app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
304
+ });
305
+ });
306
+ resizeObserver.observe(viewerContainer);
307
+
308
+ window.addEventListener("resize", () =>
309
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight)
310
+ );
311
+
312
+ // Nettoyage complet
313
+ app.on("destroy", () => {
314
+ try {
315
+ resizeObserver.disconnect();
316
+ } catch {}
317
+ if (opts.keyboard && opts.keyboard.detach) opts.keyboard.detach();
318
+
319
+ window.removeEventListener("keydown", onKeyDownCapture, true);
320
+
321
+ canvas.removeEventListener("pointerenter", onPointerEnter);
322
+ canvas.removeEventListener("pointerleave", onPointerLeave);
323
+ canvas.removeEventListener("mouseenter", onPointerEnter);
324
+ canvas.removeEventListener("mouseleave", onPointerLeave);
325
+ canvas.removeEventListener("mousedown", focusCanvas);
326
+ canvas.removeEventListener("touchstart", focusCanvas);
327
+ canvas.removeEventListener("blur", onCanvasBlur);
328
+ });
329
+
330
+ // --- Enregistre les assets ---
331
+ // IMPORTANT : pour .sog on déclare un asset de type "gsplat" avec l'URL .sog
332
+ const assets = {
333
+ sog: new pc.Asset("gsplat", "gsplat", { url: sogUrl }),
334
+ glb: new pc.Asset("glb", "container", { url: glbUrl }),
335
+ presentoir: new pc.Asset("presentoir", "container", { url: presentoirUrl })
336
+ };
337
+ for (const k in assets) app.assets.add(assets[k]);
338
+
339
+ const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
340
+
341
+ // Assure orbit-camera.js une seule fois
342
+ await ensureOrbitScriptsLoaded();
343
+
344
+ loader.load(() => {
345
+ app.start();
346
+ progressDialog.style.display = "none";
347
+
348
+ // --- Modèle principal (GSplat via .sog) ---
349
+ modelEntity = new pc.Entity("model");
350
+ modelEntity.addComponent("gsplat", { asset: assets.sog });
351
+ modelEntity.setLocalPosition(modelX, modelY, modelZ);
352
+ modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
353
+ modelEntity.setLocalScale(modelScale, modelScale, modelScale);
354
+ app.root.addChild(modelEntity);
355
+
356
+ // --- Sol / environnement ---
357
+ const glbEntity = assets.glb.resource.instantiateRenderEntity();
358
+ app.root.addChild(glbEntity);
359
+
360
+ const presentoirEntity = assets.presentoir.resource.instantiateRenderEntity();
361
+ presentoirEntity.setLocalScale(presentoirScaleX, presentoirScaleY, presentoirScaleZ);
362
+ app.root.addChild(presentoirEntity);
363
+
364
+ if (!espace_expo_bool) {
365
+ const matSol = new pc.StandardMaterial();
366
+ matSol.blendType = pc.BLEND_NONE;
367
+ matSol.emissive = new pc.Color(color_bg);
368
+ matSol.emissiveIntensity = 1;
369
+ matSol.useLighting = false;
370
+ matSol.update();
371
+
372
+ traverse(presentoirEntity, (node) => {
373
+ if (node.render && node.render.meshInstances) {
374
+ for (const mi of node.render.meshInstances) mi.material = matSol;
375
+ }
376
+ });
377
+
378
+ traverse(glbEntity, (node) => {
379
+ if (node.render && node.render.meshInstances) {
380
+ for (const mi of node.render.meshInstances) mi.material = matSol;
381
+ }
382
+ });
383
+
384
+ ////// MODIFIE A LA MANO FAIRE GAFFE //////
385
+ glbEntity.setLocalScale(10, 10, 10);
386
+ }
387
+
388
+ // --- Caméra + scripts d’input (disponibles car orbit chargé globalement) ---
389
+ cameraEntity = new pc.Entity("camera");
390
+ cameraEntity.addComponent("camera", {
391
+ clearColor: new pc.Color(color_bg),
392
+ nearClip: 0.001,
393
+ farClip: 100
394
+ });
395
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
396
+ cameraEntity.lookAt(modelEntity.getPosition());
397
+ cameraEntity.addComponent("script");
398
+
399
+ cameraEntity.script.create("orbitCamera", {
400
+ attributes: {
401
+ focusEntity: modelEntity,
402
+ inertiaFactor: 0.2,
403
+ distanceMax: maxZoom,
404
+ distanceMin: minZoom,
405
+ pitchAngleMax: maxAngle,
406
+ pitchAngleMin: minAngle,
407
+ yawAngleMax: maxAzimuth,
408
+ yawAngleMin: minAzimuth,
409
+ minY: minY,
410
+ frameOnStart: false
411
+ }
412
+ });
413
+ cameraEntity.script.create("orbitCameraInputMouse");
414
+ cameraEntity.script.create("orbitCameraInputTouch");
415
+ cameraEntity.script.create("orbitCameraInputKeyboard", {
416
+ attributes: {
417
+ forwardSpeed: 1.2,
418
+ strafeSpeed: 1.2
419
+ }
420
+ });
421
+ app.root.addChild(cameraEntity);
422
+
423
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
424
+ app.once("update", () => resetViewerCamera());
425
+
426
+ // --- Tooltips (optionnels) ---
427
+ try {
428
+ if (config.tooltips_url) {
429
+ import("./tooltips.js")
430
+ .then((tooltipsModule) => {
431
+ tooltipsModule.initializeTooltips({
432
+ app,
433
+ cameraEntity,
434
+ modelEntity,
435
+ tooltipsUrl: config.tooltips_url,
436
+ defaultVisible: !!config.showTooltipsDefault,
437
+ moveDuration: config.tooltipMoveDuration || 0.6
438
+ });
439
+ })
440
+ .catch(() => {
441
+ /* optional */
442
+ });
443
+ }
444
+ } catch (e) {
445
+ /* optional */
446
+ }
447
+
448
+ viewerInitialized = true;
449
+ });
450
+ }
451
+
452
+ /* -------------------------------------------
453
+ Reset caméra (API)
454
+ -------------------------------------------- */
455
+
456
+ export function resetViewerCamera() {
457
+ try {
458
+ if (!cameraEntity || !modelEntity || !app) return;
459
+ const orbitCam = cameraEntity.script.orbitCamera;
460
+ if (!orbitCam) return;
461
+
462
+ const modelPos = modelEntity.getPosition();
463
+ const tempEnt = new pc.Entity();
464
+ tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
465
+ tempEnt.lookAt(modelPos);
466
+
467
+ const dist = new pc.Vec3()
468
+ .sub2(new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ), modelPos)
469
+ .length();
470
+
471
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
472
+ cameraEntity.lookAt(modelPos);
473
+
474
+ orbitCam.pivotPoint = modelPos.clone();
475
+ orbitCam._targetDistance = dist;
476
+ orbitCam._distance = dist;
477
+
478
+ const rot = tempEnt.getRotation();
479
+ const fwd = new pc.Vec3();
480
+ rot.transformVector(pc.Vec3.FORWARD, fwd);
481
+
482
+ const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
483
+ const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
484
+ const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
485
+ const fNoYaw = new pc.Vec3();
486
+ rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
487
+ const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
488
+
489
+ orbitCam._targetYaw = yaw;
490
+ orbitCam._yaw = yaw;
491
+ orbitCam._targetPitch = pitch;
492
+ orbitCam._pitch = pitch;
493
+ if (orbitCam._updatePosition) orbitCam._updatePosition();
494
+
495
+ tempEnt.destroy();
496
+ } catch (e) {
497
+ console.error("[viewer.js] resetViewerCamera error:", e);
498
+ }
499
+ }