@flighthq/camera-controls 0.2.1-next.451.d9a4a4e → 0.2.1-next.530.a223ca9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/flyCameraController.d.ts +4 -0
- package/dist/flyCameraController.d.ts.map +1 -1
- package/dist/flyCameraController.js +48 -4
- package/dist/flyCameraController.js.map +1 -1
- package/dist/framing.d.ts +5 -0
- package/dist/framing.d.ts.map +1 -0
- package/dist/framing.js +46 -0
- package/dist/framing.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/orbitCameraController.d.ts +5 -0
- package/dist/orbitCameraController.d.ts.map +1 -1
- package/dist/orbitCameraController.js +72 -6
- package/dist/orbitCameraController.js.map +1 -1
- package/package.json +7 -6
- package/src/flyCameraController.test.ts +63 -0
- package/src/framing.test.ts +65 -0
- package/src/orbitCameraController.test.ts +82 -1
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Camera3D, FlyCameraController, FlyCameraControllerOptions } from '@flighthq/types';
|
|
2
|
+
export declare function cloneFlyCameraController(source: Readonly<FlyCameraController>): FlyCameraController;
|
|
3
|
+
export declare function copyFlyCameraController(out: FlyCameraController, source: Readonly<FlyCameraController>): void;
|
|
2
4
|
export declare function createFlyCameraController(options?: Readonly<FlyCameraControllerOptions>): FlyCameraController;
|
|
3
5
|
export declare function lookFlyCameraController(controller: FlyCameraController, deltaYaw: number, deltaPitch: number): void;
|
|
4
6
|
export declare function moveFlyCameraController(controller: FlyCameraController, forward: number, right: number, up: number): void;
|
|
7
|
+
export declare function resetFlyCameraController(controller: FlyCameraController, options?: Readonly<FlyCameraControllerOptions>): void;
|
|
8
|
+
export declare function snapFlyCameraController(controller: FlyCameraController): void;
|
|
5
9
|
export declare function updateFlyCameraController(controller: FlyCameraController, camera: Camera3D, deltaTime: number): void;
|
|
6
10
|
//# sourceMappingURL=flyCameraController.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flyCameraController.d.ts","sourceRoot":"","sources":["../src/flyCameraController.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"flyCameraController.d.ts","sourceRoot":"","sources":["../src/flyCameraController.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAGjG,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,QAAQ,CAAC,mBAAmB,CAAC,GAAG,mBAAmB,CAInG;AAID,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,mBAAmB,EAAE,MAAM,EAAE,QAAQ,CAAC,mBAAmB,CAAC,GAAG,IAAI,CAW7G;AAKD,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,0BAA0B,CAAC,GAAG,mBAAmB,CAc7G;AAKD,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAGnH;AAMD,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,mBAAmB,EAC/B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,GACT,IAAI,CAON;AAID,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,mBAAmB,EAC/B,OAAO,CAAC,EAAE,QAAQ,CAAC,0BAA0B,CAAC,GAC7C,IAAI,CAcN;AAGD,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,mBAAmB,GAAG,IAAI,CAG7E;AAMD,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,mBAAmB,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAqBpH"}
|
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import { setCamera3DViewMatrix4FromLookAt } from '@flighthq/camera';
|
|
2
|
+
import { createEntity } from '@flighthq/entity';
|
|
2
3
|
import { createVector3 } from '@flighthq/geometry';
|
|
3
|
-
import { clamp, damp } from '@flighthq/math';
|
|
4
|
+
import { clamp, damp, deltaAngle } from '@flighthq/math';
|
|
5
|
+
// Allocates an independent Entity containing the same controller value state.
|
|
6
|
+
export function cloneFlyCameraController(source) {
|
|
7
|
+
const clone = createFlyCameraController();
|
|
8
|
+
copyFlyCameraController(clone, source);
|
|
9
|
+
return clone;
|
|
10
|
+
}
|
|
11
|
+
// Copies all controller state without sharing its mutable position. Entity runtime/binding state is
|
|
12
|
+
// deliberately left on `out`; this is a value-state operation, not an ownership transfer.
|
|
13
|
+
export function copyFlyCameraController(out, source) {
|
|
14
|
+
out.goalPitch = source.goalPitch;
|
|
15
|
+
out.goalYaw = source.goalYaw;
|
|
16
|
+
out.maxPitch = source.maxPitch;
|
|
17
|
+
out.minPitch = source.minPitch;
|
|
18
|
+
out.pitch = source.pitch;
|
|
19
|
+
out.position.x = source.position.x;
|
|
20
|
+
out.position.y = source.position.y;
|
|
21
|
+
out.position.z = source.position.z;
|
|
22
|
+
out.smoothTime = source.smoothTime;
|
|
23
|
+
out.yaw = source.yaw;
|
|
24
|
+
}
|
|
4
25
|
// Allocates a fly / first-person controller. `position` defaults to the origin, `yaw`/`pitch` to 0
|
|
5
26
|
// (looking down -Z), `pitch` limits to just inside ±90° (no gimbal flip), and `smoothTime` to 0 (the
|
|
6
27
|
// look angles snap to their goal each update). Current and goal angles start equal.
|
|
@@ -8,7 +29,7 @@ export function createFlyCameraController(options) {
|
|
|
8
29
|
const yaw = options?.yaw ?? 0;
|
|
9
30
|
const pitch = options?.pitch ?? 0;
|
|
10
31
|
const position = options?.position;
|
|
11
|
-
return {
|
|
32
|
+
return createEntity({
|
|
12
33
|
goalPitch: pitch,
|
|
13
34
|
goalYaw: yaw,
|
|
14
35
|
maxPitch: options?.maxPitch ?? DEFAULT_MAX_PITCH,
|
|
@@ -17,7 +38,7 @@ export function createFlyCameraController(options) {
|
|
|
17
38
|
position: createVector3(position?.x ?? 0, position?.y ?? 0, position?.z ?? 0),
|
|
18
39
|
smoothTime: options?.smoothTime ?? 0,
|
|
19
40
|
yaw,
|
|
20
|
-
};
|
|
41
|
+
});
|
|
21
42
|
}
|
|
22
43
|
// Moves the goal look angles by the given radian deltas: `deltaYaw` turns horizontally (unbounded),
|
|
23
44
|
// `deltaPitch` looks up/down (clamped to [minPitch, maxPitch]). The app maps a mouse-look delta to
|
|
@@ -38,6 +59,28 @@ export function moveFlyCameraController(controller, forward, right, up) {
|
|
|
38
59
|
position.y += up;
|
|
39
60
|
position.z += -cosYaw * forward + sinYaw * right;
|
|
40
61
|
}
|
|
62
|
+
// Restores the controller to constructor defaults or the supplied seed. Current and goal angles are
|
|
63
|
+
// synchronized, which makes this suitable for scene changes without a one-frame eased transition.
|
|
64
|
+
export function resetFlyCameraController(controller, options) {
|
|
65
|
+
const yaw = options?.yaw ?? 0;
|
|
66
|
+
const pitch = options?.pitch ?? 0;
|
|
67
|
+
const position = options?.position;
|
|
68
|
+
controller.goalPitch = pitch;
|
|
69
|
+
controller.goalYaw = yaw;
|
|
70
|
+
controller.maxPitch = options?.maxPitch ?? DEFAULT_MAX_PITCH;
|
|
71
|
+
controller.minPitch = options?.minPitch ?? DEFAULT_MIN_PITCH;
|
|
72
|
+
controller.pitch = pitch;
|
|
73
|
+
controller.position.x = position?.x ?? 0;
|
|
74
|
+
controller.position.y = position?.y ?? 0;
|
|
75
|
+
controller.position.z = position?.z ?? 0;
|
|
76
|
+
controller.smoothTime = options?.smoothTime ?? 0;
|
|
77
|
+
controller.yaw = yaw;
|
|
78
|
+
}
|
|
79
|
+
// Snaps current look state to the clamped goal without writing a camera.
|
|
80
|
+
export function snapFlyCameraController(controller) {
|
|
81
|
+
controller.pitch = clamp(controller.goalPitch, controller.minPitch, controller.maxPitch);
|
|
82
|
+
controller.yaw = controller.goalYaw;
|
|
83
|
+
}
|
|
41
84
|
// Advances the controller one step and writes the resulting view into `camera` (in place). Eases the
|
|
42
85
|
// current yaw/pitch toward their clamped goals with `@flighthq/math` `damp` (frame-rate independent;
|
|
43
86
|
// `smoothTime` <= 0 or `deltaTime` <= 0 snaps), builds the forward direction from the angles, and
|
|
@@ -46,7 +89,8 @@ export function updateFlyCameraController(controller, camera, deltaTime) {
|
|
|
46
89
|
const goalPitch = clamp(controller.goalPitch, controller.minPitch, controller.maxPitch);
|
|
47
90
|
if (controller.smoothTime > 0 && deltaTime > 0) {
|
|
48
91
|
const lambda = 1 / controller.smoothTime;
|
|
49
|
-
controller.yaw
|
|
92
|
+
const nearestGoalYaw = controller.yaw + deltaAngle(controller.yaw, controller.goalYaw);
|
|
93
|
+
controller.yaw = damp(controller.yaw, nearestGoalYaw, lambda, deltaTime);
|
|
50
94
|
controller.pitch = damp(controller.pitch, goalPitch, lambda, deltaTime);
|
|
51
95
|
}
|
|
52
96
|
else {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flyCameraController.js","sourceRoot":"","sources":["../src/flyCameraController.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gCAAgC,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"flyCameraController.js","sourceRoot":"","sources":["../src/flyCameraController.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BoundingSphereLike, OrbitCameraController, OrthographicProjection, PerspectiveProjection, Projection } from '@flighthq/types';
|
|
2
|
+
export declare function frameOrbitCameraControllerToSphere(controller: OrbitCameraController, projection: Projection, sphere: Readonly<BoundingSphereLike>, aspect: number, padding?: number): boolean;
|
|
3
|
+
export declare function getPerspectiveFrameDistanceToSphere(projection: Readonly<PerspectiveProjection>, radius: number, aspect: number, padding?: number): number;
|
|
4
|
+
export declare function setOrthographicProjectionFrameToSphere(projection: OrthographicProjection, radius: number, aspect: number, padding?: number): void;
|
|
5
|
+
//# sourceMappingURL=framing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framing.d.ts","sourceRoot":"","sources":["../src/framing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,kBAAkB,EAClB,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,UAAU,EACX,MAAM,iBAAiB,CAAC;AAOzB,wBAAgB,kCAAkC,CAChD,UAAU,EAAE,qBAAqB,EACjC,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EACpC,MAAM,EAAE,MAAM,EACd,OAAO,SAAI,GACV,OAAO,CAeT;AAKD,wBAAgB,mCAAmC,CACjD,UAAU,EAAE,QAAQ,CAAC,qBAAqB,CAAC,EAC3C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,OAAO,SAAI,GACV,MAAM,CAKR;AAID,wBAAgB,sCAAsC,CACpD,UAAU,EAAE,sBAAsB,EAClC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,OAAO,SAAI,GACV,IAAI,CASN"}
|
package/dist/framing.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { clamp } from '@flighthq/math';
|
|
2
|
+
// Frames a non-empty sphere with an orbit controller and active viewport aspect. The target moves to
|
|
3
|
+
// the sphere center immediately. Perspective framing changes only `goalDistance`; orthographic
|
|
4
|
+
// framing changes only projection half-extents because orbit distance does not affect its visible
|
|
5
|
+
// size. Call `snapOrbitCameraController` separately when an immediate perspective dolly is desired.
|
|
6
|
+
// Near/far clip planes are never changed. Returns false without mutation for invalid inputs.
|
|
7
|
+
export function frameOrbitCameraControllerToSphere(controller, projection, sphere, aspect, padding = 1) {
|
|
8
|
+
if (!(sphere.radius >= 0) || !(aspect > 0) || !(padding > 0))
|
|
9
|
+
return false;
|
|
10
|
+
if (projection.kind === 'perspective') {
|
|
11
|
+
const distance = getPerspectiveFrameDistanceToSphere(projection, sphere.radius, aspect, padding);
|
|
12
|
+
if (!Number.isFinite(distance))
|
|
13
|
+
return false;
|
|
14
|
+
controller.goalDistance = clamp(distance, controller.minDistance, controller.maxDistance);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
setOrthographicProjectionFrameToSphere(projection, sphere.radius, aspect, padding);
|
|
18
|
+
}
|
|
19
|
+
controller.target.x = sphere.center.x;
|
|
20
|
+
controller.target.y = sphere.center.y;
|
|
21
|
+
controller.target.z = sphere.center.z;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
// Computes the eye-to-center distance that contains a sphere in both dimensions of a perspective
|
|
25
|
+
// viewport. `padding` is a radius multiplier (1 is tangent). Clip-plane policy remains with the
|
|
26
|
+
// camera owner and is intentionally not hidden here.
|
|
27
|
+
export function getPerspectiveFrameDistanceToSphere(projection, radius, aspect, padding = 1) {
|
|
28
|
+
const paddedRadius = radius * padding;
|
|
29
|
+
const verticalHalfFov = projection.fovY * 0.5;
|
|
30
|
+
const horizontalHalfFov = Math.atan(Math.tan(verticalHalfFov) * aspect);
|
|
31
|
+
return paddedRadius / Math.sin(Math.min(verticalHalfFov, horizontalHalfFov));
|
|
32
|
+
}
|
|
33
|
+
// Sets orthographic half-extents that contain a sphere without distortion at `aspect`. The tighter
|
|
34
|
+
// viewport dimension touches the padded sphere; the other expands to preserve aspect.
|
|
35
|
+
export function setOrthographicProjectionFrameToSphere(projection, radius, aspect, padding = 1) {
|
|
36
|
+
const paddedRadius = radius * padding;
|
|
37
|
+
if (aspect >= 1) {
|
|
38
|
+
projection.halfHeight = paddedRadius;
|
|
39
|
+
projection.halfWidth = paddedRadius * aspect;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
projection.halfHeight = paddedRadius / aspect;
|
|
43
|
+
projection.halfWidth = paddedRadius;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=framing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framing.js","sourceRoot":"","sources":["../src/framing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AASvC,qGAAqG;AACrG,+FAA+F;AAC/F,kGAAkG;AAClG,oGAAoG;AACpG,6FAA6F;AAC7F,MAAM,UAAU,kCAAkC,CAChD,UAAiC,EACjC,UAAsB,EACtB,MAAoC,EACpC,MAAc,EACd,OAAO,GAAG,CAAC;IAEX,IAAI,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAE3E,IAAI,UAAU,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;QACtC,MAAM,QAAQ,GAAG,mCAAmC,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QACjG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,OAAO,KAAK,CAAC;QAC7C,UAAU,CAAC,YAAY,GAAG,KAAK,CAAC,QAAQ,EAAE,UAAU,CAAC,WAAW,EAAE,UAAU,CAAC,WAAW,CAAC,CAAC;IAC5F,CAAC;SAAM,CAAC;QACN,sCAAsC,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IACrF,CAAC;IAED,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IACtC,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IACtC,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IACtC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,iGAAiG;AACjG,gGAAgG;AAChG,qDAAqD;AACrD,MAAM,UAAU,mCAAmC,CACjD,UAA2C,EAC3C,MAAc,EACd,MAAc,EACd,OAAO,GAAG,CAAC;IAEX,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,CAAC;IACtC,MAAM,eAAe,GAAG,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC;IAC9C,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,GAAG,MAAM,CAAC,CAAC;IACxE,OAAO,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,iBAAiB,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED,mGAAmG;AACnG,sFAAsF;AACtF,MAAM,UAAU,sCAAsC,CACpD,UAAkC,EAClC,MAAc,EACd,MAAc,EACd,OAAO,GAAG,CAAC;IAEX,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,CAAC;IACtC,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;QAChB,UAAU,CAAC,UAAU,GAAG,YAAY,CAAC;QACrC,UAAU,CAAC,SAAS,GAAG,YAAY,GAAG,MAAM,CAAC;IAC/C,CAAC;SAAM,CAAC;QACN,UAAU,CAAC,UAAU,GAAG,YAAY,GAAG,MAAM,CAAC;QAC9C,UAAU,CAAC,SAAS,GAAG,YAAY,CAAC;IACtC,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,UAAU,CAAC;AACzB,cAAc,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,yBAAyB,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,UAAU,CAAC;AACzB,cAAc,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,yBAAyB,CAAC"}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { Camera3D, OrbitCameraController, OrbitCameraControllerOptions } from '@flighthq/types';
|
|
2
|
+
export declare function cloneOrbitCameraController(source: Readonly<OrbitCameraController>): OrbitCameraController;
|
|
3
|
+
export declare function copyOrbitCameraController(out: OrbitCameraController, source: Readonly<OrbitCameraController>): void;
|
|
2
4
|
export declare function createOrbitCameraController(options?: Readonly<OrbitCameraControllerOptions>): OrbitCameraController;
|
|
3
5
|
export declare function dollyCameraController(controller: OrbitCameraController, deltaDistance: number): void;
|
|
4
6
|
export declare function orbitCameraController(controller: OrbitCameraController, deltaAzimuth: number, deltaPolar: number): void;
|
|
5
7
|
export declare function panCameraController(controller: OrbitCameraController, deltaRight: number, deltaUp: number): void;
|
|
8
|
+
export declare function panOrbitCameraControllerInViewPlane(controller: OrbitCameraController, deltaRight: number, deltaUp: number): void;
|
|
9
|
+
export declare function resetOrbitCameraController(controller: OrbitCameraController, options?: Readonly<OrbitCameraControllerOptions>): void;
|
|
10
|
+
export declare function snapOrbitCameraController(controller: OrbitCameraController): void;
|
|
6
11
|
export declare function updateOrbitCameraController(controller: OrbitCameraController, camera: Camera3D, deltaTime: number): void;
|
|
7
12
|
//# sourceMappingURL=orbitCameraController.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orbitCameraController.d.ts","sourceRoot":"","sources":["../src/orbitCameraController.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"orbitCameraController.d.ts","sourceRoot":"","sources":["../src/orbitCameraController.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAGrG,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GAAG,qBAAqB,CAIzG;AAID,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,qBAAqB,EAAE,MAAM,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GAAG,IAAI,CAenH;AAMD,wBAAgB,2BAA2B,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,4BAA4B,CAAC,GAAG,qBAAqB,CAmBnH;AAID,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,CAMpG;AAKD,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,qBAAqB,EACjC,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,GACjB,IAAI,CAGN;AAMD,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAOhH;AAID,wBAAgB,mCAAmC,CACjD,UAAU,EAAE,qBAAqB,EACjC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACd,IAAI,CASN;AAID,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,qBAAqB,EACjC,OAAO,CAAC,EAAE,QAAQ,CAAC,4BAA4B,CAAC,GAC/C,IAAI,CAmBN;AAGD,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,qBAAqB,GAAG,IAAI,CAIjF;AAOD,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,qBAAqB,EACjC,MAAM,EAAE,QAAQ,EAChB,SAAS,EAAE,MAAM,GAChB,IAAI,CAwBN"}
|
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import { setCamera3DViewMatrix4FromLookAt } from '@flighthq/camera';
|
|
2
|
+
import { createEntity } from '@flighthq/entity';
|
|
2
3
|
import { createVector3 } from '@flighthq/geometry';
|
|
3
|
-
import { clamp, damp } from '@flighthq/math';
|
|
4
|
+
import { clamp, damp, deltaAngle } from '@flighthq/math';
|
|
5
|
+
// Allocates an independent Entity containing the same controller value state.
|
|
6
|
+
export function cloneOrbitCameraController(source) {
|
|
7
|
+
const clone = createOrbitCameraController();
|
|
8
|
+
copyOrbitCameraController(clone, source);
|
|
9
|
+
return clone;
|
|
10
|
+
}
|
|
11
|
+
// Copies all controller state without sharing its mutable target. Entity runtime/binding state is
|
|
12
|
+
// deliberately left on `out`; this is a value-state operation, not an ownership transfer.
|
|
13
|
+
export function copyOrbitCameraController(out, source) {
|
|
14
|
+
out.azimuth = source.azimuth;
|
|
15
|
+
out.distance = source.distance;
|
|
16
|
+
out.goalAzimuth = source.goalAzimuth;
|
|
17
|
+
out.goalDistance = source.goalDistance;
|
|
18
|
+
out.goalPolar = source.goalPolar;
|
|
19
|
+
out.maxDistance = source.maxDistance;
|
|
20
|
+
out.maxPolar = source.maxPolar;
|
|
21
|
+
out.minDistance = source.minDistance;
|
|
22
|
+
out.minPolar = source.minPolar;
|
|
23
|
+
out.polar = source.polar;
|
|
24
|
+
out.smoothTime = source.smoothTime;
|
|
25
|
+
out.target.x = source.target.x;
|
|
26
|
+
out.target.y = source.target.y;
|
|
27
|
+
out.target.z = source.target.z;
|
|
28
|
+
}
|
|
4
29
|
// Allocates an orbit controller. `target` defaults to the origin, `distance` to 10, angles to 0;
|
|
5
30
|
// `polar` limits default to just inside ±90° so the look-at up vector never degenerates, `distance`
|
|
6
31
|
// to (0.01, +∞), and `smoothTime` to 0 (no damping — the camera snaps to the goal each update). The
|
|
@@ -10,7 +35,7 @@ export function createOrbitCameraController(options) {
|
|
|
10
35
|
const polar = options?.polar ?? 0;
|
|
11
36
|
const distance = options?.distance ?? 10;
|
|
12
37
|
const target = options?.target;
|
|
13
|
-
return {
|
|
38
|
+
return createEntity({
|
|
14
39
|
azimuth,
|
|
15
40
|
distance,
|
|
16
41
|
goalAzimuth: azimuth,
|
|
@@ -23,7 +48,7 @@ export function createOrbitCameraController(options) {
|
|
|
23
48
|
polar,
|
|
24
49
|
smoothTime: options?.smoothTime ?? 0,
|
|
25
50
|
target: createVector3(target?.x ?? 0, target?.y ?? 0, target?.z ?? 0),
|
|
26
|
-
};
|
|
51
|
+
});
|
|
27
52
|
}
|
|
28
53
|
// Moves the goal distance (dolly / zoom) by `deltaDistance`, clamped to [minDistance, maxDistance].
|
|
29
54
|
// Negative moves the eye toward the target.
|
|
@@ -37,8 +62,8 @@ export function orbitCameraController(controller, deltaAzimuth, deltaPolar) {
|
|
|
37
62
|
controller.goalAzimuth += deltaAzimuth;
|
|
38
63
|
controller.goalPolar = clamp(controller.goalPolar + deltaPolar, controller.minPolar, controller.maxPolar);
|
|
39
64
|
}
|
|
40
|
-
// Slides the orbit `target` in
|
|
41
|
-
// (at the current goal azimuth), `deltaUp` along world
|
|
65
|
+
// Slides the orbit `target` in a world-up plane: `deltaRight` along the camera's horizontal right axis
|
|
66
|
+
// (at the current goal azimuth), `deltaUp` along world Y. Because the eye is derived from the target
|
|
42
67
|
// each update, panning the target pans the whole view. Reads the target into locals before writing so
|
|
43
68
|
// aliasing is safe.
|
|
44
69
|
export function panCameraController(controller, deltaRight, deltaUp) {
|
|
@@ -49,6 +74,46 @@ export function panCameraController(controller, deltaRight, deltaUp) {
|
|
|
49
74
|
target.y += deltaUp;
|
|
50
75
|
target.z += -sinAzimuth * deltaRight;
|
|
51
76
|
}
|
|
77
|
+
// Slides the orbit target in the actual camera view plane. Unlike `panCameraController`, `deltaUp`
|
|
78
|
+
// follows screen-up at the goal polar angle, so it can change all three target coordinates.
|
|
79
|
+
export function panOrbitCameraControllerInViewPlane(controller, deltaRight, deltaUp) {
|
|
80
|
+
const cosAzimuth = Math.cos(controller.goalAzimuth);
|
|
81
|
+
const sinAzimuth = Math.sin(controller.goalAzimuth);
|
|
82
|
+
const cosPolar = Math.cos(controller.goalPolar);
|
|
83
|
+
const sinPolar = Math.sin(controller.goalPolar);
|
|
84
|
+
const target = controller.target;
|
|
85
|
+
target.x += cosAzimuth * deltaRight - sinAzimuth * sinPolar * deltaUp;
|
|
86
|
+
target.y += cosPolar * deltaUp;
|
|
87
|
+
target.z += -sinAzimuth * deltaRight - cosAzimuth * sinPolar * deltaUp;
|
|
88
|
+
}
|
|
89
|
+
// Restores the controller to constructor defaults or the supplied seed. Current and goal spherical
|
|
90
|
+
// coordinates are synchronized, which makes this suitable for scene changes without an eased tail.
|
|
91
|
+
export function resetOrbitCameraController(controller, options) {
|
|
92
|
+
const azimuth = options?.azimuth ?? 0;
|
|
93
|
+
const polar = options?.polar ?? 0;
|
|
94
|
+
const distance = options?.distance ?? 10;
|
|
95
|
+
const target = options?.target;
|
|
96
|
+
controller.azimuth = azimuth;
|
|
97
|
+
controller.distance = distance;
|
|
98
|
+
controller.goalAzimuth = azimuth;
|
|
99
|
+
controller.goalDistance = distance;
|
|
100
|
+
controller.goalPolar = polar;
|
|
101
|
+
controller.maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
102
|
+
controller.maxPolar = options?.maxPolar ?? DEFAULT_MAX_POLAR;
|
|
103
|
+
controller.minDistance = options?.minDistance ?? DEFAULT_MIN_DISTANCE;
|
|
104
|
+
controller.minPolar = options?.minPolar ?? DEFAULT_MIN_POLAR;
|
|
105
|
+
controller.polar = polar;
|
|
106
|
+
controller.smoothTime = options?.smoothTime ?? 0;
|
|
107
|
+
controller.target.x = target?.x ?? 0;
|
|
108
|
+
controller.target.y = target?.y ?? 0;
|
|
109
|
+
controller.target.z = target?.z ?? 0;
|
|
110
|
+
}
|
|
111
|
+
// Snaps current spherical state to the clamped goal without writing a camera.
|
|
112
|
+
export function snapOrbitCameraController(controller) {
|
|
113
|
+
controller.azimuth = controller.goalAzimuth;
|
|
114
|
+
controller.distance = clamp(controller.goalDistance, controller.minDistance, controller.maxDistance);
|
|
115
|
+
controller.polar = clamp(controller.goalPolar, controller.minPolar, controller.maxPolar);
|
|
116
|
+
}
|
|
52
117
|
// Advances the controller one step and writes the resulting view into `camera` (in place). Eases the
|
|
53
118
|
// current azimuth/polar/distance toward their clamped goals with `@flighthq/math` `damp`
|
|
54
119
|
// (frame-rate independent; `smoothTime` <= 0 or `deltaTime` <= 0 snaps), places the eye on the sphere
|
|
@@ -59,7 +124,8 @@ export function updateOrbitCameraController(controller, camera, deltaTime) {
|
|
|
59
124
|
const goalDistance = clamp(controller.goalDistance, controller.minDistance, controller.maxDistance);
|
|
60
125
|
if (controller.smoothTime > 0 && deltaTime > 0) {
|
|
61
126
|
const lambda = 1 / controller.smoothTime;
|
|
62
|
-
controller.azimuth
|
|
127
|
+
const nearestGoalAzimuth = controller.azimuth + deltaAngle(controller.azimuth, controller.goalAzimuth);
|
|
128
|
+
controller.azimuth = damp(controller.azimuth, nearestGoalAzimuth, lambda, deltaTime);
|
|
63
129
|
controller.polar = damp(controller.polar, goalPolar, lambda, deltaTime);
|
|
64
130
|
controller.distance = damp(controller.distance, goalDistance, lambda, deltaTime);
|
|
65
131
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orbitCameraController.js","sourceRoot":"","sources":["../src/orbitCameraController.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gCAAgC,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"orbitCameraController.js","sourceRoot":"","sources":["../src/orbitCameraController.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/camera-controls",
|
|
3
|
-
"version": "0.2.1-next.
|
|
3
|
+
"version": "0.2.1-next.530.a223ca9",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/flighthq/flight.git",
|
|
@@ -32,14 +32,15 @@
|
|
|
32
32
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@flighthq/camera": "0.2.1-next.
|
|
36
|
-
"@flighthq/
|
|
37
|
-
"@flighthq/
|
|
38
|
-
"@flighthq/
|
|
35
|
+
"@flighthq/camera": "0.2.1-next.530.a223ca9",
|
|
36
|
+
"@flighthq/entity": "0.2.1-next.530.a223ca9",
|
|
37
|
+
"@flighthq/geometry": "0.2.1-next.530.a223ca9",
|
|
38
|
+
"@flighthq/math": "0.2.1-next.530.a223ca9",
|
|
39
|
+
"@flighthq/types": "0.2.1-next.530.a223ca9"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"typescript": "^5.3.0"
|
|
42
43
|
},
|
|
43
|
-
"description": "
|
|
44
|
+
"description": "Input-agnostic 2D follow and 3D orbit / fly camera-control primitives",
|
|
44
45
|
"sideEffects": false
|
|
45
46
|
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { createCamera3D, createPerspectiveProjection, setCamera3DViewMatrix4FromLookAt } from '@flighthq/camera';
|
|
2
2
|
import { createVector3 } from '@flighthq/geometry';
|
|
3
|
+
import { EntityRuntimeKey } from '@flighthq/types';
|
|
3
4
|
import { describe, expect, it } from 'vitest';
|
|
4
5
|
|
|
5
6
|
import {
|
|
7
|
+
cloneFlyCameraController,
|
|
8
|
+
copyFlyCameraController,
|
|
6
9
|
createFlyCameraController,
|
|
7
10
|
lookFlyCameraController,
|
|
8
11
|
moveFlyCameraController,
|
|
12
|
+
resetFlyCameraController,
|
|
13
|
+
snapFlyCameraController,
|
|
9
14
|
updateFlyCameraController,
|
|
10
15
|
} from './flyCameraController';
|
|
11
16
|
|
|
@@ -13,6 +18,32 @@ function testCamera() {
|
|
|
13
18
|
return createCamera3D({ far: 100, near: 0.1, projection: createPerspectiveProjection({ aspect: 1, fovY: 1 }) });
|
|
14
19
|
}
|
|
15
20
|
|
|
21
|
+
describe('cloneFlyCameraController', () => {
|
|
22
|
+
it('creates an independent Entity with the same value state', () => {
|
|
23
|
+
const source = createFlyCameraController({ pitch: 0.2, position: createVector3(1, 2, 3), yaw: 0.4 });
|
|
24
|
+
const clone = cloneFlyCameraController(source);
|
|
25
|
+
expect(clone).toMatchObject({ pitch: 0.2, yaw: 0.4 });
|
|
26
|
+
expect(EntityRuntimeKey in clone).toBe(true);
|
|
27
|
+
expect(clone.position).not.toBe(source.position);
|
|
28
|
+
clone.position.x = 10;
|
|
29
|
+
expect(source.position.x).toBe(1);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('copyFlyCameraController', () => {
|
|
34
|
+
it('copies value state without sharing position or replacing runtime state', () => {
|
|
35
|
+
const source = createFlyCameraController({ pitch: 0.2, position: createVector3(1, 2, 3), yaw: 0.4 });
|
|
36
|
+
const out = createFlyCameraController();
|
|
37
|
+
const runtime = { binding: null };
|
|
38
|
+
out[EntityRuntimeKey] = runtime;
|
|
39
|
+
copyFlyCameraController(out, source);
|
|
40
|
+
|
|
41
|
+
expect(out).toMatchObject({ pitch: 0.2, yaw: 0.4 });
|
|
42
|
+
expect(out[EntityRuntimeKey]).toBe(runtime);
|
|
43
|
+
expect(out.position).not.toBe(source.position);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
16
47
|
describe('createFlyCameraController', () => {
|
|
17
48
|
it('applies documented defaults with current equal to goal', () => {
|
|
18
49
|
const c = createFlyCameraController();
|
|
@@ -28,6 +59,10 @@ describe('createFlyCameraController', () => {
|
|
|
28
59
|
expect(c.yaw).toBe(0.5);
|
|
29
60
|
expect(c.position.z).toBe(3);
|
|
30
61
|
});
|
|
62
|
+
|
|
63
|
+
it('produces an Entity', () => {
|
|
64
|
+
expect(EntityRuntimeKey in createFlyCameraController()).toBe(true);
|
|
65
|
+
});
|
|
31
66
|
});
|
|
32
67
|
|
|
33
68
|
describe('lookFlyCameraController', () => {
|
|
@@ -49,6 +84,26 @@ describe('moveFlyCameraController', () => {
|
|
|
49
84
|
});
|
|
50
85
|
});
|
|
51
86
|
|
|
87
|
+
describe('resetFlyCameraController', () => {
|
|
88
|
+
it('resets all coupled state from a seed', () => {
|
|
89
|
+
const c = createFlyCameraController({ position: createVector3(1, 2, 3), yaw: 1 });
|
|
90
|
+
resetFlyCameraController(c, { maxPitch: 0.5, pitch: 0.25, position: createVector3(4, 5, 6), yaw: -1 });
|
|
91
|
+
expect(c).toMatchObject({ goalPitch: 0.25, goalYaw: -1, pitch: 0.25, yaw: -1 });
|
|
92
|
+
expect(c.position).toMatchObject({ x: 4, y: 5, z: 6 });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('snapFlyCameraController', () => {
|
|
97
|
+
it('snaps current angles to clamped goals', () => {
|
|
98
|
+
const c = createFlyCameraController({ maxPitch: 0.5 });
|
|
99
|
+
c.goalPitch = 2;
|
|
100
|
+
c.goalYaw = 3;
|
|
101
|
+
snapFlyCameraController(c);
|
|
102
|
+
expect(c.pitch).toBe(0.5);
|
|
103
|
+
expect(c.yaw).toBe(3);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
52
107
|
describe('updateFlyCameraController', () => {
|
|
53
108
|
it('snaps to the goal when smoothTime is 0 and writes the look-at view', () => {
|
|
54
109
|
const c = createFlyCameraController({ position: createVector3(0, 0, 5) }); // yaw/pitch 0 => looks toward -Z
|
|
@@ -68,4 +123,12 @@ describe('updateFlyCameraController', () => {
|
|
|
68
123
|
expect(c.yaw).toBeGreaterThan(0);
|
|
69
124
|
expect(c.yaw).toBeLessThan(1);
|
|
70
125
|
});
|
|
126
|
+
|
|
127
|
+
it('takes the shortest arc across the wrapped yaw seam', () => {
|
|
128
|
+
const c = createFlyCameraController({ smoothTime: 0.5, yaw: Math.PI - 0.05 });
|
|
129
|
+
c.goalYaw = -Math.PI + 0.05;
|
|
130
|
+
updateFlyCameraController(c, testCamera(), 0.016);
|
|
131
|
+
expect(c.yaw).toBeGreaterThan(Math.PI - 0.05);
|
|
132
|
+
expect(c.yaw).toBeLessThan(Math.PI + 0.05);
|
|
133
|
+
});
|
|
71
134
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createOrthographicProjection, createPerspectiveProjection } from '@flighthq/camera';
|
|
2
|
+
import { createBoundingSphere } from '@flighthq/geometry';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
frameOrbitCameraControllerToSphere,
|
|
7
|
+
getPerspectiveFrameDistanceToSphere,
|
|
8
|
+
setOrthographicProjectionFrameToSphere,
|
|
9
|
+
} from './framing';
|
|
10
|
+
import { createOrbitCameraController } from './orbitCameraController';
|
|
11
|
+
|
|
12
|
+
describe('frameOrbitCameraControllerToSphere', () => {
|
|
13
|
+
it('targets the center and changes only goal distance for perspective', () => {
|
|
14
|
+
const controller = createOrbitCameraController({ distance: 10 });
|
|
15
|
+
const projection = createPerspectiveProjection({ aspect: 1, fovY: Math.PI / 2 });
|
|
16
|
+
const sphere = createBoundingSphere(1, 2, 3, 2);
|
|
17
|
+
expect(frameOrbitCameraControllerToSphere(controller, projection, sphere, 1)).toBe(true);
|
|
18
|
+
expect(controller.target).toMatchObject({ x: 1, y: 2, z: 3 });
|
|
19
|
+
expect(controller.distance).toBe(10);
|
|
20
|
+
expect(controller.goalDistance).toBeCloseTo(2 / Math.sin(Math.PI / 4));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('targets the center and changes only projection extent for orthographic', () => {
|
|
24
|
+
const controller = createOrbitCameraController({ distance: 10 });
|
|
25
|
+
const projection = createOrthographicProjection({ halfHeight: 1, halfWidth: 1 });
|
|
26
|
+
const sphere = createBoundingSphere(1, 2, 3, 2);
|
|
27
|
+
expect(frameOrbitCameraControllerToSphere(controller, projection, sphere, 2)).toBe(true);
|
|
28
|
+
expect(controller.target).toMatchObject({ x: 1, y: 2, z: 3 });
|
|
29
|
+
expect(controller.goalDistance).toBe(10);
|
|
30
|
+
expect(projection).toMatchObject({ halfHeight: 2, halfWidth: 4 });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('does not mutate for empty spheres or invalid viewport inputs', () => {
|
|
34
|
+
const controller = createOrbitCameraController({ target: { x: 1, y: 2, z: 3 } });
|
|
35
|
+
const projection = createPerspectiveProjection({ aspect: 1, fovY: Math.PI / 2 });
|
|
36
|
+
expect(frameOrbitCameraControllerToSphere(controller, projection, createBoundingSphere(), 1)).toBe(false);
|
|
37
|
+
expect(frameOrbitCameraControllerToSphere(controller, projection, createBoundingSphere(0, 0, 0, 2), 0)).toBe(false);
|
|
38
|
+
expect(controller.target).toMatchObject({ x: 1, y: 2, z: 3 });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('getPerspectiveFrameDistanceToSphere', () => {
|
|
43
|
+
it('uses the limiting vertical field for a wide viewport', () => {
|
|
44
|
+
const projection = createPerspectiveProjection({ aspect: 2, fovY: Math.PI / 2 });
|
|
45
|
+
expect(getPerspectiveFrameDistanceToSphere(projection, 2, 2)).toBeCloseTo(2 / Math.sin(Math.PI / 4));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('uses the limiting horizontal field for a tall viewport', () => {
|
|
49
|
+
const projection = createPerspectiveProjection({ aspect: 0.5, fovY: Math.PI / 2 });
|
|
50
|
+
const horizontalHalfFov = Math.atan(0.5);
|
|
51
|
+
expect(getPerspectiveFrameDistanceToSphere(projection, 2, 0.5)).toBeCloseTo(2 / Math.sin(horizontalHalfFov));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('setOrthographicProjectionFrameToSphere', () => {
|
|
56
|
+
it('preserves wide and tall viewport aspect while containing the sphere', () => {
|
|
57
|
+
const wide = createOrthographicProjection({ halfHeight: 1, halfWidth: 1 });
|
|
58
|
+
setOrthographicProjectionFrameToSphere(wide, 2, 2, 1.5);
|
|
59
|
+
expect(wide).toMatchObject({ halfHeight: 3, halfWidth: 6 });
|
|
60
|
+
|
|
61
|
+
const tall = createOrthographicProjection({ halfHeight: 1, halfWidth: 1 });
|
|
62
|
+
setOrthographicProjectionFrameToSphere(tall, 2, 0.5, 1.5);
|
|
63
|
+
expect(tall).toMatchObject({ halfHeight: 6, halfWidth: 3 });
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { createCamera3D, createPerspectiveProjection, setCamera3DViewMatrix4FromLookAt } from '@flighthq/camera';
|
|
2
|
-
import {
|
|
2
|
+
import { createVector3 } from '@flighthq/geometry';
|
|
3
|
+
import { EntityRuntimeKey } from '@flighthq/types';
|
|
3
4
|
import { describe, expect, it } from 'vitest';
|
|
4
5
|
|
|
5
6
|
import {
|
|
7
|
+
cloneOrbitCameraController,
|
|
8
|
+
copyOrbitCameraController,
|
|
6
9
|
createOrbitCameraController,
|
|
7
10
|
dollyCameraController,
|
|
8
11
|
orbitCameraController,
|
|
9
12
|
panCameraController,
|
|
13
|
+
panOrbitCameraControllerInViewPlane,
|
|
14
|
+
resetOrbitCameraController,
|
|
15
|
+
snapOrbitCameraController,
|
|
10
16
|
updateOrbitCameraController,
|
|
11
17
|
} from './orbitCameraController';
|
|
12
18
|
|
|
@@ -14,6 +20,32 @@ function testCamera() {
|
|
|
14
20
|
return createCamera3D({ far: 100, near: 0.1, projection: createPerspectiveProjection({ aspect: 1, fovY: 1 }) });
|
|
15
21
|
}
|
|
16
22
|
|
|
23
|
+
describe('cloneOrbitCameraController', () => {
|
|
24
|
+
it('creates an independent Entity with the same value state', () => {
|
|
25
|
+
const source = createOrbitCameraController({ azimuth: 0.2, distance: 4, target: createVector3(1, 2, 3) });
|
|
26
|
+
const clone = cloneOrbitCameraController(source);
|
|
27
|
+
expect(clone).toMatchObject({ azimuth: 0.2, distance: 4 });
|
|
28
|
+
expect(EntityRuntimeKey in clone).toBe(true);
|
|
29
|
+
expect(clone.target).not.toBe(source.target);
|
|
30
|
+
clone.target.x = 10;
|
|
31
|
+
expect(source.target.x).toBe(1);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('copyOrbitCameraController', () => {
|
|
36
|
+
it('copies value state without sharing target or replacing runtime state', () => {
|
|
37
|
+
const source = createOrbitCameraController({ azimuth: 0.2, distance: 4, target: createVector3(1, 2, 3) });
|
|
38
|
+
const out = createOrbitCameraController();
|
|
39
|
+
const runtime = { binding: null };
|
|
40
|
+
out[EntityRuntimeKey] = runtime;
|
|
41
|
+
copyOrbitCameraController(out, source);
|
|
42
|
+
|
|
43
|
+
expect(out).toMatchObject({ azimuth: 0.2, distance: 4 });
|
|
44
|
+
expect(out[EntityRuntimeKey]).toBe(runtime);
|
|
45
|
+
expect(out.target).not.toBe(source.target);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
17
49
|
describe('createOrbitCameraController', () => {
|
|
18
50
|
it('applies documented defaults with current equal to goal', () => {
|
|
19
51
|
const c = createOrbitCameraController();
|
|
@@ -32,6 +64,10 @@ describe('createOrbitCameraController', () => {
|
|
|
32
64
|
expect(c.target.x).toBe(1);
|
|
33
65
|
expect(c.target.z).toBe(3);
|
|
34
66
|
});
|
|
67
|
+
|
|
68
|
+
it('produces an Entity', () => {
|
|
69
|
+
expect(EntityRuntimeKey in createOrbitCameraController()).toBe(true);
|
|
70
|
+
});
|
|
35
71
|
});
|
|
36
72
|
|
|
37
73
|
describe('dollyCameraController', () => {
|
|
@@ -65,6 +101,43 @@ describe('panCameraController', () => {
|
|
|
65
101
|
});
|
|
66
102
|
});
|
|
67
103
|
|
|
104
|
+
describe('panOrbitCameraControllerInViewPlane', () => {
|
|
105
|
+
it('follows screen-up at nonzero polar', () => {
|
|
106
|
+
const c = createOrbitCameraController({ polar: Math.PI / 4 });
|
|
107
|
+
panOrbitCameraControllerInViewPlane(c, 0, 2);
|
|
108
|
+
expect(c.target.x).toBeCloseTo(0);
|
|
109
|
+
expect(c.target.y).toBeCloseTo(Math.SQRT2);
|
|
110
|
+
expect(c.target.z).toBeCloseTo(-Math.SQRT2);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('resetOrbitCameraController', () => {
|
|
115
|
+
it('resets all coupled state from a seed', () => {
|
|
116
|
+
const c = createOrbitCameraController({ target: createVector3(1, 2, 3) });
|
|
117
|
+
resetOrbitCameraController(c, {
|
|
118
|
+
azimuth: 1,
|
|
119
|
+
distance: 4,
|
|
120
|
+
maxDistance: 6,
|
|
121
|
+
maxPolar: 0.5,
|
|
122
|
+
polar: 0.25,
|
|
123
|
+
target: createVector3(4, 5, 6),
|
|
124
|
+
});
|
|
125
|
+
expect(c).toMatchObject({ azimuth: 1, goalAzimuth: 1, distance: 4, goalDistance: 4, polar: 0.25 });
|
|
126
|
+
expect(c.target).toMatchObject({ x: 4, y: 5, z: 6 });
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('snapOrbitCameraController', () => {
|
|
131
|
+
it('snaps current values to clamped goals', () => {
|
|
132
|
+
const c = createOrbitCameraController({ maxDistance: 6, maxPolar: 0.5 });
|
|
133
|
+
c.goalDistance = 20;
|
|
134
|
+
c.goalPolar = 2;
|
|
135
|
+
snapOrbitCameraController(c);
|
|
136
|
+
expect(c.distance).toBe(6);
|
|
137
|
+
expect(c.polar).toBe(0.5);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
68
141
|
describe('updateOrbitCameraController', () => {
|
|
69
142
|
it('snaps to the goal when smoothTime is 0 and writes the look-at view', () => {
|
|
70
143
|
const c = createOrbitCameraController({ distance: 10 }); // azimuth 0, polar 0 => eye (0,0,10)
|
|
@@ -84,4 +157,12 @@ describe('updateOrbitCameraController', () => {
|
|
|
84
157
|
expect(c.azimuth).toBeGreaterThan(0);
|
|
85
158
|
expect(c.azimuth).toBeLessThan(1); // has not reached the goal in one small step
|
|
86
159
|
});
|
|
160
|
+
|
|
161
|
+
it('takes the shortest arc across the wrapped azimuth seam', () => {
|
|
162
|
+
const c = createOrbitCameraController({ azimuth: Math.PI - 0.05, smoothTime: 0.5 });
|
|
163
|
+
c.goalAzimuth = -Math.PI + 0.05;
|
|
164
|
+
updateOrbitCameraController(c, testCamera(), 0.016);
|
|
165
|
+
expect(c.azimuth).toBeGreaterThan(Math.PI - 0.05);
|
|
166
|
+
expect(c.azimuth).toBeLessThan(Math.PI + 0.05);
|
|
167
|
+
});
|
|
87
168
|
});
|