micro-gl 0.1.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 (82) hide show
  1. package/README.md +556 -0
  2. package/package.json +37 -0
  3. package/src/2d/cameras/Camera2d.js +46 -0
  4. package/src/2d/constants.js +3 -0
  5. package/src/2d/controls/DragControls2d.js +174 -0
  6. package/src/2d/controls/PanZoomControls.js +64 -0
  7. package/src/2d/core/GpuResources2d.js +163 -0
  8. package/src/2d/core/InstancedShape2d.js +74 -0
  9. package/src/2d/core/Object2d.js +107 -0
  10. package/src/2d/core/Pipelines2d.js +110 -0
  11. package/src/2d/core/Renderer2d.js +353 -0
  12. package/src/2d/core/Scene2d.js +13 -0
  13. package/src/2d/core/Shape2d.js +13 -0
  14. package/src/2d/core/Uniforms2d.js +13 -0
  15. package/src/2d/geometries/CircleGeometry.js +27 -0
  16. package/src/2d/geometries/Geometry2d.js +108 -0
  17. package/src/2d/geometries/RectGeometry.js +23 -0
  18. package/src/2d/materials/BasicMaterial2d.js +12 -0
  19. package/src/2d/materials/Material2d.js +78 -0
  20. package/src/2d/materials/SpriteMaterial2d.js +28 -0
  21. package/src/2d/shaders/fragments.js +23 -0
  22. package/src/2d/shaders/shared.js +28 -0
  23. package/src/2d/shaders/vertexLayout.js +74 -0
  24. package/src/2d/shaders/vertexStages.js +59 -0
  25. package/src/3d/cameras/Camera.js +66 -0
  26. package/src/3d/cameras/OrthographicCamera.js +35 -0
  27. package/src/3d/cameras/PerspectiveCamera.js +25 -0
  28. package/src/3d/constants.js +18 -0
  29. package/src/3d/controls/DragControls.js +168 -0
  30. package/src/3d/controls/OrbitControls.js +125 -0
  31. package/src/3d/core/DirectionalShadowMap.js +283 -0
  32. package/src/3d/core/GpuResources.js +174 -0
  33. package/src/3d/core/InstanceMatrix.js +55 -0
  34. package/src/3d/core/InstancedBounds.js +114 -0
  35. package/src/3d/core/InstancedMesh.js +124 -0
  36. package/src/3d/core/Mesh.js +26 -0
  37. package/src/3d/core/Object3d.js +101 -0
  38. package/src/3d/core/Pipelines.js +125 -0
  39. package/src/3d/core/RayIntersection.js +184 -0
  40. package/src/3d/core/Raycaster.js +246 -0
  41. package/src/3d/core/Renderer.js +492 -0
  42. package/src/3d/core/Scene.js +13 -0
  43. package/src/3d/core/ShadowPipelines.js +64 -0
  44. package/src/3d/core/Uniforms.js +132 -0
  45. package/src/3d/geometries/BoxGeometry.js +56 -0
  46. package/src/3d/geometries/Geometry.js +97 -0
  47. package/src/3d/geometries/PlaneGeometry.js +24 -0
  48. package/src/3d/geometries/SphereGeometry.js +46 -0
  49. package/src/3d/geometries/WireframeGeometry.js +39 -0
  50. package/src/3d/helpers/GridHelper.js +43 -0
  51. package/src/3d/lights/AmbientLight.js +18 -0
  52. package/src/3d/lights/DirectionalLight.js +26 -0
  53. package/src/3d/lights/DirectionalShadow.js +29 -0
  54. package/src/3d/lights/PointLight.js +23 -0
  55. package/src/3d/materials/BasicMaterial.js +11 -0
  56. package/src/3d/materials/LambertMaterial.js +13 -0
  57. package/src/3d/materials/Material.js +87 -0
  58. package/src/3d/materials/TextureMaterial.js +24 -0
  59. package/src/3d/shaders/fragments.js +34 -0
  60. package/src/3d/shaders/shadows.js +52 -0
  61. package/src/3d/shaders/shared.js +129 -0
  62. package/src/3d/shaders/vertexLayout.js +85 -0
  63. package/src/3d/shaders/vertexStages.js +70 -0
  64. package/src/core/PointerControls.js +258 -0
  65. package/src/core/Texture.js +87 -0
  66. package/src/core/createMaterialPipelineLayouts.js +114 -0
  67. package/src/core/deviceLease.js +67 -0
  68. package/src/core/generateMipmaps.js +115 -0
  69. package/src/core/indexedTriangles.js +53 -0
  70. package/src/core/initWebGpu.js +65 -0
  71. package/src/core/materialResources.js +18 -0
  72. package/src/core/objectGpuResources.js +68 -0
  73. package/src/core/pipelineConstants.js +68 -0
  74. package/src/core/rendererConfig.js +66 -0
  75. package/src/core/uploadTexture.js +57 -0
  76. package/src/index.js +68 -0
  77. package/src/math/Frustum.js +143 -0
  78. package/src/math/Mat3.js +165 -0
  79. package/src/math/Mat4.js +359 -0
  80. package/src/math/Vec2.js +72 -0
  81. package/src/math/Vec3.js +98 -0
  82. package/src/math/color.js +20 -0
