@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.
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/meshGeometry.d.ts +15 -0
- package/dist/meshGeometry.d.ts.map +1 -0
- package/dist/meshGeometry.js +128 -0
- package/dist/meshGeometry.js.map +1 -0
- package/dist/meshGeometryAttributes.d.ts +28 -0
- package/dist/meshGeometryAttributes.d.ts.map +1 -0
- package/dist/meshGeometryAttributes.js +144 -0
- package/dist/meshGeometryAttributes.js.map +1 -0
- package/dist/meshGeometryBuilders.d.ts +19 -0
- package/dist/meshGeometryBuilders.d.ts.map +1 -0
- package/dist/meshGeometryBuilders.js +772 -0
- package/dist/meshGeometryBuilders.js.map +1 -0
- package/dist/meshGeometryCompute.d.ts +7 -0
- package/dist/meshGeometryCompute.d.ts.map +1 -0
- package/dist/meshGeometryCompute.js +299 -0
- package/dist/meshGeometryCompute.js.map +1 -0
- package/dist/meshGeometryIndex.d.ts +4 -0
- package/dist/meshGeometryIndex.d.ts.map +1 -0
- package/dist/meshGeometryIndex.js +70 -0
- package/dist/meshGeometryIndex.js.map +1 -0
- package/dist/meshGeometryLayout.d.ts +4 -0
- package/dist/meshGeometryLayout.d.ts.map +1 -0
- package/dist/meshGeometryLayout.js +83 -0
- package/dist/meshGeometryLayout.js.map +1 -0
- package/dist/meshGeometryOperations.d.ts +12 -0
- package/dist/meshGeometryOperations.d.ts.map +1 -0
- package/dist/meshGeometryOperations.js +209 -0
- package/dist/meshGeometryOperations.js.map +1 -0
- package/dist/meshGeometrySubset.d.ts +5 -0
- package/dist/meshGeometrySubset.d.ts.map +1 -0
- package/dist/meshGeometrySubset.js +40 -0
- package/dist/meshGeometrySubset.js.map +1 -0
- package/dist/meshGeometryTransforms.d.ts +7 -0
- package/dist/meshGeometryTransforms.d.ts.map +1 -0
- package/dist/meshGeometryTransforms.js +235 -0
- package/dist/meshGeometryTransforms.js.map +1 -0
- package/dist/meshGeometryUvs.d.ts +5 -0
- package/dist/meshGeometryUvs.d.ts.map +1 -0
- package/dist/meshGeometryUvs.js +58 -0
- package/dist/meshGeometryUvs.js.map +1 -0
- package/package.json +39 -0
- package/src/meshGeometry.test.ts +155 -0
- package/src/meshGeometryAttributes.test.ts +195 -0
- package/src/meshGeometryBuilders.test.ts +220 -0
- package/src/meshGeometryCompute.test.ts +218 -0
- package/src/meshGeometryIndex.test.ts +84 -0
- package/src/meshGeometryLayout.test.ts +132 -0
- package/src/meshGeometryOperations.test.ts +154 -0
- package/src/meshGeometrySubset.test.ts +75 -0
- package/src/meshGeometryTransforms.test.ts +169 -0
- 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
|
+
});
|