MikaFil commited on
Commit
32fe69b
·
verified ·
1 Parent(s): ec465e2

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +231 -328
viewer.js CHANGED
@@ -1,7 +1,7 @@
1
  // viewer.js
2
  // ==============================
3
 
4
- let pc;
5
  export let app = null;
6
  let cameraEntity = null;
7
  let modelEntity = null;
@@ -11,344 +11,247 @@ let resizeObserver = null;
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 sogsMetaUrl, gsosBaseUrl, glbUrl;
15
-
16
- // --- Core function to load an image and decode RGBA data as typed array ---
17
- async function loadImageAsArray(url, dtype = "float32") {
18
- const img = await new Promise((resolve, reject) => {
19
- const image = new window.Image();
20
- image.crossOrigin = "anonymous";
21
- image.onload = () => resolve(image);
22
- image.onerror = reject;
23
- image.src = url;
24
- });
25
-
26
- // Draw to canvas and extract pixel data
27
- const canvas = document.createElement('canvas');
28
- canvas.width = img.width;
29
- canvas.height = img.height;
30
- const ctx = canvas.getContext('2d');
31
- ctx.drawImage(img, 0, 0);
32
- const { data } = ctx.getImageData(0, 0, img.width, img.height);
33
-
34
- // Reinterpret as correct type
35
- if (dtype === "float32") {
36
- // Each 4 bytes is a float (big-endian RGBA) -- adjust if your packing is different!
37
- const buffer = new ArrayBuffer(data.length);
38
- const buf8 = new Uint8Array(buffer);
39
- buf8.set(data);
40
- return new Float32Array(buffer);
41
- } else if (dtype === "uint8") {
42
- return new Uint8Array(data.buffer, data.byteOffset, data.length);
43
- } else {
44
- throw new Error("Unknown dtype: " + dtype);
45
- }
46
- }
47
-
48
- // --- Helper to concatenate arrays (typed) ---
49
- function concatenate(arrays, dtype) {
50
- if (dtype === "float32") {
51
- let len = arrays.reduce((sum, arr) => sum + arr.length, 0);
52
- let out = new Float32Array(len);
53
- let offset = 0;
54
- for (let arr of arrays) {
55
- out.set(arr, offset);
56
- offset += arr.length;
57
- }
58
- return out;
59
- } else if (dtype === "uint8") {
60
- let len = arrays.reduce((sum, arr) => sum + arr.length, 0);
61
- let out = new Uint8Array(len);
62
- let offset = 0;
63
- for (let arr of arrays) {
64
- out.set(arr, offset);
65
- offset += arr.length;
66
- }
67
- return out;
68
- }
69
- }
70
-
71
- // --- Load all GSOS data blocks from meta.json and webp images ---
72
- async function loadGSOSMeta(metaUrl, baseUrl) {
73
- const meta = await fetch(metaUrl).then(r => r.json());
74
- const blocks = {};
75
-
76
- for (const [key, block] of Object.entries(meta)) {
77
- const dtype = block.dtype;
78
- const files = block.files || [];
79
- let arrays = [];
80
- for (const fname of files) {
81
- const arr = await loadImageAsArray(baseUrl + '/' + fname, dtype);
82
- arrays.push(arr);
83
- }
84
- blocks[key] = arrays.length === 1 ? arrays[0] : concatenate(arrays, dtype);
85
- }
86
- // Attach all block meta info
87
- blocks._meta = meta;
88
- return blocks;
89
- }
90
 
