MikaFil commited on
Commit
004a743
·
verified ·
1 Parent(s): ae97d01

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +399 -653
viewer.js CHANGED
@@ -1,7 +1,10 @@
1
  // viewer.js
2
  // ==============================
3
 
4
- /* Util: charge une image comme texture PlayCanvas */
 
 
 
5
  async function loadImageAsTexture(url, app) {
6
  return new Promise((resolve, reject) => {
7
  const img = new window.Image();
@@ -20,7 +23,7 @@ async function loadImageAsTexture(url, app) {
20
  });
21
  }
22
 
23
- /* Patch global: toutes les Image() -> crossOrigin="anonymous" */
24
  (function () {
25
  const OriginalImage = window.Image;
26
  window.Image = function (...args) {
@@ -30,10 +33,9 @@ async function loadImageAsTexture(url, app) {
30
  };
31
  })();
32
 
33
- /* Couleur hex -> tableau RGBA [0..1] */
34
  function hexToRgbaArray(hex) {
35
  try {
36
- hex = hex.replace("#", "");
37
  if (hex.length === 6) hex += "FF";
38
  if (hex.length !== 8) return [1, 1, 1, 1];
39
  const num = parseInt(hex, 16);
@@ -44,12 +46,12 @@ function hexToRgbaArray(hex) {
44
  (num & 0xff) / 255
45
  ];
46
  } catch (e) {
47
- alert("hexToRgbaArray error: " + e);
48
  return [1, 1, 1, 1];
49
  }
50
  }
51
 
52
- /* Parcours récursif d'entités */
53
  function traverse(entity, callback) {
54
  callback(entity);
55
  if (entity.children) {
@@ -57,7 +59,39 @@ function traverse(entity, callback) {
57
  }
58
  }
59
 
60
- /* --- État module (1 import = 1 instance) --- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  let pc;
62
  export let app = null;
63
  let cameraEntity = null;
@@ -65,680 +99,392 @@ let modelEntity = null;
65
  let viewerInitialized = false;
66
  let resizeObserver = null;
67
 
68
- /* Contexte instance */
69
  let chosenCameraX, chosenCameraY, chosenCameraZ;
70
- let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, minY;
71
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
72
  let presentoirScaleX, presentoirScaleY, presentoirScaleZ;
73
  let sogsUrl, glbUrl, presentoirUrl;
74
  let color_bg_hex, color_bg, espace_expo_bool;
75
 
76
- /* Nom dynamique des scripts d'orbit pour CETTE instance */
77
- let ORBIT_BASE = ""; // ex: orbitCamera__abcd1234
78
- let ORBIT_MOUSE = "";
79
- let ORBIT_TOUCH = "";
80
- let ORBIT_KEYS = "";
81
-
82
- /* Enregistre les 4 scripts d’orbit avec des NOMS UNIQUES par instance */
83
- function registerOrbitScriptsForInstance(suffix) {
84
- ORBIT_BASE = `orbitCamera__${suffix}`;
85
- ORBIT_MOUSE = `orbitCameraInputMouse__${suffix}`;
86
- ORBIT_TOUCH = `orbitCameraInputTouch__${suffix}`;
87
- ORBIT_KEYS = `orbitCameraInputKeyboard__${suffix}`;
88
-
89
- // ============== Orbit Camera (base) =================
90
- const OrbitCamera = pc.createScript(ORBIT_BASE);
91
-
92
- OrbitCamera.attributes.add("distanceMax", { type: "number", default: 20, title: "Distance Max" });
93
- OrbitCamera.attributes.add("distanceMin", { type: "number", default: 1, title: "Distance Min" });
94
- OrbitCamera.attributes.add("pitchAngleMax", { type: "number", default: 90, title: "Pitch Angle Max (degrees)" });
95
- OrbitCamera.attributes.add("pitchAngleMin", { type: "number", default: 0, title: "Pitch Angle Min (degrees)" });
96
- OrbitCamera.attributes.add("yawAngleMax", { type: "number", default: 360, title: "Yaw Angle Max (degrees)" });
97
- OrbitCamera.attributes.add("yawAngleMin", { type: "number", default: -360, title: "Yaw Angle Min (degrees)" });
98
- OrbitCamera.attributes.add("minY", { type: "number", default: 0, title: "Minimum Y" });
99
-
100
- OrbitCamera.attributes.add("inertiaFactor", { type: "number", default: 0.2, title: "Inertia Factor" });
101
- OrbitCamera.attributes.add("focusEntity", { type: "entity", title: "Focus Entity" });
102
- OrbitCamera.attributes.add("frameOnStart", { type: "boolean", default: true, title: "Frame on Start" });
103
-
104
- Object.defineProperty(OrbitCamera.prototype, "distance", {
105
- get: function () { return this._targetDistance; },
106
- set: function (value) { this._targetDistance = this._clampDistance(value); }
107
- });
108
-
109
- Object.defineProperty(OrbitCamera.prototype, "orthoHeight", {
110
- get: function () { return this.entity.camera.orthoHeight; },
111
- set: function (value) { this.entity.camera.orthoHeight = Math.max(0, value); }
112
- });
113
-
114
- Object.defineProperty(OrbitCamera.prototype, "pitch", {
115
- get: function () { return this._targetPitch; },
116
- set: function (value) { this._targetPitch = this._clampPitchAngle(value); }
117
- });
118
-
119
- Object.defineProperty(OrbitCamera.prototype, "yaw", {
120
- get: function () { return this._targetYaw; },
121
- set: function (value) { this._targetYaw = this._clampYawAngle(value); }
122
- });
123
-
124
- Object.defineProperty(OrbitCamera.prototype, "pivotPoint", {
125
- get: function () { return this._pivotPoint; },
126
- set: function (value) { this._pivotPoint.copy(value); }
127
- });
128
-
129
- OrbitCamera.prototype.focus = function (focusEntity) {
130
- this._buildAabb(focusEntity);
131
- const halfExtents = this._modelsAabb.halfExtents;
132
- const radius = Math.max(halfExtents.x, Math.max(halfExtents.y, halfExtents.z));
133
- this.distance = (radius * 1.5) / Math.sin(0.5 * this.entity.camera.fov * pc.math.DEG_TO_RAD);
134
- this._removeInertia();
135
- this._pivotPoint.copy(this._modelsAabb.center);
136
- };
137
-
138
- OrbitCamera.distanceBetween = new pc.Vec3();
139
-
140
- OrbitCamera.prototype.resetAndLookAtPoint = function (resetPoint, lookAtPoint) {
141
- this.pivotPoint.copy(lookAtPoint);
142
- this.entity.setPosition(resetPoint);
143
- this.entity.lookAt(lookAtPoint);
144
- const distance = OrbitCamera.distanceBetween;
145
- distance.sub2(lookAtPoint, resetPoint);
146
- this.distance = distance.length();
147
- this.pivotPoint.copy(lookAtPoint);
148
- const cameraQuat = this.entity.getRotation();
149
- this.yaw = this._calcYaw(cameraQuat);
150
- this.pitch = this._calcPitch(cameraQuat, this.yaw);
151
- this._removeInertia();
152
- this._updatePosition();
153
- };
154
-
155
- OrbitCamera.prototype.resetAndLookAtEntity = function (resetPoint, entity) {
156
- this._buildAabb(entity);
157
- this.resetAndLookAtPoint(resetPoint, this._modelsAabb.center);
158
- };
159
-
160
- OrbitCamera.prototype.reset = function (yaw, pitch, distance) {
161
- this.pitch = pitch;
162
- this.yaw = yaw;
163
- this.distance = distance;
164
- this._removeInertia();
165
- };
166
-
167
- OrbitCamera.prototype.resetToPosition = function (position, lookAtPoint) {
168
- this.entity.setPosition(position);
169
- this.entity.lookAt(lookAtPoint);
170
- this._pivotPoint.copy(lookAtPoint);
171
- const distanceVec = new pc.Vec3();
172
- distanceVec.sub2(position, lookAtPoint);
173
- this._targetDistance = this._distance = distanceVec.length();
174
- const cameraQuat = this.entity.getRotation();
175
- this._targetYaw = this._yaw = this._calcYaw(cameraQuat);
176
- this._targetPitch = this._pitch = this._calcPitch(cameraQuat, this._yaw);
177
- this._removeInertia();
178
- this._updatePosition();
179
- };
180
-
181
- // Helper: calc cam Y if pivot became 'pivot' (enforce minY)
182
- OrbitCamera.prototype.worldCameraYForPivot = function (pivot) {
183
- const quat = new pc.Quat().setFromEulerAngles(this._pitch, this._yaw, 0);
184
- const forward = new pc.Vec3();
185
- quat.transformVector(pc.Vec3.FORWARD, forward);
186
- const camPos = pivot.clone().add(forward.clone().scale(-this._distance));
187
- return camPos.y;
188
- };
189
-
190
- // --------- Private ----------
191
- OrbitCamera.prototype.initialize = function () {
192
- const self = this;
193
- const onWindowResize = function () { self._checkAspectRatio(); };
194
- window.addEventListener("resize", onWindowResize, false);
195
- this._checkAspectRatio();
196
-
197
- this._modelsAabb = new pc.BoundingBox();
198
- this._buildAabb(this.focusEntity || this.app.root);
199
-
200
- this.entity.lookAt(this._modelsAabb.center);
201
- this._pivotPoint = new pc.Vec3().copy(this._modelsAabb.center);
202
-
203
- const cameraQuat = this.entity.getRotation();
204
- this._yaw = this._calcYaw(cameraQuat);
205
- this._pitch = this._clampPitchAngle(this._calcPitch(cameraQuat, this._yaw));
206
- this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
207
-
208
- this._distance = 0;
209
- this._targetYaw = this._yaw;
210
- this._targetPitch = this._pitch;
211
-
212
- if (this.frameOnStart) {
213
- this.focus(this.focusEntity || this.app.root);
214
- } else {
215
- const distanceBetween = new pc.Vec3();
216
- distanceBetween.sub2(this.entity.getPosition(), this._pivotPoint);
217
- this._distance = this._clampDistance(distanceBetween.length());
218
- }
219
- this._targetDistance = this._distance;
220
-
221
- this.on("attr:distanceMin", function () { this._distance = this._clampDistance(this._distance); });
222
- this.on("attr:distanceMax", function () { this._distance = this._clampDistance(this._distance); });
223
- this.on("attr:pitchAngleMin", function () { this._pitch = this._clampPitchAngle(this._pitch); });
224
- this.on("attr:pitchAngleMax", function () { this._pitch = this._clampPitchAngle(this._pitch); });
225
-
226
- this.on("attr:focusEntity", function (value) {
227
- if (this.frameOnStart) this.focus(value || this.app.root);
228
- else this.resetAndLookAtEntity(this.entity.getPosition(), value || this.app.root);
229
- });
230
-
231
- this.on("attr:frameOnStart", function (value) {
232
- if (value) this.focus(this.focusEntity || this.app.root);
233
- });
234
-
235
- this.on("destroy", function () { window.removeEventListener("resize", onWindowResize, false); });
236
- };
237
-
238
- OrbitCamera.prototype.update = function (dt) {
239
- const t = this.inertiaFactor === 0 ? 1 : Math.min(dt / this.inertiaFactor, 1);
240
- this._distance = pc.math.lerp(this._distance, this._targetDistance, t);
241
- this._yaw = pc.math.lerp(this._yaw, this._targetYaw, t);
242
- this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t);
243
- this._updatePosition();
244
- };
245
-
246
- OrbitCamera.prototype._updatePosition = function () {
247
- this.entity.setLocalPosition(0, 0, 0);
248
- this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
249
-
250
- const position = this.entity.getPosition();
251
- position.copy(this.entity.forward);
252
- position.mulScalar(-this._distance);
253
- position.add(this.pivotPoint);
254
-
255
- // Ne jamais passer sous minY
256
- position.y = Math.max(position.y, this.minY);
257
-
258
- this.entity.setPosition(position);
259
- };
260
-
261
- OrbitCamera.prototype._removeInertia = function () {
262
- this._yaw = this._targetYaw;
263
- this._pitch = this._targetPitch;
264
- this._distance = this._targetDistance;
265
- };
266
 
267
- OrbitCamera.prototype._checkAspectRatio = function () {
268
- const h = this.app.graphicsDevice.height;
269
- const w = this.app.graphicsDevice.width;
270
- this.entity.camera.horizontalFov = (h > w);
271
- };
272
 
273
- OrbitCamera.prototype._buildAabb = function (entity) {
274
- let i, m;
275
- const meshInstances = [];
276
 
277
- const renders = entity.findComponents("render");
278
- for (i = 0; i < renders.length; i++) {
279
- const render = renders[i];
280
- for (m = 0; m < render.meshInstances.length; m++) {
281
- meshInstances.push(render.meshInstances[m]);
282
- }
283
- }
284
 
285
- const models = entity.findComponents("model");
286
- for (i = 0; i < models.length; i++) {
287
- const model = models[i];
288
- for (m = 0; m < model.meshInstances.length; m++) {
289
- meshInstances.push(model.meshInstances[m]);
290
- }
291
- }
292
 
293
- const gsplats = entity.findComponents("gsplat");
294
- for (i = 0; i < gsplats.length; i++) {
295
- const gsplat = gsplats[i];
296
- const instance = gsplat.instance;
297
- if (instance?.meshInstance) meshInstances.push(instance.meshInstance);
298
- }
299
 
300
- for (i = 0; i < meshInstances.length; i++) {
301
- if (i === 0) this._modelsAabb.copy(meshInstances[i].aabb);
302
- else this._modelsAabb.add(meshInstances[i].aabb);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  }
304
  };
 
305
 
306
- OrbitCamera.prototype._calcYaw = function (quat) {
307
- const transformedForward = new pc.Vec3();
308
- quat.transformVector(pc.Vec3.FORWARD, transformedForward);
309
- return Math.atan2(-transformedForward.x, -transformedForward.z) * pc.math.RAD_TO_DEG;
310
- };
311
-
312
- OrbitCamera.prototype._clampDistance = function (distance) {
313
- if (this.distanceMax > 0) return pc.math.clamp(distance, this.distanceMin, this.distanceMax);
314
- return Math.max(distance, this.distanceMin);
315
- };
316
 
317
- OrbitCamera.prototype._clampPitchAngle = function (pitch) {
318
- return pc.math.clamp(pitch, this.pitchAngleMin, this.pitchAngleMax);
319
- };
320
-
321
- OrbitCamera.prototype._clampYawAngle = function (yaw) {
322
- return pc.math.clamp(yaw, -this.yawAngleMax, -this.yawAngleMin);
323
- };
324
 
325
- const quatWithoutYaw = new pc.Quat();
326
- const yawOffset = new pc.Quat();
327
- OrbitCamera.prototype._calcPitch = function (quat, yaw) {
328
- yawOffset.setFromEulerAngles(0, -yaw, 0);
329
- quatWithoutYaw.mul2(yawOffset, quat);
330
- const transformedForward = new pc.Vec3();
331
- quatWithoutYaw.transformVector(pc.Vec3.FORWARD, transformedForward);
332
- return Math.atan2(-transformedForward.y, -transformedForward.z) * pc.math.RAD_TO_DEG;
333
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
- // ============== Orbit Camera Input Mouse =================
336
- const OrbitCameraInputMouse = pc.createScript(ORBIT_MOUSE);
337
- OrbitCameraInputMouse.attributes.add("orbitSensitivity", { type: "number", default: 0.3, title: "Orbit Sensitivity" });
338
- OrbitCameraInputMouse.attributes.add("distanceSensitivity", { type: "number", default: 0.4, title: "Distance Sensitivity" });
339
-
340
- OrbitCameraInputMouse.prototype.initialize = function () {
341
- this.orbitCamera = this.entity.script[ORBIT_BASE];
342
-
343
- if (this.orbitCamera) {
344
- const self = this;
345
- const onMouseOut = function () { self.onMouseOut(); };
346
-
347
- this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
348
- this.app.mouse.on(pc.EVENT_MOUSEUP, this.onMouseUp, this);
349
- this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
350
- this.app.mouse.on(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this);
351
- window.addEventListener("mouseout", onMouseOut, false);
352
-
353
- this.on("destroy", function () {
354
- this.app.mouse.off(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
355
- this.app.mouse.off(pc.EVENT_MOUSEUP, this.onMouseUp, this);
356
- this.app.mouse.off(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
357
- this.app.mouse.off(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this);
358
- window.removeEventListener("mouseout", onMouseOut, false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  });
360
- }
361
-
362
- this.app.mouse.disableContextMenu();
363
- this.lookButtonDown = false;
364
- this.panButtonDown = false;
365
- this.lastPoint = new pc.Vec2();
366
- };
367
-
368
- OrbitCameraInputMouse.fromWorldPoint = new pc.Vec3();
369
- OrbitCameraInputMouse.toWorldPoint = new pc.Vec3();
370
- OrbitCameraInputMouse.worldDiff = new pc.Vec3();
371
-
372
- OrbitCameraInputMouse.prototype.pan = function (screenPoint) {
373
- const fromWorldPoint = OrbitCameraInputMouse.fromWorldPoint;
374
- const toWorldPoint = OrbitCameraInputMouse.toWorldPoint;
375
- const worldDiff = OrbitCameraInputMouse.worldDiff;
376
-
377
- const camera = this.entity.camera;
378
- const distance = this.orbitCamera.distance;
379
-
380
- camera.screenToWorld(screenPoint.x, screenPoint.y, distance, fromWorldPoint);
381
- camera.screenToWorld(this.lastPoint.x, this.lastPoint.y, distance, toWorldPoint);
382
-
383
- worldDiff.sub2(toWorldPoint, fromWorldPoint);
384
-
385
- // Respecter minY (comme les flèches)
386
- let proposedPivot = this.orbitCamera.pivotPoint.clone().add(worldDiff);
387
- const minY = this.orbitCamera.minY;
388
- let resultingY = this.orbitCamera.worldCameraYForPivot(proposedPivot);
389
-
390
- if (resultingY >= minY - 1e-4) {
391
- this.orbitCamera.pivotPoint.add(worldDiff);
392
- } else {
393
- worldDiff.y = 0;
394
- proposedPivot = this.orbitCamera.pivotPoint.clone().add(worldDiff);
395
- resultingY = this.orbitCamera.worldCameraYForPivot(proposedPivot);
396
- if (resultingY >= minY - 1e-4) this.orbitCamera.pivotPoint.add(worldDiff);
397
- }
398
- };
399
-
400
- OrbitCameraInputMouse.prototype.onMouseDown = function (event) {
401
- switch (event.button) {
402
- case pc.MOUSEBUTTON_LEFT: this.panButtonDown = true; break;
403
- case pc.MOUSEBUTTON_MIDDLE:
404
- case pc.MOUSEBUTTON_RIGHT: this.lookButtonDown = true; break;
405
- }
406
- };
407
-
408
- OrbitCameraInputMouse.prototype.onMouseUp = function (event) {
409
- switch (event.button) {
410
- case pc.MOUSEBUTTON_LEFT: this.panButtonDown = false; break;
411
- case pc.MOUSEBUTTON_MIDDLE:
412
- case pc.MOUSEBUTTON_RIGHT: this.lookButtonDown = false; break;
413
- }
414
- };
415
-
416
- OrbitCameraInputMouse.prototype.onMouseMove = function (event) {
417
- if (this.lookButtonDown) {
418
- const sens = this.orbitSensitivity;
419
-
420
- const deltaPitch = event.dy * sens;
421
- const deltaYaw = event.dx * sens;
422
-
423
- const currPitch = this.orbitCamera.pitch;
424
- const currYaw = this.orbitCamera.yaw;
425
- const currDist = this.orbitCamera.distance;
426
- const currPivot = this.orbitCamera.pivotPoint.clone();
427
-
428
- const camQuat = new pc.Quat().setFromEulerAngles(currPitch, currYaw, 0);
429
- const forward = new pc.Vec3(); camQuat.transformVector(pc.Vec3.FORWARD, forward);
430
- const preY = currPivot.y + (-forward.y) * currDist;
431
-
432
- const proposedPitch = currPitch - deltaPitch;
433
- const testQuat = new pc.Quat().setFromEulerAngles(proposedPitch, currYaw, 0);
434
- const testForward = new pc.Vec3(); testQuat.transformVector(pc.Vec3.FORWARD, testForward);
435
- const proposedY = currPivot.y + (-testForward.y) * currDist;
436
-
437
- const minY = this.orbitCamera.minY;
438
- const wouldGoBelow = proposedY < minY - 1e-4;
439
-
440
- if (wouldGoBelow && (proposedY < preY)) {
441
- this.orbitCamera.yaw = currYaw - deltaYaw;
442
- } else {
443
- this.orbitCamera.pitch = proposedPitch;
444
- this.orbitCamera.yaw = currYaw - deltaYaw;
445
- }
446
- } else if (this.panButtonDown) {
447
- this.pan(new pc.Vec2(event.x, event.y));
448
- }
449
-
450
- this.lastPoint.set(event.x, event.y);
451
- };
452
-
453
- OrbitCameraInputMouse.prototype.onMouseWheel = function (event) {
454
- if (this.entity.camera.projection === pc.PROJECTION_PERSPECTIVE) {
455
- this.orbitCamera.distance -= event.wheelDelta * this.distanceSensitivity * (this.orbitCamera.distance * 0.1);
456
- } else {
457
- this.orbitCamera.orthoHeight -= event.wheelDelta * this.distanceSensitivity * (this.orbitCamera.orthoHeight * 0.1);
458
- }
459
- event.event.preventDefault();
460
- };
461
-
462
- OrbitCameraInputMouse.prototype.onMouseOut = function () {
463
- this.lookButtonDown = false;
464
- this.panButtonDown = false;
465
- };
466
 
467
- // ============== Orbit Camera Input Touch =================
468
- const OrbitCameraInputTouch = pc.createScript(ORBIT_TOUCH);
469
- OrbitCameraInputTouch.attributes.add("orbitSensitivity", { type: "number", default: 0.6, title: "Orbit Sensitivity" });
470
- OrbitCameraInputTouch.attributes.add("distanceSensitivity", { type: "number", default: 0.5, title: "Distance Sensitivity" });
471
-
472
- OrbitCameraInputTouch.prototype.initialize = function () {
473
- this.orbitCamera = this.entity.script[ORBIT_BASE];
474
- this.lastTouchPoint = new pc.Vec2();
475
- this.lastPinchMidPoint = new pc.Vec2();
476
- this.lastPinchDistance = 0;
477
-
478
- if (this.orbitCamera && this.app.touch) {
479
- this.app.touch.on(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
480
- this.app.touch.on(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
481
- this.app.touch.on(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
482
- this.app.touch.on(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
483
-
484
- this.on("destroy", function () {
485
- this.app.touch.off(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
486
- this.app.touch.off(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
487
- this.app.touch.off(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
488
- this.app.touch.off(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
489
  });
490
  }
491
- };
492
-
493
- OrbitCameraInputTouch.prototype.getPinchDistance = function (pointA, pointB) {
494
- const dx = pointA.x - pointB.x;
495
- const dy = pointA.y - pointB.y;
496
- return Math.sqrt((dx * dx) + (dy * dy));
497
- };
498
-
499
- OrbitCameraInputTouch.prototype.calcMidPoint = function (pointA, pointB, result) {
500
- result.set(pointB.x - pointA.x, pointB.y - pointA.y);
501
- result.mulScalar(0.5);
502
- result.x += pointA.x;
503
- result.y += pointA.y;
504
- };
505
-
506
- OrbitCameraInputTouch.prototype.onTouchStartEndCancel = function (event) {
507
- const touches = event.touches;
508
- if (touches.length === 1) {
509
- this.lastTouchPoint.set(touches[0].x, touches[0].y);
510
- } else if (touches.length === 2) {
511
- this.lastPinchDistance = this.getPinchDistance(touches[0], touches[1]);
512
- this.calcMidPoint(touches[0], touches[1], this.lastPinchMidPoint);
513
- }
514
- };
515
-
516
- OrbitCameraInputTouch.fromWorldPoint = new pc.Vec3();
517
- OrbitCameraInputTouch.toWorldPoint = new pc.Vec3();
518
- OrbitCameraInputTouch.worldDiff = new pc.Vec3();
519
-
520
- OrbitCameraInputTouch.prototype.pan = function (midPoint) {
521
- const fromWorldPoint = OrbitCameraInputTouch.fromWorldPoint;
522
- const toWorldPoint = OrbitCameraInputTouch.toWorldPoint;
523
- const worldDiff = OrbitCameraInputTouch.worldDiff;
524
-
525
- const camera = this.entity.camera;
526
- const distance = this.orbitCamera.distance;
527
-
528
- camera.screenToWorld(midPoint.x, midPoint.y, distance, fromWorldPoint);
529
- camera.screenToWorld(this.lastPinchMidPoint.x, this.lastPinchMidPoint.y, distance, toWorldPoint);
530
-
531
- worldDiff.sub2(toWorldPoint, fromWorldPoint);
532
-
533
- let proposedPivot = this.orbitCamera.pivotPoint.clone().add(worldDiff);
534
- const minY = this.orbitCamera.minY;
535
- let resultingY = this.orbitCamera.worldCameraYForPivot(proposedPivot);
536
-
537
- if (resultingY >= minY - 1e-4) {
538
- this.orbitCamera.pivotPoint.add(worldDiff);
539
- } else {
540
- worldDiff.y = 0;
541
- proposedPivot = this.orbitCamera.pivotPoint.clone().add(worldDiff);
542
- resultingY = this.orbitCamera.worldCameraYForPivot(proposedPivot);
543
- if (resultingY >= minY - 1e-4) this.orbitCamera.pivotPoint.add(worldDiff);
544
- }
545
- };
546
-
547
- OrbitCameraInputTouch.pinchMidPoint = new pc.Vec2();
548
-
549
- OrbitCameraInputTouch.prototype.onTouchMove = function (event) {
550
- const pinchMidPoint = OrbitCameraInputTouch.pinchMidPoint;
551
- const touches = event.touches;
552
-
553
- if (touches.length === 1) {
554
- const touch = touches[0];
555
- const sens = this.orbitSensitivity;
556
-
557
- const deltaPitch = (touch.y - this.lastTouchPoint.y) * sens;
558
- const deltaYaw = (touch.x - this.lastTouchPoint.x) * sens;
559
-
560
- const currPitch = this.orbitCamera.pitch;
561
- const currYaw = this.orbitCamera.yaw;
562
- const currDist = this.orbitCamera.distance;
563
- const currPivot = this.orbitCamera.pivotPoint.clone();
564
-
565
- const camQuat = new pc.Quat().setFromEulerAngles(currPitch, currYaw, 0);
566
- const forward = new pc.Vec3(); camQuat.transformVector(pc.Vec3.FORWARD, forward);
567
- const preY = currPivot.y + (-forward.y) * currDist;
568
-
569
- const proposedPitch = currPitch - deltaPitch;
570
- const testQuat = new pc.Quat().setFromEulerAngles(proposedPitch, currYaw, 0);
571
- const testForward = new pc.Vec3(); testQuat.transformVector(pc.Vec3.FORWARD, testForward);
572
- const proposedY = currPivot.y + (-testForward.y) * currDist;
573
-
574
- const minY = this.orbitCamera.minY;
575
- const wouldGoBelow = proposedY < minY - 1e-4;
576
-
577
- if (wouldGoBelow && (proposedY < preY)) {
578
- this.orbitCamera.yaw = currYaw - deltaYaw;
579
- } else {
580
- this.orbitCamera.pitch = proposedPitch;
581
- this.orbitCamera.yaw = currYaw - deltaYaw;
582
- }
583
-
584
- this.lastTouchPoint.set(touch.x, touch.y);
585
- } else if (touches.length === 2) {
586
- const currentPinchDistance = this.getPinchDistance(touches[0], touches[1]);
587
- const diffInPinchDistance = currentPinchDistance - this.lastPinchDistance;
588
- this.lastPinchDistance = currentPinchDistance;
589
-
590
- this.orbitCamera.distance -= (diffInPinchDistance * this.distanceSensitivity * 0.1) * (this.orbitCamera.distance * 0.1);
591
-
592
- this.calcMidPoint(touches[0], touches[1], pinchMidPoint);
593
- this.pan(pinchMidPoint);
594
- this.lastPinchMidPoint.copy(pinchMidPoint);
595
- }
596
- };
597
-
598
- // ============== Orbit Camera Input Keyboard =================
599
- const OrbitCameraInputKeyboard = pc.createScript(ORBIT_KEYS);
600
-
601
- OrbitCameraInputKeyboard.attributes.add("forwardSpeed", { type: "number", default: 1.2, title: "Vertical Speed (rel. to distance)" });
602
- OrbitCameraInputKeyboard.attributes.add("strafeSpeed", { type: "number", default: 1.2, title: "Left/Right Speed (rel. to distance)" });
603
-
604
- // Fine-tuning
605
- OrbitCameraInputKeyboard.attributes.add("orbitPitchSpeedDeg", { type: "number", default: 90, title: "Orbit Pitch Speed (deg/s) [Shift+Up/Down]" });
606
- OrbitCameraInputKeyboard.attributes.add("orbitYawSpeedDeg", { type: "number", default: 120, title: "Orbit Yaw Speed (deg/s) [Shift+Left/Right]" });
607
- OrbitCameraInputKeyboard.attributes.add("zoomKeySensitivity", { type: "number", default: 3.0, title: "Zoom Sensitivity (1/s) [Ctrl+Up/Down]" });
608
-
609
- OrbitCameraInputKeyboard.prototype.initialize = function () {
610
- this.orbitCamera = this.entity.script[ORBIT_BASE];
611
- this.keyboard = this.app.keyboard || null;
612
- };
613
-
614
- OrbitCameraInputKeyboard.prototype.update = function (dt) {
615
- if (!this.orbitCamera || !this.keyboard) return;
616
-
617
- const up = this.keyboard.isPressed(pc.KEY_UP);
618
- const dn = this.keyboard.isPressed(pc.KEY_DOWN);
619
- const lt = this.keyboard.isPressed(pc.KEY_LEFT);
620
- const rt = this.keyboard.isPressed(pc.KEY_RIGHT);
621
-
622
- const shift = this.keyboard.isPressed(pc.KEY_SHIFT);
623
- const ctrl = this.keyboard.isPressed(pc.KEY_CONTROL);
624
 
625
- // ---- SHIFT: Orbit (pitch / yaw) ----
626
- if (shift && (up || dn || lt || rt)) {
627
- // Yaw: Shift+Right => orbit à droite
628
- const yawDir = (rt ? 1 : 0) - (lt ? 1 : 0);
629
- if (yawDir !== 0) {
630
- const dYaw = yawDir * this.attributes.orbitYawSpeedDeg * dt;
631
- this.orbitCamera.yaw = this.orbitCamera.yaw + dYaw;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
  }
633
-
634
- // Pitch: Shift+Up => orbit up
635
- const pitchDir = (up ? 1 : 0) - (dn ? 1 : 0);
636
- if (pitchDir !== 0) {
637
- const dPitch = pitchDir * this.attributes.orbitPitchSpeedDeg * dt;
638
-
639
- const currPitch = this.orbitCamera.pitch;
640
- const currYaw = this.orbitCamera.yaw;
641
- const currDist = this.orbitCamera.distance;
642
- const currPivot = this.orbitCamera.pivotPoint.clone();
643
-
644
- const camQuat = new pc.Quat().setFromEulerAngles(currPitch, currYaw, 0);
645
- const forward = new pc.Vec3(); camQuat.transformVector(pc.Vec3.FORWARD, forward);
646
- const preY = currPivot.y + (-forward.y) * currDist;
647
-
648
- const testPitch = currPitch + dPitch;
649
- const testQuat = new pc.Quat().setFromEulerAngles(testPitch, currYaw, 0);
650
- const testForward = new pc.Vec3(); testQuat.transformVector(pc.Vec3.FORWARD, testForward);
651
- const proposedY = currPivot.y + (-testForward.y) * currDist;
652
-
653
- const minY = this.orbitCamera.minY;
654
- const wouldGoBelow = proposedY < minY - 1e-4;
655
-
656
- if (!(wouldGoBelow && (proposedY < preY))) {
657
- this.orbitCamera.pitch = testPitch;
658
- }
659
  }
660
- return;
661
- }
662
-
663
- // ---- CTRL: Zoom (up/down) ----
664
- if (ctrl && (up || dn)) {
665
- const zoomSign = (up ? 1 : 0) - (dn ? 1 : 0);
666
- if (zoomSign !== 0) {
667
- if (this.entity.camera.projection === pc.PROJECTION_PERSPECTIVE) {
668
- const dz = zoomSign * this.attributes.zoomKeySensitivity * (this.orbitCamera.distance * 0.5) * dt;
669
- this.orbitCamera.distance -= dz;
670
- } else {
671
- const doh = zoomSign * this.attributes.zoomKeySensitivity * (this.orbitCamera.orthoHeight * 0.5) * dt;
672
- this.orbitCamera.orthoHeight -= doh;
673
- }
 
 
 
 
 
 
 
 
 
674
  }
675
- return;
676
- }
677
-
678
- // ---- Sans modif: translation (vertical + strafe) ----
679
- const moveVert = (up ? 1 : 0) - (dn ? 1 : 0);
680
- const moveRight = (rt ? 1 : 0) - (lt ? 1 : 0);
681
-
682
- if (moveVert === 0 && moveRight === 0) return;
683
-
684
- const dist = Math.max(0.1, this.orbitCamera.distance);
685
- const speedV = this.attributes.forwardSpeed * dist;
686
- const speedR = this.attributes.strafeSpeed * dist;
687
-
688
- // Vertical pur Y (respect minY)
689
- let dy = moveVert * speedV * dt;
690
- if (dy !== 0) {
691
- const currentCamY = this.orbitCamera.worldCameraYForPivot(this.orbitCamera.pivotPoint);
692
- const minY = this.orbitCamera.minY;
693
- const proposedCamY = currentCamY + dy;
694
- if (proposedCamY < minY) dy = Math.max(dy, minY - currentCamY);
695
- if (dy !== 0) this.orbitCamera._pivotPoint.y += dy;
696
  }
697
 
698
- // Strafe en XZ
699
- const right = this.entity.right.clone(); right.y = 0;
700
- if (right.lengthSq() > 1e-8) right.normalize();
701
- const dx = moveRight * speedR * dt;
702
- if (dx !== 0) this.orbitCamera._pivotPoint.add(right.mulScalar(dx));
703
- };
704
  }
705
 
706
- /* ======================= INITIALISATION VIEWER =========================== */
707
- export async function initializeViewer(config, instanceId) {
708
- // 1 import = 1 instance; garde-fou en plus
709
- if (viewerInitialized) return;
710
 
711
- const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
712
- const isMobile = isIOS || /Android/i.test(navigator.userAgent);
713
-
714
- // --- Config ---
715
- sogsUrl = config.sogs_json_url;
716
- glbUrl = (config.glb_url !== undefined)
717
- ? config.glb_url
718
- : "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/ressources/espace_expo/sol_blanc_2.glb";
719
- presentoirUrl = (config.presentoir_url !== undefined)
720
- ? config.presentoir_url
721
- : "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/ressources/espace_expo/sol_blanc_2.glb";
722
-
723
- minZoom = parseFloat(config.minZoom || "1");
724
- maxZoom = parseFloat(config.maxZoom || "20");
725
- minAngle = parseFloat(config.minAngle || "-45");
726
- maxAngle = parseFloat(config.maxAngle || "90");
727
- minAzimuth = (config.minAzimuth !== undefined) ? parseFloat(config.minAzimuth) : -360;
728
- maxAzimuth = (config.maxAzimuth !== undefined) ? parseFloat(config.maxAzimuth) : 360;
729
- minPivotY = parseFloat(config.minPivotY || "0");
730
- minY = (config.minY !== undefined) ? parseFloat(config.minY) : 0;
731
-
732
- modelX = (config.modelX !== undefined) ? parseFloat(config.modelX) : 0;
733
- modelY = (config.modelY !== undefined) ? parseFloat(config.modelY) : 0;
734
- modelZ = (config.modelZ !== undefined) ? parseFloat(config.modelZ) : 0;
735
- modelScale = (config.modelScale !== undefined) ? parseFloat(config.modelScale) : 1;
736
- modelRotationX = (config.modelRotationX !== undefined) ? parseFloat(config.modelRotationX) : 0;
737
- modelRotationY = (config.modelRotationY !== undefined) ? parseFloat(config.modelRotationY) : 0;
738
- modelRotationZ = (config.modelRotationZ !== undefined) ? parseFloat(config.modelRotationZ) : 0;
739
-
740
- presentoirScaleX = (config.presentoirScaleX !== undefined) ? parseFloat(config.presentoirScaleX) : 0;
741
- presentoirScaleY = (config.presentoirScaleY !== undefined) ? parseFloat(config.presentoirScaleY) : 0;
742
- presentoirScaleZ = (config.presentoirScaleZ !== undefined) ? parseFloat(config.presentoirScaleZ) : 0;
743
-
744
- const cameraX = (config.camera
 
 
 
 
 
 
 
 
 
 
 
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();
 
23
  });
24
  }
25
 
26
+ // Patch global Image -> force CORS
27
  (function () {
28
  const OriginalImage = window.Image;
29
  window.Image = function (...args) {
 
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);
 
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) {
 
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/orbit-camera.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;
 
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
+ }