@forgeax/engine-geometry 0.1.23 → 0.1.25

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.
@@ -1 +1 @@
1
- {"version":3,"file":"tangent.d.ts","sourceRoot":"","sources":["../src/tangent.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAqB,UAAU,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAmF5F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,YAAY,EACvB,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,YAAY,EACjB,OAAO,CAAC,EAAE,WAAW,GAAG,WAAW,GAClC,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CA6KlC"}
1
+ {"version":3,"file":"tangent.d.ts","sourceRoot":"","sources":["../src/tangent.ts"],"names":[],"mappings":"AA6BA,OAAO,EAAqB,UAAU,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAmF5F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,YAAY,EACvB,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,YAAY,EACjB,OAAO,CAAC,EAAE,WAAW,GAAG,WAAW,GAClC,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAyLlC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-geometry",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -22,10 +22,10 @@
22
22
  "LICENSE"
23
23
  ],
24
24
  "dependencies": {
25
- "@forgeax/engine-ecs": "0.1.23",
26
- "@forgeax/engine-math": "0.1.23",
27
- "@forgeax/engine-pack": "0.1.23",
28
- "@forgeax/engine-types": "0.1.23"
25
+ "@forgeax/engine-ecs": "0.1.25",
26
+ "@forgeax/engine-math": "0.1.25",
27
+ "@forgeax/engine-pack": "0.1.25",
28
+ "@forgeax/engine-types": "0.1.25"
29
29
  },
30
30
  "forgeax": {
31
31
  "metrics": {
@@ -0,0 +1,34 @@
1
+ import { computeTangentVec4 } from '@forgeax/engine-geometry';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ const positions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]);
5
+ const normals = new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1]);
6
+
7
+ describe('physical material tangent admission', () => {
8
+ it('preserves vec4 handedness for a valid rigid tangent', () => {
9
+ const result = computeTangentVec4(
10
+ positions,
11
+ normals,
12
+ new Float32Array([0, 0, 1, 0, 0, 1]),
13
+ new Uint32Array([0, 1, 2]),
14
+ );
15
+ expect(result.ok).toBe(true);
16
+ if (result.ok) expect([...result.value]).toEqual([1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1]);
17
+ });
18
+
19
+ it('fails closed for a degenerate UV triangle instead of inventing a tangent', () => {
20
+ const result = computeTangentVec4(
21
+ positions,
22
+ normals,
23
+ new Float32Array([0, 0, 0, 0, 0, 0]),
24
+ new Uint32Array([0, 1, 2]),
25
+ );
26
+ expect(result.ok).toBe(false);
27
+ if (!result.ok) {
28
+ expect(result.error.code).toBe('asset-parse-failed');
29
+ expect(result.error.detail).toMatchObject({
30
+ reason: expect.stringContaining('material-tangent-required'),
31
+ });
32
+ }
33
+ });
34
+ });
package/src/tangent.ts CHANGED
@@ -22,8 +22,10 @@
22
22
  // or side effects. Indices may be Uint16Array, Uint32Array, or
23
23
  // undefined (sequential 0..vertexCount-1 -- triangle list). Degenerate
24
24
  // triangles (zero area UV; det ~ 0) are skipped from the accumulation;
25
- // vertices touched only by degenerate triangles fall back to a stable
26
- // frame: tangent perpendicular to the supplied normal with .w = +1.
25
+ // Inputs with no valid UV-derived triangle fail closed. Procedural pole/seam
26
+ // vertices may have no local contribution while sharing valid neighboring
27
+ // faces; they retain the established deterministic frame until the material
28
+ // admission layer has mesh-level tangent context.
27
29
 
28
30
  import { ASSET_ERROR_HINTS, AssetError, err, ok, type Result } from '@forgeax/engine-types';
29
31
 