91
- // --- MAIN VIEWER ---
92
  export async function initializeViewer(config, instanceId) {
93
- if (viewerInitialized) return;
94
-
95
- // Parse config and model params
96
- sogsMetaUrl = config.sogs_json_url; // e.g. .../meta.json
97
- glbUrl = config.glb_url;
98
- // Derive the base URL for images
99
- gsosBaseUrl = sogsMetaUrl.substring(0, sogsMetaUrl.lastIndexOf('/'));
100
-
101
- minZoom = parseFloat(config.minZoom || "1");
102
- maxZoom = parseFloat(config.maxZoom || "20");
103
- minAngle = parseFloat(config.minAngle || "-45");
104
- maxAngle = parseFloat(config.maxAngle || "90");
105
- minAzimuth= (config.minAzimuth !== undefined) ? parseFloat(config.minAzimuth) : -360;
106
- maxAzimuth= (config.maxAzimuth !== undefined) ? parseFloat(config.maxAzimuth) : 360;
107
- minPivotY = parseFloat(config.minPivotY || "0");
108
- minY = (config.minY !== undefined) ? parseFloat(config.minY) : 0;
109
-
110
- modelX = (config.modelX !== undefined) ? parseFloat(config.modelX) : 0;
111
- modelY = (config.modelY !== undefined) ? parseFloat(config.modelY) : 0;
112
- modelZ = (config.modelZ !== undefined) ? parseFloat(config.modelZ) : 0;
113
- modelScale = (config.modelScale !== undefined) ? parseFloat(config.modelScale) : 1;
114
- modelRotationX = (config.modelRotationX !== undefined) ? parseFloat(config.modelRotationX) : 0;
115
- modelRotationY = (config.modelRotationY !== undefined) ? parseFloat(config.modelRotationY) : 0;
116
- modelRotationZ = (config.modelRotationZ !== undefined) ? parseFloat(config.modelRotationZ) : 0;
117
-
118
- const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
119
- const isMobile = isIOS || /Android/i.test(navigator.userAgent);
120
-
121
- const cameraX = (config.cameraX !== undefined) ? parseFloat(config.cameraX) : 0;
122
- const cameraY = (config.cameraY !== undefined) ? parseFloat(config.cameraY) : 2;
123
- const cameraZ = (config.cameraZ !== undefined) ? parseFloat(config.cameraZ) : 5;
124
- const cameraXPhone = (config.cameraXPhone !== undefined) ? parseFloat(config.cameraXPhone) : cameraX;
125
- const cameraYPhone = (config.cameraYPhone !== undefined) ? parseFloat(config.cameraYPhone) : cameraY;
126
- const cameraZPhone = (config.cameraZPhone !== undefined) ? parseFloat(config.cameraZPhone) : (cameraZ * 1.5);
127
-
128
- chosenCameraX = isMobile ? cameraXPhone : cameraX;
129
- chosenCameraY = isMobile ? cameraYPhone : cameraY;
130
- chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
131
-
132
- const canvasId = 'canvas-' + instanceId;
133
- const progressDialog = document.getElementById('progress-dialog-' + instanceId);
134
- const viewerContainer = document.getElementById('viewer-container-' + instanceId);
135
-
136
- let oldCanvas = document.getElementById(canvasId);
137
- if (oldCanvas) oldCanvas.remove();
138
-
139
- const canvas = document.createElement('canvas');
140
- canvas.id = canvasId;
141
- canvas.className = 'ply-canvas';
142
- canvas.style.width = "100%";
143
- canvas.style.height = "100%";
144
- canvas.setAttribute('tabindex', '0');
145
- viewerContainer.insertBefore(canvas, progressDialog);
146
-
147
- canvas.style.touchAction = "none";
148
- canvas.style.webkitTouchCallout = "none";
149
- canvas.addEventListener('gesturestart', e => e.preventDefault());
150
- canvas.addEventListener('gesturechange', e => e.preventDefault());
151
- canvas.addEventListener('gestureend', e => e.preventDefault());
152
- canvas.addEventListener('dblclick', e => e.preventDefault());
153
- canvas.addEventListener('touchstart', e => { if (e.touches.length > 1) e.preventDefault(); }, { passive: false });
154
- canvas.addEventListener('wheel', (e) => { e.preventDefault(); }, { passive: false });
155
-
156
- if (progressDialog) progressDialog.style.display = 'block';
157
-
158
- // --- PlayCanvas Engine load ---
159
- if (!pc) {
160
- pc = await import("https://esm.run/playcanvas");
161
- window.pc = pc;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  }
163
 
164
- // --- Graphics device/app setup ---
165
- const device = await pc.createGraphicsDevice(canvas, {
166
- deviceTypes: ["webgl2", "webgl1"],
167
- glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
168
- twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
169
- antialias: false
170
- });
171
- device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
172
-
173
- const opts = new pc.AppOptions();
174
- opts.graphicsDevice = device;
175
- opts.mouse = new pc.Mouse(canvas);
176
- opts.touch = new pc.TouchDevice(canvas);
177
- opts.componentSystems = [
178
- pc.RenderComponentSystem,
179
- pc.CameraComponentSystem,
180
- pc.LightComponentSystem,
181
- pc.ScriptComponentSystem,
182
- pc.GSplatComponentSystem,
183
- pc.CollisionComponentSystem,
184
- pc.RigidbodyComponentSystem
185
- ];
186
- opts.resourceHandlers = [
187
- pc.TextureHandler,
188
- pc.ContainerHandler,
189
- pc.ScriptHandler,
190
- pc.GSplatHandler
191
- ];
192
-
193
- app = new pc.Application(canvas, opts);
194
- app.setCanvasFillMode(pc.FILLMODE_NONE);
195
- app.setCanvasResolution(pc.RESOLUTION_AUTO);
196
-
197
- // --- Responsive resize ---
198
- resizeObserver = new ResizeObserver(entries => {
199
- entries.forEach(entry => {
200
- app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
201
- });
202
  });
203
- resizeObserver.observe(viewerContainer);
204
- window.addEventListener('resize', () => app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight));
205
- app.on('destroy', () => resizeObserver.disconnect());
 
 
 
 
206
 
