three-zoo 0.13.0 → 0.14.0

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.
Files changed (33) hide show
  1. package/README.md +18 -199
  2. package/dist/camera/EnhancedPerspectiveCamera.d.ts +23 -0
  3. package/dist/camera/EnhancedPerspectiveCamera.js +66 -0
  4. package/dist/camera/fovPolicies/FovPolicy.d.ts +4 -0
  5. package/dist/camera/fovPolicies/FovPolicy.js +5 -0
  6. package/dist/camera/fovPolicies/FovPolicyCover.d.ts +26 -0
  7. package/dist/camera/fovPolicies/FovPolicyCover.js +44 -0
  8. package/dist/camera/fovPolicies/FovPolicyFit.d.ts +26 -0
  9. package/dist/camera/fovPolicies/FovPolicyFit.js +44 -0
  10. package/dist/camera/fovPolicies/FovPolicyFixedHorizontal.d.ts +13 -0
  11. package/dist/camera/fovPolicies/FovPolicyFixedHorizontal.js +27 -0
  12. package/dist/camera/fovPolicies/FovPolicyFixedVertical.d.ts +13 -0
  13. package/dist/camera/fovPolicies/FovPolicyFixedVertical.js +27 -0
  14. package/dist/camera/fovPolicies/FovPolicyHybrid.d.ts +25 -0
  15. package/dist/camera/fovPolicies/FovPolicyHybrid.js +45 -0
  16. package/dist/camera/fovPolicies/FovPolicyHybridInverted.d.ts +21 -0
  17. package/dist/camera/fovPolicies/FovPolicyHybridInverted.js +40 -0
  18. package/dist/camera/fovPolicies/fovMath.d.ts +5 -0
  19. package/dist/camera/fovPolicies/fovMath.js +14 -0
  20. package/dist/ik/AimChainIK.d.ts +3 -3
  21. package/dist/ik/AimChainIK.js +2 -2
  22. package/dist/index.d.ts +8 -1
  23. package/dist/index.js +8 -1
  24. package/dist/instancedMeshPool/InstancedMeshInstance.d.ts +1 -1
  25. package/dist/instancedMeshPool/InstancedMeshInstance.js +5 -5
  26. package/dist/instancedMeshPool/InstancedMeshPool.d.ts +1 -2
  27. package/dist/instancedMeshPool/InstancedMeshPool.js +3 -1
  28. package/dist/lighting/Sun.d.ts +2 -2
  29. package/dist/lighting/Sun.js +2 -2
  30. package/dist/miscellaneous/SceneResolver.d.ts +6 -0
  31. package/package.json +1 -1
  32. package/dist/miscellaneous/DualFovCamera.d.ts +0 -43
  33. package/dist/miscellaneous/DualFovCamera.js +0 -170
package/README.md CHANGED
@@ -1,211 +1,30 @@
1
- <p align="center">
2
- <h1 align="center">🦁 🐘 🦊 three-zoo</h1>
3
- <p align="center">Reusable Three.js utilities.</p>
4
- </p>
1
+ # three-zoo
5
2
 
