@flighthq/mesh 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 (55) hide show
  1. package/dist/index.d.ts +11 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +11 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/meshGeometry.d.ts +15 -0
  6. package/dist/meshGeometry.d.ts.map +1 -0
  7. package/dist/meshGeometry.js +128 -0
  8. package/dist/meshGeometry.js.map +1 -0
  9. package/dist/meshGeometryAttributes.d.ts +28 -0
  10. package/dist/meshGeometryAttributes.d.ts.map +1 -0
  11. package/dist/meshGeometryAttributes.js +144 -0
  12. package/dist/meshGeometryAttributes.js.map +1 -0
  13. package/dist/meshGeometryBuilders.d.ts +19 -0
  14. package/dist/meshGeometryBuilders.d.ts.map +1 -0
  15. package/dist/meshGeometryBuilders.js +772 -0
  16. package/dist/meshGeometryBuilders.js.map +1 -0
  17. package/dist/meshGeometryCompute.d.ts +7 -0
  18. package/dist/meshGeometryCompute.d.ts.map +1 -0
  19. package/dist/meshGeometryCompute.js +299 -0
  20. package/dist/meshGeometryCompute.js.map +1 -0
  21. package/dist/meshGeometryIndex.d.ts +4 -0
  22. package/dist/meshGeometryIndex.d.ts.map +1 -0
  23. package/dist/meshGeometryIndex.js +70 -0
  24. package/dist/meshGeometryIndex.js.map +1 -0
  25. package/dist/meshGeometryLayout.d.ts +4 -0
  26. package/dist/meshGeometryLayout.d.ts.map +1 -0
  27. package/dist/meshGeometryLayout.js +83 -0
  28. package/dist/meshGeometryLayout.js.map +1 -0
  29. package/dist/meshGeometryOperations.d.ts +12 -0
  30. package/dist/meshGeometryOperations.d.ts.map +1 -0
  31. package/dist/meshGeometryOperations.js +209 -0
  32. package/dist/meshGeometryOperations.js.map +1 -0
  33. package/dist/meshGeometrySubset.d.ts +5 -0
  34. package/dist/meshGeometrySubset.d.ts.map +1 -0
  35. package/dist/meshGeometrySubset.js +40 -0
  36. package/dist/meshGeometrySubset.js.map +1 -0
  37. package/dist/meshGeometryTransforms.d.ts +7 -0
  38. package/dist/meshGeometryTransforms.d.ts.map +1 -0
  39. package/dist/meshGeometryTransforms.js +235 -0
  40. package/dist/meshGeometryTransforms.js.map +1 -0
  41. package/dist/meshGeometryUvs.d.ts +5 -0
  42. package/dist/meshGeometryUvs.d.ts.map +1 -0
  43. package/dist/meshGeometryUvs.js +58 -0
  44. package/dist/meshGeometryUvs.js.map +1 -0
  45. package/package.json +39 -0
  46. package/src/meshGeometry.test.ts +155 -0
  47. package/src/meshGeometryAttributes.test.ts +195 -0
  48. package/src/meshGeometryBuilders.test.ts +220 -0
  49. package/src/meshGeometryCompute.test.ts +218 -0
  50. package/src/meshGeometryIndex.test.ts +84 -0
  51. package/src/meshGeometryLayout.test.ts +132 -0
  52. package/src/meshGeometryOperations.test.ts +154 -0
  53. package/src/meshGeometrySubset.test.ts +75 -0
  54. package/src/meshGeometryTransforms.test.ts +169 -0
  55. package/src/meshGeometryUvs.test.ts +128 -0