207
- // --- LOAD GSOS/GSplat DATA ---
208
- let gsosBlocks = null;
209
  try {
210
- gsosBlocks = await loadGSOSMeta(sogsMetaUrl, gsosBaseUrl);
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  } catch (e) {
212
- console.error("Failed to load GSOS meta/images:", e);
213
- if (progressDialog) progressDialog.style.display = 'none';
214
- return;
215
  }
216
 
217
- // --- Assets: GLB, orbit camera, etc ---
218
- const assets = {
219
- orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
220
- };
221
- if (glbUrl) {
222
- assets.glb = new pc.Asset('glb', 'container', { url: glbUrl });
223
- }
224
- for (const key in assets) app.assets.add(assets[key]);
225
-
226
- const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
227
- loader.load(() => {
228
- app.start();
229
- if (progressDialog) progressDialog.style.display = 'none';
230
-
231
- // --- Create GSplat entity from blocks ---
232
- // You'll need to adapt this to your PlayCanvas GSplat API!
233
- // Below is a skeleton; you'll need a custom GSplatComponent, or render using a Mesh/Shader, etc.
234
- modelEntity = new pc.Entity('gsos-model');
235
-
236
- // Here is where you would plug in your GSplat/GSOS loader logic:
237
- // (Below assumes you have a 'GSplatComponent' or similar)
238
- if (pc && pc.GSplatComponent && modelEntity.addComponent) {
239
- // Replace this with the actual GSplat API for your PlayCanvas fork/module!
240
- modelEntity.addComponent('gsplat', {
241
- means: gsosBlocks.means,
242
- scales: gsosBlocks.scales,
243
- quats: gsosBlocks.quats,
244
- sh0: gsosBlocks.sh0,
245
- meta: gsosBlocks._meta
246
- });
247
- } else {
248
- // Otherwise: custom point rendering, or just dump the arrays for debug
249
- console.warn("No GSplatComponent found! Model will not be visible.");
250
- window._gsosBlocks = gsosBlocks;
251
- }
252
- modelEntity.setLocalPosition(modelX, modelY, modelZ);
253
- modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
254
- modelEntity.setLocalScale(modelScale, modelScale, modelScale);
255
- app.root.addChild(modelEntity);
256
-
257
- // --- Add the GLB entity if provided ---
258
- if (assets.glb && assets.glb.resource && assets.glb.resource.instantiateRenderEntity) {
259
- const glbEntity = assets.glb.resource.instantiateRenderEntity();
260
- app.root.addChild(glbEntity);
261
- }
262
-
263
- // --- Camera setup ---
264
- cameraEntity = new pc.Entity('camera');
265
- cameraEntity.addComponent('camera', { clearColor: new pc.Color(1, 1, 1, 1) });
266
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
267
- cameraEntity.lookAt(modelEntity.getPosition());
268
- cameraEntity.addComponent('script');
269
- cameraEntity.script.create('orbitCamera', {
270
- attributes: {
271
- focusEntity: modelEntity,
272
- inertiaFactor: 0.2,
273
- distanceMax: maxZoom,
274
- distanceMin: minZoom,
275
- pitchAngleMax: maxAngle,
276
- pitchAngleMin: minAngle,
277
- yawAngleMax: maxAzimuth,
278
- yawAngleMin: minAzimuth,
279
- minY: minY,
280
- frameOnStart: false
281
- }
282
- });
283
- cameraEntity.script.create('orbitCameraInputMouse');
284
- cameraEntity.script.create('orbitCameraInputTouch');
285
- app.root.addChild(cameraEntity);
286
-
287
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
288
-
289
- app.once('update', () => resetViewerCamera());
290
-
291
- // --- Optional: Tooltips support ---
292
- try {
293
- if (config.tooltips_url) {
294
- import('./tooltips.js').then(tooltipsModule => {
295
- tooltipsModule.initializeTooltips({
296
- app,
297
- cameraEntity,
298
- modelEntity,
299
- tooltipsUrl: config.tooltips_url,
300
- defaultVisible: !!config.showTooltipsDefault,
301
- moveDuration: config.tooltipMoveDuration || 0.6
302
- });
303
- }).catch(e => { });
304
- }
305
- } catch (e) { }
306
-
307
- viewerInitialized = true;
308
- });
309
  }