@@ -154,6 +156,7 @@ export function computeTangentVec4(
154
156
  // the running sum is the per-vertex .w handedness.
155
157
  const accumT = new Float32Array(vertexCount * 3);
156
158
  const accumSign = new Float32Array(vertexCount);
159
+ let validTriangleCount = 0;
157
160
 
158
161
  const triangleCount = indices !== undefined ? indices.length / 3 : vertexCount / 3;
159
162
 
@@ -207,6 +210,7 @@ export function computeTangentVec4(
207
210
  const cz = dP1x * dP2y - dP1y * dP2x;
208
211
  const faceArea = 0.5 * Math.sqrt(cx * cx + cy * cy + cz * cz);
209
212
  if (faceArea < EPSILON) continue;
213
+ validTriangleCount += 1;
210
214
 
211
215
  const signDet = det >= 0 ? 1 : -1;
212
216
  const signedWeight = faceArea * signDet;
@@ -219,6 +223,16 @@ export function computeTangentVec4(
219
223
  }
220
224
  }
221
225
 
226
+ if (validTriangleCount === 0) {
227
+ return err(
228
+ tangentInputError(
229
+ 'tangent',
230
+ 0,
231
+ 'material-tangent-required: no triangle has a valid UV-derived tangent',
232
+ ),
233
+ );
234
+ }
235
+
222
236
  const out = new Float32Array(vertexCount * 4);
223
237
  for (let v = 0; v < vertexCount; v++) {
224
238
  let tx = accumT[v * 3] ?? 0;
@@ -230,36 +244,35 @@ export function computeTangentVec4(
230
244
 
231
245
  const tLen = Math.sqrt(tx * tx + ty * ty + tz * tz);
232
246
  if (tLen < EPSILON) {
233
- // Fallback: pick any direction perpendicular to N. Use the axis
234
- // with the smallest |normal component| to maximise numerical
235
- // stability of the cross product.
236
247
  const ax = Math.abs(nx);
237
248
  const ay = Math.abs(ny);
238
249
  const az = Math.abs(nz);
239
250
  let rx = 1;
240
251
  let ry = 0;
241
252
  let rz = 0;
242
- if (ax <= ay && ax <= az) {
243
- rx = 1;
244
- ry = 0;
245
- rz = 0;
246
- } else if (ay <= ax && ay <= az) {
253
+ if (ax > ay && ax > az) {
247
254
  rx = 0;
248
255
  ry = 1;
249
- rz = 0;
250
- } else {
256
+ } else if (ay > ax && ay > az) {
251
257
  rx = 0;
252
- ry = 0;
253
258
  rz = 1;
254
259
  }
255
- // T = normalize(cross(N, R))
256
260
  tx = ny * rz - nz * ry;
257
261
  ty = nz * rx - nx * rz;
258
262
  tz = nx * ry - ny * rx;
259
- const fLen = Math.sqrt(tx * tx + ty * ty + tz * tz) || 1;
260
- tx /= fLen;
261
- ty /= fLen;
262
- tz /= fLen;
263
+ const fallbackLength = Math.sqrt(tx * tx + ty * ty + tz * tz);
264
+ if (fallbackLength < EPSILON) {
265
+ return err(
266
+ tangentInputError(
267
+ 'tangent',
268
+ v,
269
+ 'material-tangent-required: supplied normal cannot define a tangent frame',
270
+ ),
271
+ );
272
+ }
273
+ tx /= fallbackLength;
274
+ ty /= fallbackLength;
275
+ tz /= fallbackLength;
263
276
  } else {
264
277
  tx /= tLen;
265
278
  ty /= tLen;
@@ -273,34 +286,35 @@ export function computeTangentVec4(
273
286
  let gz = tz - dotTN * nz;
274
287
  const gLen = Math.sqrt(gx * gx + gy * gy + gz * gz);
275
288
  if (gLen < EPSILON) {
276
- // tangent collinear with normal after accumulation; reuse fallback
277
- // basis. Pick perpendicular via the smallest-axis trick again.
278
289
  const ax = Math.abs(nx);
279
290
  const ay = Math.abs(ny);
280
291
  const az = Math.abs(nz);
281
292
  let rx = 1;
282
293
  let ry = 0;
283
294
  let rz = 0;
284
- if (ax <= ay && ax <= az) {
285
- rx = 1;
286
- ry = 0;
287
- rz = 0;
288
- } else if (ay <= ax && ay <= az) {
295
+ if (ax > ay && ax > az) {
289
296
  rx = 0;
290
297
  ry = 1;
291
- rz = 0;
292
- } else {
298
+ } else if (ay > ax && ay > az) {
293
299
  rx = 0;
294
- ry = 0;
295
300
  rz = 1;
296
301
  }
297
302
  gx = ny * rz - nz * ry;
298
303
  gy = nz * rx - nx * rz;
299
304
  gz = nx * ry - ny * rx;
300
- const fLen = Math.sqrt(gx * gx + gy * gy + gz * gz) || 1;
301
- gx /= fLen;
302
- gy /= fLen;
303
- gz /= fLen;
305
+ const fallbackLength = Math.sqrt(gx * gx + gy * gy + gz * gz);
306
+ if (fallbackLength < EPSILON) {
307
+ return err(
308
+ tangentInputError(
309
+ 'tangent',
310
+ v,
311
+ 'material-tangent-required: supplied normal cannot define a tangent frame',
312
+ ),
313
+ );
314
+ }
315
+ gx /= fallbackLength;
316
+ gy /= fallbackLength;
317
+ gz /= fallbackLength;
304
318
  } else {
305
319
  gx /= gLen;
306
320
  gy /= gLen;