MikaFil commited on
Commit
3453317
·
verified ·
1 Parent(s): d1d9c09

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +163 -806
viewer.js CHANGED
@@ -2,27 +2,27 @@
2
  // ==============================
3
 
4
  async function loadImageAsTexture(url, app) {
5
- return new Promise((resolve, reject) => {
6
- const img = new window.Image();
7
- img.crossOrigin = "anonymous";
8
- img.onload = function () {
9
- const tex = new pc.Texture(app.graphicsDevice, {
10
- width: img.width,
11
- height: img.height,
12
- format: pc.PIXELFORMAT_R8_G8_B8_A8
13
- });
14
- tex.setSource(img);
15
- resolve(tex);
16
- };
17
- img.onerror = reject;
18
- img.src = url;
19
- });
20
  }
21
 
22
  // --- Patch all window.Image to force crossOrigin="anonymous" ---
23
- (function () {
24
  const OriginalImage = window.Image;
25
- window.Image = function (...args) {
26
  const img = new OriginalImage(...args);
27
  img.crossOrigin = "anonymous";
28
  return img;
@@ -36,10 +36,10 @@ function hexToRgbaArray(hex) {
36
  if (hex.length !== 8) return [1, 1, 1, 1];
37
  const num = parseInt(hex, 16);
38
  return [
39
- ((num >> 24) & 0xff) / 255,
40
- ((num >> 16) & 0xff) / 255,
41
- ((num >> 8) & 0xff) / 255,
42
- (num & 0xff) / 255
43
  ];
44
  } catch (e) {
45
  alert("hexToRgbaArray error: " + e);
@@ -49,709 +49,12 @@ function hexToRgbaArray(hex) {
49
 
50
  // ----- Utility: Recursive scene traversal -----
51
  function traverse(entity, callback) {
52
- callback(entity);
53
- if (entity.children) {
54
- entity.children.forEach((child) => traverse(child, callback));
55
- }
56
- }
57
-
58
- // ---------- Enregistre les scripts d’orbite sous des NOMS UNIQUES ----------
59
- function registerOrbitScriptsForInstance(suffix) {
60
- const ORBIT = "orbitCamera_" + suffix;
61
- const MOUSE = "orbitCameraInputMouse_" + suffix;
62
- const TOUCH = "orbitCameraInputTouch_" + suffix;
63
- const KEYBD = "orbitCameraInputKeyboard_" + suffix;
64
-
65
- // ---------------- OrbitCamera ----------------
66
- var OrbitCamera = pc.createScript(ORBIT);
67
-
68
- OrbitCamera.attributes.add('distanceMax', { type: 'number', default: 20, title: 'Distance Max' });
69
- OrbitCamera.attributes.add('distanceMin', { type: 'number', default: 1, title: 'Distance Min' });
70
- OrbitCamera.attributes.add('pitchAngleMax', { type: 'number', default: 90, title: 'Pitch Angle Max (degrees)' });
71
- OrbitCamera.attributes.add('pitchAngleMin', { type: 'number', default: 0, title: 'Pitch Angle Min (degrees)' });
72
- OrbitCamera.attributes.add('yawAngleMax', { type: 'number', default: 360, title: 'Yaw Angle Max (degrees)' });
73
- OrbitCamera.attributes.add('yawAngleMin', { type: 'number', default: -360, title: 'Yaw Angle Min (degrees)' });
74
- OrbitCamera.attributes.add('minY', { type: 'number', default: 0, title: 'Minimum Y' });
75
-
76
- OrbitCamera.attributes.add('inertiaFactor', { type: 'number', default: 0.2, title: 'Inertia Factor' });
77
- OrbitCamera.attributes.add('focusEntity', { type: 'entity', title: 'Focus Entity' });
78
- OrbitCamera.attributes.add('frameOnStart', { type: 'boolean', default: true, title: 'Frame on Start' });
79
-
80
- Object.defineProperty(OrbitCamera.prototype, 'distance', {
81
- get: function () { return this._targetDistance; },
82
- set: function (value) { this._targetDistance = this._clampDistance(value); }
83
- });
84
-
85
- Object.defineProperty(OrbitCamera.prototype, 'orthoHeight', {
86
- get: function () { return this.entity.camera.orthoHeight; },
87
- set: function (value) { this.entity.camera.orthoHeight = Math.max(0, value); }
88
- });
89
-
90
- Object.defineProperty(OrbitCamera.prototype, 'pitch', {
91
- get: function () { return this._targetPitch; },
92
- set: function (value) { this._targetPitch = this._clampPitchAngle(value); }
93
- });
94
-
95
- Object.defineProperty(OrbitCamera.prototype, 'yaw', {
96
- get: function () { return this._targetYaw; },
97
- set: function (value) { this._targetYaw = this._clampYawAngle(value); }
98
- });
99
-
100
- Object.defineProperty(OrbitCamera.prototype, 'pivotPoint', {
101
- get: function () { return this._pivotPoint; },
102
- set: function (value) { this._pivotPoint.copy(value); }
103
- });
104
-
105
- OrbitCamera.prototype.focus = function (focusEntity) {
106
- this._buildAabb(focusEntity);
107
- var halfExtents = this._modelsAabb.halfExtents;
108
- var radius = Math.max(halfExtents.x, Math.max(halfExtents.y, halfExtents.z));
109
- this.distance = (radius * 1.5) / Math.sin(0.5 * this.entity.camera.fov * pc.math.DEG_TO_RAD);
110
- this._removeInertia();
111
- this._pivotPoint.copy(this._modelsAabb.center);
112
- };
113
-
114
- OrbitCamera.distanceBetween = new pc.Vec3();
115
-
116
- OrbitCamera.prototype.resetAndLookAtPoint = function (resetPoint, lookAtPoint) {
117
- this.pivotPoint.copy(lookAtPoint);
118
- this.entity.setPosition(resetPoint);
119
- this.entity.lookAt(lookAtPoint);
120
- var distance = OrbitCamera.distanceBetween;
121
- distance.sub2(lookAtPoint, resetPoint);
122
- this.distance = distance.length();
123
- this.pivotPoint.copy(lookAtPoint);
124
- var cameraQuat = this.entity.getRotation();
125
- this.yaw = this._calcYaw(cameraQuat);
126
- this.pitch = this._calcPitch(cameraQuat, this.yaw);
127
- this._removeInertia();
128
- this._updatePosition();
129
- };
130
-
131
- OrbitCamera.prototype.resetAndLookAtEntity = function (resetPoint, entity) {
132
- this._buildAabb(entity);
133
- this.resetAndLookAtPoint(resetPoint, this._modelsAabb.center);
134
- };
135
-
136
- OrbitCamera.prototype.reset = function (yaw, pitch, distance) {
137
- this.pitch = pitch;
138
- this.yaw = yaw;
139
- this.distance = distance;
140
- this._removeInertia();
141
- };
142
-
143
- OrbitCamera.prototype.resetToPosition = function (position, lookAtPoint) {
144
- this.entity.setPosition(position);
145
- this.entity.lookAt(lookAtPoint);
146
- this._pivotPoint.copy(lookAtPoint);
147
- var distanceVec = new pc.Vec3();
148
- distanceVec.sub2(position, lookAtPoint);
149
- this._targetDistance = this._distance = distanceVec.length();
150
- var cameraQuat = this.entity.getRotation();
151
- this._targetYaw = this._yaw = this._calcYaw(cameraQuat);
152
- this._targetPitch = this._pitch = this._calcPitch(cameraQuat, this._yaw);
153
- this._removeInertia();
154
- this._updatePosition();
155
- };
156
-
157
- // Helper: calc cam Y if pivot became 'pivot'
158
- OrbitCamera.prototype.worldCameraYForPivot = function(pivot) {
159
- var quat = new pc.Quat();
160
- quat.setFromEulerAngles(this._pitch, this._yaw, 0);
161
- var forward = new pc.Vec3();
162
- quat.transformVector(pc.Vec3.FORWARD, forward);
163
- var camPos = pivot.clone();
164
- camPos.add(forward.clone().scale(-this._distance));
165
- return camPos.y;
166
- };
167
-
168
- OrbitCamera.prototype.initialize = function () {
169
- var self = this;
170
- var onWindowResize = function () { self._checkAspectRatio(); };
171
- window.addEventListener('resize', onWindowResize, false);
172
- this._checkAspectRatio();
173
-
174
- this._modelsAabb = new pc.BoundingBox();
175
- this._buildAabb(this.focusEntity || this.app.root);
176
-
177
- this.entity.lookAt(this._modelsAabb.center);
178
- this._pivotPoint = new pc.Vec3();
179
- this._pivotPoint.copy(this._modelsAabb.center);
180
-
181
- var cameraQuat = this.entity.getRotation();
182
- this._yaw = this._calcYaw(cameraQuat);
183
- this._pitch = this._clampPitchAngle(this._calcPitch(cameraQuat, this._yaw));
184
- this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
185
-
186
- this._distance = 0;
187
- this._targetYaw = this._yaw;
188
- this._targetPitch = this._pitch;
189
-
190
- if (this.frameOnStart) {
191
- this.focus(this.focusEntity || this.app.root);
192
- } else {
193
- var distanceBetween = new pc.Vec3();
194
- distanceBetween.sub2(this.entity.getPosition(), this._pivotPoint);
195
- this._distance = this._clampDistance(distanceBetween.length());
196
- }
197
- this._targetDistance = this._distance;
198
-
199
- this.on('attr:distanceMin', function () { this._distance = this._clampDistance(this._distance); });
200
- this.on('attr:distanceMax', function () { this._distance = this._clampDistance(this._distance); });
201
- this.on('attr:pitchAngleMin', function () { this._pitch = this._clampPitchAngle(this._pitch); });
202
- this.on('attr:pitchAngleMax', function () { this._pitch = this._clampPitchAngle(this._pitch); });
203
-
204
- this.on('attr:focusEntity', function (value) {
205
- if (this.frameOnStart) { this.focus(value || this.app.root); }
206
- else { this.resetAndLookAtEntity(this.entity.getPosition(), value || this.app.root); }
207
- });
208
-
209
- this.on('attr:frameOnStart', function (value) {
210
- if (value) { this.focus(this.focusEntity || this.app.root); }
211
- });
212
-
213
- this.on('destroy', function () { window.removeEventListener('resize', onWindowResize, false); });
214
- };
215
-
216
- OrbitCamera.prototype.update = function (dt) {
217
- var t = this.inertiaFactor === 0 ? 1 : Math.min(dt / this.inertiaFactor, 1);
218
- this._distance = pc.math.lerp(this._distance, this._targetDistance, t);
219
- this._yaw = pc.math.lerp(this._yaw, this._targetYaw, t);
220
- this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t);
221
- this._updatePosition();
222
- };
223
-
224
- OrbitCamera.prototype._updatePosition = function () {
225
- this.entity.setLocalPosition(0, 0, 0);
226
- this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
227
-
228
- var position = this.entity.getPosition();
229
- position.copy(this.entity.forward);
230
- position.mulScalar(-this._distance);
231
- position.add(this.pivotPoint);
232
-
233
- position.y = Math.max(position.y, this.minY);
234
- this.entity.setPosition(position);
235
- };
236
-
237
- OrbitCamera.prototype._removeInertia = function () {
238
- this._yaw = this._targetYaw;
239
- this._pitch = this._targetPitch;
240
- this._distance = this._targetDistance;
241
- };
242
-
243
- OrbitCamera.prototype._checkAspectRatio = function () {
244
- var height = this.app.graphicsDevice.height;
245
- var width = this.app.graphicsDevice.width;
246
- this.entity.camera.horizontalFov = (height > width);
247
- };
248
-
249
- OrbitCamera.prototype._buildAabb = function (entity) {
250
- var i, m, meshInstances = [];
251
-
252
- var renders = entity.findComponents('render');
253
- for (i = 0; i < renders.length; i++) {
254
- var render = renders[i];
255
- for (m = 0; m < render.meshInstances.length; m++) {
256
- meshInstances.push(render.meshInstances[m]);
257
- }
258
- }
259
-
260
- var models = entity.findComponents('model');
261
- for (i = 0; i < models.length; i++) {
262
- var model = models[i];
263
- for (m = 0; m < model.meshInstances.length; m++) {
264
- meshInstances.push(model.meshInstances[m]);
265
- }
266
- }
267
-
268
- var gsplats = entity.findComponents('gsplat');
269
- for (i = 0; i < gsplats.length; i++) {
270
- var gsplat = gsplats[i];
271
- var instance = gsplat.instance;
272
- if (instance?.meshInstance) {
273
- meshInstances.push(instance.meshInstance);
274
- }
275
- }
276
-
277
- for (i = 0; i < meshInstances.length; i++) {
278
- if (i === 0) {
279
- this._modelsAabb.copy(meshInstances[i].aabb);
280
- } else {
281
- this._modelsAabb.add(meshInstances[i].aabb);
282
- }
283
- }
284
- };
285
-
286
- OrbitCamera.prototype._calcYaw = function (quat) {
287
- var transformedForward = new pc.Vec3();
288
- quat.transformVector(pc.Vec3.FORWARD, transformedForward);
289
- return Math.atan2(-transformedForward.x, -transformedForward.z) * pc.math.RAD_TO_DEG;
290
- };
291
-
292
- OrbitCamera.prototype._clampDistance = function (distance) {
293
- if (this.distanceMax > 0) {
294
- return pc.math.clamp(distance, this.distanceMin, this.distanceMax);
295
- }
296
- return Math.max(distance, this.distanceMin);
297
- };
298
-
299
- OrbitCamera.prototype._clampPitchAngle = function (pitch) {
300
- return pc.math.clamp(pitch, this.pitchAngleMin, this.pitchAngleMax);
301
- };
302
-
303
- OrbitCamera.prototype._clampYawAngle = function (yaw) {
304
- return pc.math.clamp(yaw, -this.yawAngleMax, -this.yawAngleMin);
305
- };
306
-
307
- OrbitCamera.quatWithoutYaw = new pc.Quat();
308
- OrbitCamera.yawOffset = new pc.Quat();
309
-
310
- OrbitCamera.prototype._calcPitch = function (quat, yaw) {
311
- var quatWithoutYaw = OrbitCamera.quatWithoutYaw;
312
- var yawOffset = OrbitCamera.yawOffset;
313
- yawOffset.setFromEulerAngles(0, -yaw, 0);
314
- quatWithoutYaw.mul2(yawOffset, quat);
315
- var transformedForward = new pc.Vec3();
316
- quatWithoutYaw.transformVector(pc.Vec3.FORWARD, transformedForward);
317
- return Math.atan2(-transformedForward.y, -transformedForward.z) * pc.math.RAD_TO_DEG;
318
- };
319
-
320
- // ---------------- Mouse ----------------
321
- var OrbitCameraInputMouse = pc.createScript(MOUSE);
322
- OrbitCameraInputMouse.attributes.add('orbitSensitivity', { type: 'number', default: 0.3 });
323
- OrbitCameraInputMouse.attributes.add('distanceSensitivity', { type: 'number', default: 0.4 });
324
-
325
- OrbitCameraInputMouse.prototype.initialize = function () {
326
- this.orbitCamera = this.entity.script[ORBIT];
327
-
328
- if (this.orbitCamera) {
329
- var self = this;
330
- var onMouseOut = function () { self.onMouseOut(); };
331
-
332
- this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
333
- this.app.mouse.on(pc.EVENT_MOUSEUP, this.onMouseUp, this);
334
- this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
335
- this.app.mouse.on(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this);
336
- window.addEventListener('mouseout', onMouseOut, false);
337
-
338
- this.on('destroy', function () {
339
- this.app.mouse.off(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
340
- this.app.mouse.off(pc.EVENT_MOUSEUP, this.onMouseUp, this);
341
- this.app.mouse.off(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
342
- this.app.mouse.off(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this);
343
- window.removeEventListener('mouseout', onMouseOut, false);
344
- });
345
- }
346
-
347
- this.app.mouse.disableContextMenu();
348
-
349
- this.lookButtonDown = false;
350
- this.panButtonDown = false;
351
- this.lastPoint = new pc.Vec2();
352
- };
353
-
354
- OrbitCameraInputMouse.fromWorldPoint = new pc.Vec3();
355
- OrbitCameraInputMouse.toWorldPoint = new pc.Vec3();
356
- OrbitCameraInputMouse.worldDiff = new pc.Vec3();
357
-
358
- OrbitCameraInputMouse.prototype.pan = function (screenPoint) {
359
- var fromWorldPoint = OrbitCameraInputMouse.fromWorldPoint;
360
- var toWorldPoint = OrbitCameraInputMouse.toWorldPoint;
361
- var worldDiff = OrbitCameraInputMouse.worldDiff;
362
-
363
- var camera = this.entity.camera;
364
- var distance = this.orbitCamera.distance;
365
-
366
- camera.screenToWorld(screenPoint.x, screenPoint.y, distance, fromWorldPoint);
367
- camera.screenToWorld(this.lastPoint.x, this.lastPoint.y, distance, toWorldPoint);
368
-
369
- worldDiff.sub2(toWorldPoint, fromWorldPoint);
370
-
371
- var proposedPivot = this.orbitCamera.pivotPoint.clone().add(worldDiff);
372
- var minY = this.orbitCamera.minY;
373
- var resultingY = this.orbitCamera.worldCameraYForPivot(proposedPivot);
374
-
375
- if (resultingY >= minY - 1e-4) {
376
- this.orbitCamera.pivotPoint.add(worldDiff);
377
- } else {
378
- worldDiff.y = 0;
379
- proposedPivot = this.orbitCamera.pivotPoint.clone().add(worldDiff);
380
- resultingY = this.orbitCamera.worldCameraYForPivot(proposedPivot);
381
- if (resultingY >= minY - 1e-4) {
382
- this.orbitCamera.pivotPoint.add(worldDiff);
383
- }
384
- }
385
- };
386
-
387
- OrbitCameraInputMouse.prototype.onMouseDown = function (event) {
388
- switch (event.button) {
389
- case pc.MOUSEBUTTON_LEFT: this.panButtonDown = true; break;
390
- case pc.MOUSEBUTTON_MIDDLE:
391
- case pc.MOUSEBUTTON_RIGHT: this.lookButtonDown = true; break;
392
- }
393
- };
394
-
395
- OrbitCameraInputMouse.prototype.onMouseUp = function (event) {
396
- switch (event.button) {
397
- case pc.MOUSEBUTTON_LEFT: this.panButtonDown = false; break;
398
- case pc.MOUSEBUTTON_MIDDLE:
399
- case pc.MOUSEBUTTON_RIGHT: this.lookButtonDown = false; break;
400
- }
401
- };
402
-
403
- OrbitCameraInputMouse.prototype.onMouseMove = function (event) {
404
- if (this.lookButtonDown) {
405
- var sens = this.orbitSensitivity;
406
-
407
- var deltaPitch = event.dy * sens;
408
- var deltaYaw = event.dx * sens;
409
-
410
- var currPitch = this.orbitCamera.pitch;
411
- var currYaw = this.orbitCamera.yaw;
412
- var currDist = this.orbitCamera.distance;
413
- var currPivot = this.orbitCamera.pivotPoint.clone();
414
-
415
- var camQuat = new pc.Quat();
416
- camQuat.setFromEulerAngles(currPitch, currYaw, 0);
417
- var forward = new pc.Vec3();
418
- camQuat.transformVector(pc.Vec3.FORWARD, forward);
419
- var preY = currPivot.y + (-forward.y) * currDist;
420
-
421
- var proposedPitch = currPitch - deltaPitch;
422
- var testQuat = new pc.Quat();
423
- testQuat.setFromEulerAngles(proposedPitch, currYaw, 0);
424
- var testForward = new pc.Vec3();
425
- testQuat.transformVector(pc.Vec3.FORWARD, testForward);
426
- var proposedY = currPivot.y + (-testForward.y) * currDist;
427
-
428
- var minY = this.orbitCamera.minY;
429
- var wouldGoBelow = proposedY < minY - 1e-4;
430
-
431
- if (wouldGoBelow && (proposedY < preY)) {
432
- this.orbitCamera.yaw = currYaw - deltaYaw;
433
- } else {
434
- this.orbitCamera.pitch = proposedPitch;
435
- this.orbitCamera.yaw = currYaw - deltaYaw;
436
- }
437
- } else if (this.panButtonDown) {
438
- this.pan(new pc.Vec2(event.x, event.y));
439
- }
440
-
441
- this.lastPoint.set(event.x, event.y);
442
- };
443
-
444
- OrbitCameraInputMouse.prototype.onMouseWheel = function (event) {
445
- if (this.entity.camera.projection === pc.PROJECTION_PERSPECTIVE) {
446
- this.orbitCamera.distance -= event.wheelDelta * this.distanceSensitivity * (this.orbitCamera.distance * 0.1);
447
- } else {
448
- this.orbitCamera.orthoHeight -= event.wheelDelta * this.distanceSensitivity * (this.orbitCamera.orthoHeight * 0.1);
449
- }
450
- event.event.preventDefault();
451
- };
452
-
453
- OrbitCameraInputMouse.prototype.onMouseOut = function () {
454
- this.lookButtonDown = false;
455
- this.panButtonDown = false;
456
- };
457
-
458
- // ---------------- Touch ----------------
459
- var OrbitCameraInputTouch = pc.createScript(TOUCH);
460
- OrbitCameraInputTouch.attributes.add('orbitSensitivity', { type: 'number', default: 0.6 });
461
- OrbitCameraInputTouch.attributes.add('distanceSensitivity', { type: 'number', default: 0.5 });
462
-
463
- OrbitCameraInputTouch.prototype.initialize = function () {
464
- this.orbitCamera = this.entity.script[ORBIT];
465
- this.lastTouchPoint = new pc.Vec2();
466
- this.lastPinchMidPoint = new pc.Vec2();
467
- this.lastPinchDistance = 0;
468
-
469
- if (this.orbitCamera && this.app.touch) {
470
- this.app.touch.on(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
471
- this.app.touch.on(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
472
- this.app.touch.on(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
473
- this.app.touch.on(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
474
-
475
- this.on('destroy', function () {
476
- this.app.touch.off(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
477
- this.app.touch.off(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
478
- this.app.touch.off(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
479
- this.app.touch.off(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
480
- });
481
- }
482
- };
483
-
484
- OrbitCameraInputTouch.prototype.getPinchDistance = function (pointA, pointB) {
485
- var dx = pointA.x - pointB.x;
486
- var dy = pointA.y - pointB.y;
487
- return Math.sqrt((dx * dx) + (dy * dy));
488
- };
489
-
490
- OrbitCameraInputTouch.prototype.calcMidPoint = function (pointA, pointB, result) {
491
- result.set(pointB.x - pointA.x, pointB.y - pointA.y);
492
- result.mulScalar(0.5);
493
- result.x += pointA.x;
494
- result.y += pointA.y;
495
- };
496
-
497
- OrbitCameraInputTouch.prototype.onTouchStartEndCancel = function (event) {
498
- var touches = event.touches;
499
- if (touches.length === 1) {
500
- this.lastTouchPoint.set(touches[0].x, touches[0].y);
501
- } else if (touches.length === 2) {
502
- this.lastPinchDistance = this.getPinchDistance(touches[0], touches[1]);
503
- this.calcMidPoint(touches[0], touches[1], this.lastPinchMidPoint);
504
- }
505
- };
506
-
507
- OrbitCameraInputTouch.fromWorldPoint = new pc.Vec3();
508
- OrbitCameraInputTouch.toWorldPoint = new pc.Vec3();
509
- OrbitCameraInputTouch.worldDiff = new pc.Vec3();
510
-
511
- OrbitCameraInputTouch.prototype.pan = function (midPoint) {
512
- var fromWorldPoint = OrbitCameraInputTouch.fromWorldPoint;
513
- var toWorldPoint = OrbitCameraInputTouch.toWorldPoint;
514
- var worldDiff = OrbitCameraInputTouch.worldDiff;
515
-
516
- var camera = this.entity.camera;
517
- var distance = this.orbitCamera.distance;
518
-
519
- camera.screenToWorld(midPoint.x, midPoint.y, distance, fromWorldPoint);
520
- camera.screenToWorld(this.lastPinchMidPoint.x, this.lastPinchMidPoint.y, distance, toWorldPoint);
521
-
522
- worldDiff.sub2(toWorldPoint, fromWorldPoint);
523
-
524
- var proposedPivot = this.orbitCamera.pivotPoint.clone().add(worldDiff);
525
- var minY = this.orbitCamera.minY;
526
- var resultingY = this.orbitCamera.worldCameraYForPivot(proposedPivot);
527
-
528
- if (resultingY >= minY - 1e-4) {
529
- this.orbitCamera.pivotPoint.add(worldDiff);
530
- } else {
531
- worldDiff.y = 0;
532
- proposedPivot = this.orbitCamera.pivotPoint.clone().add(worldDiff);
533
- resultingY = this.orbitCamera.worldCameraYForPivot(proposedPivot);
534
- if (resultingY >= minY - 1e-4) {
535
- this.orbitCamera.pivotPoint.add(worldDiff);
536
- }
537
- }
538
- };
539
-
540
- OrbitCameraInputTouch.pinchMidPoint = new pc.Vec2();
541
-
542
- OrbitCameraInputTouch.prototype.onTouchMove = function (event) {
543
- var pinchMidPoint = OrbitCameraInputTouch.pinchMidPoint;
544
- var touches = event.touches;
545
-
546
- if (touches.length === 1) {
547
- var touch = touches[0];
548
- var sens = this.orbitSensitivity;
549
-
550
- var deltaPitch = (touch.y - this.lastTouchPoint.y) * sens;
551
- var deltaYaw = (touch.x - this.lastTouchPoint.x) * sens;
552
-
553
- var currPitch = this.orbitCamera.pitch;
554
- var currYaw = this.orbitCamera.yaw;
555
- var currDist = this.orbitCamera.distance;
556
- var currPivot = this.orbitCamera.pivotPoint.clone();
557
-
558
- var camQuat = new pc.Quat();
559
- camQuat.setFromEulerAngles(currPitch, currYaw, 0);
560
- var forward = new pc.Vec3();
561
- camQuat.transformVector(pc.Vec3.FORWARD, forward);
562
- var preY = currPivot.y + (-forward.y) * currDist;
563
-
564
- var proposedPitch = currPitch - deltaPitch;
565
- var testQuat = new pc.Quat();
566
- testQuat.setFromEulerAngles(proposedPitch, currYaw, 0);
567
- var testForward = new pc.Vec3();
568
- testQuat.transformVector(pc.Vec3.FORWARD, testForward);
569
- var proposedY = currPivot.y + (-testForward.y) * currDist;
570
-
571
- var minY = this.orbitCamera.minY;
572
- var wouldGoBelow = proposedY < minY - 1e-4;
573
-
574
- if (wouldGoBelow && (proposedY < preY)) {
575
- this.orbitCamera.yaw = currYaw - deltaYaw;
576
- } else {
577
- this.orbitCamera.pitch = proposedPitch;
578
- this.orbitCamera.yaw = currYaw - deltaYaw;
579
- }
580
-
581
- this.lastTouchPoint.set(touch.x, touch.y);
582
- } else if (touches.length === 2) {
583
- var currentPinchDistance = this.getPinchDistance(touches[0], touches[1]);
584
- var diffInPinchDistance = currentPinchDistance - this.lastPinchDistance;
585
- this.lastPinchDistance = currentPinchDistance;
586
-
587
- this.orbitCamera.distance -= (diffInPinchDistance * this.distanceSensitivity * 0.1) * (this.orbitCamera.distance * 0.1);
588
-
589
- this.calcMidPoint(touches[0], touches[1], pinchMidPoint);
590
- this.pan(pinchMidPoint);
591
- this.lastPinchMidPoint.copy(pinchMidPoint);
592
- }
593
- };
594
-
595
- // ---------------- Keyboard (par canvas) ----------------
596
- var OrbitCameraInputKeyboard = pc.createScript(KEYBD);
597
-
598
- OrbitCameraInputKeyboard.attributes.add('forwardSpeed', { type: 'number', default: 1.2 });
599
- OrbitCameraInputKeyboard.attributes.add('strafeSpeed', { type: 'number', default: 1.2 });
600
- OrbitCameraInputKeyboard.attributes.add('orbitPitchSpeedDeg', { type: 'number', default: 90 });
601
- OrbitCameraInputKeyboard.attributes.add('orbitYawSpeedDeg', { type: 'number', default: 120 });
602
- OrbitCameraInputKeyboard.attributes.add('zoomKeySensitivity', { type: 'number', default: 3.0 });
603
-
604
- OrbitCameraInputKeyboard.prototype.initialize = function () {
605
- this.orbitCamera = this.entity.script[ORBIT];
606
-
607
- this._keys = Object.create(null);
608
- this._active = false;
609
-
610
- var el = this.app.graphicsDevice.canvas;
611
- if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '0');
612
-
613
- var shouldBlock = function(ev) {
614
- if (!ev) return false;
615
- var k = ev.key;
616
- return k === 'ArrowUp' || k === 'ArrowDown' || k === 'ArrowLeft' || k === 'ArrowRight' ||
617
- k === 'PageUp' || k === 'PageDown' || k === 'Home' || k === 'End' ||
618
- k === ' ' || k === 'Spacebar' || (ev.ctrlKey && (k === 'ArrowUp' || k === 'ArrowDown'));
619
- };
620
-
621
- this._onKeyDown = (ev) => {
622
- if (!this._active) return;
623
- this._keys[ev.key] = true;
624
- if (shouldBlock(ev)) ev.preventDefault();
625
- };
626
-
627
- this._onKeyUp = (ev) => { this._keys[ev.key] = false; };
628
- this._onBlur = () => { this._keys = Object.create(null); };
629
-
630
- this._onEnter = () => {
631
- this._active = true;
632
- try { el.focus({ preventScroll: true }); } catch(e) { el.focus(); }
633
- };
634
-
635
- this._onLeave = () => {
636
- this._active = false;
637
- el.blur();
638
- this._keys = Object.create(null);
639
- };
640
-
641
- el.addEventListener('keydown', this._onKeyDown);
642
- el.addEventListener('keyup', this._onKeyUp);
643
- el.addEventListener('blur', this._onBlur);
644
- el.addEventListener('pointerenter', this._onEnter);
645
- el.addEventListener('pointerleave', this._onLeave);
646
-
647
- this.on('destroy', function () {
648
- el.removeEventListener('keydown', this._onKeyDown);
649
- el.removeEventListener('keyup', this._onKeyUp);
650
- el.removeEventListener('blur', this._onBlur);
651
- el.removeEventListener('pointerenter', this._onEnter);
652
- el.removeEventListener('pointerleave', this._onLeave);
653
- }, this);
654
- };
655
-
656
- OrbitCameraInputKeyboard.prototype._pressed = function(name) {
657
- return !!(this._keys[name] || this._keys[name + 'Left'] || this._keys[name + 'Right']);
658
- };
659
-
660
- OrbitCameraInputKeyboard.prototype.update = function (dt) {
661
- if (!this.orbitCamera) return;
662
-
663
- var up = !!this._keys['ArrowUp'];
664
- var dn = !!this._keys['ArrowDown'];
665
- var lt = !!this._keys['ArrowLeft'];
666
- var rt = !!this._keys['ArrowRight'];
667
-
668
- var shift = this._pressed('Shift');
669
- var ctrl = this._pressed('Control');
670
-
671
- if (shift && (up || dn || lt || rt)) {
672
- var yawDir = (rt ? 1 : 0) - (lt ? 1 : 0);
673
- if (yawDir !== 0) {
674
- var dYaw = yawDir * this.orbitYawSpeedDeg * dt;
675
- this.orbitCamera.yaw = this.orbitCamera.yaw + dYaw;
676
- }
677
-
678
- var pitchDir = (up ? 1 : 0) - (dn ? 1 : 0);
679
- if (pitchDir !== 0) {
680
- var dPitch = pitchDir * this.orbitPitchSpeedDeg * dt;
681
-
682
- var currPitch = this.orbitCamera.pitch;
683
- var currYaw = this.orbitCamera.yaw;
684
- var currDist = this.orbitCamera.distance;
685
- var currPivot = this.orbitCamera.pivotPoint.clone();
686
-
687
- var camQuat = new pc.Quat().setFromEulerAngles(currPitch, currYaw, 0);
688
- var forward = new pc.Vec3(); camQuat.transformVector(pc.Vec3.FORWARD, forward);
689
- var preY = currPivot.y + (-forward.y) * currDist;
690
-
691
- var testPitch = currPitch + dPitch;
692
- var testQuat = new pc.Quat().setFromEulerAngles(testPitch, currYaw, 0);
693
- var testForward = new pc.Vec3(); testQuat.transformVector(pc.Vec3.FORWARD, testForward);
694
- var proposedY = currPivot.y + (-testForward.y) * currDist;
695
-
696
- var minY = this.orbitCamera.minY;
697
- var wouldGoBelow = proposedY < minY - 1e-4;
698
-
699
- if (!(wouldGoBelow && (proposedY < preY))) {
700
- this.orbitCamera.pitch = testPitch;
701
- }
702
- }
703
- return;
704
- }
705
-
706
- if (ctrl && (up || dn)) {
707
- var zoomSign = (up ? 1 : 0) - (dn ? 1 : 0);
708
- if (zoomSign !== 0) {
709
- if (this.entity.camera.projection === pc.PROJECTION_PERSPECTIVE) {
710
- var dz = zoomSign * this.zoomKeySensitivity * (this.orbitCamera.distance * 0.5) * dt;
711
- this.orbitCamera.distance -= dz;
712
- } else {
713
- var doh = zoomSign * this.zoomKeySensitivity * (this.orbitCamera.orthoHeight * 0.5) * dt;
714
- this.orbitCamera.orthoHeight -= doh;
715
- }
716
- }
717
- return;
718
- }
719
-
720
- var moveVert = (up ? 1 : 0) - (dn ? 1 : 0);
721
- var moveRight = (rt ? 1 : 0) - (lt ? 1 : 0);
722
-
723
- if (moveVert === 0 && moveRight === 0) return;
724
-
725
- var dist = Math.max(0.1, this.orbitCamera.distance);
726
- var speedV = this.forwardSpeed * dist;
727
- var speedR = this.strafeSpeed * dist;
728
-
729
- var dy = moveVert * speedV * dt;
730
- if (dy !== 0) {
731
- var currentCamY = this.orbitCamera.worldCameraYForPivot(this.orbitCamera.pivotPoint);
732
- var minY = this.orbitCamera.minY;
733
- var proposedCamY = currentCamY + dy;
734
- if (proposedCamY < minY) {
735
- dy = Math.max(dy, minY - currentCamY);
736
- }
737
- if (dy !== 0) {
738
- this.orbitCamera._pivotPoint.y += dy;
739
- }
740
- }
741
-
742
- var right = this.entity.right.clone(); right.y = 0; if (right.lengthSq() > 1e-8) right.normalize();
743
- var dx = moveRight * speedR * dt;
744
- if (dx !== 0) {
745
- this.orbitCamera._pivotPoint.add(right.mulScalar(dx));
746
- }
747
- };
748
-
749
- // renvoie les noms pour créer les scripts
750
- return { ORBIT, MOUSE, TOUCH, KEYBD };
751
  }
752
 
753
- // ---------------------------------------------------------------------------
754
-
755
  let pc;
756
  export let app = null;
757
  let cameraEntity = null;
@@ -775,7 +78,7 @@ export async function initializeViewer(config, instanceId) {
775
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
776
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
777
 
778
- // Params
779
  sogsUrl = config.sogs_json_url;
780
  glbUrl = (config.glb_url !== undefined) ? config.glb_url : "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/ressources/espace_expo/sol_blanc_2.glb";
781
  presentoirUrl = (config.presentoir_url !== undefined) ? config.presentoir_url : "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/ressources/espace_expo/sol_blanc_2.glb";
@@ -815,47 +118,90 @@ export async function initializeViewer(config, instanceId) {
815
  chosenCameraY = isMobile ? cameraYPhone : cameraY;
816
  chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
817
 
818
- // Canvas
819
- const canvasId = "canvas-" + instanceId;
820
- const progressDialog = document.getElementById("progress-dialog-" + instanceId);
821
- const progressIndicator = document.getElementById("progress-indicator-" + instanceId);
822
- const viewerContainer = document.getElementById("viewer-container-" + instanceId);
823
 
824
  let oldCanvas = document.getElementById(canvasId);
825
  if (oldCanvas) oldCanvas.remove();
826
 
827
- const canvas = document.createElement("canvas");
828
  canvas.id = canvasId;
829
- canvas.className = "ply-canvas";
830
  canvas.style.width = "100%";
831
  canvas.style.height = "100%";
832
- canvas.setAttribute("tabindex", "0");
833
  viewerContainer.insertBefore(canvas, progressDialog);
834
 
835
  canvas.style.touchAction = "none";
836
  canvas.style.webkitTouchCallout = "none";
837
- canvas.addEventListener("gesturestart", (e) => e.preventDefault());
838
- canvas.addEventListener("gesturechange", (e) => e.preventDefault());
839
- canvas.addEventListener("gestureend", (e) => e.preventDefault());
840
- canvas.addEventListener("dblclick", (e) => e.preventDefault());
841
- canvas.addEventListener("touchstart", (e) => {
842
- if (e.touches.length > 1) e.preventDefault();
843
- }, { passive: false });
844
- canvas.addEventListener("wheel", (e) => { e.preventDefault(); }, { passive: false });
 
 
 
 
 
 
845
 
846
- // focus / blur au survol (pour clavier local)
847
- const focusCanvas = () => { try { canvas.focus({ preventScroll: true }); } catch (e) { canvas.focus(); } };
848
- canvas.addEventListener('pointerenter', focusCanvas);
849
- canvas.addEventListener('pointerleave', () => canvas.blur());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
850
 
851
- progressDialog.style.display = "block";
 
 
 
852
 
853
  if (!pc) {
854
  pc = await import("https://esm.run/playcanvas");
855
  window.pc = pc;
856
  }
857
 
858
- // Create app
859
  const device = await pc.createGraphicsDevice(canvas, {
860
  deviceTypes: ["webgl2"],
861
  glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
@@ -868,7 +214,7 @@ export async function initializeViewer(config, instanceId) {
868
  opts.graphicsDevice = device;
869
  opts.mouse = new pc.Mouse(canvas);
870
  opts.touch = new pc.TouchDevice(canvas);
871
- // ⚠️ on ne crée PAS pc.Keyboard ici ; notre clavier est par-canvas dans le script
872
  opts.componentSystems = [
873
  pc.RenderComponentSystem,
874
  pc.CameraComponentSystem,
@@ -889,41 +235,48 @@ export async function initializeViewer(config, instanceId) {
889
  app.setCanvasFillMode(pc.FILLMODE_NONE);
890
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
891
 
892
- resizeObserver = new ResizeObserver((entries) => {
893
- entries.forEach((entry) => {
894
  app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
895
  });
896
  });
897
  resizeObserver.observe(viewerContainer);
898
 
899
- window.addEventListener("resize", () =>
900
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight)
901
- );
902
 
903
- app.on("destroy", () => {
 
904
  resizeObserver.disconnect();
905
- canvas.removeEventListener('pointerenter', focusCanvas);
906
- canvas.removeEventListener('pointerleave', () => canvas.blur());
 
 
 
 
 
 
 
 
 
907
  });
908
 
909
- // Assets (sans orbit-camera.js)
910
  const assets = {
911
- sogs: new pc.Asset("gsplat", "gsplat", { url: sogsUrl }),
912
- glb: new pc.Asset("glb", "container", { url: glbUrl }),
913
- presentoir: new pc.Asset("presentoir", "container", { url: presentoirUrl })
 
914
  };
 
915
  for (const key in assets) app.assets.add(assets[key]);
916
 
917
  const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
918
- loader.load(async () => {
919
- // Enregistre les scripts d’orbite pour CETTE instance
920
- const ScriptNames = registerOrbitScriptsForInstance(instanceId);
921
-
922
  app.start();
923
- progressDialog.style.display = "none";
924
 
925
- modelEntity = new pc.Entity("model");
926
- modelEntity.addComponent("gsplat", { asset: assets.sogs });
927
  modelEntity.setLocalPosition(modelX, modelY, modelZ);
928
  modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
929
  modelEntity.setLocalScale(modelScale, modelScale, modelScale);
@@ -944,28 +297,34 @@ export async function initializeViewer(config, instanceId) {
944
  matSol.useLighting = false;
945
  matSol.update();
946
 
947
- traverse(presentoirEntity, (node) => {
948
  if (node.render && node.render.meshInstances) {
949
- for (let mi of node.render.meshInstances) mi.material = matSol;
 
 
950
  }
951
  });
952
- traverse(glbEntity, (node) => {
 
953
  if (node.render && node.render.meshInstances) {
954
- for (let mi of node.render.meshInstances) mi.material = matSol;
 
 
955
  }
956
  });
957
  }
958
 
959
- cameraEntity = new pc.Entity("camera");
960
- cameraEntity.addComponent("camera", {
961
- clearColor: new pc.Color(color_bg),
962
- nearClip: 0.001,
963
- farClip: 100
964
- });
 
965
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
966
  cameraEntity.lookAt(modelEntity.getPosition());
967
- cameraEntity.addComponent("script");
968
- cameraEntity.script.create(ScriptNames.ORBIT, {
969
  attributes: {
970
  focusEntity: modelEntity,
971
  inertiaFactor: 0.2,
@@ -979,36 +338,36 @@ export async function initializeViewer(config, instanceId) {
979
  frameOnStart: false
980
  }
981
  });
982
- cameraEntity.script.create(ScriptNames.MOUSE);
983
- cameraEntity.script.create(ScriptNames.TOUCH);
984
- cameraEntity.script.create(ScriptNames.KEYBD, {
985
  attributes: {
986
- forwardSpeed: 1.2,
987
- strafeSpeed: 1.2
988
  }
989
  });
990
  app.root.addChild(cameraEntity);
991
 
992
  app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
993
- app.once("update", () => resetViewerCamera());
994
 
995
- // Tooltips
996
  try {
997
  if (config.tooltips_url) {
998
- import("./tooltips.js")
999
- .then((tooltipsModule) => {
1000
- tooltipsModule.initializeTooltips({
1001
- app,
1002
- cameraEntity,
1003
- modelEntity,
1004
- tooltipsUrl: config.tooltips_url,
1005
- defaultVisible: !!config.showTooltipsDefault,
1006
- moveDuration: config.tooltipMoveDuration || 0.6
1007
- });
1008
- })
1009
- .catch(() => {});
1010
  }
1011
- } catch (e) {}
 
 
1012
 
1013
  viewerInitialized = true;
1014
  });
@@ -1017,22 +376,20 @@ export async function initializeViewer(config, instanceId) {
1017
  export function resetViewerCamera() {
1018
  try {
1019
  if (!cameraEntity || !modelEntity || !app) return;
1020
- // le nom du script d’orbite dépend de l’instance ; on le retrouve automatiquement
1021
- const orbitKey = Object.keys(cameraEntity.script._scriptsIndex).find(k => k.startsWith('orbitCamera_'));
1022
- if (!orbitKey) return;
1023
- const orbitCam = cameraEntity.script[orbitKey];
1024
  if (!orbitCam) return;
1025
 
1026
  const modelPos = modelEntity.getPosition();
1027
  const tempEnt = new pc.Entity();
1028
- tempEnt.setPosition(cameraEntity.getPosition());
1029
  tempEnt.lookAt(modelPos);
1030
 
1031
- const dist = new pc.Vec3()
1032
- .sub2(new pc.Vec3(cameraEntity.getPosition().x, cameraEntity.getPosition().y, cameraEntity.getPosition().z), modelPos)
1033
- .length();
 
1034
 
1035
- cameraEntity.setPosition(cameraEntity.getPosition());
1036
  cameraEntity.lookAt(modelPos);
1037
 
1038
  orbitCam.pivotPoint = modelPos.clone();
 
2
  // ==============================
3
 
4
  async function loadImageAsTexture(url, app) {
5
+ return new Promise((resolve, reject) => {
6
+ const img = new window.Image();
7
+ img.crossOrigin = "anonymous";
8
+ img.onload = function() {
9
+ const tex = new pc.Texture(app.graphicsDevice, {
10
+ width: img.width,
11
+ height: img.height,
12
+ format: pc.PIXELFORMAT_R8_G8_B8_A8,
13
+ });
14
+ tex.setSource(img);
15
+ resolve(tex);
16
+ };
17
+ img.onerror = reject;
18
+ img.src = url;
19
+ });
20
  }
21
 
22
  // --- Patch all window.Image to force crossOrigin="anonymous" ---
23
+ (function() {
24
  const OriginalImage = window.Image;
25
+ window.Image = function(...args) {
26
  const img = new OriginalImage(...args);
27
  img.crossOrigin = "anonymous";
28
  return img;
 
36
  if (hex.length !== 8) return [1, 1, 1, 1];
37
  const num = parseInt(hex, 16);
38
  return [
39
+ ((num >> 24) & 0xFF) / 255,
40
+ ((num >> 16) & 0xFF) / 255,
41
+ ((num >> 8) & 0xFF) / 255,
42
+ (num & 0xFF) / 255
43
  ];
44
  } catch (e) {
45
  alert("hexToRgbaArray error: " + e);
 
49
 
50
  // ----- Utility: Recursive scene traversal -----
51
  function traverse(entity, callback) {
52
+ callback(entity);
53
+ if (entity.children) {
54
+ entity.children.forEach(child => traverse(child, callback));
55
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  }
57
 
 
 
58
  let pc;
59
  export let app = null;
60
  let cameraEntity = null;
 
78
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
79
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
80
 
81
+ //prends les differents arguments de config.json
82
  sogsUrl = config.sogs_json_url;
83
  glbUrl = (config.glb_url !== undefined) ? config.glb_url : "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/ressources/espace_expo/sol_blanc_2.glb";
84
  presentoirUrl = (config.presentoir_url !== undefined) ? config.presentoir_url : "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/ressources/espace_expo/sol_blanc_2.glb";
 
118
  chosenCameraY = isMobile ? cameraYPhone : cameraY;
119
  chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
120
 
121
+ //cree le nouveau canva (instanceId est une valeur aléatoire pour gérer plusieurs canvas sur la meme page)
122
+ const canvasId = 'canvas-' + instanceId;
123
+ const progressDialog = document.getElementById('progress-dialog-' + instanceId);
124
+ const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
125
+ const viewerContainer = document.getElementById('viewer-container-' + instanceId);
126
 
127
  let oldCanvas = document.getElementById(canvasId);
128
  if (oldCanvas) oldCanvas.remove();
129
 
130
+ const canvas = document.createElement('canvas');
131
  canvas.id = canvasId;
132
+ canvas.className = 'ply-canvas';
133
  canvas.style.width = "100%";
134
  canvas.style.height = "100%";
135
+ canvas.setAttribute('tabindex', '0');
136
  viewerContainer.insertBefore(canvas, progressDialog);
137
 
138
  canvas.style.touchAction = "none";
139
  canvas.style.webkitTouchCallout = "none";
140
+ canvas.addEventListener('gesturestart', e => e.preventDefault());
141
+ canvas.addEventListener('gesturechange', e => e.preventDefault());
142
+ canvas.addEventListener('gestureend', e => e.preventDefault());
143
+ canvas.addEventListener('dblclick', e => e.preventDefault());
144
+ canvas.addEventListener('touchstart', e => { if (e.touches.length > 1) e.preventDefault(); }, { passive: false });
145
+ canvas.addEventListener('wheel', (e) => { e.preventDefault(); }, { passive: false });
146
+
147
+ // --- Clavier : bloquer le scroll seulement quand le pointeur est au-dessus du canvas ---
148
+ const scrollKeys = new Set([
149
+ 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
150
+ 'PageUp', 'PageDown', 'Home', 'End', ' ', 'Space', 'Spacebar'
151
+ ]);
152
+
153
+ let isPointerOverCanvas = false;
154
 
155
+ const focusCanvas = () => canvas.focus({ preventScroll: true });
156
+
157
+ const onPointerEnter = () => {
158
+ isPointerOverCanvas = true;
159
+ focusCanvas();
160
+ };
161
+
162
+ const onPointerLeave = () => {
163
+ isPointerOverCanvas = false;
164
+ // rendre immédiatement le scroll à la page sans recliquer
165
+ if (document.activeElement === canvas) {
166
+ canvas.blur();
167
+ }
168
+ };
169
+
170
+ // Fallback souris si pointer events indisponibles
171
+ canvas.addEventListener('pointerenter', onPointerEnter);
172
+ canvas.addEventListener('pointerleave', onPointerLeave);
173
+ canvas.addEventListener('mouseenter', onPointerEnter);
174
+ canvas.addEventListener('mouseleave', onPointerLeave);
175
+
176
+ // Forcer le focus quand on interagit
177
+ canvas.addEventListener('mousedown', focusCanvas);
178
+ canvas.addEventListener('touchstart', () => { focusCanvas(); }, { passive: false });
179
+
180
+ // Si le canvas perd le focus (Alt+Tab, click ailleurs…), on arrête de bloquer
181
+ const onCanvasBlur = () => { isPointerOverCanvas = false; };
182
+ canvas.addEventListener('blur', onCanvasBlur);
183
+
184
+ // Capture au niveau fenêtre pour bloquer le scroll, mais seulement si pointeur dessus
185
+ const onKeyDownCapture = (e) => {
186
+ if (!isPointerOverCanvas) return;
187
+ if (scrollKeys.has(e.key) || scrollKeys.has(e.code)) {
188
+ e.preventDefault(); // bloque le scroll page
189
+ // ne pas stopPropagation: PlayCanvas doit recevoir les touches
190
+ }
191
+ };
192
+ window.addEventListener('keydown', onKeyDownCapture, true);
193
 
194
+ // ⚠️ IMPORTANT : on ne bloque plus keydown/keyup sur le canvas lui-même.
195
+ // Ça évite de devoir “recliquer” pour retrouver le scroll.
196
+
197
+ progressDialog.style.display = 'block';
198
 
199
  if (!pc) {
200
  pc = await import("https://esm.run/playcanvas");
201
  window.pc = pc;
202
  }
203
 
204
+ // Create app first
205
  const device = await pc.createGraphicsDevice(canvas, {
206
  deviceTypes: ["webgl2"],
207
  glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
 
214
  opts.graphicsDevice = device;
215
  opts.mouse = new pc.Mouse(canvas);
216
  opts.touch = new pc.TouchDevice(canvas);
217
+ opts.keyboard = new pc.Keyboard(canvas);
218
  opts.componentSystems = [
219
  pc.RenderComponentSystem,
220
  pc.CameraComponentSystem,
 
235
  app.setCanvasFillMode(pc.FILLMODE_NONE);
236
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
237
 
238
+ resizeObserver = new ResizeObserver(entries => {
239
+ entries.forEach(entry => {
240
  app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
241
  });
242
  });
243
  resizeObserver.observe(viewerContainer);
244
 
245
+ window.addEventListener('resize', () => app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight));
 
 
246
 
247
+ // Nettoyage complet à la destruction
248
+ app.on('destroy', () => {
249
  resizeObserver.disconnect();
250
+ if (opts.keyboard && opts.keyboard.detach) opts.keyboard.detach();
251
+
252
+ window.removeEventListener('keydown', onKeyDownCapture, true);
253
+
254
+ canvas.removeEventListener('pointerenter', onPointerEnter);
255
+ canvas.removeEventListener('pointerleave', onPointerLeave);
256
+ canvas.removeEventListener('mouseenter', onPointerEnter);
257
+ canvas.removeEventListener('mouseleave', onPointerLeave);
258
+ canvas.removeEventListener('mousedown', focusCanvas);
259
+ canvas.removeEventListener('touchstart', focusCanvas);
260
+ canvas.removeEventListener('blur', onCanvasBlur);
261
  });
262
 
263
+ // Assets after app exists
264
  const assets = {
265
+ sogs: new pc.Asset('gsplat', 'gsplat', { url: sogsUrl }),
266
+ orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-sgos.static.hf.space/orbit-camera.js" }),
267
+ glb: new pc.Asset('glb', 'container', { url: glbUrl }),
268
+ presentoir: new pc.Asset('presentoir', 'container', { url: presentoirUrl }),
269
  };
270
+
271
  for (const key in assets) app.assets.add(assets[key]);
272
 
273
  const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
274
+ loader.load(() => {
 
 
 
275
  app.start();
276
+ progressDialog.style.display = 'none';
277
 
278
+ modelEntity = new pc.Entity('model');
279
+ modelEntity.addComponent('gsplat', { asset: assets.sogs });
280
  modelEntity.setLocalPosition(modelX, modelY, modelZ);
281
  modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
282
  modelEntity.setLocalScale(modelScale, modelScale, modelScale);
 
297
  matSol.useLighting = false;
298
  matSol.update();
299
 
300
+ traverse(presentoirEntity, node => {
301
  if (node.render && node.render.meshInstances) {
302
+ for (let mi of node.render.meshInstances) {
303
+ mi.material = matSol;
304
+ }
305
  }
306
  });
307
+
308
+ traverse(glbEntity, node => {
309
  if (node.render && node.render.meshInstances) {
310
+ for (let mi of node.render.meshInstances) {
311
+ mi.material = matSol;
312
+ }
313
  }
314
  });
315
  }
316
 
317
+ cameraEntity = new pc.Entity('camera');
318
+ cameraEntity.addComponent('camera',
319
+ {
320
+ clearColor: new pc.Color(color_bg),
321
+ nearClip: 0.001,
322
+ farClip: 100
323
+ });
324
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
325
  cameraEntity.lookAt(modelEntity.getPosition());
326
+ cameraEntity.addComponent('script');
327
+ cameraEntity.script.create('orbitCamera', {
328
  attributes: {
329
  focusEntity: modelEntity,
330
  inertiaFactor: 0.2,
 
338
  frameOnStart: false
339
  }
340
  });
341
+ cameraEntity.script.create('orbitCameraInputMouse');
342
+ cameraEntity.script.create('orbitCameraInputTouch');
343
+ cameraEntity.script.create('orbitCameraInputKeyboard', {
344
  attributes: {
345
+ forwardSpeed: 1.2, // vertical (relatif à la distance)
346
+ strafeSpeed: 1.2 // gauche/droite (relatif à la distance)
347
  }
348
  });
349
  app.root.addChild(cameraEntity);
350
 
351
  app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
352
+ app.once('update', () => resetViewerCamera());
353
 
354
+ // Tooltips supported if tooltips_url set
355
  try {
356
  if (config.tooltips_url) {
357
+ import('./tooltips.js').then(tooltipsModule => {
358
+ tooltipsModule.initializeTooltips({
359
+ app,
360
+ cameraEntity,
361
+ modelEntity,
362
+ tooltipsUrl: config.tooltips_url,
363
+ defaultVisible: !!config.showTooltipsDefault,
364
+ moveDuration: config.tooltipMoveDuration || 0.6
365
+ });
366
+ }).catch(() => { /* optional */ });
 
 
367
  }
368
+ } catch (e) {
369
+ // optional
370
+ }
371
 
372
  viewerInitialized = true;
373
  });
 
376
  export function resetViewerCamera() {
377
  try {
378
  if (!cameraEntity || !modelEntity || !app) return;
379
+ const orbitCam = cameraEntity.script.orbitCamera;
 
 
 
380
  if (!orbitCam) return;
381
 
382
  const modelPos = modelEntity.getPosition();
383
  const tempEnt = new pc.Entity();
384
+ tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
385
  tempEnt.lookAt(modelPos);
386
 
387
+ const dist = new pc.Vec3().sub2(
388
+ new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
389
+ modelPos
390
+ ).length();
391
 
392
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
393
  cameraEntity.lookAt(modelPos);
394
 
395
  orbitCam.pivotPoint = modelPos.clone();