@@ -0,0 +1,169 @@
1
+ import { createMatrix4, setMatrix4Identity } from '@flighthq/geometry';
2
+ import type { Aabb, VertexAttributeLayout } from '@flighthq/types';
3
+
4
+ import { createMeshGeometry } from './meshGeometry';
5
+ import { computeMeshGeometryBounds } from './meshGeometryCompute';
6
+ import {
7
+ centerMeshGeometry,
8
+ scaleMeshGeometry,
9
+ transformMeshGeometry,
10
+ transformMeshGeometryInto,
11
+ translateMeshGeometry,
12
+ } from './meshGeometryTransforms';
13
+
14
+ const CANONICAL_LAYOUT: VertexAttributeLayout = {
15
+ attributes: [
16
+ { byteOffset: 0, format: 'float32x3', semantic: 'position' },
17
+ { byteOffset: 12, format: 'float32x3', semantic: 'normal' },
18
+ { byteOffset: 24, format: 'float32x4', semantic: 'tangent' },
19
+ { byteOffset: 40, format: 'float32x2', semantic: 'uv0' },
20
+ ],
21
+ stride: 48,
22
+ };
23
+
24
+ // One vertex at (1, 0, 0) with normal (1, 0, 0) and tangent (0, 1, 0, 1).
25
+ function makeVertex(px: number, py: number, pz: number) {
26
+ const vertices = new Float32Array(12);
27
+ vertices[0] = px;
28
+ vertices[1] = py;
29
+ vertices[2] = pz;
30
+ vertices[3] = 1;
31
+ vertices[4] = 0;
32
+ vertices[5] = 0; // normal
33
+ vertices[6] = 0;
34
+ vertices[7] = 1;
35
+ vertices[8] = 0;
36
+ vertices[9] = 1; // tangent
37
+ vertices[10] = 0;
38
+ vertices[11] = 0;
39
+ return createMeshGeometry({ layout: CANONICAL_LAYOUT, vertices });
40
+ }
41
+
42
+ describe('centerMeshGeometry', () => {
43
+ it('translates geometry so bounds center is at origin', () => {
44
+ // Manually set bounds to span 0..4 on X.
45
+ const v4 = new Float32Array(12 * 2);
46
+ v4[0] = 0;
47
+ v4[12] = 4;
48
+ const multiGeo = createMeshGeometry({ layout: CANONICAL_LAYOUT, vertices: v4 });
49
+ computeMeshGeometryBounds(
50
+ (multiGeo.bounds = { min: { x: 0, y: 0, z: 0 }, max: { x: 4, y: 0, z: 0 } } as Aabb),
51
+ multiGeo,
52
+ );
53
+ centerMeshGeometry(multiGeo);
54
+ expect(multiGeo.vertices[0]).toBeCloseTo(-2);
55
+ expect(multiGeo.vertices[12]).toBeCloseTo(2);
56
+ });
57
+ it('is a no-op when already centered', () => {
58
+ const geo = makeVertex(0, 0, 0);
59
+ const v0 = geo.version;
60
+ // bounds already centered
61
+ geo.bounds = { min: { x: -0.5, y: -0.5, z: -0.5 }, max: { x: 0.5, y: 0.5, z: 0.5 } } as Aabb;
62
+ // center is (0,0,0), skip
63
+ centerMeshGeometry(geo);
64
+ // no version bump because cx==cy==cz==0 -> early return
65
+ expect(geo.version).toBe(v0);
66
+ });
67
+ });
68
+
69
+ describe('scaleMeshGeometry', () => {
70
+ it('scales positions by the given factors', () => {
71
+ const geo = makeVertex(1, 2, 3);
72
+ scaleMeshGeometry(geo, 2, 3, 4);
73
+ expect(geo.vertices[0]).toBeCloseTo(2);
74
+ expect(geo.vertices[1]).toBeCloseTo(6);
75
+ expect(geo.vertices[2]).toBeCloseTo(12);
76
+ });
77
+ it('re-normalizes normals under non-uniform scale', () => {
78
+ const geo = makeVertex(1, 0, 0);
79
+ // normal is (1,0,0); after scale(2,1,1), normal should still be unit.
80
+ scaleMeshGeometry(geo, 2, 1, 1);
81
+ const nx = geo.vertices[3],
82
+ ny = geo.vertices[4],
83
+ nz = geo.vertices[5];
84
+ expect(Math.sqrt(nx * nx + ny * ny + nz * nz)).toBeCloseTo(1);
85
+ });
86
+ it('bumps version', () => {
87
+ const geo = makeVertex(1, 0, 0);
88
+ const v0 = geo.version;
89
+ scaleMeshGeometry(geo, 1, 1, 1);
90
+ expect(geo.version).toBe(v0 + 1);
91
+ });
92
+ });
93
+
94
+ describe('transformMeshGeometry', () => {
95
+ it('applies translation to positions', () => {
96
+ const geo = makeVertex(0, 0, 0);
97
+ const m = createMatrix4();
98
+ setMatrix4Identity(m);
99
+ m.m[12] = 5;
100
+ m.m[13] = 6;
101
+ m.m[14] = 7; // translate column
102
+ expect(transformMeshGeometry(geo, m)).toBe(true);
103
+ expect(geo.vertices[0]).toBeCloseTo(5);
104
+ expect(geo.vertices[1]).toBeCloseTo(6);
105
+ expect(geo.vertices[2]).toBeCloseTo(7);
106
+ });
107
+ it('returns false for a singular matrix', () => {
108
+ const geo = makeVertex(1, 0, 0);
109
+ const m = createMatrix4();
110
+ // zero matrix is singular
111
+ for (let i = 0; i < 16; i++) m.m[i] = 0;
112
+ expect(transformMeshGeometry(geo, m)).toBe(false);
113
+ });
114
+ it('bumps version on success', () => {
115
+ const geo = makeVertex(1, 0, 0);
116
+ const m = createMatrix4();
117
+ setMatrix4Identity(m);
118
+ const v0 = geo.version;
119
+ transformMeshGeometry(geo, m);
120
+ expect(geo.version).toBe(v0 + 1);
121
+ });
122
+ });
123
+
124
+ describe('transformMeshGeometryInto', () => {
125
+ it('alias-safe: works when out === source', () => {
126
+ const geo = makeVertex(1, 0, 0);
127
+ const m = createMatrix4();
128
+ setMatrix4Identity(m);
129
+ m.m[12] = 3;
130
+ expect(transformMeshGeometryInto(geo, geo, m)).toBe(true);
131
+ expect(geo.vertices[0]).toBeCloseTo(4);
132
+ });
133
+ it('distinct out: writes to out without modifying source', () => {
134
+ const source = makeVertex(1, 0, 0);
135
+ const out = makeVertex(0, 0, 0);
136
+ const m = createMatrix4();
137
+ setMatrix4Identity(m);
138
+ m.m[12] = 2;
139
+ expect(transformMeshGeometryInto(out, source, m)).toBe(true);
140
+ expect(out.vertices[0]).toBeCloseTo(3);
141
+ expect(source.vertices[0]).toBeCloseTo(1); // source unchanged
142
+ });
143
+ });
144
+
145
+ describe('translateMeshGeometry', () => {
146
+ it('offsets all positions by (x, y, z)', () => {
147
+ const geo = makeVertex(1, 2, 3);
148
+ translateMeshGeometry(geo, 10, 20, 30);
149
+ expect(geo.vertices[0]).toBeCloseTo(11);
150
+ expect(geo.vertices[1]).toBeCloseTo(22);
151
+ expect(geo.vertices[2]).toBeCloseTo(33);
152
+ });
153
+ it('does not affect normals', () => {
154
+ const geo = makeVertex(0, 0, 0);
155
+ translateMeshGeometry(geo, 5, 5, 5);
156
+ expect(geo.vertices[3]).toBe(1); // normal x unchanged
157
+ expect(geo.vertices[4]).toBe(0);
158
+ expect(geo.vertices[5]).toBe(0);
159
+ });
160
+ it('bumps version and updates bounds', () => {
161
+ const geo = makeVertex(0, 0, 0);
162
+ geo.bounds = { min: { x: 0, y: 0, z: 0 }, max: { x: 0, y: 0, z: 0 } } as Aabb;
163
+ const v0 = geo.version;
164
+ translateMeshGeometry(geo, 5, 0, 0);
165
+ expect(geo.version).toBe(v0 + 1);
166
+ expect(geo.bounds!.min.x).toBeCloseTo(5);
167
+ expect(geo.bounds!.max.x).toBeCloseTo(5);
168
+ });
169
+ });
@@ -0,0 +1,128 @@
1
+ import type { VertexAttributeLayout } from '@flighthq/types';
2
+
3
+ import { createMeshGeometry } from './meshGeometry';
4
+ import { offsetMeshGeometryUvs, scaleMeshGeometryUvs, wrapMeshGeometryUvs } from './meshGeometryUvs';
5
+
6
+ const CANONICAL_LAYOUT: VertexAttributeLayout = {
7
+ attributes: [
8
+ { byteOffset: 0, format: 'float32x3', semantic: 'position' },
9
+ { byteOffset: 12, format: 'float32x3', semantic: 'normal' },
10
+ { byteOffset: 24, format: 'float32x4', semantic: 'tangent' },
11
+ { byteOffset: 40, format: 'float32x2', semantic: 'uv0' },
12
+ ],
13
+ stride: 48,
14
+ };
15
+
16
+ const NO_UV_LAYOUT: VertexAttributeLayout = {
17
+ attributes: [{ byteOffset: 0, format: 'float32x3', semantic: 'position' }],
18
+ stride: 12,
19
+ };
20
+
21
+ // Two-vertex geometry with known UV values.
22
+ function makeUvGeometry() {
23
+ const vertices = new Float32Array(2 * 12);
24
+ // Vertex 0: uv = (0.25, 0.75)
25
+ vertices[0 * 12 + 10] = 0.25;
26
+ vertices[0 * 12 + 11] = 0.75;
27
+ // Vertex 1: uv = (0.5, 0.5)
28
+ vertices[1 * 12 + 10] = 0.5;
29
+ vertices[1 * 12 + 11] = 0.5;
30
+ return createMeshGeometry({ layout: CANONICAL_LAYOUT, vertices: vertices });
31
+ }
32
+
33
+ describe('offsetMeshGeometryUvs', () => {
34
+ it('shifts all UV coordinates by (du, dv)', () => {
35
+ const geo = makeUvGeometry();
36
+ offsetMeshGeometryUvs(geo, 0.1, 0.2);
37
+ expect(geo.vertices[0 * 12 + 10]).toBeCloseTo(0.35);
38
+ expect(geo.vertices[0 * 12 + 11]).toBeCloseTo(0.95);
39
+ expect(geo.vertices[1 * 12 + 10]).toBeCloseTo(0.6);
40
+ expect(geo.vertices[1 * 12 + 11]).toBeCloseTo(0.7);
41
+ });
42
+ it('bumps version', () => {
43
+ const geo = makeUvGeometry();
44
+ const prev = geo.version;
45
+ offsetMeshGeometryUvs(geo, 0.1, 0.2);
46
+ expect(geo.version).toBe(prev + 1);
47
+ });
48
+ it('does nothing when uv0 is absent from the layout', () => {
49
+ const geo = createMeshGeometry({
50
+ layout: NO_UV_LAYOUT,
51
+ vertices: new Float32Array([1, 2, 3]),
52
+ });
53
+ const prev = geo.version;
54
+ offsetMeshGeometryUvs(geo, 0.5, 0.5);
55
+ expect(geo.version).toBe(prev);
56
+ });
57
+ it('does not bump version for an empty geometry', () => {
58
+ const geo = createMeshGeometry({ layout: CANONICAL_LAYOUT, vertices: new Float32Array(0) });
59
+ const prev = geo.version;
60
+ offsetMeshGeometryUvs(geo, 1, 1);
61
+ expect(geo.version).toBe(prev);
62
+ });
63
+ });
64
+
65
+ describe('scaleMeshGeometryUvs', () => {
66
+ it('scales all UV coordinates by (su, sv)', () => {
67
+ const geo = makeUvGeometry();
68
+ scaleMeshGeometryUvs(geo, 2, 4);
69
+ expect(geo.vertices[0 * 12 + 10]).toBeCloseTo(0.5);
70
+ expect(geo.vertices[0 * 12 + 11]).toBeCloseTo(3.0);
71
+ expect(geo.vertices[1 * 12 + 10]).toBeCloseTo(1.0);
72
+ expect(geo.vertices[1 * 12 + 11]).toBeCloseTo(2.0);
73
+ });
74
+ it('bumps version', () => {
75
+ const geo = makeUvGeometry();
76
+ const prev = geo.version;
77
+ scaleMeshGeometryUvs(geo, 2, 2);
78
+ expect(geo.version).toBe(prev + 1);
79
+ });
80
+ it('does nothing when uv0 is absent from the layout', () => {
81
+ const geo = createMeshGeometry({
82
+ layout: NO_UV_LAYOUT,
83
+ vertices: new Float32Array([1, 2, 3]),
84
+ });
85
+ const prev = geo.version;
86
+ scaleMeshGeometryUvs(geo, 2, 2);
87
+ expect(geo.version).toBe(prev);
88
+ });
89
+ });
90
+
91
+ describe('wrapMeshGeometryUvs', () => {
92
+ it('wraps coordinates in [0, 1)', () => {
93
+ const geo = makeUvGeometry();
94
+ // Scale first so UVs go out of range.
95
+ scaleMeshGeometryUvs(geo, 3, 3);
96
+ wrapMeshGeometryUvs(geo);
97
+ // vertex 0: u = 0.25*3 = 0.75 → 0.75, v = 0.75*3 = 2.25 → 0.25
98
+ expect(geo.vertices[0 * 12 + 10]).toBeCloseTo(0.75);
99
+ expect(geo.vertices[0 * 12 + 11]).toBeCloseTo(0.25);
100
+ // vertex 1: u = 0.5*3 = 1.5 → 0.5, v = 0.5*3 = 1.5 → 0.5
101
+ expect(geo.vertices[1 * 12 + 10]).toBeCloseTo(0.5);
102
+ expect(geo.vertices[1 * 12 + 11]).toBeCloseTo(0.5);
103
+ });
104
+ it('maps exactly 1.0 to 0.0', () => {
105
+ const vertices = new Float32Array(12);
106
+ vertices[10] = 1.0;
107
+ vertices[11] = 1.0;
108
+ const geo = createMeshGeometry({ layout: CANONICAL_LAYOUT, vertices: vertices });
109
+ wrapMeshGeometryUvs(geo);
110
+ expect(geo.vertices[10]).toBeCloseTo(0);
111
+ expect(geo.vertices[11]).toBeCloseTo(0);
112
+ });
113
+ it('bumps version', () => {
114
+ const geo = makeUvGeometry();
115
+ const prev = geo.version;
116
+ wrapMeshGeometryUvs(geo);
117
+ expect(geo.version).toBe(prev + 1);
118
+ });
119
+ it('does nothing when uv0 is absent from the layout', () => {
120
+ const geo = createMeshGeometry({
121
+ layout: NO_UV_LAYOUT,
122
+ vertices: new Float32Array([1, 2, 3]),
123
+ });
124
+ const prev = geo.version;
125
+ wrapMeshGeometryUvs(geo);
126
+ expect(geo.version).toBe(prev);
127
+ });
128
+ });