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

Update deplacement_dans_env/ctrl_camera_pr_env.js

Browse files
deplacement_dans_env/ctrl_camera_pr_env.js CHANGED
@@ -6,22 +6,17 @@
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
- // - Placement initial : respecte la config; auto-spawn inside seulement si nécessaire
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
-
17
- // Limites de pitch
18
  FreeCamera.attributes.add('pitchAngleMin', { type: 'number', default: -89, title: 'Pitch Min (deg)' });
19
  FreeCamera.attributes.add('pitchAngleMax', { type: 'number', default: 89, title: 'Pitch Max (deg)' });
20
-
21
- // minY (altitude min du point CAMÉRA)
22
  FreeCamera.attributes.add('minY', { type: 'number', default: 0, title: 'Minimum camera Y' });
23
 
24
- // Vitesses
25
  FreeCamera.attributes.add('moveSpeed', { type: 'number', default: 2.2, title: 'Move Speed' });
26
  FreeCamera.attributes.add('strafeSpeed', { type: 'number', default: 2.2, title: 'Strafe Speed' });
27
  FreeCamera.attributes.add('dollySpeed', { type: 'number', default: 2.0, title: 'Mouse/Pinch Dolly Speed' });
@@ -45,9 +40,6 @@ FreeCamera.attributes.add('stepAhead', { type: 'number', default: 0.20, title
45
  FreeCamera.attributes.add('stepDownMax', { type: 'number', default: 0.60, title: 'Max snap-down (m)' });
46
  FreeCamera.attributes.add('enableGroundSnap', { type: 'boolean', default: true, title: 'Enable ground snap' });
47
 
48
- // Spawn auto si nécessaire seulement
49
- FreeCamera.attributes.add('autoSpawnInside', { type: 'boolean', default: true, title: 'Auto-spawn inside if needed' });
50
-
51
  // BBox globale optionnelle (Xmin..Zmax)
52
  FreeCamera.attributes.add('Xmin', { type: 'number', default: -Infinity, title: 'BBox Xmin' });
53
  FreeCamera.attributes.add('Xmax', { type: 'number', default: Infinity, title: 'BBox Xmax' });
@@ -63,476 +55,400 @@ FreeCamera.attributes.add('yawAngleMin', { type: 'number', default: -360, title
63
  FreeCamera.attributes.add('yawAngleMax', { type: 'number', default: 360, title: 'Compat: Yaw Max (unused)' });
64
  FreeCamera.attributes.add('distanceMin', { type: 'number', default: 0.1, title: 'Compat: Distance Min (unused)' });
65
 
66
- // ======================== Helpers internes ===========================
67
  function vec3Sub(a, b) { return new pc.Vec3(a.x-b.x, a.y-b.y, a.z-b.z); }
68
  function vec3Add(a, b) { return new pc.Vec3(a.x+b.x, a.y+b.y, a.z+b.z); }
69
  function vec3Scale(a, s){ return new pc.Vec3(a.x*s, a.y*s, a.z*s); }
70
  function length(v) { return Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z); }
71
  function dot(a, b){ return a.x*b.x + a.y*b.y + a.z*b.z; }
72
  function normalize(v){ var l=length(v)||1; return new pc.Vec3(v.x/l, v.y/l, v.z/l); }
73
- function projectOnPlane(v, n){ // enlève la composante selon n
74
- var d = dot(v, n);
75
- return new pc.Vec3(v.x - d*n.x, v.y - d*n.y, v.z - d*n.z);
76
- }
77
 
78
  // ======================== Initialisation ===========================
79
  Object.defineProperty(FreeCamera.prototype, 'pitch', {
80
- get: function () { return this._targetPitch; },
81
- set: function (v) { this._targetPitch = pc.math.clamp(v, this.pitchAngleMin, this.pitchAngleMax); }
82
  });
83
  Object.defineProperty(FreeCamera.prototype, 'yaw', {
84
- get: function () { return this._targetYaw; },
85
- set: function (v) { this._targetYaw = v; } // yaw libre
86
  });
87
 
88
  FreeCamera.prototype.initialize = function () {
89
- // angles init
90
- var q = this.entity.getRotation();
91
- var f = new pc.Vec3(); q.transformVector(pc.Vec3.FORWARD, f);
92
- this._yaw = Math.atan2(f.x, f.z) * pc.math.RAD_TO_DEG;
93
- var yawQ = new pc.Quat().setFromEulerAngles(0, -this._yaw, 0);
94
- var noYawQ = new pc.Quat().mul2(yawQ, q);
95
- var fNoYaw = new pc.Vec3(); noYawQ.transformVector(pc.Vec3.FORWARD, fNoYaw);
96
- this._pitch = pc.math.clamp(Math.atan2(-fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG, this.pitchAngleMin, this.pitchAngleMax);
97
- this._targetYaw = this._yaw; this._targetPitch = this._pitch;
98
- this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
99
-
100
- // état partagé
101
- this.app.systems.script.app.freeCamState = this.app.systems.script.app.freeCamState || {};
102
- this.state = this.app.systems.script.app.freeCamState;
103
-
104
- // colliders
105
- this._buildIndoorSafeColliders();
106
-
107
- // Respecter la position initiale; ne spawner dedans que si nécessaire
108
- if (this.autoSpawnInside && this._useCollision) {
109
- var start = this.entity.getPosition().clone();
110
- if (this._shouldAutoSpawn(start)) {
111
- this._spawnInside();
112
- } else {
113
- var safe = this._resolveCollisions(start, this.maxResolveIters);
114
- this._clampPosition(safe);
115
- this.entity.setPosition(safe);
116
- }
117
- }
118
-
119
- // aspect
120
- var self = this;
121
- this._onResize = function(){ self._checkAspectRatio(); };
122
- window.addEventListener('resize', this._onResize, false);
123
- this._checkAspectRatio();
 
 
 
 
124
  };
125
 
126
  FreeCamera.prototype.update = function (dt) {
127
- // rotation inertielle
128
- var t = this.inertiaFactor === 0 ? 1 : Math.min(dt / this.inertiaFactor, 1);
129
- this._yaw = pc.math.lerp(this._yaw, this._targetYaw, t);
130
- this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t);
131
- this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
132
-
133
- // maintien propre (au cas où)
134
- var pos = this.entity.getPosition().clone();
135
- pos = this._resolveCollisions(pos, this.maxResolveIters);
136
- this._clampPosition(pos);
137
- this.entity.setPosition(pos);
138
  };
139
 
140
  FreeCamera.prototype._checkAspectRatio = function () {
141
- var gd = this.app.graphicsDevice;
142
- if (!gd) return;
143
- this.entity.camera.horizontalFov = (gd.height > gd.width);
144
  };
145
 
146
  // ======================== Colliders indoor-safe ===========================
147
  FreeCamera.prototype._buildIndoorSafeColliders = function () {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  this._colliders = [];
149
- this._worldAabb = null;
150
  this._useCollision = false;
151
-
152
- if (!this.focusEntity) return;
153
-
154
- // 1) Collecter toutes les AABBs world des meshInstances
155
- var boxes = [];
156
- var stack = [this.focusEntity];
157
- while (stack.length) {
158
- var e = stack.pop();
159
- var rc = e.render;
160
- if (rc && rc.meshInstances && rc.meshInstances.length) {
161
- for (var i = 0; i < rc.meshInstances.length; i++) {
162
- var a = rc.meshInstances[i].aabb;
163
- boxes.push(new pc.BoundingBox(a.center.clone(), a.halfExtents.clone()));
164
- }
165
- }
166
- var ch = e.children;
167
- if (ch && ch.length) for (var c = 0; c < ch.length; c++) stack.push(ch[c]);
168
- }
169
- if (boxes.length === 0) return;
170
-
171
- // 2) AABB monde (non gonflée)
172
- var world = boxes[0].clone();
173
- for (var j = 1; j < boxes.length; j++) world.add(boxes[j]);
174
-
175
- // 3) Filtrer les AABBs "quasi globales" (anti-coquille)
176
- var frac = pc.math.clamp(this.globalCullFrac, 0, 0.49);
177
- var wh = world.halfExtents;
178
- var filtered = [];
179
- for (var k = 0; k < boxes.length; k++) {
180
- var h = boxes[k].halfExtents;
181
- var nearGlobal =
182
- (h.x >= wh.x * (1 - frac)) &&
183
- (h.y >= wh.y * (1 - frac)) &&
184
- (h.z >= wh.z * (1 - frac));
185
- if (!nearGlobal) filtered.push(boxes[k]);
186
- }
187
-
188
- // 4) Merge strict (chevauchement réel uniquement)
189
- var merged = this._mergeAabbs(filtered, Math.max(0, this.mergeGap || 0));
190
-
191
- // 5) Petit gonflage (epsilon), pas du rayon
192
- var inflate = Math.max(0, this.inflateBias || 0);
193
- for (var m = 0; m < merged.length; m++) {
194
- merged[m].halfExtents.add(new pc.Vec3(inflate, inflate, inflate));
195
- }
196
-
197
- // 6) Enregistrer
198
- if (merged.length > 0) {
199
- for (var t = 0; t < merged.length; t++) this._colliders.push({ aabb: merged[t] });
200
- this._worldAabb = world;
201
- this._useCollision = true;
202
- } else {
203
- this._colliders = [];
204
- this._useCollision = false;
205
- console.warn('[orbitCamera] Aucun collider utile trouvé (désactivation des collisions)');
206
- }
207
  };
208
 
209
- // Merge strict (gap==0 => chevauchement seulement)
210
  FreeCamera.prototype._mergeAabbs = function (boxes, gap) {
211
- if (!boxes || boxes.length <= 1) return boxes.slice();
212
- var out = boxes.slice();
213
- var changed = true;
214
- var tol = Math.max(0, gap || 0);
215
-
216
- function overlap(a, b, t) {
217
- var amin = a.getMin(), amax = a.getMax();
218
- var bmin = b.getMin(), bmax = b.getMax();
219
- return !(
220
- amax.x < bmin.x - t || amin.x > bmax.x + t ||
221
- amax.y < bmin.y - t || amin.y > bmax.y + t ||
222
- amax.z < bmin.z - t || amin.z > bmax.z + t
223
- );
224
- }
225
-
226
- while (changed) {
227
- changed = false;
228
- var next = [];
229
- var used = new Array(out.length).fill(false);
230
-
231
- for (var i = 0; i < out.length; i++) {
232
- if (used[i]) continue;
233
- var acc = out[i];
234
- for (var j = i + 1; j < out.length; j++) {
235
- if (used[j]) continue;
236
- if (overlap(acc, out[j], tol)) {
237
- var aMin = acc.getMin(), aMax = acc.getMax();
238
- var bMin = out[j].getMin(), bMax = out[j].getMax();
239
-
240
- var nMin = new pc.Vec3(
241
- Math.min(aMin.x, bMin.x),
242
- Math.min(aMin.y, bMin.y),
243
- Math.min(aMin.z, bMin.z)
244
- );
245
- var nMax = new pc.Vec3(
246
- Math.max(aMax.x, bMax.x),
247
- Math.max(aMax.y, bMax.y),
248
- Math.max(aMax.z, bMax.z)
249
- );
250
- var nCenter = nMin.clone().add(nMax).mulScalar(0.5);
251
- var nHalf = new pc.Vec3(
252
- Math.abs(nMax.x - nCenter.x),
253
- Math.abs(nMax.y - nCenter.y),
254
- Math.abs(nMax.z - nCenter.z)
255
- );
256
-
257
- acc = new pc.BoundingBox(nCenter, nHalf);
258
- used[j] = true;
259
- changed = true;
260
- }
261
- }
262
- used[i] = true;
263
- next.push(acc);
264
- }
265
- out = next;
266
- }
267
- return out;
268
- };
269
-
270
- // ======================== SPAWN / VALIDATION POSITION ===========================
271
- FreeCamera.prototype._shouldAutoSpawn = function (pos) {
272
- if (!this._useCollision || !this._worldAabb) return false;
273
-
274
- var R = Math.max(0, this.collisionRadius);
275
- var eps = Math.max(1e-4, this.collisionEpsilon);
276
-
277
- var wmin = this._worldAabb.getMin();
278
- var wmax = this._worldAabb.getMax();
279
-
280
- // hors monde (avec marge du rayon)
281
- if (pos.x < wmin.x + R + eps || pos.x > wmax.x - R - eps) return true;
282
- if (pos.z < wmin.z + R + eps || pos.z > wmax.z - R - eps) return true;
283
- if (pos.y < wmin.y - eps || pos.y > wmax.y + eps) return true;
284
-
285
- // pas de "sol" sous ce XZ
286
- var bestY = this._highestYUnderXZ(pos.x, pos.z, eps);
287
- if (bestY === -Infinity) return true;
288
-
289
- // trop en dessous/au dessus
290
- if (pos.y < bestY - 2.0) return true;
291
- if (pos.y > wmax.y + 0.5) return true;
292
-
293
- // forte intersection initiale
294
- var resolved = this._resolveCollisions(pos, this.maxResolveIters);
295
- var move = resolved.clone().sub(pos).length();
296
- if (move > R * 0.5) return true;
297
-
298
- return false;
299
- };
300
-
301
- FreeCamera.prototype._highestYUnderXZ = function (x, z, eps) {
302
- var bestY = -Infinity;
303
- if (!this._colliders) return bestY;
304
- for (var i = 0; i < this._colliders.length; i++) {
305
- var aabb = this._colliders[i].aabb;
306
- var amin = aabb.getMin(), amax = aabb.getMax();
307
- if (x >= amin.x - eps && x <= amax.x + eps && z >= amin.z - eps && z <= amax.z + eps) {
308
- if (amax.y > bestY) bestY = amax.y;
309
  }
 
 
 
310
  }
311
- return bestY;
 
 
312
  };
313
 
314
- FreeCamera.prototype._spawnInside = function () {
315
- if (!this._useCollision || !this._worldAabb) return;
316
-
317
- var R = Math.max(0, this.collisionRadius);
318
- var eps = Math.max(1e-4, this.collisionEpsilon);
319
-
320
- var world = this._worldAabb;
321
- var c = world.center;
322
- var min = world.getMin(), max = world.getMax();
323
-
324
- var x = pc.math.clamp(c.x, min.x + R + eps, max.x - R - eps);
325
- var z = pc.math.clamp(c.z, min.z + R + eps, max.z - R - eps);
326
-
327
- var bestY = this._highestYUnderXZ(x, z, eps);
328
- var y = (bestY > -Infinity) ? (bestY + R + 0.03) : Math.max(this.minY, (min.y + max.y) * 0.5);
329
-
330
- var target = new pc.Vec3(x, y, z);
331
- target = this._resolveCollisions(target, this.maxResolveIters);
332
- this._clampPosition(target);
333
- this.entity.setPosition(target);
334
- };
335
 
336
  // ======================== Contraintes génériques ===========================
337
  FreeCamera.prototype._bboxEnabled = function () {
338
- return (this.Xmin < this.Xmax) && (this.Ymin < this.Ymax) && (this.Zmin < this.Zmax);
339
  };
340
-
341
  FreeCamera.prototype._clampPosition = function (p) {
342
- if (p.y < this.minY) p.y = this.minY;
343
- if (!this._bboxEnabled()) return;
344
- p.x = pc.math.clamp(p.x, this.Xmin, this.Xmax);
345
- p.y = pc.math.clamp(p.y, Math.max(this.Ymin, this.minY), this.Ymax);
346
- p.z = pc.math.clamp(p.z, this.Zmin, this.Zmax);
347
  };
348
 
349
  // ======================== MOUVEMENT PRINCIPAL ===========================
350
  FreeCamera.prototype._moveWithCollisions = function (from, desiredDelta) {
351
- // 1) tentative pure sans collision si désactivées
352
- if (!this._useCollision) return from.clone().add(desiredDelta);
353
-
354
- // 2) swept: divise en petits pas
355
- var maxStep = Math.max(0.01, this.maxStepDistance || 0.2);
356
- var total = desiredDelta.clone();
357
- var dist = length(total);
358
- if (dist <= maxStep) return this._moveStep(from, total);
359
-
360
- var steps = Math.ceil(dist / maxStep);
361
- var stepVec = vec3Scale(total, 1/steps);
362
- var cur = from.clone();
363
- for (var i = 0; i < steps; i++) {
364
- cur = this._moveStep(cur, stepVec);
365
- }
366
- return cur;
367
  };
368
 
369
- // Un "pas" unique avec slide + step-up + snap-down
370
  FreeCamera.prototype._moveStep = function (from, delta) {
371
- var target = vec3Add(from, delta);
372
 
373
- // a) tentative simple
374
- var after = this._resolveCollisions(target, this.maxResolveIters);
375
- var movedVec = vec3Sub(after, target);
376
- var movedLen = length(movedVec);
377
 
378
- // si aucune collision, on snap-down optionnel
379
- if (movedLen === 0) {
380
- if (this.enableGroundSnap) after = this._snapDown(after);
381
- return after;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  }
383
 
384
- // b) collision détectée → slide (supprime la composante dans la normale estimée)
385
- var n = this._estimateCollisionNormal(after); // approx par rapport aux AABB proches
386
- if (n) {
387
- var desire = delta.clone();
388
- var slideDelta = projectOnPlane(desire, n);
389
- var slideTarget = vec3Add(from, slideDelta);
390
- var slideAfter = this._resolveCollisions(slideTarget, this.maxResolveIters);
391
-
392
- // c) Step-up si le slide ne suffit pas (test petite marche)
393
- if (length(vec3Sub(slideAfter, slideTarget)) !== 0) {
394
- var stepped = this._tryStepUp(from, desire);
395
- if (stepped) {
396
- if (this.enableGroundSnap) stepped = this._snapDown(stepped);
397
- return stepped;
398
- }
399
- }
400
-
401
- if (this.enableGroundSnap) slideAfter = this._snapDown(slideAfter);
402
- return slideAfter;
403
- }
404
 
405
- // pas de normale estimable (rare) : accepte la position résolue
406
- if (this.enableGroundSnap) after = this._snapDown(after);
407
- return after;
408
  };
409
 
410
- // Estimation grossière d’une normale de collision autour de "p"
411
  FreeCamera.prototype._estimateCollisionNormal = function (p) {
412
- if (!this._colliders) return null;
413
- var probe = 0.02;
414
- var base = this._resolveCollisions(p, this.maxResolveIters);
415
- var nx = this._resolveCollisions(new pc.Vec3(p.x+probe, p.y, p.z), this.maxResolveIters);
416
- var px = this._resolveCollisions(new pc.Vec3(p.x-probe, p.y, p.z), this.maxResolveIters);
417
- var ny = this._resolveCollisions(new pc.Vec3(p.x, p.y+probe, p.z), this.maxResolveIters);
418
- var py = this._resolveCollisions(new pc.Vec3(p.x, p.y-probe, p.z), this.maxResolveIters);
419
- var nz = this._resolveCollisions(new pc.Vec3(p.x, p.y, p.z+probe), this.maxResolveIters);
420
- var pz = this._resolveCollisions(new pc.Vec3(p.x, p.y, p.z-probe), this.maxResolveIters);
421
-
422
- var dx = length(vec3Sub(nx, base)) - length(vec3Sub(px, base));
423
- var dy = length(vec3Sub(ny, base)) - length(vec3Sub(py, base));
424
- var dz = length(vec3Sub(nz, base)) - length(vec3Sub(pz, base));
425
- var n = new pc.Vec3(dx, dy, dz);
426
- var L = length(n);
427
- if (L < 1e-5) return null;
428
- return vec3Scale(n, 1/L);
429
  };
430
 
431
- // Step-up : essaie de monter une marche jusqu’à stepHeight
432
  FreeCamera.prototype._tryStepUp = function (from, wishDelta) {
433
- var R = Math.max(0, this.collisionRadius);
434
- var eps = Math.max(1e-4, this.collisionEpsilon);
435
- var maxH = Math.max(0, this.stepHeight || 0.35);
436
- var ahead = Math.max(0.05, this.stepAhead || 0.20);
437
-
438
- // direction de déplacement horizontal
439
- var horiz = new pc.Vec3(wishDelta.x, 0, wishDelta.z);
440
- var hLen = length(horiz);
441
- if (hLen < 1e-6) return null;
442
- horiz = vec3Scale(horiz, 1/hLen);
443
-
444
- // point de test un peu devant
445
- var probeXZ = vec3Add(from, vec3Scale(horiz, Math.min(hLen, ahead)));
446
- var bestY = this._highestYUnderXZ(probeXZ.x, probeXZ.z, eps);
447
-
448
- if (bestY === -Infinity) return null; // rien pour se poser
449
-
450
- // si la marche est à maxH au-dessus de la position actuelle → tenter un step
451
- var curY = from.y;
452
- var desiredY = bestY + R + 0.02; // poser sur la marche
453
- var up = desiredY - curY;
454
-
455
- if (up > 0 && up <= maxH + 1e-4) {
456
- // tenter : monter puis appliquer le delta complet
457
- var raised = new pc.Vec3(from.x, curY + up, from.z);
458
- raised = this._resolveCollisions(raised, this.maxResolveIters);
459
- var stepped = this._resolveCollisions(vec3Add(raised, wishDelta), this.maxResolveIters);
460
- // valider si on a bien avancé
461
- if (length(vec3Sub(stepped, raised)) > 0.01) return stepped;
462
- }
463
-
464
- return null;
465
  };
466
 
467
- // Ground snap : colle vers le sol si on est en l’air (descente d’escaliers)
468
  FreeCamera.prototype._snapDown = function (p) {
469
- var R = Math.max(0, this.collisionRadius);
470
- var eps = Math.max(1e-4, this.collisionEpsilon);
471
- var maxDown = Math.max(0, this.stepDownMax || 0.6);
472
-
473
- var bestY = this._highestYUnderXZ(p.x, p.z, eps);
474
- if (bestY === -Infinity) return p;
 
 
 
 
 
 
 
 
475
 
476
- var floorY = bestY + R + 0.01;
477
- if (p.y - floorY > 0 && p.y - floorY <= maxDown) {
478
- var snapped = new pc.Vec3(p.x, floorY, p.z);
479
- return this._resolveCollisions(snapped, this.maxResolveIters);
 
 
 
 
480
  }
481
- return p;
 
482
  };
483
 
484
- // ======================== Résolution collision sphère/AABB =====================
485
- // Déplace et “expulse” si p (centre sphère) intersecte un AABB.
486
  FreeCamera.prototype._resolveCollisions = function (pos, maxIters) {
487
- if (!this._useCollision) return pos.clone();
488
-
489
- var p = pos.clone();
490
- var R = Math.max(0, this.collisionRadius);
491
- var eps = Math.max(1e-7, this.collisionEpsilon);
492
-
493
- if (!this._colliders || this._colliders.length === 0) return p;
494
-
495
- var iters = Math.max(1, maxIters || 1);
496
- for (var iter = 0; iter < iters; iter++) {
497
- var moved = false;
498
-
499
- for (var i = 0; i < this._colliders.length; i++) {
500
- var aabb = this._colliders[i].aabb;
501
- var amin = aabb.getMin(); var amax = aabb.getMax();
502
-
503
- // Point le plus proche sur l'AABB
504
- var cx = pc.math.clamp(p.x, amin.x, amax.x);
505
- var cy = pc.math.clamp(p.y, amin.y, amax.y);
506
- var cz = pc.math.clamp(p.z, amin.z, amax.z);
507
-
508
- var dx = p.x - cx, dy = p.y - cy, dz = p.z - cz;
509
- var distSq = dx*dx + dy*dy + dz*dz;
510
-
511
- if (distSq < (R*R)) {
512
- var dist = Math.sqrt(Math.max(distSq, 1e-12));
513
- var pen = R - dist + eps;
514
-
515
- if (dist > 1e-6) {
516
- p.x += (dx / dist) * pen;
517
- p.y += (dy / dist) * pen;
518
- p.z += (dz / dist) * pen;
519
- } else {
520
- // pousser selon l'axe le plus "proche"
521
- var ex = Math.min(Math.abs(p.x - amin.x), Math.abs(amax.x - p.x));
522
- var ey = Math.min(Math.abs(p.y - amin.y), Math.abs(amax.y - p.y));
523
- var ez = Math.min(Math.abs(p.z - amin.z), Math.abs(amax.z - p.z));
524
- if (ex <= ey && ex <= ez) p.x += (Math.abs(p.x - amin.x) < Math.abs(amax.x - p.x) ? -pen : pen);
525
- else if (ey <= ex && ey <= ez) p.y += (Math.abs(p.y - amin.y) < Math.abs(amax.y - p.y) ? -pen : pen);
526
- else p.z += (Math.abs(p.z - amin.z) < Math.abs(amax.z - p.z) ? -pen : pen);
527
- }
528
- moved = true;
529
- }
530
- }
531
 
532
- if (!moved) break;
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  }
 
 
534
 
535
- return p;
536
  };
537
 
538
  // ===================== INPUT SOURIS =====================
@@ -541,56 +457,56 @@ FreeCameraInputMouse.attributes.add('lookSensitivity', { type: 'number', default
541
  FreeCameraInputMouse.attributes.add('wheelSensitivity',{ type: 'number', default: 1.0, title: 'Wheel Sensitivity' });
542
 
543
  FreeCameraInputMouse.prototype.initialize = function () {
544
- this.freeCam = this.entity.script.orbitCamera;
545
- this.last = new pc.Vec2();
546
- this.isLooking = false;
547
-
 
 
 
 
 
 
 
 
 
 
 
 
548
  if (this.app.mouse) {
549
- this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
550
- this.app.mouse.on(pc.EVENT_MOUSEUP, this.onMouseUp, this);
551
- this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
552
- this.app.mouse.on(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this);
553
- this.app.mouse.disableContextMenu();
554
  }
555
- var self = this;
556
- this._onOut = function(){ self.isLooking = false; };
557
- window.addEventListener('mouseout', this._onOut, false);
558
-
559
- this.on('destroy', () => {
560
- if (this.app.mouse) {
561
- this.app.mouse.off(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
562
- this.app.mouse.off(pc.EVENT_MOUSEUP, this.onMouseUp, this);
563
- this.app.mouse.off(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
564
- this.app.mouse.off(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this);
565
- }
566
- window.removeEventListener('mouseout', this._onOut, false);
567
- });
568
  };
569
 
570
  FreeCameraInputMouse.prototype.onMouseDown = function (e) { this.isLooking = true; this.last.set(e.x, e.y); };
571
  FreeCameraInputMouse.prototype.onMouseUp = function () { this.isLooking = false; };
572
 
573
  FreeCameraInputMouse.prototype.onMouseMove = function (e) {
574
- if (!this.isLooking || !this.freeCam) return;
575
- var s = this.lookSensitivity;
576
- this.freeCam.yaw = this.freeCam.yaw - e.dx * s;
577
- this.freeCam.pitch = this.freeCam.pitch - e.dy * s;
578
- this.last.set(e.x, e.y);
579
  };
580
 
581
  FreeCameraInputMouse.prototype.onMouseWheel = function (e) {
582
- if (!this.freeCam) return;
583
- var cam = this.entity;
584
- var move = -e.wheelDelta * this.wheelSensitivity * this.freeCam.dollySpeed * 0.05;
585
- var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
586
-
587
- var from = cam.getPosition().clone();
588
- var to = from.clone().add(forward.mulScalar(move));
589
- var next = this.freeCam._moveWithCollisions(from, to.sub(from));
590
- this.freeCam._clampPosition(next);
591
- cam.setPosition(next);
592
-
593
- e.event.preventDefault();
594
  };
595
 
596
  // ===================== INPUT TOUCH =====================
@@ -599,63 +515,63 @@ FreeCameraInputTouch.attributes.add('lookSensitivity', { type: 'number', defaul
599
  FreeCameraInputTouch.attributes.add('pinchDollyFactor', { type: 'number', default: 0.02, title: 'Pinch Dolly Factor' });
600
 
601
  FreeCameraInputTouch.prototype.initialize = function () {
602
- this.freeCam = this.entity.script.orbitCamera;
603
- this.last = new pc.Vec2();
604
- this.isLooking = false;
605
- this.lastPinch = 0;
606
-
 
 
 
 
 
 
 
607
  if (this.app.touch) {
608
- this.app.touch.on(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
609
- this.app.touch.on(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
610
- this.app.touch.on(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
611
- this.app.touch.on(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
612
  }
613
- this.on('destroy', () => {
614
- if (this.app.touch) {
615
- this.app.touch.off(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
616
- this.app.touch.off(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
617
- this.app.touch.off(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
618
- this.app.touch.off(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
619
- }
620
- });
621
  };
622
 
623
  FreeCameraInputTouch.prototype.onTouchStartEndCancel = function (e) {
624
- var t = e.touches;
625
- if (t.length === 1) {
626
- this.isLooking = (e.event.type === 'touchstart');
627
- this.last.set(t[0].x, t[0].y);
628
- } else if (t.length === 2) {
629
- var dx = t[0].x - t[1].x, dy = t[0].y - t[1].y;
630
- this.lastPinch = Math.sqrt(dx*dx + dy*dy);
631
- } else {
632
- this.isLooking = false;
633
- }
634
  };
635
 
636
  FreeCameraInputTouch.prototype.onTouchMove = function (e) {
637
- var t = e.touches;
638
- if (!this.freeCam) return;
639
-
640
- if (t.length === 1 && this.isLooking) {
641
- var s = this.lookSensitivity;
642
- var dx = t[0].x - this.last.x, dy = t[0].y - this.last.y;
643
- this.freeCam.yaw = this.freeCam.yaw - dx * s;
644
- this.freeCam.pitch = this.freeCam.pitch - dy * s;
645
- this.last.set(t[0].x, t[0].y);
646
- } else if (t.length === 2) {
647
- var dx = t[0].x - t[1].x, dy = t[0].y - t[1].y;
648
- var dist = Math.sqrt(dx*dx + dy*dy);
649
- var delta = dist - this.lastPinch; this.lastPinch = dist;
650
-
651
- var cam = this.entity;
652
- var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
653
- var from = cam.getPosition().clone();
654
- var to = from.clone().add(forward.mulScalar(delta * this.pinchDollyFactor * this.freeCam.dollySpeed));
655
- var next = this.freeCam._moveWithCollisions(from, to.sub(from));
656
- this.freeCam._clampPosition(next);
657
- cam.setPosition(next);
658
- }
659
  };
660
 
661
  // ===================== INPUT CLAVIER =====================
@@ -663,40 +579,40 @@ var FreeCameraInputKeyboard = pc.createScript('orbitCameraInputKeyboard');
663
  FreeCameraInputKeyboard.attributes.add('acceleration', { type: 'number', default: 1.0, title: 'Accel (unused)' });
664
 
665
  FreeCameraInputKeyboard.prototype.initialize = function () {
666
- this.freeCam = this.entity.script.orbitCamera;
667
- this.kb = this.app.keyboard || null;
668
  };
669
 
670
  FreeCameraInputKeyboard.prototype.update = function (dt) {
671
- if (!this.freeCam || !this.kb) return;
672
-
673
- var fwd = (this.kb.isPressed(pc.KEY_UP) || this.kb.isPressed(pc.KEY_Z)) ? 1 :
674
- (this.kb.isPressed(pc.KEY_DOWN) || this.kb.isPressed(pc.KEY_S)) ? -1 : 0;
675
- var strf = (this.kb.isPressed(pc.KEY_RIGHT) || this.kb.isPressed(pc.KEY_D)) ? 1 :
676
- (this.kb.isPressed(pc.KEY_LEFT) || this.kb.isPressed(pc.KEY_Q)) ? -1 : 0;
677
 
678
- if (fwd !== 0 || strf !== 0) {
679
- var cam = this.entity;
680
- var from = cam.getPosition().clone();
 
681
 
682
- var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
683
- var right = cam.right.clone(); if (right.lengthSq() > 1e-8) right.normalize();
 
684
 
685
- var delta = new pc.Vec3()
686
- .add(forward.mulScalar(fwd * this.freeCam.moveSpeed * dt))
687
- .add(right .mulScalar(strf * this.freeCam.strafeSpeed * dt));
688
 
689
- var next = this.freeCam._moveWithCollisions(from, delta);
690
- this.freeCam._clampPosition(next);
691
- cam.setPosition(next);
692
- }
693
 
694
- var shift = this.kb.isPressed(pc.KEY_SHIFT);
695
- if (shift) {
696
- var yawDir = (this.kb.isPressed(pc.KEY_LEFT) ? 1 : 0) - (this.kb.isPressed(pc.KEY_RIGHT) ? 1 : 0);
697
- var pitchDir = (this.kb.isPressed(pc.KEY_UP) ? 1 : 0) - (this.kb.isPressed(pc.KEY_DOWN) ? 1 : 0);
698
- var yawSpeed = 120, pitchSpeed = 90;
699
- if (yawDir !== 0) this.freeCam.yaw = this.freeCam.yaw + yawDir * yawSpeed * dt;
700
- if (pitchDir !== 0) this.freeCam.pitch = this.freeCam.pitch + pitchDir * pitchSpeed * dt;
701
- }
 
 
 
 
 
702
  };
 
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' });
 
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' });
 
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 =====================
 
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 =====================
 
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 =====================
 
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
  };