@@ -0,0 +1,23 @@
1
+ import {
2
+ SHADER_BIND_GROUP,
3
+ SHADER_BINDING,
4
+ } from '../../core/pipelineConstants.js';
5
+
6
+ export const BASIC_FRAGMENT_SHADER_2D = /* wgsl */ `
7
+ @fragment
8
+ fn fs(input: VertexOut) -> @location(0) vec4f {
9
+ return objectColor(input);
10
+ }
11
+ `;
12
+
13
+ export const SPRITE_FRAGMENT_SHADER_2D = /* wgsl */ `
14
+ @group(${SHADER_BIND_GROUP.object}) @binding(${SHADER_BINDING.map})
15
+ var uMap: texture_2d<f32>;
16
+ @group(${SHADER_BIND_GROUP.object}) @binding(${SHADER_BINDING.sampler})
17
+ var uMapSampler: sampler;
18
+
19
+ @fragment
20
+ fn fs(input: VertexOut) -> @location(0) vec4f {
21
+ return textureSample(uMap, uMapSampler, input.uv) * objectColor(input);
22
+ }
23
+ `;
@@ -0,0 +1,28 @@
1
+ import {
2
+ SHADER_BIND_GROUP,
3
+ SHADER_BINDING,
4
+ } from '../../core/pipelineConstants.js';
5
+ import { VERTEX_ATTRIBUTE_2D } from './vertexLayout.js';
6
+
7
+ /** Uniform declarations shared by every 2D shader. */
8
+ export const SHARED_SHADER_CHUNKS_2D = /* wgsl */ `
9
+ struct FrameUniforms {
10
+ viewProjection: mat3x3f,
11
+ };
12
+
13
+ struct ObjectUniforms {
14
+ transform: mat3x3f,
15
+ color: vec4f,
16
+ };
17
+
18
+ @group(${SHADER_BIND_GROUP.frame}) @binding(${SHADER_BINDING.uniforms})
19
+ var<uniform> uFrame: FrameUniforms;
20
+ @group(${SHADER_BIND_GROUP.object}) @binding(${SHADER_BINDING.uniforms})
21
+ var<uniform> uObject: ObjectUniforms;
22
+
23
+ struct VertexIn {
24
+ @location(${VERTEX_ATTRIBUTE_2D.position}) position: vec2f,
25
+ @location(${VERTEX_ATTRIBUTE_2D.uv}) uv: vec2f,
26
+ };
27
+
28
+ `;
@@ -0,0 +1,74 @@
1
+ import { VERTEX_STRIDE_2D } from '../geometries/Geometry2d.js';
2
+ import { INSTANCE_SIZE_2D } from '../constants.js';
3
+ import { INSTANCE_STEP_MODE } from '../../core/pipelineConstants.js';
4
+
5
+ const FLOAT_BYTES = Float32Array.BYTES_PER_ELEMENT;
6
+ const VEC2_BYTES = 2 * FLOAT_BYTES;
7
+ const PADDED_VEC3_BYTES = 4 * FLOAT_BYTES;
8
+ const MAT3_BYTES = 3 * PADDED_VEC3_BYTES;
9
+
10
+ /** Vertex input locations shared by WGSL and the GPU buffer descriptors. */
11
+ export const VERTEX_ATTRIBUTE_2D = Object.freeze({
12
+ position: 0,
13
+ uv: 1,
14
+ instanceMatrix0: 2,
15
+ instanceMatrix1: 3,
16
+ instanceMatrix2: 4,
17
+ instanceColor: 5,
18
+ });
19
+
20
+ export const GEOMETRY_VERTEX_BUFFER_LAYOUT_2D = Object.freeze({
21
+ arrayStride: VERTEX_STRIDE_2D,
22
+ attributes: Object.freeze([
23
+ Object.freeze({
24
+ shaderLocation: VERTEX_ATTRIBUTE_2D.position,
25
+ offset: 0,
26
+ format: 'float32x2',
27
+ }),
28
+ Object.freeze({
29
+ shaderLocation: VERTEX_ATTRIBUTE_2D.uv,
30
+ offset: VEC2_BYTES,
31
+ format: 'float32x2',
32
+ }),
33
+ ]),
34
+ });
35
+
36
+ export const INSTANCE_VERTEX_BUFFER_LAYOUT_2D = Object.freeze({
37
+ arrayStride: INSTANCE_SIZE_2D * FLOAT_BYTES,
38
+ stepMode: INSTANCE_STEP_MODE,
39
+ attributes: Object.freeze([
40
+ Object.freeze({
41
+ shaderLocation: VERTEX_ATTRIBUTE_2D.instanceMatrix0,
42
+ offset: 0,
43
+ format: 'float32x3',
44
+ }),
45
+ Object.freeze({
46
+ shaderLocation: VERTEX_ATTRIBUTE_2D.instanceMatrix1,
47
+ offset: PADDED_VEC3_BYTES,
48
+ format: 'float32x3',
49
+ }),
50
+ Object.freeze({
51
+ shaderLocation: VERTEX_ATTRIBUTE_2D.instanceMatrix2,
52
+ offset: 2 * PADDED_VEC3_BYTES,
53
+ format: 'float32x3',
54
+ }),
55
+ Object.freeze({
56
+ shaderLocation: VERTEX_ATTRIBUTE_2D.instanceColor,
57
+ offset: MAT3_BYTES,
58
+ format: 'float32x4',
59
+ }),
60
+ ]),
61
+ });
62
+
63
+ const STANDARD_VERTEX_BUFFERS_2D = Object.freeze([
64
+ GEOMETRY_VERTEX_BUFFER_LAYOUT_2D,
65
+ ]);
66
+ const INSTANCED_VERTEX_BUFFERS_2D = Object.freeze([
67
+ GEOMETRY_VERTEX_BUFFER_LAYOUT_2D,
68
+ INSTANCE_VERTEX_BUFFER_LAYOUT_2D,
69
+ ]);
70
+
71
+ /** GPU vertex buffer layouts for a regular or instanced shape pipeline. */
72
+ export function vertexBufferLayouts2d(instanced) {
73
+ return instanced ? INSTANCED_VERTEX_BUFFERS_2D : STANDARD_VERTEX_BUFFERS_2D;
74
+ }
@@ -0,0 +1,59 @@
1
+ import { SHARED_SHADER_CHUNKS_2D } from './shared.js';
2
+ import { VERTEX_ATTRIBUTE_2D } from './vertexLayout.js';
3
+
4
+ /** Shared chunks plus the regular shape vertex stage. Append a fragment stage. */
5
+ export const SHAPE_SHADER_PREFIX =
6
+ SHARED_SHADER_CHUNKS_2D +
7
+ /* wgsl */ `
8
+ struct VertexOut {
9
+ @builtin(position) position: vec4f,
10
+ @location(0) uv: vec2f,
11
+ };
12
+
13
+ @vertex
14
+ fn vs(input: VertexIn) -> VertexOut {
15
+ var out: VertexOut;
16
+ let p = uFrame.viewProjection * uObject.transform * vec3f(input.position, 1.0);
17
+ out.position = vec4f(p.xy, 0.0, 1.0);
18
+ out.uv = input.uv;
19
+ return out;
20
+ }
21
+
22
+ fn objectColor(input: VertexOut) -> vec4f {
23
+ return uObject.color;
24
+ }
25
+ `;
26
+
27
+ /** Shared chunks plus the per-instance shape vertex stage. Append a fragment stage. */
28
+ export const INSTANCED_SHAPE_SHADER_PREFIX =
29
+ SHARED_SHADER_CHUNKS_2D +
30
+ /* wgsl */ `
31
+ struct InstanceIn {
32
+ @location(${VERTEX_ATTRIBUTE_2D.instanceMatrix0}) im0: vec3f,
33
+ @location(${VERTEX_ATTRIBUTE_2D.instanceMatrix1}) im1: vec3f,
34
+ @location(${VERTEX_ATTRIBUTE_2D.instanceMatrix2}) im2: vec3f,
35
+ @location(${VERTEX_ATTRIBUTE_2D.instanceColor}) color: vec4f,
36
+ };
37
+
38
+ struct VertexOut {
39
+ @builtin(position) position: vec4f,
40
+ @location(0) uv: vec2f,
41
+ @location(1) color: vec4f,
42
+ };
43
+
44
+ @vertex
45
+ fn vs(input: VertexIn, instance: InstanceIn) -> VertexOut {
46
+ var out: VertexOut;
47
+ let instanceMatrix = mat3x3f(instance.im0, instance.im1, instance.im2);
48
+ let p = uFrame.viewProjection * uObject.transform * instanceMatrix
49
+ * vec3f(input.position, 1.0);
50
+ out.position = vec4f(p.xy, 0.0, 1.0);
51
+ out.uv = input.uv;
52
+ out.color = instance.color;
53
+ return out;
54
+ }
55
+
56
+ fn objectColor(input: VertexOut) -> vec4f {
57
+ return input.color * uObject.color;
58
+ }
59
+ `;
@@ -0,0 +1,66 @@
1
+ import { Object3d } from '../core/Object3d.js';
2
+ import { Vec3 } from '../../math/Vec3.js';
3
+ import { Mat4 } from '../../math/Mat4.js';
4
+
5
+ /**
6
+ * Base class for cameras. Set `position`, call `lookAt(...)` to aim it,
7
+ * and the renderer takes care of the rest (it keeps `aspect` in sync
8
+ * with the canvas and refreshes the matrices every frame).
9
+ *
10
+ * Subclasses implement `updateProjectionMatrix()` and must call it once
11
+ * at the end of their constructor.
12
+ *
13
+ * Note: for simplicity a camera derives its orientation from
14
+ * position + target (not from `rotation`), and is expected to be a
15
+ * direct child of the scene (or unparented).
16
+ */
17
+ export class Camera extends Object3d {
18
+ /**
19
+ * @param {number} aspect width / height
20
+ */
21
+ constructor(aspect = 1, near = 0.1, far = 1000) {
22
+ super();
23
+ this.aspect = aspect;
24
+ this.near = near;
25
+ this.far = far;
26
+
27
+ this.up = new Vec3(0, 1, 0);
28
+ this.target = new Vec3(0, 0, 0);
29
+
30
+ this.projectionMatrix = new Mat4();
31
+ this.viewMatrix = new Mat4();
32
+ this.viewProjectionMatrix = new Mat4();
33
+ }
34
+
35
+ /** Aims the camera at a point. Accepts a Vec3 or (x, y, z). */
36
+ lookAt(x, y, z) {
37
+ if (typeof x === 'object') {
38
+ this.target.copy(x);
39
+ } else {
40
+ this.target.set(x, y, z);
41
+ }
42
+ return this;
43
+ }
44
+
45
+ updateProjectionMatrix() {
46
+ throw new Error('Camera subclasses must implement updateProjectionMatrix');
47
+ }
48
+
49
+ /** Called by the renderer every frame. */
50
+ updateMatrices() {
51
+ this.updateProjectionMatrix();
52
+ this.worldMatrix.targetTo(this.position, this.target, this.up);
53
+ this.viewMatrix.copy(this.worldMatrix).invert();
54
+ this.viewProjectionMatrix.multiplyMatrices(
55
+ this.projectionMatrix,
56
+ this.viewMatrix,
57
+ );
58
+
59
+ // The scene graph was updated before the camera replaced its TRS world
60
+ // matrix with the look-at orientation. Refresh camera-mounted objects so
61
+ // they inherit the final camera basis (for example, a HUD or headlight).
62
+ for (const child of this.children) {
63
+ child.updateWorldMatrix();
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,35 @@
1
+ import { Camera } from './Camera.js';
2
+
3
+ /**
4
+ * An orthographic camera: objects keep the same size regardless of
5
+ * distance.
6
+ *
7
+ * The view volume is defined by `size` (half the visible height in
8
+ * world units) and `aspect`. Increase `zoom` to magnify.
9
+ */
10
+ export class OrthographicCamera extends Camera {
11
+ /**
12
+ * @param {number} size half of the visible height in world units
13
+ * @param {number} aspect width / height
14
+ */
15
+ constructor(size = 5, aspect = 1, near = 0.1, far = 1000) {
16
+ super(aspect, near, far);
17
+ this.isOrthographic = true;
18
+ this.size = size;
19
+ this.zoom = 1;
20
+ this.updateProjectionMatrix();
21
+ }
22
+
23
+ updateProjectionMatrix() {
24
+ const halfH = this.size / this.zoom;
25
+ const halfW = halfH * this.aspect;
26
+ this.projectionMatrix.orthographic(
27
+ -halfW,
28
+ halfW,
29
+ -halfH,
30
+ halfH,
31
+ this.near,
32
+ this.far,
33
+ );
34
+ }
35
+ }
@@ -0,0 +1,25 @@
1
+ import { Camera } from './Camera.js';
2
+
3
+ /**
4
+ * A perspective camera: distant objects appear smaller, like a real lens.
5
+ */
6
+ export class PerspectiveCamera extends Camera {
7
+ /**
8
+ * @param {number} fov vertical field of view in degrees
9
+ * @param {number} aspect width / height
10
+ */
11
+ constructor(fov = 60, aspect = 1, near = 0.1, far = 1000) {
12
+ super(aspect, near, far);
13
+ this.fov = fov;
14
+ this.updateProjectionMatrix();
15
+ }
16
+
17
+ updateProjectionMatrix() {
18
+ this.projectionMatrix.perspective(
19
+ (this.fov * Math.PI) / 180,
20
+ this.aspect,
21
+ this.near,
22
+ this.far,
23
+ );
24
+ }
25
+ }
@@ -0,0 +1,18 @@
1
+ /** Point-light array length shared by CPU uniform writers and WGSL. */
2
+ export const MAX_POINT_LIGHTS = 4;
3
+
4
+ /** Floats occupied by an instance's mat4 transform. */
5
+ export const INSTANCE_MATRIX_COMPONENTS = 16;
6
+
7
+ /** Float offset at which an instance's rgba color starts. */
8
+ export const INSTANCE_COLOR_OFFSET = INSTANCE_MATRIX_COMPONENTS;
9
+
10
+ /** Floats per instance: a mat4 followed by an rgba color. */
11
+ export const INSTANCE_SIZE = INSTANCE_MATRIX_COMPONENTS + 4;
12
+
13
+ /** Sampled depth format used by directional shadow maps. */
14
+ export const DIRECTIONAL_SHADOW_DEPTH_FORMAT = 'depth32float';
15
+
16
+ /** A shadow map is always rendered without MSAA. */
17
+ export const SHADOW_SAMPLE_COUNT = 1;
18
+
@@ -0,0 +1,168 @@
1
+ import { Raycaster } from '../core/Raycaster.js';
2
+ import { Vec3 } from '../../math/Vec3.js';
3
+ import { Mat4 } from '../../math/Mat4.js';
4
+
5
+ const _parentInverse = new Mat4();
6
+ const _worldPos = new Vec3();
7
+ const PARALLEL_PLANE_EPSILON = 1e-9;
8
+
9
+ /**
10
+ * Lets the user pick meshes with the pointer and drag them around.
11
+ * Dragged objects slide on a camera-facing plane through the point
12
+ * where they were grabbed, so they follow the cursor exactly.
13
+ *
14
+ * Assign the callbacks to react to interaction:
15
+ * onSelect(mesh | null) selection changed (click on object / empty space)
16
+ * onDragStart(mesh), onDrag(mesh), onDragEnd(mesh)
17
+ *
18
+ * Disable your camera controls in onDragStart / re-enable in onDragEnd
19
+ * so orbiting doesn't fight the drag. If you switch the active camera,
20
+ * update `controls.camera` too.
21
+ */
22
+ export class DragControls {
23
+ /**
24
+ * @param {Mesh[]} objects meshes that can be selected and dragged
25
+ */
26
+ constructor(objects, camera, domElement) {
27
+ this.objects = objects;
28
+ this.camera = camera;
29
+ this.domElement = domElement;
30
+ /** Set to false to ignore input; it also pauses an in-progress drag. */
31
+ this.enabled = true;
32
+ this.selected = null;
33
+
34
+ this.onSelect = null;
35
+ this.onDragStart = null;
36
+ this.onDrag = null;
37
+ this.onDragEnd = null;
38
+
39
+ this._raycaster = new Raycaster();
40
+ this._dragging = false;
41
+ this._activePointerId = null;
42
+ this._grabPoint = new Vec3(); // world point where the object was grabbed
43
+ this._grabOffset = new Vec3(); // object world position - grab point
44
+ this._planeNormal = new Vec3();
45
+
46
+ this._onPointerDown = this._handlePointerDown.bind(this);
47
+ this._onPointerMove = this._handlePointerMove.bind(this);
48
+ this._onPointerUp = this._handlePointerUp.bind(this);
49
+
50
+ domElement.addEventListener('pointerdown', this._onPointerDown);
51
+ domElement.addEventListener('pointermove', this._onPointerMove);
52
+ domElement.addEventListener('pointerup', this._onPointerUp);
53
+ domElement.addEventListener('pointercancel', this._onPointerUp);
54
+ }
55
+
56
+ _handlePointerDown(event) {
57
+ const orbitGesture = event.button !== 0 || event.altKey;
58
+ if (!this.enabled || orbitGesture || this._activePointerId !== null) return;
59
+
60
+ const hit = this._pick(event);
61
+ if (!hit) {
62
+ this._select(null);
63
+ return;
64
+ }
65
+
66
+ this._select(hit.object);
67
+ this._dragging = true;
68
+ this._activePointerId = event.pointerId;
69
+ this.domElement.setPointerCapture(event.pointerId);
70
+
71
+ // Drag on the camera-facing plane through the grab point.
72
+ const cameraMatrix = this.camera.worldMatrix.elements;
73
+ this._planeNormal.set(cameraMatrix[8], cameraMatrix[9], cameraMatrix[10]);
74
+ this._grabPoint.copy(hit.point);
75
+ const objectMatrix = hit.object.worldMatrix.elements;
76
+ this._grabOffset
77
+ .set(objectMatrix[12], objectMatrix[13], objectMatrix[14])
78
+ .sub(hit.point);
79
+
80
+ if (this.onDragStart) this.onDragStart(hit.object);
81
+ }
82
+
83
+ _handlePointerMove(event) {
84
+ if (
85
+ !this.enabled ||
86
+ !this._dragging ||
87
+ !this.selected ||
88
+ event.pointerId !== this._activePointerId
89
+ ) {
90
+ return;
91
+ }
92
+
93
+ const point = this._intersectDragPlane(event);
94
+ if (!point) return;
95
+
96
+ // New world position, then into the parent's local space.
97
+ _worldPos.copy(point).add(this._grabOffset);
98
+ const parent = this.selected.parent;
99
+ if (parent) {
100
+ if (!_parentInverse.copy(parent.worldMatrix).tryInvert()) return;
101
+ _worldPos.applyMat4(_parentInverse);
102
+ }
103
+ this.selected.position.copy(_worldPos);
104
+ if (this.onDrag) this.onDrag(this.selected);
105
+ }
106
+
107
+ _handlePointerUp(event) {
108
+ if (!this._dragging || event.pointerId !== this._activePointerId) return;
109
+ this._finishDrag();
110
+ }
111
+
112
+ _finishDrag() {
113
+ const pointerId = this._activePointerId;
114
+ this._dragging = false;
115
+ this._activePointerId = null;
116
+ if (
117
+ pointerId !== null &&
118
+ this.domElement.hasPointerCapture(pointerId)
119
+ ) {
120
+ this.domElement.releasePointerCapture(pointerId);
121
+ }
122
+ if (this.onDragEnd) this.onDragEnd(this.selected);
123
+ }
124
+
125
+ _select(mesh) {
126
+ if (mesh === this.selected) return;
127
+ this.selected = mesh;
128
+ if (this.onSelect) this.onSelect(mesh);
129
+ }
130
+
131
+ _ray(e) {
132
+ const rect = this.domElement.getBoundingClientRect();
133
+ const ndcX = ((e.clientX - rect.left) / rect.width) * 2 - 1;
134
+ const ndcY = 1 - ((e.clientY - rect.top) / rect.height) * 2;
135
+ return this._raycaster.setFromCamera(ndcX, ndcY, this.camera);
136
+ }
137
+
138
+ _pick(e) {
139
+ const hits = this._ray(e).intersectObjects(this.objects);
140
+ return hits.length > 0 ? hits[0] : null;
141
+ }
142
+
143
+ /** Where the pointer ray crosses the drag plane, or null if parallel. */
144
+ _intersectDragPlane(e) {
145
+ const ray = this._ray(e);
146
+ const n = this._planeNormal;
147
+ const denom = n.dot(ray.direction);
148
+ if (Math.abs(denom) < PARALLEL_PLANE_EPSILON) return null;
149
+ const t =
150
+ (n.x * (this._grabPoint.x - ray.origin.x) +
151
+ n.y * (this._grabPoint.y - ray.origin.y) +
152
+ n.z * (this._grabPoint.z - ray.origin.z)) /
153
+ denom;
154
+ return new Vec3(
155
+ ray.origin.x + ray.direction.x * t,
156
+ ray.origin.y + ray.direction.y * t,
157
+ ray.origin.z + ray.direction.z * t,
158
+ );
159
+ }
160
+
161
+ dispose() {
162
+ if (this._dragging) this._finishDrag();
163
+ this.domElement.removeEventListener('pointerdown', this._onPointerDown);
164
+ this.domElement.removeEventListener('pointermove', this._onPointerMove);
165
+ this.domElement.removeEventListener('pointerup', this._onPointerUp);
166
+ this.domElement.removeEventListener('pointercancel', this._onPointerUp);
167
+ }
168
+ }
@@ -0,0 +1,125 @@
1
+ import {
2
+ PointerControls,
3
+ TOUCH_GESTURE,
4
+ } from '../../core/PointerControls.js';
5
+
6
+ const DEFAULT_POLAR_ANGLE = Math.PI / 2;
7
+ const DIRECTION_EPSILON = 1e-12;
8
+ const POLE_TILT_EPSILON = 1e-4;
9
+ const DEGREES_TO_HALF_RADIANS = Math.PI / 360;
10
+
11
+ /**
12
+ * Mouse / touch controls that orbit a camera around a target point.
13
+ * Alt + left-drag to rotate, right-drag to pan, scroll (or pinch-zoom
14
+ * trackpad) to dolly in and out. A right-click without dragging still
15
+ * opens the browser context menu. On touch screens: one-finger drag
16
+ * orbits, two-finger drag pans, pinch zooms.
17
+ *
18
+ * The gesture plumbing lives in PointerControls; this class is the
19
+ * orbit math. Call `controls.update()` once per frame before rendering.
20
+ */
21
+ export class OrbitControls extends PointerControls {
22
+ constructor(camera, domElement) {
23
+ super(domElement);
24
+ this.camera = camera;
25
+ this.singleTouchGesture = TOUCH_GESTURE.ROTATE;
26
+
27
+ // Shares the camera's target so lookAt stays in sync.
28
+ this.target = camera.target;
29
+
30
+ this.minRadius = 0.5;
31
+ this.maxRadius = 100;
32
+ this.minZoom = 0.1;
33
+ this.maxZoom = 20;
34
+ this.minPhi = 0.05;
35
+ this.maxPhi = Math.PI - 0.05;
36
+
37
+ // Spherical coordinates derived from the camera's starting position.
38
+ const dx = camera.position.x - this.target.x;
39
+ const dy = camera.position.y - this.target.y;
40
+ const dz = camera.position.z - this.target.z;
41
+ const initialRadius = Math.hypot(dx, dy, dz);
42
+ this.radius = Math.min(
43
+ Math.max(initialRadius, this.minRadius),
44
+ this.maxRadius,
45
+ );
46
+ this.theta = Math.atan2(dx, dz);
47
+ this.phi =
48
+ initialRadius > DIRECTION_EPSILON
49
+ ? Math.acos(Math.min(Math.max(dy / initialRadius, -1), 1))
50
+ : DEFAULT_POLAR_ANGLE;
51
+ }
52
+
53
+ /**
54
+ * Half the height of the view volume at the target's distance,
55
+ * in world units.
56
+ */
57
+ _visibleHalfHeight() {
58
+ return this.camera.isOrthographic
59
+ ? this.camera.size / this.camera.zoom
60
+ : this.radius * Math.tan(this.camera.fov * DEGREES_TO_HALF_RADIANS);
61
+ }
62
+
63
+ _rotate(rx, ry) {
64
+ this.theta -= rx;
65
+ this.phi = Math.min(Math.max(this.phi - ry, this.minPhi), this.maxPhi);
66
+ }
67
+
68
+ /**
69
+ * Moves the target along the camera's screen-space axes so the scene
70
+ * follows the cursor (1 pixel of drag = 1 pixel on screen).
71
+ */
72
+ _pan(dx, dy) {
73
+ const e = this.camera.worldMatrix.elements;
74
+ const worldPerPixel =
75
+ (2 * this._visibleHalfHeight()) / this.domElement.clientHeight;
76
+ // Columns 0 and 1 of the world matrix are the camera's right/up axes.
77
+ this.target.x -= (e[0] * dx - e[4] * dy) * worldPerPixel;
78
+ this.target.y -= (e[1] * dx - e[5] * dy) * worldPerPixel;
79
+ this.target.z -= (e[2] * dx - e[6] * dy) * worldPerPixel;
80
+ }
81
+
82
+ _zoom(factor, ndcX, ndcY) {
83
+ const halfHBefore = this._visibleHalfHeight();
84
+ if (this.camera.isOrthographic) {
85
+ // Dollying doesn't change apparent size in ortho; scale the
86
+ // camera's zoom instead.
87
+ this.camera.zoom /= factor;
88
+ this.camera.zoom = Math.min(
89
+ Math.max(this.camera.zoom, this.minZoom),
90
+ this.maxZoom,
91
+ );
92
+ } else {
93
+ this.radius *= factor;
94
+ this.radius = Math.min(
95
+ Math.max(this.radius, this.minRadius),
96
+ this.maxRadius,
97
+ );
98
+ }
99
+
100
+ // Zoom toward the cursor: shift the target so the world point
101
+ // under the pointer stays under it as the view scales.
102
+ const dh = halfHBefore - this._visibleHalfHeight();
103
+ const ox = ndcX * dh * this.camera.aspect;
104
+ const oy = ndcY * dh;
105
+ const m = this.camera.worldMatrix.elements;
106
+ this.target.x += m[0] * ox + m[4] * oy;
107
+ this.target.y += m[1] * ox + m[5] * oy;
108
+ this.target.z += m[2] * ox + m[6] * oy;
109
+ }
110
+
111
+ /** Applies the current orbit state to the camera. Call once per frame. */
112
+ update() {
113
+ // Keep an imperceptible tilt so that even at phi = 0 (straight
114
+ // top-down) the camera's screen orientation follows theta, letting
115
+ // the view spin around the vertical axis.
116
+ const phi = Math.max(this.phi, POLE_TILT_EPSILON);
117
+ const sinPhi = Math.sin(phi);
118
+ this.camera.position.set(
119
+ this.target.x + this.radius * sinPhi * Math.sin(this.theta),
120
+ this.target.y + this.radius * Math.cos(phi),
121
+ this.target.z + this.radius * sinPhi * Math.cos(this.theta),
122
+ );
123
+ this.camera.lookAt(this.target);
124
+ }
125
+ }