6
- <p align="center">
7
- <a href="https://www.npmjs.com/package/three-zoo"><img src="https://img.shields.io/npm/v/three-zoo.svg" alt="npm version"></a>
8
- <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
9
- <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-%5E5.8.0-blue" alt="TypeScript"></a>
10
- <a href="https://threejs.org/"><img src="https://img.shields.io/badge/Three.js-%5E0.175.0-green" alt="Three.js"></a>
11
- </p>
3
+ [![npm](https://img.shields.io/npm/v/three-zoo.svg)](https://www.npmjs.com/package/three-zoo)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
12
5
 
13
- ## Installation
6
+ Reusable Three.js utilities. WebGL 1 compatible.
14
7
 
15
- ```bash
16
- npm install three-zoo
17
- ```
18
-
19
- ## Contents
20
-
21
- - **IK** - `TwoBoneIK`, `AimChainIK`
22
- - **Instanced Mesh Pool** - `InstancedMeshPool`, `InstancedMeshInstance`, `InstancedMeshGroup`
23
- - **Lighting** - `Sun`, `SkyLight`
24
- - **Material Converters** - Standard to Basic / Lambert / Phong / Toon / Physical, Basic to Physical
25
- - **Miscellaneous** - `DualFovCamera`, `SceneTraversal`, `SceneSorter`, `SkinnedMeshBaker`
26
-
27
- ---
28
-
29
- ## IK
30
-
31
- ### TwoBoneIK
32
-
33
- Analytical two-bone IK solver. Chain: `root -> middle -> end`. Pole controls bend direction. Writes local quaternions to `root` and `middle`.
34
-
35
- ```typescript
36
- const twoBoneIK = new TwoBoneIK(upperArm, foreArm, hand, poleObject, targetObject);
37
-
38
- // call after AnimationMixer.update() each frame
39
- twoBoneIK.solve();
40
-
41
- // tune pole twist per bone
42
- twoBoneIK.rootPoleAxis.set(0, 1, 0);
43
- twoBoneIK.middlePoleTwist = false;
44
- ```
45
-
46
- ### AimChainIK
47
-
48
- Distributes aim rotation across a bone chain according to per-bone weights.
49
-
50
- ```typescript
51
- const aimChainIK = new AimChainIK([spine1, spine2, spine3, head]);
52
-
53
- aimChainIK.curve = [0.2, 0.5, 0.8, 1.0]; // root gets least, tip gets most
54
- aimChainIK.weight = 0.8; // global blend
55
-
56
- // sample directions before calling - mutates bone quaternions
57
- aimChainIK.solve(currentForward, targetDirection);
58
- ```
59
-
60
- ---
61
-
62
- ## Instanced Mesh Pool
63
-
64
- Manages `InstancedMesh` instances keyed by geometry+material. Grows capacity automatically.
65
-
66
- ```typescript
67
- const pool = new InstancedMeshPool(scene, { initialCapacity: 32, capacityStep: 16 });
68
-
69
- // allocate / update / release individual instances
70
- const instance = new InstancedMeshInstance(pool, geometry, material);
71
- instance.setPosition3f(1, 0, 0).setScale3f(2, 2, 2).flushTransform();
72
- instance.destroy();
73
-
74
- // group multiple instances under a shared Object3D transform
75
- const group = new InstancedMeshGroup([instanceA, instanceB]);
76
- scene.add(group);
77
- group.position.set(10, 0, 0);
78
- group.flushTransform(); // propagates group world matrix to all instances
79
- group.destroy();
80
- ```
81
-
82
- ---
83
-
84
- ## Lighting
85
-
86
- ### Sun
87
-
88
- `DirectionalLight` with spherical positioning and shadow auto-configuration.
89
-
90
- ```typescript
91
- const sun = new Sun();
92
- sun.elevation = Math.PI / 4;
93
- sun.azimuth = Math.PI / 2;
94
- sun.distance = 100;
95
-
96
- sun.configureShadowsForBoundingBox(sceneBounds);
97
- sun.setDirectionFromHDRTexture(hdrTexture, 50);
98
- ```
99
-
100
- ### SkyLight
8
+ ## API
101
9
 
102
- `HemisphereLight` that extracts sky and ground colors from an HDR environment map.
10
+ - **IK**: `TwoBoneIK` (analytical two-bone solver), `AimChainIK` (distributes aim across a bone chain with per-bone weights).
11
+ - **Instanced Mesh Pool**: `InstancedMeshPool`, `InstancedMeshInstance`, `InstancedMeshGroup`. Keyed by geometry+material, auto-grows capacity.
12
+ - **Lighting**: `Sun` (DirectionalLight with spherical positioning, shadow auto-config from bounding box or HDR), `SkyLight` (HemisphereLight that samples sky/ground from HDR).
13
+ - **Material Converters**: Standard to Basic/Lambert/Phong/Toon/Physical, Basic to Physical. Single static `convert()` call each.
14
+ - **DualFovCamera**: PerspectiveCamera with independent horizontal and vertical FOV. Can fit FOV to points, boxes, or skinned meshes.
15
+ - **EnhancedPerspectiveCamera**: PerspectiveCamera whose vertical FOV is derived from a `FovPolicy` and the current aspect ratio. Policies: `FovPolicyFixedVertical`, `FovPolicyFixedHorizontal`, `FovPolicyHybrid`, `FovPolicyHybridInverted`, `FovPolicyCover`, `FovPolicyFit`.
16
+ - **SceneTraversal**: static helpers for finding/filtering objects and materials by name, regex, or predicate.
17
+ - **SceneSorter**: assigns `renderOrder` by distance to a point (front-to-back or back-to-front).
18
+ - **SkinnedMeshBaker**: bakes a SkinnedMesh to static geometry at current pose or a specific animation frame.
103
19
 
104
- ```typescript
105
- const skyLight = new SkyLight();
106
- skyLight.setColorsFromHDRTexture(hdrTexture, {
107
- skySampleCount: 100,
108
- groundSampleCount: 100,
109
- });
110
- ```
111
-
112
- ---
113
-
114
- ## Material Converters
115
-
116
- All converters expose a single static `convert(material, options?)`. Common options:
117
-
118
- | Option | Default | Description |
119
- |------------------|---------|--------------------------------------|
120
- | `preserveName` | `true` | Copy `.name` to the new material |
121
- | `copyUserData` | `true` | Copy `.userData` |
122
- | `disposeOriginal`| `false` | Dispose source material after conversion |
20
+ ## Install
123
21
 
124
- ```typescript
125
- // Standard -> unlit
126
- const basicMaterial = StandardToBasicConverter.convert(standardMaterial, { brightnessFactor: 1.3, combineEmissive: true });
127
-
128
- // Standard -> diffuse-only lit
129
- const lambertMaterial = StandardToLambertConverter.convert(standardMaterial);
130
- const phongMaterial = StandardToPhongConverter.convert(standardMaterial);
131
- const toonMaterial = StandardToToonConverter.convert(standardMaterial);
132
-
133
- // Standard <-> Physical
134
- const physicalMaterial = StandardToPhysicalConverter.convert(standardMaterial);
135
- const standardMaterial2 = BasicToPhysicalConverter.convert(basicMaterial);
136
22
  ```
137
-
138
- ---
139
-
140
- ## Miscellaneous
141
-
142
- ### DualFovCamera
143
-
144
- `PerspectiveCamera` with independent horizontal and vertical FOV.
145
-
146
- ```typescript
147
- const camera = new DualFovCamera(90, 60);
148
- camera.horizontalFov = 100;
149
- camera.verticalFov = 70;
150
-
151
- camera.fitVerticalFovToPoints(vertices);
152
- camera.fitVerticalFovToBox(boundingBox);
153
- camera.fitVerticalFovToMesh(skinnedMesh);
154
- camera.lookAtMeshCenterOfMass(skinnedMesh);
155
- ```
156
-
157
- ### SceneTraversal
158
-
159
- Static helpers for depth-first scene graph traversal.
160
-
161
- ```typescript
162
- // find by name
163
- const playerObject = SceneTraversal.getObjectByName(scene, 'Player');
164
- const metalMaterial = SceneTraversal.getMaterialByName(scene, 'Metal');
165
-
166
- // filter by regex or predicate
167
- const enemies = SceneTraversal.filterObjects(scene, /^enemy_/);
168
- const glassMaterials = SceneTraversal.filterMaterials(scene, /glass/i);
169
-
170
- // enumerate with callback
171
- SceneTraversal.enumerateMaterials(scene, (material) => {
172
- material.needsUpdate = true;
173
- });
174
-
175
- // find meshes that use specific materials
176
- const glassMeshes = SceneTraversal.findMaterialUsers(scene, glassMaterials);
177
- ```
178
-
179
- ### SceneSorter
180
-
181
- Assigns sequential `renderOrder` values sorted by distance to a point. Useful for transparent meshes.
182
-
183
- ```typescript
184
- // front-to-back
185
- SceneSorter.sortByDistanceToPoint(object, cameraPosition, 0);
186
-
187
- // back-to-front (transparent objects)
188
- SceneSorter.sortByDistanceToPoint(object, cameraPosition, 0, true);
189
- ```
190
-
191
- ### SkinnedMeshBaker
192
-
193
- Bakes a `SkinnedMesh` to static geometry.
194
-
195
- ```typescript
196
- // current pose
197
- const staticMesh = SkinnedMeshBaker.bakePose(skinnedMesh);
198
-
199
- // specific animation frame
200
- const frameMesh = SkinnedMeshBaker.bakeAnimationFrame(armature, skinnedMesh, 1.5, animationClip);
23
+ npm install three-zoo
201
24
  ```
202
25
 
203
- ---
204
-
205
- ## Requirements
206
-
207
- - `three` >=0.157.0 <0.180.0 (peer dependency)
26
+ Peer dep: `three` >=0.157.0 <0.180.0
208
27
 
209
28
  ## License
210
29
 
211
- MIT © [jango](https://github.com/jango-git)
30
+ MIT
@@ -0,0 +1,23 @@
1
+ import type { SkinnedMesh } from "three";
2
+ import { PerspectiveCamera } from "three";
3
+ import type { FovPolicy } from "./fovPolicies/FovPolicy";
4
+ /**
5
+ * PerspectiveCamera whose vertical FOV is derived from a {@link FovPolicy} and the current aspect
6
+ * ratio. The policy is applied on every `updateProjectionMatrix()` call, so the standard resize
7
+ * flow (`camera.aspect = w / h; camera.updateProjectionMatrix()`) is all that is needed.
8
+ */
9
+ export declare class EnhancedPerspectiveCamera extends PerspectiveCamera {
10
+ private fovPolicyInternal;
11
+ constructor(fovPolicy: FovPolicy, aspect?: number, near?: number, far?: number);
12
+ get fovPolicy(): FovPolicy;
13
+ /** Replacing the policy re-applies it immediately. */
14
+ set fovPolicy(value: FovPolicy);
15
+ /** @override */
16
+ updateProjectionMatrix(): void;
17
+ /**
18
+ * Orients the camera toward the mesh's vertex centroid (mean of the skinned vertices in world
19
+ * space). Calls `skeleton.update()` internally before sampling vertices.
20
+ */
21
+ lookAtMeshCenterOfMass(skinnedMesh: SkinnedMesh): void;
22
+ clone(): this;
23
+ }
@@ -0,0 +1,66 @@
1
+ import { PerspectiveCamera, Vector3 } from 'three';
2
+ import { clampFov } from './fovPolicies/fovMath.js';
3
+
4
+ const DEFAULT_ASPECT = 1;
5
+ const DEFAULT_NEAR = 1;
6
+ const DEFAULT_FAR = 1000;
7
+ /**
8
+ * PerspectiveCamera whose vertical FOV is derived from a {@link FovPolicy} and the current aspect
9
+ * ratio. The policy is applied on every `updateProjectionMatrix()` call, so the standard resize
10
+ * flow (`camera.aspect = w / h; camera.updateProjectionMatrix()`) is all that is needed.
11
+ */
12
+ class EnhancedPerspectiveCamera extends PerspectiveCamera {
13
+ constructor(fovPolicy, aspect = DEFAULT_ASPECT, near = DEFAULT_NEAR, far = DEFAULT_FAR) {
14
+ super(clampFov(fovPolicy.calculateFov(aspect)), aspect, near, far);
15
+ this._private_fovPolicyInternal = fovPolicy;
16
+ this.updateProjectionMatrix();
17
+ }
18
+ get fovPolicy() {
19
+ return this._private_fovPolicyInternal;
20
+ }
21
+ /** Replacing the policy re-applies it immediately. */
22
+ set fovPolicy(value) {
23
+ this._private_fovPolicyInternal = value;
24
+ this.updateProjectionMatrix();
25
+ }
26
+ /** @override */
27
+ updateProjectionMatrix() {
28
+ // The PerspectiveCamera constructor calls this before fovPolicyInternal is assigned, so the
29
+ // field may briefly be undefined despite its declared type.
30
+ const policy = this._private_fovPolicyInternal;
31
+ if (policy !== undefined) {
32
+ this.fov = clampFov(policy.calculateFov(this.aspect));
33
+ }
34
+ super.updateProjectionMatrix();
35
+ }
36
+ /**
37
+ * Orients the camera toward the mesh's vertex centroid (mean of the skinned vertices in world
38
+ * space). Calls `skeleton.update()` internally before sampling vertices.
39
+ */
40
+ lookAtMeshCenterOfMass(skinnedMesh) {
41
+ skinnedMesh.updateWorldMatrix(true, true);
42
+ skinnedMesh.skeleton.update();
43
+ const position = skinnedMesh.geometry.attributes["position"];
44
+ if (position.count === 0) {
45
+ return;
46
+ }
47
+ const vertex = new Vector3();
48
+ const centroid = new Vector3();
49
+ for (let i = 0; i < position.count; i++) {
50
+ vertex.fromBufferAttribute(position, i);
51
+ skinnedMesh.applyBoneTransform(i, vertex);
52
+ vertex.applyMatrix4(skinnedMesh.matrixWorld);
53
+ centroid.add(vertex);
54
+ }
55
+ centroid.divideScalar(position.count);
56
+ this.lookAt(centroid);
57
+ }
58
+ clone() {
59
+ const camera = new EnhancedPerspectiveCamera(this._private_fovPolicyInternal.clone(), this.aspect, this.near, this.far);
60
+ camera.copy(this, true);
61
+ camera.updateProjectionMatrix();
62
+ return camera;
63
+ }
64
+ }
65
+
66
+ export { EnhancedPerspectiveCamera };
@@ -0,0 +1,4 @@
1
+ /** Base class for FOV policies that derive a vertical FOV from the current aspect ratio. */
2
+ export declare abstract class FovPolicy {
3
+ abstract clone(): FovPolicy;
4
+ }
@@ -0,0 +1,5 @@
1
+ /** Base class for FOV policies that derive a vertical FOV from the current aspect ratio. */
2
+ class FovPolicy {
3
+ }
4
+
5
+ export { FovPolicy };
@@ -0,0 +1,26 @@
1
+ import { FovPolicy } from "./FovPolicy";
2
+ /**
3
+ * Guarantees the target frustum is fully visible at any aspect ratio.
4
+ *
5
+ * Takes the wider of the two vertical FOVs implied by the horizontal and vertical targets, so
6
+ * both target angles are always contained (the view may show more than the target rect).
7
+ */
8
+ export declare class FovPolicyCover extends FovPolicy {
9
+ private fovHorizontalInternal;
10
+ private fovVerticalInternal;
11
+ /**
12
+ * @param fovHorizontal - Target horizontal FOV. Clamped to 1-179 degrees.
13
+ * @param fovVertical - Target vertical FOV. Clamped to 1-179 degrees.
14
+ */
15
+ constructor(fovHorizontal: number, fovVertical: number);
16
+ /** Target horizontal FOV. */
17
+ get fovHorizontal(): number;
18
+ /** Target vertical FOV. */
19
+ get fovVertical(): number;
20
+ /** Clamped to 1-179 degrees. */
21
+ set fovHorizontal(value: number);
22
+ /** Clamped to 1-179 degrees. */
23
+ set fovVertical(value: number);
24
+ calculateFov(aspect: number): number;
25
+ clone(): FovPolicyCover;
26
+ }
@@ -0,0 +1,44 @@
1
+ import { clampFov, verticalFovFromHorizontal } from './fovMath.js';
2
+ import { FovPolicy } from './FovPolicy.js';
3
+
4
+ /**
5
+ * Guarantees the target frustum is fully visible at any aspect ratio.
6
+ *
7
+ * Takes the wider of the two vertical FOVs implied by the horizontal and vertical targets, so
8
+ * both target angles are always contained (the view may show more than the target rect).
9
+ */
10
+ class FovPolicyCover extends FovPolicy {
11
+ /**
12
+ * @param fovHorizontal - Target horizontal FOV. Clamped to 1-179 degrees.
13
+ * @param fovVertical - Target vertical FOV. Clamped to 1-179 degrees.
14
+ */
15
+ constructor(fovHorizontal, fovVertical) {
16
+ super();
17
+ this._private_fovHorizontalInternal = clampFov(fovHorizontal);
18
+ this._private_fovVerticalInternal = clampFov(fovVertical);
19
+ }
20
+ /** Target horizontal FOV. */
21
+ get fovHorizontal() {
22
+ return this._private_fovHorizontalInternal;
23
+ }
24
+ /** Target vertical FOV. */
25
+ get fovVertical() {
26
+ return this._private_fovVerticalInternal;
27
+ }
28
+ /** Clamped to 1-179 degrees. */
29
+ set fovHorizontal(value) {
30
+ this._private_fovHorizontalInternal = clampFov(value);
31
+ }
32
+ /** Clamped to 1-179 degrees. */
33
+ set fovVertical(value) {
34
+ this._private_fovVerticalInternal = clampFov(value);
35
+ }
36
+ calculateFov(aspect) {
37
+ return Math.max(this._private_fovVerticalInternal, verticalFovFromHorizontal(this._private_fovHorizontalInternal, aspect));
38
+ }
39
+ clone() {
40
+ return new FovPolicyCover(this._private_fovHorizontalInternal, this._private_fovVerticalInternal);
41
+ }
42
+ }
43
+
44
+ export { FovPolicyCover };
@@ -0,0 +1,26 @@
1
+ import { FovPolicy } from "./FovPolicy";
2
+ /**
3
+ * Fills the viewport with the target frustum at any aspect ratio.
4
+ *
5
+ * Takes the narrower of the two vertical FOVs implied by the horizontal and vertical targets, so
6
+ * neither target angle is ever exceeded (content outside the target rect is cropped).
7
+ */
8
+ export declare class FovPolicyFit extends FovPolicy {
9
+ private fovHorizontalInternal;
10
+ private fovVerticalInternal;
11
+ /**
12
+ * @param fovHorizontal - Target horizontal FOV. Clamped to 1-179 degrees.
13
+ * @param fovVertical - Target vertical FOV. Clamped to 1-179 degrees.
14
+ */
15
+ constructor(fovHorizontal: number, fovVertical: number);
16
+ /** Target horizontal FOV. */
17
+ get fovHorizontal(): number;
18
+ /** Target vertical FOV. */
19
+ get fovVertical(): number;
20
+ /** Clamped to 1-179 degrees. */
21
+ set fovHorizontal(value: number);
22
+ /** Clamped to 1-179 degrees. */
23
+ set fovVertical(value: number);
24
+ calculateFov(aspect: number): number;
25
+ clone(): FovPolicyFit;
26
+ }
@@ -0,0 +1,44 @@
1
+ import { clampFov, verticalFovFromHorizontal } from './fovMath.js';
2
+ import { FovPolicy } from './FovPolicy.js';
3
+
4
+ /**
5
+ * Fills the viewport with the target frustum at any aspect ratio.
6
+ *
7
+ * Takes the narrower of the two vertical FOVs implied by the horizontal and vertical targets, so
8
+ * neither target angle is ever exceeded (content outside the target rect is cropped).
9
+ */
10
+ class FovPolicyFit extends FovPolicy {
11
+ /**
12
+ * @param fovHorizontal - Target horizontal FOV. Clamped to 1-179 degrees.
13
+ * @param fovVertical - Target vertical FOV. Clamped to 1-179 degrees.
14
+ */
15
+ constructor(fovHorizontal, fovVertical) {
16
+ super();
17
+ this._private_fovHorizontalInternal = clampFov(fovHorizontal);
18
+ this._private_fovVerticalInternal = clampFov(fovVertical);
19
+ }
20
+ /** Target horizontal FOV. */
21
+ get fovHorizontal() {
22
+ return this._private_fovHorizontalInternal;
23
+ }
24
+ /** Target vertical FOV. */
25
+ get fovVertical() {
26
+ return this._private_fovVerticalInternal;
27
+ }
28
+ /** Clamped to 1-179 degrees. */
29
+ set fovHorizontal(value) {
30
+ this._private_fovHorizontalInternal = clampFov(value);
31
+ }
32
+ /** Clamped to 1-179 degrees. */
33
+ set fovVertical(value) {
34
+ this._private_fovVerticalInternal = clampFov(value);
35
+ }
36
+ calculateFov(aspect) {
37
+ return Math.min(this._private_fovVerticalInternal, verticalFovFromHorizontal(this._private_fovHorizontalInternal, aspect));
38
+ }
39
+ clone() {
40
+ return new FovPolicyFit(this._private_fovHorizontalInternal, this._private_fovVerticalInternal);
41
+ }
42
+ }
43
+
44
+ export { FovPolicyFit };
@@ -0,0 +1,13 @@
1
+ import { FovPolicy } from "./FovPolicy";
2
+ /** Keeps the horizontal FOV constant; derives the vertical FOV from the aspect ratio. */
3
+ export declare class FovPolicyFixedHorizontal extends FovPolicy {
4
+ private fovHorizontalInternal;
5
+ /** @param fovHorizontal - Clamped to 1-179 degrees. */
6
+ constructor(fovHorizontal: number);
7
+ /** Target horizontal FOV. */
8
+ get fovHorizontal(): number;
9
+ /** Clamped to 1-179 degrees. */
10
+ set fovHorizontal(value: number);
11
+ calculateFov(aspect: number): number;
12
+ clone(): FovPolicyFixedHorizontal;
13
+ }
@@ -0,0 +1,27 @@
1
+ import { clampFov, verticalFovFromHorizontal } from './fovMath.js';
2
+ import { FovPolicy } from './FovPolicy.js';
3
+
4
+ /** Keeps the horizontal FOV constant; derives the vertical FOV from the aspect ratio. */
5
+ class FovPolicyFixedHorizontal extends FovPolicy {
6
+ /** @param fovHorizontal - Clamped to 1-179 degrees. */
7
+ constructor(fovHorizontal) {
8
+ super();
9
+ this._private_fovHorizontalInternal = clampFov(fovHorizontal);
10
+ }
11
+ /** Target horizontal FOV. */
12
+ get fovHorizontal() {
13
+ return this._private_fovHorizontalInternal;
14
+ }
15
+ /** Clamped to 1-179 degrees. */
16
+ set fovHorizontal(value) {
17
+ this._private_fovHorizontalInternal = clampFov(value);
18
+ }
19
+ calculateFov(aspect) {
20
+ return verticalFovFromHorizontal(this._private_fovHorizontalInternal, aspect);
21
+ }
22
+ clone() {
23
+ return new FovPolicyFixedHorizontal(this._private_fovHorizontalInternal);
24
+ }
25
+ }
26
+
27
+ export { FovPolicyFixedHorizontal };
@@ -0,0 +1,13 @@
1
+ import { FovPolicy } from "./FovPolicy";
2
+ /** Keeps the vertical FOV constant regardless of aspect (default three.js behavior). */
3
+ export declare class FovPolicyFixedVertical extends FovPolicy {
4
+ private fovVerticalInternal;
5
+ /** @param fovVertical - Clamped to 1-179 degrees. */
6
+ constructor(fovVertical: number);
7
+ /** Target vertical FOV. */
8
+ get fovVertical(): number;
9
+ /** Clamped to 1-179 degrees. */
10
+ set fovVertical(value: number);
11
+ calculateFov(): number;
12
+ clone(): FovPolicyFixedVertical;
13
+ }
@@ -0,0 +1,27 @@
1
+ import { clampFov } from './fovMath.js';
2
+ import { FovPolicy } from './FovPolicy.js';
3
+
4
+ /** Keeps the vertical FOV constant regardless of aspect (default three.js behavior). */
5
+ class FovPolicyFixedVertical extends FovPolicy {
6
+ /** @param fovVertical - Clamped to 1-179 degrees. */
7
+ constructor(fovVertical) {
8
+ super();
9
+ this._private_fovVerticalInternal = clampFov(fovVertical);
10
+ }
11
+ /** Target vertical FOV. */
12
+ get fovVertical() {
13
+ return this._private_fovVerticalInternal;
14
+ }
15
+ /** Clamped to 1-179 degrees. */
16
+ set fovVertical(value) {
17
+ this._private_fovVerticalInternal = clampFov(value);
18
+ }
19
+ calculateFov() {
20
+ return this._private_fovVerticalInternal;
21
+ }
22
+ clone() {
23
+ return new FovPolicyFixedVertical(this._private_fovVerticalInternal);
24
+ }
25
+ }
26
+
27
+ export { FovPolicyFixedVertical };
@@ -0,0 +1,25 @@
1
+ import { FovPolicy } from "./FovPolicy";
2
+ /**
3
+ * Preserves the horizontal FOV in landscape (aspect > 1) and the vertical FOV in portrait.
4
+ *
5
+ * Equivalent to DualFovCamera's projection behavior.
6
+ */
7
+ export declare class FovPolicyHybrid extends FovPolicy {
8
+ private fovHorizontalInternal;
9
+ private fovVerticalInternal;
10
+ /**
11
+ * @param fovHorizontal - Preserved in landscape. Clamped to 1-179 degrees.
12
+ * @param fovVertical - Preserved in portrait. Clamped to 1-179 degrees.
13
+ */
14
+ constructor(fovHorizontal: number, fovVertical: number);
15
+ /** Horizontal FOV preserved in landscape. */
16
+ get fovHorizontal(): number;
17
+ /** Vertical FOV preserved in portrait. */
18
+ get fovVertical(): number;
19
+ /** Clamped to 1-179 degrees. */
20
+ set fovHorizontal(value: number);
21
+ /** Clamped to 1-179 degrees. */
22
+ set fovVertical(value: number);
23
+ calculateFov(aspect: number): number;
24
+ clone(): FovPolicyHybrid;
25
+ }
@@ -0,0 +1,45 @@
1
+ import { clampFov, verticalFovFromHorizontal } from './fovMath.js';
2
+ import { FovPolicy } from './FovPolicy.js';
3
+
4
+ /**
5
+ * Preserves the horizontal FOV in landscape (aspect > 1) and the vertical FOV in portrait.
6
+ *
7
+ * Equivalent to DualFovCamera's projection behavior.
8
+ */
9
+ class FovPolicyHybrid extends FovPolicy {
10
+ /**
11
+ * @param fovHorizontal - Preserved in landscape. Clamped to 1-179 degrees.
12
+ * @param fovVertical - Preserved in portrait. Clamped to 1-179 degrees.
13
+ */
14
+ constructor(fovHorizontal, fovVertical) {
15
+ super();
16
+ this._private_fovHorizontalInternal = clampFov(fovHorizontal);
17
+ this._private_fovVerticalInternal = clampFov(fovVertical);
18
+ }
19
+ /** Horizontal FOV preserved in landscape. */
20
+ get fovHorizontal() {
21
+ return this._private_fovHorizontalInternal;
22
+ }
23
+ /** Vertical FOV preserved in portrait. */
24
+ get fovVertical() {
25
+ return this._private_fovVerticalInternal;
26
+ }
27
+ /** Clamped to 1-179 degrees. */
28
+ set fovHorizontal(value) {
29
+ this._private_fovHorizontalInternal = clampFov(value);
30
+ }
31
+ /** Clamped to 1-179 degrees. */
32
+ set fovVertical(value) {
33
+ this._private_fovVerticalInternal = clampFov(value);
34
+ }
35
+ calculateFov(aspect) {
36
+ return aspect > 1
37
+ ? verticalFovFromHorizontal(this._private_fovHorizontalInternal, aspect)
38
+ : this._private_fovVerticalInternal;
39
+ }
40
+ clone() {
41
+ return new FovPolicyHybrid(this._private_fovHorizontalInternal, this._private_fovVerticalInternal);
42
+ }
43
+ }
44
+
45
+ export { FovPolicyHybrid };
@@ -0,0 +1,21 @@
1
+ import { FovPolicy } from "./FovPolicy";
2
+ /** Preserves the vertical FOV in landscape (aspect > 1) and the horizontal FOV in portrait. */
3
+ export declare class FovPolicyHybridInverted extends FovPolicy {
4
+ private fovHorizontalInternal;
5
+ private fovVerticalInternal;
6
+ /**
7
+ * @param fovHorizontal - Preserved in portrait. Clamped to 1-179 degrees.
8
+ * @param fovVertical - Preserved in landscape. Clamped to 1-179 degrees.
9
+ */
10
+ constructor(fovHorizontal: number, fovVertical: number);
11
+ /** Horizontal FOV preserved in portrait. */
12
+ get fovHorizontal(): number;
13
+ /** Vertical FOV preserved in landscape. */
14
+ get fovVertical(): number;
15
+ /** Clamped to 1-179 degrees. */
16
+ set fovHorizontal(value: number);
17
+ /** Clamped to 1-179 degrees. */
18
+ set fovVertical(value: number);
19
+ calculateFov(aspect: number): number;
20
+ clone(): FovPolicyHybridInverted;
21
+ }
@@ -0,0 +1,40 @@
1
+ import { clampFov, verticalFovFromHorizontal } from './fovMath.js';
2
+ import { FovPolicy } from './FovPolicy.js';
3
+
4
+ /** Preserves the vertical FOV in landscape (aspect > 1) and the horizontal FOV in portrait. */
5
+ class FovPolicyHybridInverted extends FovPolicy {
6
+ /**
7
+ * @param fovHorizontal - Preserved in portrait. Clamped to 1-179 degrees.
8
+ * @param fovVertical - Preserved in landscape. Clamped to 1-179 degrees.
9
+ */
10
+ constructor(fovHorizontal, fovVertical) {
11
+ super();
12
+ this._private_fovHorizontalInternal = clampFov(fovHorizontal);
13
+ this._private_fovVerticalInternal = clampFov(fovVertical);
14
+ }
15
+ /** Horizontal FOV preserved in portrait. */
16
+ get fovHorizontal() {
17
+ return this._private_fovHorizontalInternal;
18
+ }
19
+ /** Vertical FOV preserved in landscape. */
20
+ get fovVertical() {
21
+ return this._private_fovVerticalInternal;
22
+ }
23
+ /** Clamped to 1-179 degrees. */
24
+ set fovHorizontal(value) {
25
+ this._private_fovHorizontalInternal = clampFov(value);
26
+ }
27
+ /** Clamped to 1-179 degrees. */
28
+ set fovVertical(value) {
29
+ this._private_fovVerticalInternal = clampFov(value);
30
+ }
31
+ calculateFov(aspect) {
32
+ return aspect > 1
33
+ ? this._private_fovVerticalInternal : verticalFovFromHorizontal(this._private_fovHorizontalInternal, aspect);
34
+ }
35
+ clone() {
36
+ return new FovPolicyHybridInverted(this._private_fovHorizontalInternal, this._private_fovVerticalInternal);
37
+ }
38
+ }
39
+
40
+ export { FovPolicyHybridInverted };
@@ -0,0 +1,5 @@
1
+ export declare const MIN_FOV = 1;
2
+ export declare const MAX_FOV = 179;
3
+ export declare function clampFov(value: number): number;
4
+ /** Vertical FOV (degrees) that yields the given horizontal FOV (degrees) at aspect = width / height. */
5
+ export declare function verticalFovFromHorizontal(horizontalFov: number, aspect: number): number;
@@ -0,0 +1,14 @@
1
+ import { MathUtils } from 'three';
2
+
3
+ const MIN_FOV = 1;
4
+ const MAX_FOV = 179;
5
+ function clampFov(value) {
6
+ return MathUtils.clamp(value, MIN_FOV, MAX_FOV);
7
+ }
8
+ /** Vertical FOV (degrees) that yields the given horizontal FOV (degrees) at aspect = width / height. */
9
+ function verticalFovFromHorizontal(horizontalFov, aspect) {
10
+ const radians = MathUtils.degToRad(horizontalFov);
11
+ return MathUtils.radToDeg(Math.atan(Math.tan(radians / 2) / aspect) * 2);
12
+ }
13
+
14
+ export { MAX_FOV, MIN_FOV, clampFov, verticalFovFromHorizontal };
@@ -14,7 +14,7 @@ export declare class AimChainIK {
14
14
  epsilon: number;
15
15
  /**
16
16
  * Global blend weight. 0 = solver has no effect, 1 = full effect.
17
- * Clamped to 01 internally.
17
+ * Clamped to 0-1 internally.
18
18
  */
19
19
  weight: number;
20
20
  /**
@@ -22,7 +22,7 @@ export declare class AimChainIK {
22
22
  *
23
23
  * `[1, 1, 1, 1]` = uniform. `[0.2, 0.5, 0.8, 1.0]` = root gets least, tip gets most.
24
24
  *
25
- * Compared by reference mutating values in-place won't trigger
25
+ * Compared by reference - mutating values in-place won't trigger
26
26
  * renormalization. Assign a new array to update.
27
27
  *
28
28
  * Length must match bone count; mismatches throw on `solve()`.
@@ -43,7 +43,7 @@ export declare class AimChainIK {
43
43
  *
44
44
  * Both vectors are in world space and are not mutated.
45
45
  *
46
- * Sample directions **before** calling this method mutates bone quaternions,
46
+ * Sample directions **before** calling - this method mutates bone quaternions,
47
47
  * so any direction derived from the chain will be stale after the call.
48
48
  *
49
49
  * @param currentDirection - Where the chain currently aims.
@@ -18,7 +18,7 @@ class AimChainIK {
18
18
  this.epsilon = 1e-5;
19
19
  /**
20
20
  * Global blend weight. 0 = solver has no effect, 1 = full effect.
21
- * Clamped to 01 internally.
21
+ * Clamped to 0-1 internally.
22
22
  */
23
23
  this.weight = 1;
24
24
  this._private_swingAxis = new Vector3();
@@ -38,7 +38,7 @@ class AimChainIK {
38
38
  *
39
39
  * Both vectors are in world space and are not mutated.
40
40
  *
41
- * Sample directions **before** calling this method mutates bone quaternions,
41
+ * Sample directions **before** calling - this method mutates bone quaternions,
42
42
  * so any direction derived from the chain will be stale after the call.
43
43
  *
44
44
  * @param currentDirection - Where the chain currently aims.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,11 @@
1
+ export { EnhancedPerspectiveCamera } from "./camera/EnhancedPerspectiveCamera";
2
+ export { FovPolicy } from "./camera/fovPolicies/FovPolicy";
3
+ export { FovPolicyCover } from "./camera/fovPolicies/FovPolicyCover";
4
+ export { FovPolicyFit } from "./camera/fovPolicies/FovPolicyFit";
5
+ export { FovPolicyFixedHorizontal } from "./camera/fovPolicies/FovPolicyFixedHorizontal";
6
+ export { FovPolicyFixedVertical } from "./camera/fovPolicies/FovPolicyFixedVertical";
7
+ export { FovPolicyHybrid } from "./camera/fovPolicies/FovPolicyHybrid";
8
+ export { FovPolicyHybridInverted } from "./camera/fovPolicies/FovPolicyHybridInverted";
1
9
  export { AimChainIK } from "./ik/AimChainIK";
2
10
  export { TwoBoneIK } from "./ik/TwoBoneIK";
3
11
  export { InstancedMeshGroup } from "./instancedMeshPool/InstancedMeshGroup";
@@ -11,7 +19,6 @@ export { StandardToLambertConverter } from "./materialConverters/StandardToLambe
11
19
  export { StandardToPhongConverter } from "./materialConverters/StandardToPhongConverter";
12
20
  export { StandardToPhysicalConverter } from "./materialConverters/StandardToPhysicalConverter";
13
21
  export { StandardToToonConverter } from "./materialConverters/StandardToToonConverter";
14
- export { DualFovCamera } from "./miscellaneous/DualFovCamera";
15
22
  export { SceneSorter } from "./miscellaneous/SceneSorter";
16
23
  export { SceneTraversal } from "./miscellaneous/SceneTraversal";
17
24
  export { SkinnedMeshBaker } from "./miscellaneous/SkinnedMeshBaker";
package/dist/index.js CHANGED
@@ -1,3 +1,11 @@
1
+ export { EnhancedPerspectiveCamera } from './camera/EnhancedPerspectiveCamera.js';
2
+ export { FovPolicy } from './camera/fovPolicies/FovPolicy.js';
3
+ export { FovPolicyCover } from './camera/fovPolicies/FovPolicyCover.js';
4
+ export { FovPolicyFit } from './camera/fovPolicies/FovPolicyFit.js';
5
+ export { FovPolicyFixedHorizontal } from './camera/fovPolicies/FovPolicyFixedHorizontal.js';
6
+ export { FovPolicyFixedVertical } from './camera/fovPolicies/FovPolicyFixedVertical.js';
7
+ export { FovPolicyHybrid } from './camera/fovPolicies/FovPolicyHybrid.js';
8
+ export { FovPolicyHybridInverted } from './camera/fovPolicies/FovPolicyHybridInverted.js';
1
9
  export { AimChainIK } from './ik/AimChainIK.js';
2
10
  export { TwoBoneIK } from './ik/TwoBoneIK.js';
3
11
  export { InstancedMeshGroup } from './instancedMeshPool/InstancedMeshGroup.js';
@@ -11,7 +19,6 @@ export { StandardToLambertConverter } from './materialConverters/StandardToLambe
11
19
  export { StandardToPhongConverter } from './materialConverters/StandardToPhongConverter.js';
12
20
  export { StandardToPhysicalConverter } from './materialConverters/StandardToPhysicalConverter.js';
13
21
  export { StandardToToonConverter } from './materialConverters/StandardToToonConverter.js';
14
- export { DualFovCamera } from './miscellaneous/DualFovCamera.js';
15
22
  export { SceneSorter } from './miscellaneous/SceneSorter.js';
16
23
  export { SceneTraversal } from './miscellaneous/SceneTraversal.js';
17
24
  export { SkinnedMeshBaker } from './miscellaneous/SkinnedMeshBaker.js';
@@ -12,9 +12,9 @@ export declare class InstancedMeshInstance {
12
12
  private needsUpdateInstancedMatrixFromLocalMatrix;
13
13
  private handler;
14
14
  constructor(pool: InstancedMeshPool, geometry: BufferGeometry, material: Material, tag?: string);
15
+ get isAlive(): boolean;
15
16
  static fromInstancedMesh(pool: InstancedMeshPool, mesh: InstancedMesh<BufferGeometry, Material>, tag?: string): InstancedMeshInstance[];
16
17
  destroy(): void;
17
- isDestroyed(): boolean;
18
18
  setPosition(source: Vector3, flushTransform?: boolean): this;
19
19
  setPosition3f(x: number, y: number, z: number, flushTransform?: boolean): this;
20
20
  setQuaternion(source: Quaternion, flushTransform?: boolean): this;
@@ -12,6 +12,9 @@ class InstancedMeshInstance {
12
12
  this._private_needsUpdateInstancedMatrixFromLocalMatrix = false;
13
13
  this._private_handler = this._private_pool.allocate(geometry, material, tag);
14
14
  }
15
+ get isAlive() {
16
+ return this._private_handler >= 0;
17
+ }
15
18
  static fromInstancedMesh(pool, mesh, tag = "") {
16
19
  const matrix = new Matrix4();
17
20
  const instances = [];
@@ -24,14 +27,11 @@ class InstancedMeshInstance {
24
27
  return instances;
25
28
  }
26
29
  destroy() {
27
- if (this._private_handler >= 0) {
30
+ if (this.isAlive) {
28
31
  this._private_pool.deallocate(this._private_handler);
29
32
  this._private_handler = -1;
30
33
  }
31
34
  }
32
- isDestroyed() {
33
- return this._private_handler < 0;
34
- }
35
35
  setPosition(source, flushTransform = false) {
36
36
  this._private_updateTransformFromMatrix();
37
37
  if (!this._private_position.equals(source)) {
@@ -114,7 +114,7 @@ class InstancedMeshInstance {
114
114
  return this;
115
115
  }
116
116
  flushTransform() {
117
- if (this._private_handler < 0) {
117
+ if (!this.isAlive) {
118
118
  return;
119
119
  }
120
120
  this._private_updateMatrixFromTransform();
@@ -15,8 +15,7 @@ export declare class InstancedMeshPool {
15
15
  isValidHandler(handler: number): boolean;
16
16
  sortMeshes(baseRenderOrder: number, compare: (a: InstancedMesh, aTag: string, b: InstancedMesh, bTag: string) => number): void;
17
17
  sortInstances(compare: (matrixA: Matrix4, matrixB: Matrix4, tag: string) => number): void;
18
- dispose(): void;
19
- protected getTransformMatrix(handler: number, target: Matrix4): Matrix4 | undefined;
18
+ destroy(): void;
20
19
  private getOrCreateEntry;
21
20
  private growEntry;
22
21
  private sortEntryInstances;
@@ -33,7 +33,7 @@ class InstancedMeshPool {
33
33
  this._private_sortEntryInstances(entry, compare);
34
34
  }
35
35
  }
36
- dispose() {
36
+ destroy() {
37
37
  for (const entry of this._private_entries.values()) {
38
38
  this._private_scene.remove(entry.mesh);
39
39
  entry.mesh.dispose();
@@ -87,6 +87,7 @@ class InstancedMeshPool {
87
87
  descriptor.entry.mesh.setMatrixAt(descriptor.index, matrix);
88
88
  descriptor.entry.mesh.instanceMatrix.needsUpdate = true;
89
89
  }
90
+ /** @internal */
90
91
  getTransformMatrix(handler, target) {
91
92
  const descriptor = this._private_descriptors.get(handler);
92
93
  if (descriptor === undefined) {
@@ -150,6 +151,7 @@ class InstancedMeshPool {
150
151
  }
151
152
  return result;
152
153
  });
154
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- needsReorder is set by the sort callback
153
155
  if (!needsReorder) {
154
156
  return;
155
157
  }
@@ -50,8 +50,8 @@ export declare class Sun extends DirectionalLight {
50
50
  /** @param texture - Must have image data. */
51
51
  setDirectionFromHDRTexture(texture: Texture, distance?: number): void;
52
52
  private findBrightestPixel;
53
- /** Stores world-space frustum corners in `tempVector3D0`–`tempVector3D7`. */
53
+ /** Stores world-space frustum corners in `tempVector3D0`-`tempVector3D7`. */
54
54
  private computeFrustumPoints;
55
- /** Stores world-space frustum corners in `tempVector3D0`–`tempVector3D7`. */
55
+ /** Stores world-space frustum corners in `tempVector3D0`-`tempVector3D7`. */
56
56
  private computeOrthographicPoints;
57
57
  }
@@ -186,7 +186,7 @@ class Sun extends DirectionalLight {
186
186
  }
187
187
  return { index: maxIndex, luminance: maxLuminance };
188
188
  }
189
- /** Stores world-space frustum corners in `tempVector3D0`–`tempVector3D7`. */
189
+ /** Stores world-space frustum corners in `tempVector3D0`-`tempVector3D7`. */
190
190
  _private_computeFrustumPoints(camera) {
191
191
  const fovRad = camera.fov * MathUtils.DEG2RAD;
192
192
  const halfTanFov = Math.tan(fovRad / 2);
@@ -211,7 +211,7 @@ class Sun extends DirectionalLight {
211
211
  this._private_tempVector3D6.applyMatrix4(camera.matrixWorld);
212
212
  this._private_tempVector3D7.applyMatrix4(camera.matrixWorld);
213
213
  }
214
- /** Stores world-space frustum corners in `tempVector3D0`–`tempVector3D7`. */
214
+ /** Stores world-space frustum corners in `tempVector3D0`-`tempVector3D7`. */
215
215
  _private_computeOrthographicPoints(camera) {
216
216
  this._private_tempVector3D0.set(camera.left, camera.bottom, -camera.near);
217
217
  this._private_tempVector3D1.set(camera.right, camera.bottom, -camera.near);
@@ -0,0 +1,6 @@
1
+ import { Material, Mesh, Object3D } from "three";
2
+ export declare class SceneResolver {
3
+ static resolveObject3DByName(scene: Object3D, name: string): Object3D;
4
+ static resolveMeshByName(scene: Object3D, name: string): Mesh;
5
+ static resolveMaterialByName(scene: Object3D, name: string): Material;
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-zoo",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Some reusable bits for building things with Three.js",
5
5
  "keywords": [
6
6
  "three.js",
@@ -1,43 +0,0 @@
1
- import type { Box3, SkinnedMesh } from "three";
2
- import { PerspectiveCamera, Vector3 } from "three";
3
- /**
4
- * Camera with independent horizontal and vertical FOV settings.
5
- */
6
- export declare class DualFovCamera extends PerspectiveCamera {
7
- private horizontalFovInternal;
8
- private verticalFovInternal;
9
- /**
10
- * @param horizontalFov - Clamped to 1–179°.
11
- * @param verticalFov - Clamped to 1–179°.
12
- */
13
- constructor(horizontalFov?: number, verticalFov?: number, aspect?: number, near?: number, far?: number);
14
- get horizontalFov(): number;
15
- get verticalFov(): number;
16
- /** Clamped to 1–179°. */
17
- set horizontalFov(value: number);
18
- /** Clamped to 1–179°. */
19
- set verticalFov(value: number);
20
- /** Both values clamped to 1–179°. */
21
- setFov(horizontal: number, vertical: number): void;
22
- copyFovSettings(source: DualFovCamera): void;
23
- /**
24
- * Landscape (aspect > 1): preserves horizontal FOV.
25
- * Portrait (aspect ≤ 1): preserves vertical FOV.
26
- *
27
- * @override
28
- */
29
- updateProjectionMatrix(): void;
30
- /** Effective horizontal FOV accounting for current aspect ratio. */
31
- getActualHorizontalFov(): number;
32
- /** Effective vertical FOV accounting for current aspect ratio. */
33
- getActualVerticalFov(): number;
34
- /** @param vertices - World-space points. */
35
- fitVerticalFovToPoints(vertices: Vector3[]): void;
36
- /** @param box - World-space box. */
37
- fitVerticalFovToBox(box: Box3): void;
38
- /** Calls `skeleton.update()` internally before sampling vertices. */
39
- fitVerticalFovToMesh(skinnedMesh: SkinnedMesh): void;
40
- /** Uses iterative vertex clustering to approximate center of mass. */
41
- lookAtMeshCenterOfMass(skinnedMesh: SkinnedMesh): void;
42
- clone(): this;
43
- }
@@ -1,170 +0,0 @@
1
- import { PerspectiveCamera, MathUtils, Vector3 } from 'three';
2
-
3
- const DEFAULT_HORIZONTAL_FOV = 90;
4
- const DEFAULT_VERTICAL_FOV = 90;
5
- const DEFAULT_ASPECT = 1;
6
- const DEFAULT_NEAR = 1;
7
- const DEFAULT_FAR = 1000;
8
- const MIN_FOV = 1;
9
- const MAX_FOV = 179;
10
- /**
11
- * Camera with independent horizontal and vertical FOV settings.
12
- */
13
- class DualFovCamera extends PerspectiveCamera {
14
- /**
15
- * @param horizontalFov - Clamped to 1–179°.
16
- * @param verticalFov - Clamped to 1–179°.
17
- */
18
- constructor(horizontalFov = DEFAULT_HORIZONTAL_FOV, verticalFov = DEFAULT_VERTICAL_FOV, aspect = DEFAULT_ASPECT, near = DEFAULT_NEAR, far = DEFAULT_FAR) {
19
- super(verticalFov, aspect, near, far);
20
- this._private_horizontalFovInternal = horizontalFov;
21
- this._private_verticalFovInternal = verticalFov;
22
- this.updateProjectionMatrix();
23
- }
24
- get horizontalFov() {
25
- return this._private_horizontalFovInternal;
26
- }
27
- get verticalFov() {
28
- return this._private_verticalFovInternal;
29
- }
30
- /** Clamped to 1–179°. */
31
- set horizontalFov(value) {
32
- this._private_horizontalFovInternal = MathUtils.clamp(value, MIN_FOV, MAX_FOV);
33
- this.updateProjectionMatrix();
34
- }
35
- /** Clamped to 1–179°. */
36
- set verticalFov(value) {
37
- this._private_verticalFovInternal = MathUtils.clamp(value, MIN_FOV, MAX_FOV);
38
- this.updateProjectionMatrix();
39
- }
40
- /** Both values clamped to 1–179°. */
41
- setFov(horizontal, vertical) {
42
- this._private_horizontalFovInternal = MathUtils.clamp(horizontal, MIN_FOV, MAX_FOV);
43
- this._private_verticalFovInternal = MathUtils.clamp(vertical, MIN_FOV, MAX_FOV);
44
- this.updateProjectionMatrix();
45
- }
46
- copyFovSettings(source) {
47
- this._private_horizontalFovInternal = source.horizontalFov;
48
- this._private_verticalFovInternal = source.verticalFov;
49
- this.updateProjectionMatrix();
50
- }
51
- /**
52
- * Landscape (aspect > 1): preserves horizontal FOV.
53
- * Portrait (aspect ≤ 1): preserves vertical FOV.
54
- *
55
- * @override
56
- */
57
- updateProjectionMatrix() {
58
- if (this.aspect > 1) {
59
- const radians = MathUtils.degToRad(this._private_horizontalFovInternal);
60
- this.fov = MathUtils.radToDeg(Math.atan(Math.tan(radians / 2) / this.aspect) * 2);
61
- }
62
- else {
63
- this.fov = this._private_verticalFovInternal;
64
- }
65
- super.updateProjectionMatrix();
66
- }
67
- /** Effective horizontal FOV accounting for current aspect ratio. */
68
- getActualHorizontalFov() {
69
- if (this.aspect >= 1) {
70
- return this._private_horizontalFovInternal;
71
- }
72
- const verticalRadians = MathUtils.degToRad(this._private_verticalFovInternal);
73
- return MathUtils.radToDeg(Math.atan(Math.tan(verticalRadians / 2) * this.aspect) * 2);
74
- }
75
- /** Effective vertical FOV accounting for current aspect ratio. */
76
- getActualVerticalFov() {
77
- if (this.aspect < 1) {
78
- return this._private_verticalFovInternal;
79
- }
80
- const horizontalRadians = MathUtils.degToRad(this._private_horizontalFovInternal);
81
- return MathUtils.radToDeg(Math.atan(Math.tan(horizontalRadians / 2) / this.aspect) * 2);
82
- }
83
- /** @param vertices - World-space points. */
84
- fitVerticalFovToPoints(vertices) {
85
- const up = new Vector3(0, 1, 0).applyQuaternion(this.quaternion);
86
- let maxVerticalAngle = 0;
87
- for (const vertex of vertices) {
88
- const vertexToCam = this.position.clone().sub(vertex);
89
- const vertexDirection = vertexToCam.normalize();
90
- const verticalAngle = Math.asin(Math.abs(vertexDirection.dot(up))) * Math.sign(vertexDirection.dot(up));
91
- if (Math.abs(verticalAngle) > maxVerticalAngle) {
92
- maxVerticalAngle = Math.abs(verticalAngle);
93
- }
94
- }
95
- const requiredFov = MathUtils.radToDeg(2 * maxVerticalAngle);
96
- this._private_verticalFovInternal = MathUtils.clamp(requiredFov, MIN_FOV, MAX_FOV);
97
- this.updateProjectionMatrix();
98
- }
99
- /** @param box - World-space box. */
100
- fitVerticalFovToBox(box) {
101
- this.fitVerticalFovToPoints([
102
- new Vector3(box.min.x, box.min.y, box.min.z),
103
- new Vector3(box.min.x, box.min.y, box.max.z),
104
- new Vector3(box.min.x, box.max.y, box.min.z),
105
- new Vector3(box.min.x, box.max.y, box.max.z),
106
- new Vector3(box.max.x, box.min.y, box.min.z),
107
- new Vector3(box.max.x, box.min.y, box.max.z),
108
- new Vector3(box.max.x, box.max.y, box.min.z),
109
- new Vector3(box.max.x, box.max.y, box.max.z),
110
- ]);
111
- }
112
- /** Calls `skeleton.update()` internally before sampling vertices. */
113
- fitVerticalFovToMesh(skinnedMesh) {
114
- skinnedMesh.updateWorldMatrix(true, true);
115
- skinnedMesh.skeleton.update();
116
- const bakedGeometry = skinnedMesh.geometry;
117
- const position = bakedGeometry.attributes["position"];
118
- const target = new Vector3();
119
- const points = [];
120
- for (let i = 0; i < position.count; i++) {
121
- target.fromBufferAttribute(position, i);
122
- skinnedMesh.applyBoneTransform(i, target);
123
- points.push(target.clone());
124
- }
125
- this.fitVerticalFovToPoints(points);
126
- }
127
- /** Uses iterative vertex clustering to approximate center of mass. */
128
- lookAtMeshCenterOfMass(skinnedMesh) {
129
- skinnedMesh.updateWorldMatrix(true, true);
130
- skinnedMesh.skeleton.update();
131
- const bakedGeometry = skinnedMesh.geometry;
132
- const position = bakedGeometry.attributes.position;
133
- const target = new Vector3();
134
- const points = [];
135
- for (let i = 0; i < position.count; i++) {
136
- target.fromBufferAttribute(position, i);
137
- skinnedMesh.applyBoneTransform(i, target);
138
- points.push(target.clone());
139
- }
140
- const findMainCluster = (points, iterations = 3) => {
141
- if (points.length === 0) {
142
- return new Vector3();
143
- }
144
- let center = points[Math.floor(points.length / 2)].clone();
145
- for (let i = 0; i < iterations; i++) {
146
- let total = new Vector3();
147
- let count = 0;
148
- for (const point of points) {
149
- if (point.distanceTo(center) < point.distanceTo(total) || count === 0) {
150
- total.add(point);
151
- count++;
152
- }
153
- }
154
- if (count > 0) {
155
- center = total.divideScalar(count);
156
- }
157
- }
158
- return center;
159
- };
160
- const centerOfMass = findMainCluster(points);
161
- this.lookAt(centerOfMass);
162
- }
163
- clone() {
164
- const camera = new DualFovCamera(this._private_horizontalFovInternal, this._private_verticalFovInternal, this.aspect, this.near, this.far);
165
- camera.copy(this, true);
166
- return camera;
167
- }
168
- }
169
-
170
- export { DualFovCamera };