310
 
311
- // --- Camera reset helper ---
312
  export function resetViewerCamera() {
313
- try {
314
- if (!cameraEntity || !modelEntity || !app) return;
315
- const orbitCam = cameraEntity.script.orbitCamera;
316
- if (!orbitCam) return;
317
-
318
- const modelPos = modelEntity.getPosition();
319
- const tempEnt = new pc.Entity();
320
- tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
321
- tempEnt.lookAt(modelPos);
322
-
323
- const dist = new pc.Vec3().sub2(
324
- new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
325
- modelPos
326
- ).length();
327
-
328
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
329
- cameraEntity.lookAt(modelPos);
330
-
331
- orbitCam.pivotPoint = modelPos.clone();
332
- orbitCam._targetDistance = dist;
333
- orbitCam._distance = dist;
334
-
335
- const rot = tempEnt.getRotation();
336
- const fwd = new pc.Vec3();
337
- rot.transformVector(pc.Vec3.FORWARD, fwd);
338
-
339
- const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
340
- const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
341
- const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
342
- const fNoYaw = new pc.Vec3();
343
- rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
344
- const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
345
-
346
- orbitCam._targetYaw = yaw;
347
- orbitCam._yaw = yaw;
348
- orbitCam._targetPitch = pitch;
349
- orbitCam._pitch = pitch;
350
- if (orbitCam._updatePosition) orbitCam._updatePosition();
351
-
352
- tempEnt.destroy();
353
- } catch (e) { }
 
 
354
  }
 
1
  // viewer.js
2
  // ==============================
3
 
4
+ let pc;
5
  export let app = null;
6
  let cameraEntity = null;
