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,124 @@
1
+ import { Mesh } from './Mesh.js';
2
+ import { srgbToLinear } from '../../math/color.js';
3
+ import {
4
+ INSTANCE_COLOR_OFFSET,
5
+ INSTANCE_SIZE,
6
+ } from '../constants.js';
7
+ import { computeInstancedBounds } from './InstancedBounds.js';
8
+
9
+ export { INSTANCE_SIZE } from '../constants.js';
10
+
11
+ /**
12
+ * A Mesh drawn `count` times in one draw call. Each instance has its
13
+ * own transform and color, packed into `instanceData` and uploaded as
14
+ * an instance-step vertex buffer — thousands of copies cost one
15
+ * uniform upload and one drawIndexed instead of thousands.
16
+ *
17
+ * Instance transforms are local to the mesh: the mesh's own scene-graph
18
+ * transform applies on top, and instance colors multiply with the
19
+ * material's color. Write instances with `setMatrixAt`/`setColorAt` —
20
+ * they mark the buffer for re-upload; set `needsUpdate = true` yourself
21
+ * only if you write `instanceData` directly.
22
+ *
23
+ * The instance count is fixed at construction. To show fewer instances,
24
+ * scale the extras to zero; to grow, make a new InstancedMesh.
25
+ *
26
+ * Raycaster tests every instance transform and includes `instanceId` in
27
+ * instanced hits. DragControls still moves the InstancedMesh as one batch.
28
+ */
29
+ export class InstancedMesh extends Mesh {
30
+ constructor(geometry, material, count) {
31
+ super(geometry, material);
32
+ this.isInstanced = true;
33
+ this.count = count;
34
+ this.instanceData = new Float32Array(count * INSTANCE_SIZE);
35
+ // Every instance starts as an identity matrix with a white color.
36
+ for (let i = 0; i < count; i++) {
37
+ const base = i * INSTANCE_SIZE;
38
+ this.instanceData[base] = 1;
39
+ this.instanceData[base + 5] = 1;
40
+ this.instanceData[base + 10] = 1;
41
+ this.instanceData[base + 15] = 1;
42
+ this.instanceData.fill(
43
+ 1,
44
+ base + INSTANCE_COLOR_OFFSET,
45
+ base + INSTANCE_SIZE,
46
+ );
47
+ }
48
+ // Resource caches compare revisions so every renderer/device receives
49
+ // an edit, even after another renderer has cleared needsUpdate.
50
+ this._instanceRevision = 0;
51
+ this._boundsRevision = 0;
52
+ this._needsUpdate = true;
53
+ this._instanceBounds = null;
54
+ this._instanceBoundsSource = null;
55
+ this._instanceBoundsRevision = -1;
56
+ this._instanceBoundsCount = -1;
57
+ }
58
+
59
+ /**
60
+ * Mesh-local union of every transformed instance. Frustum culling remains
61
+ * batch-level: if this box intersects, all `count` instances draw together.
62
+ */
63
+ get bounds() {
64
+ const sourceBounds = this.geometry?.bounds || null;
65
+ if (
66
+ this._instanceBoundsSource !== sourceBounds ||
67
+ this._instanceBoundsRevision !== this._boundsRevision ||
68
+ this._instanceBoundsCount !== this.count
69
+ ) {
70
+ this._instanceBounds = computeInstancedBounds(
71
+ sourceBounds,
72
+ this.instanceData,
73
+ this.count,
74
+ );
75
+ this._instanceBoundsSource = sourceBounds;
76
+ this._instanceBoundsRevision = this._boundsRevision;
77
+ this._instanceBoundsCount = this.count;
78
+ }
79
+ return this._instanceBounds;
80
+ }
81
+
82
+ /** Upload hint; set true after changing `instanceData` directly. */
83
+ get needsUpdate() {
84
+ return this._needsUpdate;
85
+ }
86
+
87
+ set needsUpdate(value) {
88
+ this._needsUpdate = Boolean(value);
89
+ if (this._needsUpdate) {
90
+ this._instanceRevision++;
91
+ // Direct writes may have changed matrices, so conservatively invalidate.
92
+ this._boundsRevision++;
93
+ }
94
+ }
95
+
96
+ /** Copies a Mat4 into instance `index`'s transform. */
97
+ setMatrixAt(index, matrix) {
98
+ this.instanceData.set(matrix.elements, index * INSTANCE_SIZE);
99
+ this._markInstanceDataUpdated(true);
100
+ return this;
101
+ }
102
+
103
+ /**
104
+ * Sets instance `index`'s color from [r, g, b] or [r, g, b, a] —
105
+ * sRGB display values like material colors. They are stored
106
+ * linearized (shading happens in linear space), so write linear
107
+ * values if you fill `instanceData` directly instead.
108
+ */
109
+ setColorAt(index, color) {
110
+ const base = index * INSTANCE_SIZE + INSTANCE_COLOR_OFFSET;
111
+ this.instanceData[base] = srgbToLinear(color[0]);
112
+ this.instanceData[base + 1] = srgbToLinear(color[1]);
113
+ this.instanceData[base + 2] = srgbToLinear(color[2]);
114
+ this.instanceData[base + 3] = color.length > 3 ? color[3] : 1;
115
+ this._markInstanceDataUpdated(false);
116
+ return this;
117
+ }
118
+
119
+ _markInstanceDataUpdated(boundsChanged) {
120
+ this._needsUpdate = true;
121
+ this._instanceRevision++;
122
+ if (boundsChanged) this._boundsRevision++;
123
+ }
124
+ }
@@ -0,0 +1,26 @@
1
+ import { Object3d } from './Object3d.js';
2
+
3
+ /**
4
+ * A renderable object: a Geometry (the shape) combined with a Material
5
+ * (how the surface is shaded).
6
+ */
7
+ export class Mesh extends Object3d {
8
+ constructor(geometry, material) {
9
+ super();
10
+ this.geometry = geometry;
11
+ this.material = material;
12
+ /** Set false to bypass camera and shadow-camera frustum culling. */
13
+ this.frustumCulled = true;
14
+ /** Whether this mesh is drawn into an enabled directional shadow map. */
15
+ this.castShadow = false;
16
+ /** Whether lit materials on this mesh are darkened by that shadow map. */
17
+ this.receiveShadow = false;
18
+ /** View-space depth, written by the renderer's transparent sort. */
19
+ this._viewDepth = 0;
20
+ }
21
+
22
+ /** Local-space bounds used for conservative renderer culling. */
23
+ get bounds() {
24
+ return this.geometry?.bounds || null;
25
+ }
26
+ }
@@ -0,0 +1,101 @@
1
+ import { Vec3 } from '../../math/Vec3.js';
2
+ import { Mat4 } from '../../math/Mat4.js';
3
+ import { disposeObjectGpuResources } from '../../core/objectGpuResources.js';
4
+
5
+ /**
6
+ * Base class for everything that lives in the scene graph.
7
+ * Holds a transform (position / rotation / scale) and a list of children,
8
+ * so objects can be parented to each other like in three.js.
9
+ */
10
+ export class Object3d {
11
+ constructor() {
12
+ this.position = new Vec3(0, 0, 0);
13
+ /** Euler angles in radians, applied in XYZ order. */
14
+ this.rotation = new Vec3(0, 0, 0);
15
+ this.scale = new Vec3(1, 1, 1);
16
+
17
+ this.parent = null;
18
+ this.children = [];
19
+ /** Invisible objects — and everything under them — are skipped by rendering and picking. */
20
+ this.visible = true;
21
+
22
+ this.localMatrix = new Mat4();
23
+ this.worldMatrix = new Mat4();
24
+ /** Renderer-local GPU resources, created lazily (see GpuResources). */
25
+ this._gpu = null;
26
+ }
27
+
28
+ add(child) {
29
+ // Adding an object to itself or to one of its own descendants would
30
+ // create a cycle and hang traverse/updateWorldMatrix — refuse it.
31
+ for (let node = this; node; node = node.parent) {
32
+ if (node === child) return this;
33
+ }
34
+ if (child.parent) child.parent.remove(child);
35
+ child.parent = this;
36
+ this.children.push(child);
37
+ return this;
38
+ }
39
+
40
+ remove(child) {
41
+ const index = this.children.indexOf(child);
42
+ if (index !== -1) {
43
+ child.parent = null;
44
+ this.children.splice(index, 1);
45
+ }
46
+ return this;
47
+ }
48
+
49
+ /** Recomputes localMatrix and worldMatrix for this object and all descendants. */
50
+ updateWorldMatrix() {
51
+ this.localMatrix.compose(this.position, this.rotation, this.scale);
52
+ if (this.parent) {
53
+ this.worldMatrix.multiplyMatrices(
54
+ this.parent.worldMatrix,
55
+ this.localMatrix,
56
+ );
57
+ } else {
58
+ this.worldMatrix.copy(this.localMatrix);
59
+ }
60
+ for (const child of this.children) {
61
+ child.updateWorldMatrix();
62
+ }
63
+ }
64
+
65
+ /** Calls `callback(object)` for this object and every descendant. */
66
+ traverse(callback) {
67
+ callback(this);
68
+ for (const child of this.children) {
69
+ child.traverse(callback);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Like traverse, but skips objects with `visible = false` and their
75
+ * whole subtree — hiding a group hides everything inside it. The
76
+ * renderer and raycaster walk the scene with this.
77
+ */
78
+ traverseVisible(callback) {
79
+ if (!this.visible) return;
80
+ callback(this);
81
+ for (const child of this.children) {
82
+ child.traverseVisible(callback);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Destroys the per-object GPU resources created by every renderer for
88
+ * this object and its descendants (see GpuResources), releasing memory
89
+ * right away instead of waiting for GC. Geometry buffers are left alone:
90
+ * geometries may be shared between objects. Drawing a disposed object
91
+ * again re-creates its resources and re-uploads its instance data.
92
+ */
93
+ dispose() {
94
+ this.traverse((object) => {
95
+ disposeObjectGpuResources(object);
96
+ // Re-created instance buffers must receive the CPU-side data before use.
97
+ if (object.isInstanced) object.needsUpdate = true;
98
+ });
99
+ return this;
100
+ }
101
+ }
@@ -0,0 +1,125 @@
1
+ import {
2
+ DEFAULT_DEPTH_COMPARE,
3
+ INDEX_FORMAT,
4
+ SHADER_ENTRY_POINT,
5
+ STRAIGHT_ALPHA_BLEND,
6
+ isStripTopology,
7
+ } from '../../core/pipelineConstants.js';
8
+ import {
9
+ createMaterialPipelineLayouts,
10
+ } from '../../core/createMaterialPipelineLayouts.js';
11
+ import {
12
+ DEPTH_FORMAT,
13
+ SINGLE_SAMPLE_COUNT,
14
+ } from '../../core/rendererConfig.js';
15
+ import { materialUsesMap } from '../../core/materialResources.js';
16
+ import { vertexBufferLayouts } from '../shaders/vertexLayout.js';
17
+
18
+ /**
19
+ * Compiles and caches the render pipelines for materials, and owns the
20
+ * bind group / pipeline layouts every shader shares (the
21
+ * @group(0) / @group(1) uniform interface in Material).
22
+ *
23
+ * A pipeline is keyed by composed shader source + pipeline state (topology,
24
+ * cull mode, front face, textured / instanced / transparent or not):
25
+ * each combination compiles once and is shared by every material
26
+ * instance that matches. Transparent variants alpha-blend like the 2D
27
+ * pipelines and don't write the depth buffer (the renderer draws them
28
+ * after the opaque meshes, sorted back-to-front).
29
+ */
30
+ export class Pipelines {
31
+ constructor(device, format, sampleCount = SINGLE_SAMPLE_COUNT) {
32
+ this.device = device;
33
+ this.format = format;
34
+ /** MSAA sample count of the renderer's color/depth targets. */
35
+ this.sampleCount = sampleCount;
36
+ // composed WGSL -> Map of pipeline-state key -> GPURenderPipeline
37
+ this._cache = new Map();
38
+ // WGSL source -> GPUShaderModule, so pipeline variants that share a
39
+ // shader (e.g. the same material at another topology) compile once.
40
+ this._modules = new Map();
41
+
42
+ Object.assign(
43
+ this,
44
+ createMaterialPipelineLayouts(
45
+ device,
46
+ GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
47
+ { shadows: true },
48
+ ),
49
+ );
50
+ }
51
+
52
+ /** The render pipeline for a material's shader and fixed-function state. */
53
+ pipelineFor(material, instanced = false) {
54
+ const { topology, cullMode, frontFace } = material;
55
+ const shaderCode = instanced
56
+ ? material.instancedShaderCode
57
+ : material.shaderCode;
58
+ let variants = this._cache.get(shaderCode);
59
+ if (!variants) {
60
+ variants = new Map();
61
+ this._cache.set(shaderCode, variants);
62
+ }
63
+ const textured = materialUsesMap(material);
64
+ const transparent = !!material.transparent;
65
+ const stateKey = `${topology}|${cullMode}|${frontFace}|${textured}|${instanced}|${transparent}`;
66
+ let pipeline = variants.get(stateKey);
67
+ if (!pipeline) {
68
+ pipeline = this._build(material, shaderCode, {
69
+ textured,
70
+ instanced,
71
+ transparent,
72
+ });
73
+ variants.set(stateKey, pipeline);
74
+ }
75
+ return pipeline;
76
+ }
77
+
78
+ _moduleFor(code) {
79
+ let module = this._modules.get(code);
80
+ if (!module) {
81
+ module = this.device.createShaderModule({ code });
82
+ this._modules.set(code, module);
83
+ }
84
+ return module;
85
+ }
86
+
87
+ _build(material, shaderCode, { textured, instanced, transparent }) {
88
+ const { topology, cullMode, frontFace } = material;
89
+ const primitive = { topology, cullMode, frontFace };
90
+ // Indexed draws on strip topologies must declare the index format
91
+ // up front; the renderer always uses uint32 indices.
92
+ if (isStripTopology(topology)) {
93
+ primitive.stripIndexFormat = INDEX_FORMAT;
94
+ }
95
+ const buffers = vertexBufferLayouts(instanced);
96
+ const target = { format: this.format };
97
+ if (transparent) {
98
+ target.blend = STRAIGHT_ALPHA_BLEND;
99
+ }
100
+ const module = this._moduleFor(shaderCode);
101
+ return this.device.createRenderPipeline({
102
+ layout: textured ? this.texturedPipelineLayout : this.pipelineLayout,
103
+ vertex: {
104
+ module,
105
+ entryPoint: SHADER_ENTRY_POINT.vertex,
106
+ buffers,
107
+ },
108
+ fragment: {
109
+ module,
110
+ entryPoint: SHADER_ENTRY_POINT.fragment,
111
+ targets: [target],
112
+ },
113
+ primitive,
114
+ multisample: { count: this.sampleCount },
115
+ depthStencil: {
116
+ format: DEPTH_FORMAT,
117
+ // Transparent meshes test against the depth buffer (opaque
118
+ // things still hide them) but don't write it: they draw last,
119
+ // back-to-front, and shouldn't mask each other.
120
+ depthWriteEnabled: !transparent,
121
+ depthCompare: DEFAULT_DEPTH_COMPARE,
122
+ },
123
+ });
124
+ }
125
+ }
@@ -0,0 +1,184 @@
1
+ import { forEachIndexedTriangle } from '../../core/indexedTriangles.js';
2
+ import { VERTEX_SIZE } from '../geometries/Geometry.js';
3
+
4
+ const POSITION_COMPONENTS = 3;
5
+ const PARALLEL_TRIANGLE_EPSILON = 1e-12;
6
+ const BARYCENTRIC_EPSILON = 1e-7;
7
+
8
+ /**
9
+ * Returns the nearest local indexed-triangle hit distance, or `null`.
10
+ * `direction` intentionally remains unnormalized: when it came from an
11
+ * inverse-transformed unit world ray, the returned value is a world distance.
12
+ */
13
+ export function intersectIndexedGeometry(
14
+ origin,
15
+ direction,
16
+ geometry,
17
+ topology,
18
+ maxDistance,
19
+ ) {
20
+ if (!intersectsBounds(origin, direction, geometry.bounds, maxDistance)) {
21
+ return null;
22
+ }
23
+
24
+ let nearestDistance = maxDistance;
25
+ let hit = false;
26
+ forEachIndexedTriangle(geometry.indices, topology, (a, b, c) => {
27
+ const distance = intersectTriangle(
28
+ origin,
29
+ direction,
30
+ geometry.vertices,
31
+ a,
32
+ b,
33
+ c,
34
+ nearestDistance,
35
+ );
36
+ if (distance === null) return false;
37
+ nearestDistance = distance;
38
+ hit = true;
39
+ return distance === 0;
40
+ });
41
+ return hit ? nearestDistance : null;
42
+ }
43
+
44
+ /** Segment-vs-AABB broad phase over the ray interval [0, maxDistance]. */
45
+ export function intersectsBounds(origin, direction, bounds, maxDistance) {
46
+ const min = bounds?.min;
47
+ const max = bounds?.max;
48
+ if (!hasFiniteBounds(min, max)) return false;
49
+
50
+ let near = 0;
51
+ let far = maxDistance;
52
+ for (let axis = 0; axis < POSITION_COMPONENTS; axis++) {
53
+ const component = componentAt(direction, axis);
54
+ const start = componentAt(origin, axis);
55
+ if (component === 0) {
56
+ if (start < min[axis] || start > max[axis]) return false;
57
+ continue;
58
+ }
59
+
60
+ let entry = (min[axis] - start) / component;
61
+ let exit = (max[axis] - start) / component;
62
+ if (entry > exit) [entry, exit] = [exit, entry];
63
+ near = Math.max(near, entry);
64
+ far = Math.min(far, exit);
65
+ if (near > far) return false;
66
+ }
67
+ return far >= 0 && near <= maxDistance;
68
+ }
69
+
70
+ function intersectTriangle(
71
+ origin,
72
+ direction,
73
+ vertices,
74
+ indexA,
75
+ indexB,
76
+ indexC,
77
+ maxDistance,
78
+ ) {
79
+ const offsetA = vertexOffset(vertices, indexA);
80
+ const offsetB = vertexOffset(vertices, indexB);
81
+ const offsetC = vertexOffset(vertices, indexC);
82
+ if (offsetA < 0 || offsetB < 0 || offsetC < 0) return null;
83
+
84
+ const ax = vertices[offsetA];
85
+ const ay = vertices[offsetA + 1];
86
+ const az = vertices[offsetA + 2];
87
+ const edge1x = vertices[offsetB] - ax;
88
+ const edge1y = vertices[offsetB + 1] - ay;
89
+ const edge1z = vertices[offsetB + 2] - az;
90
+ const edge2x = vertices[offsetC] - ax;
91
+ const edge2y = vertices[offsetC + 1] - ay;
92
+ const edge2z = vertices[offsetC + 2] - az;
93
+ if (
94
+ !Number.isFinite(ax) ||
95
+ !Number.isFinite(ay) ||
96
+ !Number.isFinite(az) ||
97
+ !Number.isFinite(edge1x) ||
98
+ !Number.isFinite(edge1y) ||
99
+ !Number.isFinite(edge1z) ||
100
+ !Number.isFinite(edge2x) ||
101
+ !Number.isFinite(edge2y) ||
102
+ !Number.isFinite(edge2z)
103
+ ) {
104
+ return null;
105
+ }
106
+
107
+ const px = direction.y * edge2z - direction.z * edge2y;
108
+ const py = direction.z * edge2x - direction.x * edge2z;
109
+ const pz = direction.x * edge2y - direction.y * edge2x;
110
+ const determinant = edge1x * px + edge1y * py + edge1z * pz;
111
+ const determinantScale =
112
+ Math.hypot(edge1x, edge1y, edge1z) *
113
+ Math.hypot(edge2x, edge2y, edge2z) *
114
+ Math.hypot(direction.x, direction.y, direction.z);
115
+ if (
116
+ !Number.isFinite(determinantScale) ||
117
+ determinantScale === 0 ||
118
+ Math.abs(determinant) <=
119
+ determinantScale * PARALLEL_TRIANGLE_EPSILON
120
+ ) {
121
+ return null;
122
+ }
123
+
124
+ const inverseDeterminant = 1 / determinant;
125
+ const tx = origin.x - ax;
126
+ const ty = origin.y - ay;
127
+ const tz = origin.z - az;
128
+ const u = (tx * px + ty * py + tz * pz) * inverseDeterminant;
129
+ if (u < -BARYCENTRIC_EPSILON || u > 1 + BARYCENTRIC_EPSILON) {
130
+ return null;
131
+ }
132
+
133
+ const qx = ty * edge1z - tz * edge1y;
134
+ const qy = tz * edge1x - tx * edge1z;
135
+ const qz = tx * edge1y - ty * edge1x;
136
+ const v =
137
+ (direction.x * qx + direction.y * qy + direction.z * qz) *
138
+ inverseDeterminant;
139
+ if (
140
+ v < -BARYCENTRIC_EPSILON ||
141
+ u + v > 1 + BARYCENTRIC_EPSILON
142
+ ) {
143
+ return null;
144
+ }
145
+
146
+ const distance =
147
+ (edge2x * qx + edge2y * qy + edge2z * qz) * inverseDeterminant;
148
+ if (
149
+ !Number.isFinite(distance) ||
150
+ distance < 0 ||
151
+ distance > maxDistance
152
+ ) {
153
+ return null;
154
+ }
155
+ return distance;
156
+ }
157
+
158
+ function vertexOffset(vertices, index) {
159
+ if (!Number.isInteger(index)) return -1;
160
+ const offset = index * VERTEX_SIZE;
161
+ return index >= 0 && offset + POSITION_COMPONENTS <= vertices.length
162
+ ? offset
163
+ : -1;
164
+ }
165
+
166
+ function hasFiniteBounds(min, max) {
167
+ if (!min || !max || min.length < 3 || max.length < 3) return false;
168
+ for (let axis = 0; axis < POSITION_COMPONENTS; axis++) {
169
+ if (
170
+ !Number.isFinite(min[axis]) ||
171
+ !Number.isFinite(max[axis]) ||
172
+ min[axis] > max[axis]
173
+ ) {
174
+ return false;
175
+ }
176
+ }
177
+ return true;
178
+ }
179
+
180
+ function componentAt(vector, axis) {
181
+ if (axis === 0) return vector.x;
182
+ if (axis === 1) return vector.y;
183
+ return vector.z;
184
+ }