MikaFil commited on
Commit
7fccc8c
·
verified ·
1 Parent(s): 76d4858

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +271 -248
viewer.js CHANGED
@@ -13,262 +13,285 @@ let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, min
13
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
14
  let sogsUrl, glbUrl, canvasBg;
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
- // Parse config fields
23
- sogsUrl = config.sogs_json_url;
24
- glbUrl = config.glb_url;
25
- canvasBg = config.canvas_background || "#ffffff";
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);
49
-
50
- chosenCameraX = isMobile ? cameraXPhone : cameraX;
51
- chosenCameraY = isMobile ? cameraYPhone : cameraY;
52
- chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
53
-
54
- const canvasId = 'canvas-' + instanceId;
55
- const progressDialog = document.getElementById('progress-dialog-' + instanceId);
56
- const progressIndicator= document.getElementById('progress-indicator-' + instanceId);
57
- const viewerContainer = document.getElementById('viewer-container-' + instanceId);
58
-
59
- let oldCanvas = document.getElementById(canvasId);
60
- if (oldCanvas) oldCanvas.remove();
61
-
62
- const canvas = document.createElement('canvas');
63
- canvas.id = canvasId;
64
- canvas.className = 'ply-canvas';
65
- canvas.style.width = "100%";
66
- canvas.style.height = "100%";
67
- canvas.setAttribute('tabindex', '0');
68
- viewerContainer.insertBefore(canvas, progressDialog);
69
-
70
- // Apply background color from config if present
71
- canvas.style.background = canvasBg;
72
-
73
- canvas.style.touchAction = "none";
74
- canvas.style.webkitTouchCallout = "none";
75
- canvas.addEventListener('gesturestart', e => e.preventDefault());
76
- canvas.addEventListener('gesturechange', e => e.preventDefault());
77
- canvas.addEventListener('gestureend', e => e.preventDefault());
78
- canvas.addEventListener('dblclick', e => e.preventDefault());
79
- canvas.addEventListener('touchstart', e => { if (e.touches.length > 1) e.preventDefault(); }, { passive: false });
80
-
81
- // --- Mouse wheel suppression
82
- canvas.addEventListener('wheel', (e) => {
83
- e.preventDefault(); // Only block page scroll if mouse is over viewer
84
- }, { passive: false });
85
-
86
- progressDialog.style.display = 'block';
87
-
88
- if (!pc) {
89
- pc = await import("https://cdn.jsdelivr.net/npm/playcanvas@latest/+esm"); // always use latest for SOGS/GSplat support
90
- window.pc = pc;
91
- }
92
-
93
- // Create app and graphics device
94
- const device = await pc.createGraphicsDevice(canvas, {
95
- deviceTypes: ["webgl2"],
96
- glslangUrl: "https://mikafil-viewer-sgos.static.hf.space/static/lib/glslang/glslang.js",
97
- twgslUrl: "https://mikafil-viewer-sgos.static.hf.space/static/lib/twgsl/twgsl.js",
98
- antialias: false
99
- });
100
- device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
101
-
102
- const opts = new pc.AppOptions();
103
- opts.graphicsDevice = device;
104
- opts.mouse = new pc.Mouse(canvas);
105
- opts.touch = new pc.TouchDevice(canvas);
106
- opts.componentSystems = [
107
- pc.RenderComponentSystem,
108
- pc.CameraComponentSystem,
109
- pc.LightComponentSystem,
110
- pc.ScriptComponentSystem,
111
- // "GSplat" component supports SOGS and GSplat
112
- pc.GSplatComponentSystem,
113
- pc.CollisionComponentSystem,
114
- pc.RigidbodyComponentSystem
115
- ];
116
- opts.resourceHandlers = [
117
- pc.TextureHandler,
118
- pc.ContainerHandler,
119
- pc.ScriptHandler,
120
- // GSplatHandler handles GSplat and SOGS
121
- pc.GSplatHandler
122
- ];
123
-
124
- app = new pc.Application(canvas, opts);
125
- app.setCanvasFillMode(pc.FILLMODE_NONE);
126
- app.setCanvasResolution(pc.RESOLUTION_AUTO);
127
-
128
- resizeObserver = new ResizeObserver(entries => {
129
- entries.forEach(entry => {
130
- app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
131
- });
132
- });
133
- resizeObserver.observe(viewerContainer);
134
-
135
- window.addEventListener('resize', () => app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight));
136
- app.on('destroy', () => resizeObserver.disconnect());
137
-
138
- // Prepare asset loading (SOGS as 'gsplat')
139
- const assets = {
140
- sogs: new pc.Asset('gsplat', 'gsplat', { url: sogsUrl }),
141
- orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-sgos.static.hf.space/orbit-camera.js" }),
142
- glb: new pc.Asset('glb', 'container', { url: glbUrl }),
143
- };
144
- for (const key in assets) app.assets.add(assets[key]);
145
-
146
- const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
147
- loader.load(() => {
148
- app.start();
149
- progressDialog.style.display = 'none';
150
-
151
- // Add SOGS model
152
- modelEntity = new pc.Entity('model');
153
- modelEntity.addComponent('gsplat', { asset: assets.sogs });
154
- modelEntity.setLocalPosition(modelX, modelY, modelZ);
155
- modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
156
- modelEntity.setLocalScale(modelScale, modelScale, modelScale);
157
- app.root.addChild(modelEntity);
158
-
159
- // Add the GLB entity if provided
160
- if (assets.glb && assets.glb.resource && assets.glb.resource.instantiateRenderEntity) {
161
- const glbEntity = assets.glb.resource.instantiateRenderEntity();
162
- app.root.addChild(glbEntity);
163
- }
164
-
165
- // CAMERA
166
- cameraEntity = new pc.Entity('camera');
167
- // Support background color
168
- let bg = [1, 1, 1, 1];
169
- if (canvasBg && /^#?[0-9a-f]{6,8}$/i.test(canvasBg.replace("#", ""))) {
170
- // convert hex color to [r,g,b,a]
171
- let hex = canvasBg.replace("#", "");
172
- if (hex.length === 6) hex += "FF";
173
- const num = parseInt(hex, 16);
174
- bg = [
175
  ((num >> 24) & 0xFF) / 255,
176
  ((num >> 16) & 0xFF) / 255,
177
  ((num >> 8) & 0xFF) / 255,
178
  (num & 0xFF) / 255
179
- ];
180
- }
181
- cameraEntity.addComponent('camera', { clearColor: new pc.Color(bg[0], bg[1], bg[2], bg[3]) });
182
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
183
- cameraEntity.lookAt(modelEntity.getPosition());
184
- cameraEntity.addComponent('script');
185
-
186
- // Pass all attributes to Orbit Camera script
187
- cameraEntity.script.create('orbitCamera', {
188
- attributes: {
189
- focusEntity: modelEntity,
190
- inertiaFactor: 0.2,
191
- distanceMax: maxZoom,
192
- distanceMin: minZoom,
193
- pitchAngleMax: maxAngle,
194
- pitchAngleMin: minAngle,
195
- yawAngleMax: maxAzimuth,
196
- yawAngleMin: minAzimuth,
197
- minY: minY,
198
- frameOnStart: false
199
- }
200
- });
201
- cameraEntity.script.create('orbitCameraInputMouse');
202
- cameraEntity.script.create('orbitCameraInputTouch');
203
- app.root.addChild(cameraEntity);
204
-
205
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
206
 
207
- app.once('update', () => resetViewerCamera());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
- // Tooltips support (optional)
 
 
210
  try {
211
- if (config.tooltips_url) {
212
- import('https://mikafil-viewer-sgos.static.hf.space/tooltips.js').then(tooltipsModule => {
213
- tooltipsModule.initializeTooltips({
214
- app,
215
- cameraEntity,
216
- modelEntity,
217
- tooltipsUrl: config.tooltips_url,
218
- defaultVisible: !!config.showTooltipsDefault,
219
- moveDuration: config.tooltipMoveDuration || 0.6
220
- });
221
- }).catch(e => {});
222
- }
223
- } catch (e) {}
224
-
225
- viewerInitialized = true;
226
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  }
228
 
229
- // Resets the viewer camera (as in your PLY example)
230
  export function resetViewerCamera() {
231
- try {
232
- if (!cameraEntity || !modelEntity || !app) return;
233
- const orbitCam = cameraEntity.script.orbitCamera;
234
- if (!orbitCam) return;
235
-
236
- const modelPos = modelEntity.getPosition();
237
- const tempEnt = new pc.Entity();
238
- tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
239
- tempEnt.lookAt(modelPos);
240
-
241
- const dist = new pc.Vec3().sub2(
242
- new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
243
- modelPos
244
- ).length();
245
-
246
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
247
- cameraEntity.lookAt(modelPos);
248
-
249
- orbitCam.pivotPoint = modelPos.clone();
250
- orbitCam._targetDistance = dist;
251
- orbitCam._distance = dist;
252
-
253
- const rot = tempEnt.getRotation();
254
- const fwd = new pc.Vec3();
255
- rot.transformVector(pc.Vec3.FORWARD, fwd);
256
-
257
- const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
258
- const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
259
- const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
260
- const fNoYaw = new pc.Vec3();
261
- rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
262
- const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
263
-
264
- orbitCam._targetYaw = yaw;
265
- orbitCam._yaw = yaw;
266
- orbitCam._targetPitch = pitch;
267
- orbitCam._pitch = pitch;
268
- if (orbitCam._updatePosition) orbitCam._updatePosition();
269
-
270
- tempEnt.destroy();
271
- } catch (e) {
272
- // Silent fail
273
- }
274
  }
 
13
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
14
  let sogsUrl, glbUrl, canvasBg;
15
 
16
+ // Helper: Convert #RRGGBB[AA] to [r,g,b,a]
17
+ function hexToRgba(hex) {
18
+ hex = hex.replace("#", "");
19
+ if (hex.length === 6) hex += "FF";
20
+ const num = parseInt(hex, 16);
21
+ return [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  ((num >> 24) & 0xFF) / 255,
23
  ((num >> 16) & 0xFF) / 255,
24
  ((num >> 8) & 0xFF) / 255,
25
  (num & 0xFF) / 255
26
+ ];
27
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ export async function initializeViewer(config, instanceId) {
30
+ if (viewerInitialized) return;
31
+
32
+ // Robust device detection
33
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
34
+ const isMobile = isIOS || /Android/i.test(navigator.userAgent);
35
+
36
+ // Parse config fields
37
+ sogsUrl = config.sogs_json_url;
38
+ glbUrl = config.glb_url;
39
+ canvasBg = config.canvas_background || "#ffffff";
40
+ minZoom = parseFloat(config.minZoom || "1");
41
+ maxZoom = parseFloat(config.maxZoom || "20");
42
+ minAngle = parseFloat(config.minAngle || "-45");
43
+ maxAngle = parseFloat(config.maxAngle || "90");
44
+ minAzimuth= (config.minAzimuth !== undefined) ? parseFloat(config.minAzimuth) : -360;
45
+ maxAzimuth= (config.maxAzimuth !== undefined) ? parseFloat(config.maxAzimuth) : 360;
46
+ minPivotY = parseFloat(config.minPivotY || "0");
47
+ minY = (config.minY !== undefined) ? parseFloat(config.minY) : 0;
48
+
49
+ modelX = (config.modelX !== undefined) ? parseFloat(config.modelX) : 0;
50
+ modelY = (config.modelY !== undefined) ? parseFloat(config.modelY) : 0;
51
+ modelZ = (config.modelZ !== undefined) ? parseFloat(config.modelZ) : 0;
52
+ modelScale = (config.modelScale !== undefined) ? parseFloat(config.modelScale) : 1;
53
+ modelRotationX = (config.modelRotationX !== undefined) ? parseFloat(config.modelRotationX) : 0;
54
+ modelRotationY = (config.modelRotationY !== undefined) ? parseFloat(config.modelRotationY) : 0;
55
+ modelRotationZ = (config.modelRotationZ !== undefined) ? parseFloat(config.modelRotationZ) : 0;
56
+
57
+ const cameraX = (config.cameraX !== undefined) ? parseFloat(config.cameraX) : 0;
58
+ const cameraY = (config.cameraY !== undefined) ? parseFloat(config.cameraY) : 2;
59
+ const cameraZ = (config.cameraZ !== undefined) ? parseFloat(config.cameraZ) : 5;
60
+ const cameraXPhone = (config.cameraXPhone !== undefined) ? parseFloat(config.cameraXPhone) : cameraX;
61
+ const cameraYPhone = (config.cameraYPhone !== undefined) ? parseFloat(config.cameraYPhone) : cameraY;
62
+ const cameraZPhone = (config.cameraZPhone !== undefined) ? parseFloat(config.cameraZPhone) : (cameraZ * 1.5);
63
+
64
+ chosenCameraX = isMobile ? cameraXPhone : cameraX;
65
+ chosenCameraY = isMobile ? cameraYPhone : cameraY;
66
+ chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
67
+
68
+ const canvasId = 'canvas-' + instanceId;
69
+ const progressDialog = document.getElementById('progress-dialog-' + instanceId);
70
+ const progressIndicator= document.getElementById('progress-indicator-' + instanceId);
71
+ const viewerContainer = document.getElementById('viewer-container-' + instanceId);
72
+
73
+ let oldCanvas = document.getElementById(canvasId);
74
+ if (oldCanvas) oldCanvas.remove();
75
+
76
+ const canvas = document.createElement('canvas');
77
+ canvas.id = canvasId;
78
+ canvas.className = 'ply-canvas';
79
+ canvas.style.width = "100%";
80
+ canvas.style.height = "100%";
81
+ canvas.setAttribute('tabindex', '0');
82
+ viewerContainer.insertBefore(canvas, progressDialog);
83
+
84
+ canvas.style.background = canvasBg;
85
+ canvas.style.touchAction = "none";
86
+ canvas.style.webkitTouchCallout = "none";
87
+ canvas.addEventListener('gesturestart', e => e.preventDefault());
88
+ canvas.addEventListener('gesturechange', e => e.preventDefault());
89
+ canvas.addEventListener('gestureend', e => e.preventDefault());
90
+ canvas.addEventListener('dblclick', e => e.preventDefault());
91
+ canvas.addEventListener('touchstart', e => { if (e.touches.length > 1) e.preventDefault(); }, { passive: false });
92
+
93
+ // Block wheel scroll
94
+ canvas.addEventListener('wheel', (e) => {
95
+ e.preventDefault();
96
+ }, { passive: false });
97
+
98
+ progressDialog.style.display = 'block';
99
+
100
+ if (!pc) {
101
+ pc = await import("https://cdn.jsdelivr.net/npm/playcanvas@latest/+esm"); // PlayCanvas up-to-date
102
+ window.pc = pc;
103
+ }
104
 
105
+ // === GRAPHICS DEVICE: Use robust fallback for iOS WebGL2 quirks ===
106
+ let device = null;
107
+ let lastError = null;
108
  try {
109
+ device = await pc.createGraphicsDevice(canvas, {
110
+ deviceTypes: ["webgl2", "webgl1"], // Fallback to WebGL1 if needed
111
+ glslangUrl: "https://mikafil-viewer-sgos.static.hf.space/static/lib/glslang/glslang.js",
112
+ twgslUrl: "https://mikafil-viewer-sgos.static.hf.space/static/lib/twgsl/twgsl.js",
113
+ antialias: false
114
+ });
115
+ } catch (e) {
116
+ lastError = e;
117
+ }
118
+ // Fallback: forcibly try WebGL1 if WebGL2 fails (especially for older iOS)
119
+ if (!device) {
120
+ try {
121
+ device = await pc.createGraphicsDevice(canvas, {
122
+ deviceTypes: ["webgl1"],
123
+ glslangUrl: "https://mikafil-viewer-sgos.static.hf.space/static/lib/glslang/glslang.js",
124
+ twgslUrl: "https://mikafil-viewer-sgos.static.hf.space/static/lib/twgsl/twgsl.js",
125
+ antialias: false
126
+ });
127
+ } catch (e) {
128
+ progressDialog.style.display = 'none';
129
+ progressIndicator.value = 0;
130
+ progressIndicator.removeAttribute('max');
131
+ if (window.confirm("WebGL failed to initialize. Your browser/device may not support it. Reload?")) location.reload();
132
+ throw new Error("Failed to create WebGL context: " + (lastError || e));
133
+ }
134
+ }
135
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
136
+
137
+ const opts = new pc.AppOptions();
138
+ opts.graphicsDevice = device;
139
+ opts.mouse = new pc.Mouse(canvas);
140
+ opts.touch = new pc.TouchDevice(canvas);
141
+ opts.componentSystems = [
142
+ pc.RenderComponentSystem,
143
+ pc.CameraComponentSystem,
144
+ pc.LightComponentSystem,
145
+ pc.ScriptComponentSystem,
146
+ pc.GSplatComponentSystem,
147
+ pc.CollisionComponentSystem,
148
+ pc.RigidbodyComponentSystem
149
+ ];
150
+ opts.resourceHandlers = [
151
+ pc.TextureHandler,
152
+ pc.ContainerHandler,
153
+ pc.ScriptHandler,
154
+ pc.GSplatHandler
155
+ ];
156
+
157
+ app = new pc.Application(canvas, opts);
158
+ app.setCanvasFillMode(pc.FILLMODE_NONE);
159
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
160
+
161
+ // --- Resize observer robust for iOS/Android window resize ---
162
+ resizeObserver = new ResizeObserver(entries => {
163
+ entries.forEach(entry => {
164
+ app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
165
+ });
166
+ });
167
+ resizeObserver.observe(viewerContainer);
168
+
169
+ window.addEventListener('resize', () => app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight));
170
+ app.on('destroy', () => resizeObserver.disconnect());
171
+
172
+ // --- Asset loading: SOGS (gsplat) as model, Orbit script, optional GLB ---
173
+ const assets = {
174
+ sogs: new pc.Asset('gsplat', 'gsplat', { url: sogsUrl }),
175
+ orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-sgos.static.hf.space/orbit-camera.js" }),
176
+ glb: new pc.Asset('glb', 'container', { url: glbUrl }),
177
+ };
178
+ for (const key in assets) app.assets.add(assets[key]);
179
+
180
+ const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
181
+
182
+ loader.load(() => {
183
+ app.start();
184
+ progressDialog.style.display = 'none';
185
+
186
+ // --- Add SOGS Model
187
+ modelEntity = new pc.Entity('model');
188
+ modelEntity.addComponent('gsplat', { asset: assets.sogs });
189
+ modelEntity.setLocalPosition(modelX, modelY, modelZ);
190
+ modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
191
+ modelEntity.setLocalScale(modelScale, modelScale, modelScale);
192
+ app.root.addChild(modelEntity);
193
+
194
+ // --- Add GLB entity if present
195
+ if (assets.glb && assets.glb.resource && assets.glb.resource.instantiateRenderEntity) {
196
+ const glbEntity = assets.glb.resource.instantiateRenderEntity();
197
+ app.root.addChild(glbEntity);
198
+ }
199
+
200
+ // --- Camera
201
+ cameraEntity = new pc.Entity('camera');
202
+ // Set clearColor
203
+ let bg = [1, 1, 1, 1];
204
+ if (canvasBg && /^#?[0-9a-f]{6,8}$/i.test(canvasBg.replace("#", ""))) {
205
+ bg = hexToRgba(canvasBg);
206
+ }
207
+ cameraEntity.addComponent('camera', { clearColor: new pc.Color(bg[0], bg[1], bg[2], bg[3]) });
208
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
209
+ cameraEntity.lookAt(modelEntity.getPosition());
210
+ cameraEntity.addComponent('script');
211
+
212
+ cameraEntity.script.create('orbitCamera', {
213
+ attributes: {
214
+ focusEntity: modelEntity,
215
+ inertiaFactor: 0.2,
216
+ distanceMax: maxZoom,
217
+ distanceMin: minZoom,
218
+ pitchAngleMax: maxAngle,
219
+ pitchAngleMin: minAngle,
220
+ yawAngleMax: maxAzimuth,
221
+ yawAngleMin: minAzimuth,
222
+ minY: minY,
223
+ frameOnStart: false
224
+ }
225
+ });
226
+ cameraEntity.script.create('orbitCameraInputMouse');
227
+ cameraEntity.script.create('orbitCameraInputTouch');
228
+ app.root.addChild(cameraEntity);
229
+
230
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
231
+ app.once('update', () => resetViewerCamera());
232
+
233
+ // --- Tooltips support (optional)
234
+ try {
235
+ if (config.tooltips_url) {
236
+ import('https://mikafil-viewer-sgos.static.hf.space/tooltips.js').then(tooltipsModule => {
237
+ tooltipsModule.initializeTooltips({
238
+ app,
239
+ cameraEntity,
240
+ modelEntity,
241
+ tooltipsUrl: config.tooltips_url,
242
+ defaultVisible: !!config.showTooltipsDefault,
243
+ moveDuration: config.tooltipMoveDuration || 0.6
244
+ });
245
+ }).catch(e => {});
246
+ }
247
+ } catch (e) {}
248
+
249
+ viewerInitialized = true;
250
+ });
251
  }
252
 
 
253
  export function resetViewerCamera() {
254
+ try {
255
+ if (!cameraEntity || !modelEntity || !app) return;
256
+ const orbitCam = cameraEntity.script.orbitCamera;
257
+ if (!orbitCam) return;
258
+
259
+ const modelPos = modelEntity.getPosition();
260
+ const tempEnt = new pc.Entity();
261
+ tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
262
+ tempEnt.lookAt(modelPos);
263
+
264
+ const dist = new pc.Vec3().sub2(
265
+ new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
266
+ modelPos
267
+ ).length();
268
+
269
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
270
+ cameraEntity.lookAt(modelPos);
271
+
272
+ orbitCam.pivotPoint = modelPos.clone();
273
+ orbitCam._targetDistance = dist;
274
+ orbitCam._distance = dist;
275
+
276
+ const rot = tempEnt.getRotation();
277
+ const fwd = new pc.Vec3();
278
+ rot.transformVector(pc.Vec3.FORWARD, fwd);
279
+
280
+ const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
281
+ const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
282
+ const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
283
+ const fNoYaw = new pc.Vec3();
284
+ rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
285
+ const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
286
+
287
+ orbitCam._targetYaw = yaw;
288
+ orbitCam._yaw = yaw;
289
+ orbitCam._targetPitch = pitch;
290
+ orbitCam._pitch = pitch;
291
+ if (orbitCam._updatePosition) orbitCam._updatePosition();
292
+
293
+ tempEnt.destroy();
294
+ } catch (e) {
295
+ // Silent fail
296
+ }
297
  }