MikaFil commited on
Commit
5010d47
·
verified ·
1 Parent(s): d6e686a

Update deplacement_dans_env/ctrl_camera_pr_env.js

Browse files
deplacement_dans_env/ctrl_camera_pr_env.js CHANGED
@@ -1,618 +1,482 @@
1
- // ctrl_camera_pr_env.js
2
- // ============================================================================
3
- // FREE CAMERA + COLLISION INDOOR-SAFE (sans Ammo) avec STEP OFFSET (escaliers)
4
- // - Souris : look
5
- // - ZQSD / Flèches : déplacement local
6
- // - Molette / Pinch : dolly
7
- // - Collisions : sphère (caméra) vs AABBs du GLB (focusEntity)
8
- // - Mouvement "swept" + slide sur murs + step-up/step-down pour escaliers
9
- // - AUCUN AUTOSPAWN : la position initiale est celle fournie par la config
10
- // ============================================================================
11
-
12
- var FreeCamera = pc.createScript('orbitCamera'); // garder ce nom pour compat viewer
13
-
14
- // ======================== Attributs ===========================
15
- FreeCamera.attributes.add('inertiaFactor', { type: 'number', default: 0.12, title: 'Inertia (rotation)' });
16
- FreeCamera.attributes.add('pitchAngleMin', { type: 'number', default: -89, title: 'Pitch Min (deg)' });
17
- FreeCamera.attributes.add('pitchAngleMax', { type: 'number', default: 89, title: 'Pitch Max (deg)' });
18
- FreeCamera.attributes.add('minY', { type: 'number', default: 0, title: 'Minimum camera Y' });
19
-
20
- FreeCamera.attributes.add('moveSpeed', { type: 'number', default: 2.2, title: 'Move Speed' });
21
- FreeCamera.attributes.add('strafeSpeed', { type: 'number', default: 2.2, title: 'Strafe Speed' });
22
- FreeCamera.attributes.add('dollySpeed', { type: 'number', default: 2.0, title: 'Mouse/Pinch Dolly Speed' });
23
-
24
- // Collision (caméra sphère)
25
- FreeCamera.attributes.add('collisionRadius', { type: 'number', default: 0.30, title: 'Camera Sphere Radius' });
26
- FreeCamera.attributes.add('collisionEpsilon', { type: 'number', default: 0.0005, title: 'Collision Epsilon' });
27
-
28
- // Mouvement "swept"
29
- FreeCamera.attributes.add('maxStepDistance', { type: 'number', default: 0.20, title: 'Max step distance (swept move)' });
30
- FreeCamera.attributes.add('maxResolveIters', { type: 'number', default: 6, title: 'Max resolve iterations per step' });
31
-
32
- // Indoor-safe colliders
33
- FreeCamera.attributes.add('inflateBias', { type: 'number', default: 0.0, title: 'Extra inflate (m, tiny)' });
34
- FreeCamera.attributes.add('mergeGap', { type: 'number', default: 0.0, title: 'Merge AABBs gap (0 = chevauchement réel seulement)' });
35
- FreeCamera.attributes.add('globalCullFrac',{ type: 'number', default: 0.08, title: 'Cull near-global AABBs (0.08=8%)' });
36
-
37
- // Step offset / Ground snap (escaliers)
38
- FreeCamera.attributes.add('stepHeight', { type: 'number', default: 0.35, title: 'Max step-up height (m)' });
39
- FreeCamera.attributes.add('stepAhead', { type: 'number', default: 0.20, title: 'Probe distance ahead for step (m)' });
40
- FreeCamera.attributes.add('stepDownMax', { type: 'number', default: 0.60, title: 'Max snap-down (m)' });
41
- FreeCamera.attributes.add('enableGroundSnap', { type: 'boolean', default: true, title: 'Enable ground snap' });
42
-
43
- // BBox globale optionnelle (Xmin..Zmax)
44
- FreeCamera.attributes.add('Xmin', { type: 'number', default: -Infinity, title: 'BBox Xmin' });
45
- FreeCamera.attributes.add('Xmax', { type: 'number', default: Infinity, title: 'BBox Xmax' });
46
- FreeCamera.attributes.add('Ymin', { type: 'number', default: -Infinity, title: 'BBox Ymin' });
47
- FreeCamera.attributes.add('Ymax', { type: 'number', default: Infinity, title: 'BBox Ymax' });
48
- FreeCamera.attributes.add('Zmin', { type: 'number', default: -Infinity, title: 'BBox Zmin' });
49
- FreeCamera.attributes.add('Zmax', { type: 'number', default: Infinity, title: 'BBox Zmax' });
50
-
51
- // Compat (gardées pour le viewer)
52
- FreeCamera.attributes.add('focusEntity', { type: 'entity', title: 'Collision Root (ENV GLB)' });
53
- FreeCamera.attributes.add('frameOnStart', { type: 'boolean', default: false, title: 'Compat: Frame on Start (unused)' });
54
- FreeCamera.attributes.add('yawAngleMin', { type: 'number', default: -360, title: 'Compat: Yaw Min (unused)' });
55
- FreeCamera.attributes.add('yawAngleMax', { type: 'number', default: 360, title: 'Compat: Yaw Max (unused)' });
56
- FreeCamera.attributes.add('distanceMin', { type: 'number', default: 0.1, title: 'Compat: Distance Min (unused)' });
57
-
58
- // ======================== Helpers ===========================
59
- function vec3Sub(a, b) { return new pc.Vec3(a.x-b.x, a.y-b.y, a.z-b.z); }
60
- function vec3Add(a, b) { return new pc.Vec3(a.x+b.x, a.y+b.y, a.z+b.z); }
61
- function vec3Scale(a, s){ return new pc.Vec3(a.x*s, a.y*s, a.z*s); }
62
- function length(v) { return Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z); }
63
- function dot(a, b){ return a.x*b.x + a.y*b.y + a.z*b.z; }
64
- function normalize(v){ var l=length(v)||1; return new pc.Vec3(v.x/l, v.y/l, v.z/l); }
65
- function projectOnPlane(v, n){ var d = dot(v, n); return new pc.Vec3(v.x - d*n.x, v.y - d*n.y, v.z - d*n.z); }
66
-
67
- // ======================== Initialisation ===========================
68
- Object.defineProperty(FreeCamera.prototype, 'pitch', {
69
- get: function () { return this._targetPitch; },
70
- set: function (v) { this._targetPitch = pc.math.clamp(v, this.pitchAngleMin, this.pitchAngleMax); }
71
- });
72
- Object.defineProperty(FreeCamera.prototype, 'yaw', {
73
- get: function () { return this._targetYaw; },
74
- set: function (v) { this._targetYaw = v; } // yaw libre
75
- });
76
-
77
- FreeCamera.prototype.initialize = function () {
78
- // Marqueur pour le viewer : c’est bien une free-cam
79
- this.isFreeCamera = true;
80
-
81
- // angles init depuis l’orientation actuelle
82
- var q = this.entity.getRotation();
83
- var f = new pc.Vec3(); q.transformVector(pc.Vec3.FORWARD, f);
84
- this._yaw = Math.atan2(f.x, f.z) * pc.math.RAD_TO_DEG;
85
- var yawQ = new pc.Quat().setFromEulerAngles(0, -this._yaw, 0);
86
- var noYawQ = new pc.Quat().mul2(yawQ, q);
87
- var fNoYaw = new pc.Vec3(); noYawQ.transformVector(pc.Vec3.FORWARD, fNoYaw);
88
- this._pitch = pc.math.clamp(Math.atan2(-fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG, this.pitchAngleMin, this.pitchAngleMax);
89
- this._targetYaw = this._yaw; this._targetPitch = this._pitch;
90
- this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
91
-
92
- // état partagé
93
- this.app.systems.script.app.freeCamState = this.app.systems.script.app.freeCamState || {};
94
- this.state = this.app.systems.script.app.freeCamState;
95
-
96
- // Colliders
97
- this._buildIndoorSafeColliders();
98
-
99
- // Respect total de la position JSON :
100
- // On fait UNIQUEMENT une micro-résolution si on spawn *déjà* interpenetré (mais sans "sauter" loin).
101
- var start = this.entity.getPosition().clone();
102
- var resolved = this._resolveCollisions(start, this.maxResolveIters);
103
- var delta = resolved.clone().sub(start);
104
- if (delta.length() > this.collisionRadius * 0.25) {
105
- // Si la correction est énorme, on préfère limiter à 25% du rayon pour éviter de sortir du bâtiment.
106
- var lim = delta.normalize().mulScalar(this.collisionRadius * 0.25);
107
- resolved = start.clone().add(lim);
108
- }
109
- this._clampPosition(resolved);
110
- this.entity.setPosition(resolved);
111
-
112
- // aspect
113
- var self = this;
114
- this._onResize = function(){ self._checkAspectRatio(); };
115
- window.addEventListener('resize', this._onResize, false);
116
- this._checkAspectRatio();
117
- };
118
-
119
- FreeCamera.prototype.update = function (dt) {
120
- var t = this.inertiaFactor === 0 ? 1 : Math.min(dt / this.inertiaFactor, 1);
121
- this._yaw = pc.math.lerp(this._yaw, this._targetYaw, t);
122
- this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t);
123
- this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
124
-
125
- var pos = this.entity.getPosition().clone();
126
- pos = this._resolveCollisions(pos, this.maxResolveIters);
127
- this._clampPosition(pos);
128
- this.entity.setPosition(pos);
129
- };
130
-
131
- FreeCamera.prototype._checkAspectRatio = function () {
132
- var gd = this.app.graphicsDevice;
133
- if (!gd) return;
134
- this.entity.camera.horizontalFov = (gd.height > gd.width);
135
- };
136
-
137
- // ======================== Colliders indoor-safe ===========================
138
- FreeCamera.prototype._buildIndoorSafeColliders = function () {
139
- this._colliders = [];
140
- this._worldAabb = null;
141
- this._useCollision = false;
142
-
143
- if (!this.focusEntity) return;
144
-
145
- // 1) Collecter toutes les AABBs world des meshInstances
146
- var boxes = [];
147
- var stack = [this.focusEntity];
148
- while (stack.length) {
149
- var e = stack.pop();
150
- var rc = e.render;
151
- if (rc && rc.meshInstances && rc.meshInstances.length) {
152
- for (var i = 0; i < rc.meshInstances.length; i++) {
153
- var a = rc.meshInstances[i].aabb;
154
- boxes.push(new pc.BoundingBox(a.center.clone(), a.halfExtents.clone()));
155
- }
156
- }
157
- var ch = e.children;
158
- if (ch && ch.length) for (var c = 0; c < ch.length; c++) stack.push(ch[c]);
159
  }
160
- if (boxes.length === 0) return;
161
-
162
- // 2) AABB monde
163
- var world = boxes[0].clone();
164
- for (var j = 1; j < boxes.length; j++) world.add(boxes[j]);
165
-
166
- // 3) Filtrer les AABBs quasi globales
167
- var frac = pc.math.clamp(this.globalCullFrac, 0, 0.49);
168
- var wh = world.halfExtents;
169
- var filtered = [];
170
- for (var k = 0; k < boxes.length; k++) {
171
- var h = boxes[k].halfExtents;
172
- var nearGlobal =
173
- (h.x >= wh.x * (1 - frac)) &&
174
- (h.y >= wh.y * (1 - frac)) &&
175
- (h.z >= wh.z * (1 - frac));
176
- if (!nearGlobal) filtered.push(boxes[k]);
177
  }
 
178
 
179
- // 4) Merge strict
180
- var merged = this._mergeAabbs(filtered, Math.max(0, this.mergeGap || 0));
 
181
 
182
- // 5) Petit gonflage
183
- var inflate = Math.max(0, this.inflateBias || 0);
184
- for (var m = 0; m < merged.length; m++) {
185
- merged[m].halfExtents.add(new pc.Vec3(inflate, inflate, inflate));
 
186
  }
187
 
188
- if (merged.length > 0) {
189
- for (var t = 0; t < merged.length; t++) this._colliders.push({ aabb: merged[t] });
190
- this._worldAabb = world;
191
- this._useCollision = true;
192
- } else {
193
- this._colliders = [];
194
- this._useCollision = false;
195
- console.warn('[orbitCamera] Aucun collider utile trouvé (désactivation des collisions)');
196
- }
197
- };
198
-
199
- FreeCamera.prototype._mergeAabbs = function (boxes, gap) {
200
- if (!boxes || boxes.length <= 1) return boxes.slice();
201
- var out = boxes.slice();
202
- var changed = true;
203
- var tol = Math.max(0, gap || 0);
204
-
205
- function overlap(a, b, t) {
206
- var amin = a.getMin(), amax = a.getMax();
207
- var bmin = b.getMin(), bmax = b.getMax();
208
- return !(
209
- amax.x < bmin.x - t || amin.x > bmax.x + t ||
210
- amax.y < bmin.y - t || amin.y > bmax.y + t ||
211
- amax.z < bmin.z - t || amin.z > bmax.z + t
212
- );
213
- }
214
 
215
- while (changed) {
216
- changed = false;
217
- var next = [];
218
- var used = new Array(out.length).fill(false);
219
-
220
- for (var i = 0; i < out.length; i++) {
221
- if (used[i]) continue;
222
- var acc = out[i];
223
- for (var j = i + 1; j < out.length; j++) {
224
- if (used[j]) continue;
225
- if (overlap(acc, out[j], tol)) {
226
- var aMin = acc.getMin(), aMax = acc.getMax();
227
- var bMin = out[j].getMin(), bMax = out[j].getMax();
228
-
229
- var nMin = new pc.Vec3(
230
- Math.min(aMin.x, bMin.x),
231
- Math.min(aMin.y, bMin.y),
232
- Math.min(aMin.z, bMin.z)
233
- );
234
- var nMax = new pc.Vec3(
235
- Math.max(aMax.x, bMax.x),
236
- Math.max(aMax.y, bMax.y),
237
- Math.max(aMax.z, bMax.z)
238
- );
239
- var nCenter = nMin.clone().add(nMax).mulScalar(0.5);
240
- var nHalf = new pc.Vec3(
241
- Math.abs(nMax.x - nCenter.x),
242
- Math.abs(nMax.y - nCenter.y),
243
- Math.abs(nMax.z - nCenter.z)
244
- );
245
-
246
- acc = new pc.BoundingBox(nCenter, nHalf);
247
- used[j] = true;
248
- changed = true;
249
- }
250
- }
251
- used[i] = true;
252
- next.push(acc);
253
- }
254
- out = next;
255
- }
256
- return out;
257
- };
258
-
259
- // ======================== SPAWN / VALIDATION POSITION : SUPPRIMÉ ===========================
260
-
261
- // ======================== Contraintes génériques ===========================
262
- FreeCamera.prototype._bboxEnabled = function () {
263
- return (this.Xmin < this.Xmax) && (this.Ymin < this.Ymax) && (this.Zmin < this.Zmax);
264
- };
265
- FreeCamera.prototype._clampPosition = function (p) {
266
- if (p.y < this.minY) p.y = this.minY;
267
- if (!this._bboxEnabled()) return;
268
- p.x = pc.math.clamp(p.x, this.Xmin, this.Xmax);
269
- p.y = pc.math.clamp(p.y, Math.max(this.Ymin, this.minY), this.Ymax);
270
- p.z = pc.math.clamp(p.z, this.Zmin, this.Zmax);
271
- };
272
-
273
- // ======================== MOUVEMENT PRINCIPAL ===========================
274
- FreeCamera.prototype._moveWithCollisions = function (from, desiredDelta) {
275
- if (!this._useCollision) return from.clone().add(desiredDelta);
276
-
277
- var maxStep = Math.max(0.01, this.maxStepDistance || 0.2);
278
- var total = desiredDelta.clone();
279
- var dist = length(total);
280
- if (dist <= maxStep) return this._moveStep(from, total);
281
-
282
- var steps = Math.ceil(dist / maxStep);
283
- var stepVec = vec3Scale(total, 1/steps);
284
- var cur = from.clone();
285
- for (var i = 0; i < steps; i++) cur = this._moveStep(cur, stepVec);
286
- return cur;
287
- };
288
-
289
- FreeCamera.prototype._moveStep = function (from, delta) {
290
- var target = vec3Add(from, delta);
291
-
292
- // a) tentative simple
293
- var after = this._resolveCollisions(target, this.maxResolveIters);
294
- var movedVec = vec3Sub(after, target);
295
- var movedLen = length(movedVec);
296
-
297
- if (movedLen === 0) {
298
- if (this.enableGroundSnap) after = this._snapDown(after);
299
- return after;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  }
301
 
302
- // b) slide
303
- var n = this._estimateCollisionNormal(after);
304
- if (n) {
305
- var desire = delta.clone();
306
- var slideDelta = projectOnPlane(desire, n);
307
- var slideTarget = vec3Add(from, slideDelta);
308
- var slideAfter = this._resolveCollisions(slideTarget, this.maxResolveIters);
309
-
310
- // c) step-up si slide insuffisant
311
- if (length(vec3Sub(slideAfter, slideTarget)) !== 0) {
312
- var stepped = this._tryStepUp(from, desire);
313
- if (stepped) {
314
- if (this.enableGroundSnap) stepped = this._snapDown(stepped);
315
- return stepped;
316
- }
317
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
 
319
- if (this.enableGroundSnap) slideAfter = this._snapDown(slideAfter);
320
- return slideAfter;
321
- }
 
322
 
323
- if (this.enableGroundSnap) after = this._snapDown(after);
324
- return after;
325
- };
326
-
327
- FreeCamera.prototype._estimateCollisionNormal = function (p) {
328
- if (!this._colliders) return null;
329
- var probe = 0.02;
330
- var base = this._resolveCollisions(p, this.maxResolveIters);
331
- var nx = this._resolveCollisions(new pc.Vec3(p.x+probe, p.y, p.z), this.maxResolveIters);
332
- var px = this._resolveCollisions(new pc.Vec3(p.x-probe, p.y, p.z), this.maxResolveIters);
333
- var ny = this._resolveCollisions(new pc.Vec3(p.x, p.y+probe, p.z), this.maxResolveIters);
334
- var py = this._resolveCollisions(new pc.Vec3(p.x, p.y-probe, p.z), this.maxResolveIters);
335
- var nz = this._resolveCollisions(new pc.Vec3(p.x, p.y, p.z+probe), this.maxResolveIters);
336
- var pz = this._resolveCollisions(new pc.Vec3(p.x, p.y, p.z-probe), this.maxResolveIters);
337
-
338
- var dx = length(vec3Sub(nx, base)) - length(vec3Sub(px, base));
339
- var dy = length(vec3Sub(ny, base)) - length(vec3Sub(py, base));
340
- var dz = length(vec3Sub(nz, base)) - length(vec3Sub(pz, base));
341
- var n = new pc.Vec3(dx, dy, dz);
342
- var L = length(n);
343
- if (L < 1e-5) return null;
344
- return vec3Scale(n, 1/L);
345
- };
346
-
347
- FreeCamera.prototype._tryStepUp = function (from, wishDelta) {
348
- var R = Math.max(0, this.collisionRadius);
349
- var eps = Math.max(1e-4, this.collisionEpsilon);
350
- var maxH = Math.max(0, this.stepHeight || 0.35);
351
- var ahead = Math.max(0.05, this.stepAhead || 0.20);
352
-
353
- var horiz = new pc.Vec3(wishDelta.x, 0, wishDelta.z);
354
- var hLen = length(horiz);
355
- if (hLen < 1e-6) return null;
356
- horiz = vec3Scale(horiz, 1/hLen);
357
-
358
- var probeXZ = vec3Add(from, vec3Scale(horiz, Math.min(hLen, ahead)));
359
- var bestY = this._highestYUnderXZ(probeXZ.x, probeXZ.z, eps);
360
- if (bestY === -Infinity) return null;
361
-
362
- var curY = from.y;
363
- var desiredY = bestY + R + 0.02;
364
- var up = desiredY - curY;
365
-
366
- if (up > 0 && up <= maxH + 1e-4) {
367
- var raised = new pc.Vec3(from.x, curY + up, from.z);
368
- raised = this._resolveCollisions(raised, this.maxResolveIters);
369
- var stepped = this._resolveCollisions(vec3Add(raised, wishDelta), this.maxResolveIters);
370
- if (length(vec3Sub(stepped, raised)) > 0.01) return stepped;
371
- }
372
- return null;
373
- };
374
 
375
- FreeCamera.prototype._snapDown = function (p) {
376
- var R = Math.max(0, this.collisionRadius);
377
- var eps = Math.max(1e-4, this.collisionEpsilon);
378
- var maxDown = Math.max(0, this.stepDownMax || 0.6);
379
 
380
- var bestY = this._highestYUnderXZ(p.x, p.z, eps);
381
- if (bestY === -Infinity) return p;
382
 
383
- var floorY = bestY + R + 0.01;
384
- if (p.y - floorY > 0 && p.y - floorY <= maxDown) {
385
- var snapped = new pc.Vec3(p.x, floorY, p.z);
386
- return this._resolveCollisions(snapped, this.maxResolveIters);
387
- }
388
- return p;
389
- };
390
-
391
- FreeCamera.prototype._highestYUnderXZ = function (x, z, eps) {
392
- var bestY = -Infinity;
393
- if (!this._colliders) return bestY;
394
- for (var i = 0; i < this._colliders.length; i++) {
395
- var aabb = this._colliders[i].aabb;
396
- var amin = aabb.getMin(), amax = aabb.getMax();
397
- if (x >= amin.x - eps && x <= amax.x + eps && z >= amin.z - eps && z <= amax.z + eps) {
398
- if (amax.y > bestY) bestY = amax.y;
399
- }
400
- }
401
- return bestY;
402
- };
403
-
404
- // ======================== Résolution sphère/AABB ===========================
405
- FreeCamera.prototype._resolveCollisions = function (pos, maxIters) {
406
- if (!this._useCollision) return pos.clone();
407
-
408
- var p = pos.clone();
409
- var R = Math.max(0, this.collisionRadius);
410
- var eps = Math.max(1e-7, this.collisionEpsilon);
411
-
412
- if (!this._colliders || this._colliders.length === 0) return p;
413
-
414
- var iters = Math.max(1, maxIters || 1);
415
- for (var iter = 0; iter < iters; iter++) {
416
- var moved = false;
417
-
418
- for (var i = 0; i < this._colliders.length; i++) {
419
- var aabb = this._colliders[i].aabb;
420
- var amin = aabb.getMin(); var amax = aabb.getMax();
421
-
422
- var cx = pc.math.clamp(p.x, amin.x, amax.x);
423
- var cy = pc.math.clamp(p.y, amin.y, amax.y);
424
- var cz = pc.math.clamp(p.z, amin.z, amax.z);
425
-
426
- var dx = p.x - cx, dy = p.y - cy, dz = p.z - cz;
427
- var distSq = dx*dx + dy*dy + dz*dz;
428
-
429
- if (distSq < (R*R)) {
430
- var dist = Math.sqrt(Math.max(distSq, 1e-12));
431
- var pen = R - dist + eps;
432
-
433
- if (dist > 1e-6) {
434
- p.x += (dx / dist) * pen;
435
- p.y += (dy / dist) * pen;
436
- p.z += (dz / dist) * pen;
437
- } else {
438
- var ex = Math.min(Math.abs(p.x - amin.x), Math.abs(amax.x - p.x));
439
- var ey = Math.min(Math.abs(p.y - amin.y), Math.abs(amax.y - p.y));
440
- var ez = Math.min(Math.abs(p.z - amin.z), Math.abs(amax.z - p.z));
441
- if (ex <= ey && ex <= ez) p.x += (Math.abs(p.x - amin.x) < Math.abs(amax.x - p.x) ? -pen : pen);
442
- else if (ey <= ex && ey <= ez) p.y += (Math.abs(p.y - amin.y) < Math.abs(amax.y - p.y) ? -pen : pen);
443
- else p.z += (Math.abs(p.z - amin.z) < Math.abs(amax.z - p.z) ? -pen : pen);
444
- }
445
- moved = true;
446
- }
447
- }
448
- if (!moved) break;
449
- }
450
 
451
- return p;
452
- };
453
-
454
- // ===================== INPUT SOURIS =====================
455
- var FreeCameraInputMouse = pc.createScript('orbitCameraInputMouse');
456
- FreeCameraInputMouse.attributes.add('lookSensitivity', { type: 'number', default: 0.3, title: 'Look Sensitivity' });
457
- FreeCameraInputMouse.attributes.add('wheelSensitivity',{ type: 'number', default: 1.0, title: 'Wheel Sensitivity' });
458
-
459
- FreeCameraInputMouse.prototype.initialize = function () {
460
- this.freeCam = this.entity.script.orbitCamera;
461
- this.last = new pc.Vec2();
462
- this.isLooking = false;
463
-
464
- if (this.app.mouse) {
465
- this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
466
- this.app.mouse.on(pc.EVENT_MOUSEUP, this.onMouseUp, this);
467
- this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
468
- this.app.mouse.on(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this);
469
- this.app.mouse.disableContextMenu();
470
- }
471
- var self = this;
472
- this._onOut = function(){ self.isLooking = false; };
473
- window.addEventListener('mouseout', this._onOut, false);
474
-
475
- this.on('destroy', () => {
476
- if (this.app.mouse) {
477
- this.app.mouse.off(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
478
- this.app.mouse.off(pc.EVENT_MOUSEUP, this.onMouseUp, this);
479
- this.app.mouse.off(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
480
- this.app.mouse.off(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this);
481
  }
482
- window.removeEventListener('mouseout', this._onOut, false);
483
- });
484
- };
485
-
486
- FreeCameraInputMouse.prototype.onMouseDown = function (e) { this.isLooking = true; this.last.set(e.x, e.y); };
487
- FreeCameraInputMouse.prototype.onMouseUp = function () { this.isLooking = false; };
488
-
489
- FreeCameraInputMouse.prototype.onMouseMove = function (e) {
490
- if (!this.isLooking || !this.freeCam) return;
491
- var s = this.lookSensitivity;
492
- this.freeCam.yaw = this.freeCam.yaw - e.dx * s;
493
- this.freeCam.pitch = this.freeCam.pitch - e.dy * s;
494
- this.last.set(e.x, e.y);
495
- };
496
-
497
- FreeCameraInputMouse.prototype.onMouseWheel = function (e) {
498
- if (!this.freeCam) return;
499
- var cam = this.entity;
500
- var move = -e.wheelDelta * this.wheelSensitivity * this.freeCam.dollySpeed * 0.05;
501
- var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
502
-
503
- var from = cam.getPosition().clone();
504
- var to = from.clone().add(forward.mulScalar(move));
505
- var next = this.freeCam._moveWithCollisions(from, to.sub(from));
506
- this.freeCam._clampPosition(next);
507
- cam.setPosition(next);
508
-
509
- e.event.preventDefault();
510
- };
511
-
512
- // ===================== INPUT TOUCH =====================
513
- var FreeCameraInputTouch = pc.createScript('orbitCameraInputTouch');
514
- FreeCameraInputTouch.attributes.add('lookSensitivity', { type: 'number', default: 0.5, title: 'Look Sensitivity' });
515
- FreeCameraInputTouch.attributes.add('pinchDollyFactor', { type: 'number', default: 0.02, title: 'Pinch Dolly Factor' });
516
-
517
- FreeCameraInputTouch.prototype.initialize = function () {
518
- this.freeCam = this.entity.script.orbitCamera;
519
- this.last = new pc.Vec2();
520
- this.isLooking = false;
521
- this.lastPinch = 0;
522
-
523
- if (this.app.touch) {
524
- this.app.touch.on(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
525
- this.app.touch.on(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
526
- this.app.touch.on(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
527
- this.app.touch.on(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
528
- }
529
- this.on('destroy', () => {
530
- if (this.app.touch) {
531
- this.app.touch.off(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
532
- this.app.touch.off(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
533
- this.app.touch.off(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
534
- this.app.touch.off(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
535
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  });
537
- };
538
-
539
- FreeCameraInputTouch.prototype.onTouchStartEndCancel = function (e) {
540
- var t = e.touches;
541
- if (t.length === 1) {
542
- this.isLooking = (e.event.type === 'touchstart');
543
- this.last.set(t[0].x, t[0].y);
544
- } else if (t.length === 2) {
545
- var dx = t[0].x - t[1].x, dy = t[0].y - t[1].y;
546
- this.lastPinch = Math.sqrt(dx*dx + dy*dy);
547
- } else {
548
- this.isLooking = false;
549
- }
550
- };
551
-
552
- FreeCameraInputTouch.prototype.onTouchMove = function (e) {
553
- var t = e.touches;
554
- if (!this.freeCam) return;
555
-
556
- if (t.length === 1 && this.isLooking) {
557
- var s = this.lookSensitivity;
558
- var dx = t[0].x - this.last.x, dy = t[0].y - this.last.y;
559
- this.freeCam.yaw = this.freeCam.yaw - dx * s;
560
- this.freeCam.pitch = this.freeCam.pitch - dy * s;
561
- this.last.set(t[0].x, t[0].y);
562
- } else if (t.length === 2) {
563
- var dx = t[0].x - t[1].x, dy = t[0].y - t[1].y;
564
- var dist = Math.sqrt(dx*dx + dy*dy);
565
- var delta = dist - this.lastPinch; this.lastPinch = dist;
566
-
567
- var cam = this.entity;
568
- var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
569
- var from = cam.getPosition().clone();
570
- var to = from.clone().add(forward.mulScalar(delta * this.pinchDollyFactor * this.freeCam.dollySpeed));
571
- var next = this.freeCam._moveWithCollisions(from, to.sub(from));
572
- this.freeCam._clampPosition(next);
573
- cam.setPosition(next);
574
- }
575
- };
576
 
577
- // ===================== INPUT CLAVIER =====================
578
- var FreeCameraInputKeyboard = pc.createScript('orbitCameraInputKeyboard');
579
- FreeCameraInputKeyboard.attributes.add('acceleration', { type: 'number', default: 1.0, title: 'Accel (unused)' });
580
 
581
- FreeCameraInputKeyboard.prototype.initialize = function () {
582
- this.freeCam = this.entity.script.orbitCamera;
583
- this.kb = this.app.keyboard || null;
584
- };
585
 
586
- FreeCameraInputKeyboard.prototype.update = function (dt) {
587
- if (!this.freeCam || !this.kb) return;
 
588
 
589
- var fwd = (this.kb.isPressed(pc.KEY_UP) || this.kb.isPressed(pc.KEY_Z)) ? 1 :
590
- (this.kb.isPressed(pc.KEY_DOWN) || this.kb.isPressed(pc.KEY_S)) ? -1 : 0;
591
- var strf = (this.kb.isPressed(pc.KEY_RIGHT) || this.kb.isPressed(pc.KEY_D)) ? 1 :
592
- (this.kb.isPressed(pc.KEY_LEFT) || this.kb.isPressed(pc.KEY_Q)) ? -1 : 0;
593
 
594
- if (fwd !== 0 || strf !== 0) {
595
- var cam = this.entity;
596
- var from = cam.getPosition().clone();
597
 
598
- var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
599
- var right = cam.right.clone(); if (right.lengthSq() > 1e-8) right.normalize();
 
600
 
601
- var delta = new pc.Vec3()
602
- .add(forward.mulScalar(fwd * this.freeCam.moveSpeed * dt))
603
- .add(right .mulScalar(strf * this.freeCam.strafeSpeed * dt));
604
 
605
- var next = this.freeCam._moveWithCollisions(from, delta);
606
- this.freeCam._clampPosition(next);
607
- cam.setPosition(next);
608
- }
 
 
609
 
610
- var shift = this.kb.isPressed(pc.KEY_SHIFT);
611
- if (shift) {
612
- var yawDir = (this.kb.isPressed(pc.KEY_LEFT) ? 1 : 0) - (this.kb.isPressed(pc.KEY_RIGHT) ? 1 : 0);
613
- var pitchDir = (this.kb.isPressed(pc.KEY_UP) ? 1 : 0) - (this.kb.isPressed(pc.KEY_DOWN) ? 1 : 0);
614
- var yawSpeed = 120, pitchSpeed = 90;
615
- if (yawDir !== 0) this.freeCam.yaw = this.freeCam.yaw + yawDir * yawSpeed * dt;
616
- if (pitchDir !== 0) this.freeCam.pitch = this.freeCam.pitch + pitchDir * pitchSpeed * dt;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
  }
618
- };
 
1
+ // viewer_pr_env.js
2
+ // ==============================
3
+
4
+ /* -------------------------------------------
5
+ Utils
6
+ -------------------------------------------- */
7
+
8
+ async function loadImageAsTexture(url, app) {
9
+ return new Promise((resolve, reject) => {
10
+ const img = new window.Image();
11
+ img.crossOrigin = "anonymous";
12
+ img.onload = function () {
13
+ const tex = new pc.Texture(app.graphicsDevice, {
14
+ width: img.width,
15
+ height: img.height,
16
+ format: pc.PIXELFORMAT_R8_G8_B8_A8
17
+ });
18
+ tex.setSource(img);
19
+ resolve(tex);
20
+ };
21
+ img.onerror = reject;
22
+ img.src = url;
23
+ });
24
+ }
25
+
26
+ // Patch global Image -> force CORS
27
+ (function () {
28
+ const OriginalImage = window.Image;
29
+ window.Image = function (...args) {
30
+ const img = new OriginalImage(...args);
31
+ img.crossOrigin = "anonymous";
32
+ return img;
33
+ };
34
+ })();
35
+
36
+ function hexToRgbaArray(hex) {
37
+ try {
38
+ hex = String(hex || "").replace("#", "");
39
+ if (hex.length === 6) hex += "FF";
40
+ if (hex.length !== 8) return [1, 1, 1, 1];
41
+ const num = parseInt(hex, 16);
42
+ return [
43
+ ((num >> 24) & 0xff) / 255,
44
+ ((num >> 16) & 0xff) / 255,
45
+ ((num >> 8) & 0xff) / 255,
46
+ (num & 0xff) / 255
47
+ ];
48
+ } catch (e) {
49
+ console.warn("hexToRgbaArray error:", e);
50
+ return [1, 1, 1, 1];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  }
52
+ }
53
+
54
+ // Parcours récursif d'une hiérarchie d'entités
55
+ function traverse(entity, callback) {
56
+ callback(entity);
57
+ if (entity.children) {
58
+ entity.children.forEach((child) => traverse(child, callback));
 
 
 
 
 
 
 
 
 
 
59
  }
60
+ }
61
 
62
+ /* -------------------------------------------
63
+ Chargement unique de ctrl_camera_pr_env.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
+ // Script caméra libre + collisions (garde le nom public "orbitCamera")
76
+ s.src = "https://mikafil-viewer-sgos.static.hf.space/deplacement_dans_env/ctrl_camera_pr_env.js";
77
+ s.async = true;
78
+ s.onload = () => {
79
+ window.__PLY_ORBIT_LOADED__ = true;
80
+ resolve();
81
+ };
82
+ s.onerror = (e) => {
83
+ console.error("[viewer.js] Failed to load orbit-camera script", e);
84
+ reject(e);
85
+ };
86
+ document.head.appendChild(s);
87
+ });
 
 
 
 
 
 
 
 
 
 
 
88
 
89
+ await window.__PLY_ORBIT_LOADING__;
90
+ }
91
+
92
+ /* -------------------------------------------
93
+ State (par module = par instance importée)
94
+ -------------------------------------------- */
95
+
96
+ let pc;
97
+ export let app = null;
98
+ let cameraEntity = null;
99
+ let modelEntity = null; // gsplat principal (oeuvre)
100
+ let envEntity = null; // GLB d'environnement / présentoir
101
+ let viewerInitialized = false;
102
+ let resizeObserver = null;
103
+
104
+ // paramètres courants de l'instance
105
+ let chosenCameraX, chosenCameraY, chosenCameraZ;
106
+ let distanceMin, minAngle, maxAngle, minAzimuth, maxAzimuth, minY;
107
+ let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
108
+ let presentoirScaleX, presentoirScaleY, presentoirScaleZ;
109
+ let sogsUrl, glbUrl;
110
+ let color_bg_hex, color_bg, espace_expo_bool;
111
+
112
+ /* -------------------------------------------
113
+ Initialisation
114
+ -------------------------------------------- */
115
+
116
+ export async function initializeViewer(config, instanceId) {
117
+ // une seule initialisation par import
118
+ if (viewerInitialized) return;
119
+
120
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
121
+ const isMobile = isIOS || /Android/i.test(navigator.userAgent);
122
+
123
+ // --- Configuration ---
124
+ sogsUrl = config.sogs_json_url || null;
125
+ glbUrl = config.glb_url || null;
126
+
127
+ // rétro-compat minZoom => distanceMin (conservé pour compat mais non utilisé par la free cam)
128
+ distanceMin = config.minZoom !== undefined
129
+ ? parseFloat(config.minZoom)
130
+ : (config.distanceMin !== undefined ? parseFloat(config.distanceMin) : 1);
131
+
132
+ minAngle = parseFloat(config.minAngle ?? "-45");
133
+ maxAngle = parseFloat(config.maxAngle ?? "90");
134
+ minAzimuth = config.minAzimuth !== undefined ? parseFloat(config.minAzimuth) : -360;
135
+ maxAzimuth = config.maxAzimuth !== undefined ? parseFloat(config.maxAzimuth) : 360;
136
+ minY = config.minY !== undefined ? parseFloat(config.minY) : 0;
137
+
138
+ modelX = config.modelX !== undefined ? parseFloat(config.modelX) : 0;
139
+ modelY = config.modelY !== undefined ? parseFloat(config.modelY) : 0;
140
+ modelZ = config.modelZ !== undefined ? parseFloat(config.modelZ) : 0;
141
+ modelScale = config.modelScale !== undefined ? parseFloat(config.modelScale) : 1;
142
+ modelRotationX = config.modelRotationX !== undefined ? parseFloat(config.modelRotationX) : 0;
143
+ modelRotationY = config.modelRotationY !== undefined ? parseFloat(config.modelRotationY) : 0;
144
+ modelRotationZ = config.modelRotationZ !== undefined ? parseFloat(config.modelRotationZ) : 0;
145
+
146
+ // défauts à 1 (et non 0) pour éviter l'invisibilité si manquants
147
+ presentoirScaleX = config.presentoirScaleX !== undefined ? parseFloat(config.presentoirScaleX) : 1;
148
+ presentoirScaleY = config.presentoirScaleY !== undefined ? parseFloat(config.presentoirScaleY) : 1;
149
+ presentoirScaleZ = config.presentoirScaleZ !== undefined ? parseFloat(config.presentoirScaleZ) : 1;
150
+
151
+ const cameraX = config.cameraX !== undefined ? parseFloat(config.cameraX) : 0;
152
+ const cameraY = config.cameraY !== undefined ? parseFloat(config.cameraY) : 2;
153
+ const cameraZ = config.cameraZ !== undefined ? parseFloat(config.cameraZ) : 5;
154
+
155
+ const cameraXPhone = config.cameraXPhone !== undefined ? parseFloat(config.cameraXPhone) : cameraX;
156
+ const cameraYPhone = config.cameraYPhone !== undefined ? parseFloat(config.cameraYPhone) : cameraY;
157
+ const cameraZPhone = config.cameraZPhone !== undefined ? parseFloat(config.cameraZPhone) : cameraZ * 1.5;
158
+
159
+ color_bg_hex = config.canvas_background !== undefined ? config.canvas_background : "#FFFFFF";
160
+ espace_expo_bool = config.espace_expo_bool !== undefined ? config.espace_expo_bool : false;
161
+ color_bg = hexToRgbaArray(color_bg_hex);
162
+
163
+ chosenCameraX = isMobile ? cameraXPhone : cameraX;
164
+ chosenCameraY = isMobile ? cameraYPhone : cameraY;
165
+ chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
166
+
167
+ // --- Prépare le canvas unique à cette instance ---
168
+ const canvasId = "canvas-" + instanceId;
169
+ const progressDialog = document.getElementById("progress-dialog-" + instanceId);
170
+ const viewerContainer = document.getElementById("viewer-container-" + instanceId);
171
+
172
+ const old = document.getElementById(canvasId);
173
+ if (old) old.remove();
174
+
175
+ const canvas = document.createElement("canvas");
176
+ canvas.id = canvasId;
177
+ canvas.className = "ply-canvas";
178
+ canvas.style.width = "100%";
179
+ canvas.style.height = "100%";
180
+ canvas.setAttribute("tabindex", "0");
181
+ viewerContainer.insertBefore(canvas, progressDialog);
182
+
183
+ // interactions de base (éviter scroll/gestes par défaut sur le canvas)
184
+ canvas.style.touchAction = "none";
185
+ canvas.style.webkitTouchCallout = "none";
186
+ canvas.addEventListener("gesturestart", (e) => e.preventDefault());
187
+ canvas.addEventListener("gesturechange", (e) => e.preventDefault());
188
+ canvas.addEventListener("gestureend", (e) => e.preventDefault());
189
+ canvas.addEventListener("dblclick", (e) => e.preventDefault());
190
+ canvas.addEventListener(
191
+ "touchstart",
192
+ (e) => {
193
+ if (e.touches.length > 1) e.preventDefault();
194
+ },
195
+ { passive: false }
196
+ );
197
+ canvas.addEventListener(
198
+ "wheel",
199
+ (e) => { e.preventDefault(); },
200
+ { passive: false }
201
+ );
202
+
203
+ // Bloque le scroll page uniquement quand le pointeur est sur le canvas
204
+ const scrollKeys = new Set([ "ArrowUp","ArrowDown","ArrowLeft","ArrowRight","PageUp","PageDown","Home","End"," ","Space","Spacebar" ]);
205
+ let isPointerOverCanvas = false;
206
+ const focusCanvas = () => canvas.focus({ preventScroll: true });
207
+
208
+ const onPointerEnter = () => { isPointerOverCanvas = true; focusCanvas(); };
209
+ const onPointerLeave = () => {
210
+ isPointerOverCanvas = false;
211
+ if (document.activeElement === canvas) canvas.blur();
212
+ };
213
+ const onCanvasBlur = () => { isPointerOverCanvas = false; };
214
+
215
+ canvas.addEventListener("pointerenter", onPointerEnter);
216
+ canvas.addEventListener("pointerleave", onPointerLeave);
217
+ canvas.addEventListener("mouseenter", onPointerEnter);
218
+ canvas.addEventListener("mouseleave", onPointerLeave);
219
+ canvas.addEventListener("mousedown", focusCanvas);
220
+ canvas.addEventListener("touchstart", () => { focusCanvas(); }, { passive: false });
221
+ canvas.addEventListener("blur", onCanvasBlur);
222
+
223
+ const onKeyDownCapture = (e) => {
224
+ if (!isPointerOverCanvas) return;
225
+ if (scrollKeys.has(e.key) || scrollKeys.has(e.code)) e.preventDefault();
226
+ };
227
+ window.addEventListener("keydown", onKeyDownCapture, true);
228
+
229
+ progressDialog.style.display = "block";
230
+
231
+ // --- Charge PlayCanvas lib ESM (une par module/instance) ---
232
+ if (!pc) {
233
+ pc = await import("https://esm.run/playcanvas");
234
+ window.pc = pc; // utile pour tooltips.js et debug
235
  }
236
 
237
+ // --- Crée l'Application ---
238
+ const device = await pc.createGraphicsDevice(canvas, {
239
+ deviceTypes: ["webgl2"],
240
+ glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
241
+ twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
242
+ antialias: false
243
+ });
244
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
245
+
246
+ const opts = new pc.AppOptions();
247
+ opts.graphicsDevice = device;
248
+ opts.mouse = new pc.Mouse(canvas);
249
+ opts.touch = new pc.TouchDevice(canvas);
250
+ opts.keyboard = new pc.Keyboard(canvas); // scoping clavier au canvas
251
+ opts.componentSystems = [
252
+ pc.RenderComponentSystem,
253
+ pc.CameraComponentSystem,
254
+ pc.LightComponentSystem,
255
+ pc.ScriptComponentSystem,
256
+ pc.GSplatComponentSystem,
257
+ pc.CollisionComponentSystem,
258
+ pc.RigidbodyComponentSystem
259
+ ];
260
+ opts.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler, pc.ScriptHandler, pc.GSplatHandler];
261
+
262
+ app = new pc.Application(canvas, opts);
263
+ app.setCanvasFillMode(pc.FILLMODE_NONE);
264
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
265
+
266
+ resizeObserver = new ResizeObserver((entries) => {
267
+ entries.forEach((entry) => {
268
+ app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
269
+ });
270
+ });
271
+ resizeObserver.observe(viewerContainer);
272
+
273
+ window.addEventListener("resize", () =>
274
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight)
275
+ );
276
+
277
+ // Nettoyage complet
278
+ app.on("destroy", () => {
279
+ try { resizeObserver.disconnect(); } catch {}
280
+ if (opts.keyboard && opts.keyboard.detach) opts.keyboard.detach();
281
+ window.removeEventListener("keydown", onKeyDownCapture, true);
282
+
283
+ canvas.removeEventListener("pointerenter", onPointerEnter);
284
+ canvas.removeEventListener("pointerleave", onPointerLeave);
285
+ canvas.removeEventListener("mouseenter", onPointerEnter);
286
+ canvas.removeEventListener("mouseleave", onPointerLeave);
287
+ canvas.removeEventListener("mousedown", focusCanvas);
288
+ canvas.removeEventListener("touchstart", focusCanvas);
289
+ canvas.removeEventListener("blur", onCanvasBlur);
290
+ });
291
 
292
+ // --- Enregistre les assets (SAUF orbit script : chargé globalement) ---
293
+ const assets = {};
294
+ if (sogsUrl) assets.sogs = new pc.Asset("gsplat", "gsplat", { url: sogsUrl });
295
+ if (glbUrl) assets.env = new pc.Asset("env", "container", { url: glbUrl });
296
 
297
+ for (const k in assets) app.assets.add(assets[k]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
+ const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
 
 
 
300
 
301
+ // Assure le chargement unique des scripts de caméra
302
+ await ensureOrbitScriptsLoaded();
303
 
304
+ loader.load(() => {
305
+ app.start();
306
+ progressDialog.style.display = "none";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
 
308
+ // --- Modèle principal (gsplat) ---
309
+ if (assets.sogs) {
310
+ modelEntity = new pc.Entity("model");
311
+ modelEntity.addComponent("gsplat", { asset: assets.sogs });
312
+ modelEntity.setLocalPosition(modelX, modelY, modelZ);
313
+ modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
314
+ modelEntity.setLocalScale(modelScale, modelScale, modelScale);
315
+ app.root.addChild(modelEntity);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  }
317
+
318
+ // --- GLB Environnement / Présentoir (optionnel) ---
319
+ if (assets.env && assets.env.resource) {
320
+ const container = assets.env.resource; // pc.ContainerResource
321
+ envEntity = container.instantiateRenderEntity({
322
+ castShadows: false,
323
+ receiveShadows: true
324
+ });
325
+
326
+ // Position/rotation/échelle (adaptables)
327
+ envEntity.setLocalPosition(0, 0, 0);
328
+ envEntity.setLocalEulerAngles(0, 0, 0);
329
+ envEntity.setLocalScale(presentoirScaleX, presentoirScaleY, presentoirScaleZ);
330
+
331
+ app.root.addChild(envEntity);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  }
333
+
334
+ // Entité qui sert d'ancrage visuel (lookAt) : l'oeuvre si dispo, sinon l'env
335
+ const focusVisual = modelEntity || envEntity;
336
+
337
+ // Racine de collision pour la free cam : l'environnement GLB de préférence
338
+ const collisionRoot = envEntity || modelEntity || null;
339
+
340
+ // --- Caméra + scripts d’input (free cam + collisions) ---
341
+ cameraEntity = new pc.Entity("camera");
342
+ cameraEntity.addComponent("camera", {
343
+ clearColor: new pc.Color(color_bg[0], color_bg[1], color_bg[2], color_bg[3]),
344
+ nearClip: 0.001,
345
+ farClip: 100
346
+ });
347
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
348
+ if (focusVisual) cameraEntity.lookAt(focusVisual.getPosition());
349
+ cameraEntity.addComponent("script");
350
+
351
+ // Prépare les attributs : les champs inconnus sont ignorés par le script
352
+ const orbitAttrs = {
353
+ // Collision root : active le blocage contre le GLB
354
+ focusEntity: collisionRoot || undefined,
355
+
356
+ // Inertie/angles (pitch clampé), yaw libre
357
+ inertiaFactor: 0.2,
358
+ pitchAngleMax: maxAngle,
359
+ pitchAngleMin: minAngle,
360
+
361
+ // Compat (ignorés par le free cam, mais gardés pour ne pas casser la config)
362
+ distanceMin: distanceMin,
363
+ yawAngleMax: maxAzimuth,
364
+ yawAngleMin: minAzimuth,
365
+ frameOnStart: false,
366
+
367
+ // Contraintes position
368
+ minY: minY
369
+ };
370
+
371
+ // Injecte la BBox uniquement si présente dans le config (évite Infinity explicite)
372
+ const maybeNum = (v) => (v === undefined || v === null || v === "" ? undefined : parseFloat(v));
373
+ const Xmin = maybeNum(config.Xmin);
374
+ const Xmax = maybeNum(config.Xmax);
375
+ const Ymin = maybeNum(config.Ymin);
376
+ const Ymax = maybeNum(config.Ymax);
377
+ const Zmin = maybeNum(config.Zmin);
378
+ const Zmax = maybeNum(config.Zmax);
379
+
380
+ if (Xmin !== undefined) orbitAttrs.Xmin = Xmin;
381
+ if (Xmax !== undefined) orbitAttrs.Xmax = Xmax;
382
+ if (Ymin !== undefined) orbitAttrs.Ymin = Ymin;
383
+ if (Ymax !== undefined) orbitAttrs.Ymax = Ymax;
384
+ if (Zmin !== undefined) orbitAttrs.Zmin = Zmin;
385
+ if (Zmax !== undefined) orbitAttrs.Zmax = Zmax;
386
+
387
+ // Paramètres collision optionnels depuis la config (sinon valeurs par défaut du script)
388
+ if (config.collisionRadius !== undefined) orbitAttrs.collisionRadius = parseFloat(config.collisionRadius);
389
+ if (config.collisionEpsilon !== undefined) orbitAttrs.collisionEpsilon = parseFloat(config.collisionEpsilon);
390
+ if (config.moveSpeed !== undefined) orbitAttrs.moveSpeed = parseFloat(config.moveSpeed);
391
+ if (config.strafeSpeed !== undefined) orbitAttrs.strafeSpeed = parseFloat(config.strafeSpeed);
392
+ if (config.dollySpeed !== undefined) orbitAttrs.dollySpeed = parseFloat(config.dollySpeed);
393
+
394
+ cameraEntity.script.create("orbitCamera", { attributes: orbitAttrs });
395
+ cameraEntity.script.create("orbitCameraInputMouse");
396
+ cameraEntity.script.create("orbitCameraInputTouch");
397
+ cameraEntity.script.create("orbitCameraInputKeyboard", { attributes: { acceleration: 1.0 } });
398
+ app.root.addChild(cameraEntity);
399
+
400
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
401
+
402
+ // Reset caméra une fois la première frame prête
403
+ app.once("update", () => resetViewerCamera());
404
+
405
+ // --- Tooltips (optionnels) ---
406
+ try {
407
+ if (config.tooltips_url) {
408
+ import("./tooltips.js")
409
+ .then((tooltipsModule) => {
410
+ tooltipsModule.initializeTooltips({
411
+ app,
412
+ cameraEntity,
413
+ modelEntity: focusVisual,
414
+ tooltipsUrl: config.tooltips_url,
415
+ defaultVisible: !!config.showTooltipsDefault,
416
+ moveDuration: config.tooltipMoveDuration || 0.6
417
+ });
418
+ })
419
+ .catch(() => { /* optional */ });
420
+ }
421
+ } catch (e) { /* optional */ }
422
+
423
+ viewerInitialized = true;
424
  });
425
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
 
427
+ /* -------------------------------------------
428
+ Reset caméra (API)
429
+ -------------------------------------------- */
430
 
431
+ export function resetViewerCamera() {
432
+ try {
433
+ if (!cameraEntity || !app) return;
 
434
 
435
+ // cible visuelle : gsplat prioritaire, sinon glb, sinon (0,0,0)
436
+ const targetEntity = modelEntity || envEntity;
437
+ const targetPos = targetEntity ? targetEntity.getPosition().clone() : new pc.Vec3(0, 0, 0);
438
 
439
+ const orbitCam = cameraEntity.script && cameraEntity.script.orbitCamera;
440
+ if (!orbitCam) return;
 
 
441
 
442
+ const tempEnt = new pc.Entity();
443
+ tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
444
+ tempEnt.lookAt(targetPos);
445
 
446
+ const dist = new pc.Vec3()
447
+ .sub2(new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ), targetPos)
448
+ .length();
449
 
450
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
451
+ cameraEntity.lookAt(targetPos);
 
452
 
453
+ // Ces champs existent dans la version "orbit" historique ; notre free cam les ignore,
454
+ // mais on conserve l'initialisation pour compat.
455
+ if (orbitCam) {
456
+ orbitCam._targetDistance = Math.max(distanceMin, dist);
457
+ orbitCam._distance = Math.max(distanceMin, dist);
458
+ }
459
 
460
+ // Recalcule yaw/pitch cibles pour aligner l'orientation
461
+ const rot = tempEnt.getRotation();
462
+ const fwd = new pc.Vec3();
463
+ rot.transformVector(pc.Vec3.FORWARD, fwd);
464
+
465
+ const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
466
+ const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
467
+ const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
468
+ const fNoYaw = new pc.Vec3();
469
+ rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
470
+ const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
471
+
472
+ orbitCam._targetYaw = yaw;
473
+ orbitCam._yaw = yaw;
474
+ orbitCam._targetPitch = pitch;
475
+ orbitCam._pitch = pitch;
476
+ if (orbitCam._updatePosition) orbitCam._updatePosition();
477
+
478
+ tempEnt.destroy();
479
+ } catch (e) {
480
+ console.error("[viewer.js] resetViewerCamera error:", e);
481
  }
482
+ }