MikaFil commited on
Commit
e2ed8cc
·
verified ·
1 Parent(s): 7e8523e

Create viewer_pr_env.js

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