7
  let modelEntity = null;
 
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 sogsUrl, glbUrl;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ sogsUrl = config.sogs_json_url;
23
+ glbUrl = config.glb_url;
24
+ minZoom = parseFloat(config.minZoom || "1");
25
+ maxZoom = parseFloat(config.maxZoom || "20");
26
+ minAngle = parseFloat(config.minAngle || "-45");
27
+ maxAngle = parseFloat(config.maxAngle || "90");
28
+ minAzimuth = (config.minAzimuth !== undefined) ? parseFloat(config.minAzimuth) : -360;
29
+ maxAzimuth = (config.maxAzimuth !== undefined) ? parseFloat(config.maxAzimuth) : 360;
30
+ minPivotY = parseFloat(config.minPivotY || "0");
31
+ minY = (config.minY !== undefined) ? parseFloat(config.minY) : 0;
32
+
33
+ modelX = (config.modelX !== undefined) ? parseFloat(config.modelX) : 0;
34
+ modelY = (config.modelY !== undefined) ? parseFloat(config.modelY) : 0;
35
+ modelZ = (config.modelZ !== undefined) ? parseFloat(config.modelZ) : 0;
36
+ modelScale = (config.modelScale !== undefined) ? parseFloat(config.modelScale) : 1;
37
+ modelRotationX = (config.modelRotationX !== undefined) ? parseFloat(config.modelRotationX) : 0;
38
+ modelRotationY = (config.modelRotationY !== undefined) ? parseFloat(config.modelRotationY) : 0;
39
+ modelRotationZ = (config.modelRotationZ !== undefined) ? parseFloat(config.modelRotationZ) : 0;
40
+
41
+ const cameraX = (config.cameraX !== undefined) ? parseFloat(config.cameraX) : 0;
42
+ const cameraY = (config.cameraY !== undefined) ? parseFloat(config.cameraY) : 2;
43
+ const cameraZ = (config.cameraZ !== undefined) ? parseFloat(config.cameraZ) : 5;
44
+ const cameraXPhone = (config.cameraXPhone !== undefined) ? parseFloat(config.cameraXPhone) : cameraX;
45
+ const cameraYPhone = (config.cameraYPhone !== undefined) ? parseFloat(config.cameraYPhone) : cameraY;
46
+ const cameraZPhone = (config.cameraZPhone !== undefined) ? parseFloat(config.cameraZPhone) : (cameraZ * 1.5);
47
+
48
+ chosenCameraX = isMobile ? cameraXPhone : cameraX;
49
+ chosenCameraY = isMobile ? cameraYPhone : cameraY;
50
+ chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
51
+
52
+ const canvasId = 'canvas-' + instanceId;
53
+ const progressDialog = document.getElementById('progress-dialog-' + instanceId);
54
+ const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
55
+ const viewerContainer = document.getElementById('viewer-container-' + instanceId);
56
+
57
+ let oldCanvas = document.getElementById(canvasId);
58
+ if (oldCanvas) oldCanvas.remove();
59
+
60
+ const canvas = document.createElement('canvas');
61
+ canvas.id = canvasId;
62
+ canvas.className = 'ply-canvas';
63
+ canvas.style.width = "100%";
64
+ canvas.style.height = "100%";
65
+ canvas.setAttribute('tabindex', '0');
66
+ viewerContainer.insertBefore(canvas, progressDialog);
67
+
68
+ canvas.style.touchAction = "none";
69
+ canvas.style.webkitTouchCallout = "none";
70
+ canvas.addEventListener('gesturestart', e => e.preventDefault());
71
+ canvas.addEventListener('gesturechange', e => e.preventDefault());
72
+ canvas.addEventListener('gestureend', e => e.preventDefault());
73
+ canvas.addEventListener('dblclick', e => e.preventDefault());
74
+ canvas.addEventListener('touchstart', e => { if (e.touches.length > 1) e.preventDefault(); }, { passive: false });
75
+
76
+ // --- The following line attaches mouse wheel suppression to canvas only ---
77
+ canvas.addEventListener('wheel', (e) => {
78
+ e.preventDefault(); // Only block page scroll if mouse is over viewer
79
+ }, { passive: false });
80
+
81
+ progressDialog.style.display = 'block';
82
+
83
+ if (!pc) {
84
+ pc = await import("https://esm.run/playcanvas");
85
+ window.pc = pc;
86
+ }
87
+
88
+ // Create app first
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
+ // Attach input only to canvas
100
+ opts.mouse = new pc.Mouse(canvas);
101
+ opts.touch = new pc.TouchDevice(canvas);
102
+ opts.componentSystems = [
103
+ pc.RenderComponentSystem,
104
+ pc.CameraComponentSystem,
105
+ pc.LightComponentSystem,
106
+ pc.ScriptComponentSystem,
107
+ pc.GSplatComponentSystem,
108
+ pc.CollisionComponentSystem,
109
+ pc.RigidbodyComponentSystem
110
+ ];
111
+ opts.resourceHandlers = [
112
+ pc.TextureHandler,
113
+ pc.ContainerHandler,
114
+ pc.ScriptHandler,
115
+ pc.GSplatHandler
116
+ ];
117
+
118
+ app = new pc.Application(canvas, opts);
119
+ app.setCanvasFillMode(pc.FILLMODE_NONE);
120
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
121
+
122
+ resizeObserver = new ResizeObserver(entries => {
123
+ entries.forEach(entry => {
124
+ app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
125
+ });
126
+ });
127
+ resizeObserver.observe(viewerContainer);
128
+
129
+ window.addEventListener('resize', () => app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight));
130
+ app.on('destroy', () => resizeObserver.disconnect());
131
+
132
+ // Assets after app exists
133
+ const assets = {
134
+ sogs: new pc.Asset('gsplat', 'gsplat', { url: sogsUrl }),
135
+ orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
136
+ glb: new pc.Asset('glb', 'container', { url: glbUrl }),
137
+ };
138
+ for (const key in assets) app.assets.add(assets[key]);
139
+
140
+ const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
141
+ loader.load(() => {
142
+ app.start();
143
+ progressDialog.style.display = 'none';
144
+
145
+ // Add PLY/GSplat model
146
+ modelEntity = new pc.Entity('model');
147
+ modelEntity.addComponent('gsplat', { asset: assets.sogs });
148
+ modelEntity.setLocalPosition(modelX, modelY, modelZ);
149
+ modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
150
+ modelEntity.setLocalScale(modelScale, modelScale, modelScale);
151
+ app.root.addChild(modelEntity);
152
+
153
+ // --- Add the GLB entity ---
154
+ if (assets.glb && assets.glb.resource && assets.glb.resource.instantiateRenderEntity) {
155
+ const glbEntity = assets.glb.resource.instantiateRenderEntity();
156
+ app.root.addChild(glbEntity);
157
  }
158
 
159
+ cameraEntity = new pc.Entity('camera');
160
+ // PURE WHITE BACKGROUND
161
+ cameraEntity.addComponent('camera', { clearColor: new pc.Color(1, 1, 1, 1) }); // White RGBA
162
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
163
+ cameraEntity.lookAt(modelEntity.getPosition());
164
+ cameraEntity.addComponent('script');
165
+
166
+ // === MAIN FIX: Pass ALL relevant attributes including minY ===
167
+ cameraEntity.script.create('orbitCamera', {
168
+ attributes: {
169
+ focusEntity: modelEntity,
170
+ inertiaFactor: 0.2,
171
+ distanceMax: maxZoom,
172
+ distanceMin: minZoom,
173
+ pitchAngleMax: maxAngle,
174
+ pitchAngleMin: minAngle,
175
+ yawAngleMax: maxAzimuth,
176
+ yawAngleMin: minAzimuth,
177
+ minY: minY,
178
+ frameOnStart: false
179
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  });
181
+ cameraEntity.script.create('orbitCameraInputMouse');
182
+ cameraEntity.script.create('orbitCameraInputTouch');
183
+ app.root.addChild(cameraEntity);
184
+
185
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
186
+
187
+ app.once('update', () => resetViewerCamera());
188
 
189
+ // Tooltips supported if tooltips_url set
 
190
  try {
191
+ if (config.tooltips_url) {
192
+ import('./tooltips.js').then(tooltipsModule => {
193
+ tooltipsModule.initializeTooltips({
194
+ app,
195
+ cameraEntity,
196
+ modelEntity,
197
+ tooltipsUrl: config.tooltips_url,
198
+ defaultVisible: !!config.showTooltipsDefault,
199
+ moveDuration: config.tooltipMoveDuration || 0.6
200
+ });
201
+ }).catch(e => {
202
+ // Tooltips optional: fail silently if missing
203
+ });
204
+ }
205
  } catch (e) {
206
+ // Tooltips optional, fail silently
 
 
207
  }
208
 
209
+ viewerInitialized = true;
210
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  }
212
 
 
213
  export function resetViewerCamera() {
214
+ try {
215
+ if (!cameraEntity || !modelEntity || !app) return;
216
+ const orbitCam = cameraEntity.script.orbitCamera;
217
+ if (!orbitCam) return;
218
+
219
+ const modelPos = modelEntity.getPosition();
220
+ const tempEnt = new pc.Entity();
221
+ tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
222
+ tempEnt.lookAt(modelPos);
223
+
224
+ const dist = new pc.Vec3().sub2(
225
+ new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
226
+ modelPos
227
+ ).length();
228
+
229
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
230
+ cameraEntity.lookAt(modelPos);
231
+
232
+ orbitCam.pivotPoint = modelPos.clone();
233
+ orbitCam._targetDistance = dist;
234
+ orbitCam._distance = dist;
235
+
236
+ const rot = tempEnt.getRotation();
237
+ const fwd = new pc.Vec3();
238
+ rot.transformVector(pc.Vec3.FORWARD, fwd);
239
+
240
+ const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
241
+ const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
242
+ const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
243
+ const fNoYaw = new pc.Vec3();
244
+ rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
245
+ const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
246
+
247
+ orbitCam._targetYaw = yaw;
248
+ orbitCam._yaw = yaw;
249
+ orbitCam._targetPitch = pitch;
250
+ orbitCam._pitch = pitch;
251
+ if (orbitCam._updatePosition) orbitCam._updatePosition();
252
+
253
+ tempEnt.destroy();
254
+ } catch (e) {
255
+ // Silent fail
256
+ }
257
  }