1import { Box3, Vector3 } from "three";
2
3/** Fit the current assembly, with independent framing dynamics so a leaf pivot does not drag it off-screen. */
4export function createDeviceFraming() {
5 const center = new Vector3();
6 const target = new Vector3();
7 const velocity = new Vector3();
8 const bounds = new Box3();
9 let distance = 1;
10 let targetDistance = 1;
11 let distanceVelocity = 0;
12 let tanX = 1;
13 let tanY = 1;
14 let at = 0;
15 let initialized = false;
16 let moving = false;
17 let held = false;
18 const advance = (now: number, immediate = false) => {
19 if (held) return false;
20 const dt = Math.max(0, (now - at) / 1000);
21 at = now;
22 if (!moving) return false;
23 const omega = 14;
24 const decay = Math.exp(-omega * dt);
25 const error = center.clone().sub(target);
26 const coefficient = velocity.clone().addScaledVector(error, omega);
27 center.copy(error).addScaledVector(coefficient, dt).multiplyScalar(decay).add(target);
28 velocity.addScaledVector(coefficient, -omega * dt).multiplyScalar(decay);
29 const distanceError = distance - targetDistance;
30 const distanceCoefficient = distanceVelocity + omega * distanceError;
31 distance = targetDistance + (distanceError + distanceCoefficient * dt) * decay;
32 distanceVelocity = (distanceVelocity - omega * distanceCoefficient * dt) * decay;
33 moving =
34 center.distanceTo(target) > 0.0005 ||
35 velocity.length() > 0.005 ||
36 Math.abs(distance - targetDistance) > 0.0005 ||
37 Math.abs(distanceVelocity) > 0.005;
38 if (immediate || !moving) {
39 center.copy(target);
40 distance = targetDistance;
41 velocity.set(0, 0, 0);
42 distanceVelocity = 0;
43 moving = false;
44 }
45 return true;
46 };
47 return {
48 center,
49 setBounds(next: Box3, vertical: number, aspect: number, now: number, immediate = false) {
50 if (next.isEmpty()) return;
51 advance(now);
52 bounds.copy(next);
53 tanY = Math.tan(vertical);
54 tanX = tanY * aspect;
55 bounds.getCenter(target);
56 target.z = 0;
57 const size = bounds.getSize(new Vector3());
58 targetDistance = Math.max(1, Math.max(size.x / tanX, size.y / tanY) * 0.565 + bounds.max.z);
59 moving = true;
60 advance(now, immediate || !initialized);
61 initialized = true;
62 },
63 advance,
64 distance() {
65 // Spring lag must never clip the fitted assembly.
66 const clearance = Math.max(
67 Math.abs(bounds.min.x - center.x) / tanX,
68 Math.abs(bounds.max.x - center.x) / tanX,
69 Math.abs(bounds.min.y - center.y) / tanY,
70 Math.abs(bounds.max.y - center.y) / tanY,
71 );
72 return Math.max(distance, clearance * 1.02 + bounds.max.z, 1);
73 },
74 hold(active: boolean, now: number) {
75 held = active;
76 at = now;
77 },
78 needsFrame() {
79 return moving && !held;
80 },
81 };
82}