@forgeax/engine-math 0.0.0-dev.8d955ade1c79

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 (150) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +294 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/_arbs.d.ts +36 -0
  5. package/dist/__tests__/_arbs.d.ts.map +1 -0
  6. package/dist/__tests__/_fixtures.d.ts +61 -0
  7. package/dist/__tests__/_fixtures.d.ts.map +1 -0
  8. package/dist/__tests__/bounds2.test.d.ts +2 -0
  9. package/dist/__tests__/bounds2.test.d.ts.map +1 -0
  10. package/dist/__tests__/box3.test.d.ts +2 -0
  11. package/dist/__tests__/box3.test.d.ts.map +1 -0
  12. package/dist/__tests__/easing.test.d.ts +2 -0
  13. package/dist/__tests__/easing.test.d.ts.map +1 -0
  14. package/dist/__tests__/euler.test-d.d.ts +2 -0
  15. package/dist/__tests__/euler.test-d.d.ts.map +1 -0
  16. package/dist/__tests__/mat3.test-d.d.ts +2 -0
  17. package/dist/__tests__/mat3.test-d.d.ts.map +1 -0
  18. package/dist/__tests__/mat4.property.test.d.ts +2 -0
  19. package/dist/__tests__/mat4.property.test.d.ts.map +1 -0
  20. package/dist/__tests__/mat4.test-d.d.ts +2 -0
  21. package/dist/__tests__/mat4.test-d.d.ts.map +1 -0
  22. package/dist/__tests__/mat4.test.d.ts +2 -0
  23. package/dist/__tests__/mat4.test.d.ts.map +1 -0
  24. package/dist/__tests__/noise.test.d.ts +2 -0
  25. package/dist/__tests__/noise.test.d.ts.map +1 -0
  26. package/dist/__tests__/quat.basis.test.d.ts +2 -0
  27. package/dist/__tests__/quat.basis.test.d.ts.map +1 -0
  28. package/dist/__tests__/quat.interpolation.test.d.ts +2 -0
  29. package/dist/__tests__/quat.interpolation.test.d.ts.map +1 -0
  30. package/dist/__tests__/quat.lookat.test.d.ts +2 -0
  31. package/dist/__tests__/quat.lookat.test.d.ts.map +1 -0
  32. package/dist/__tests__/quat.property.test.d.ts +2 -0
  33. package/dist/__tests__/quat.property.test.d.ts.map +1 -0
  34. package/dist/__tests__/quat.rotateaxis.test.d.ts +2 -0
  35. package/dist/__tests__/quat.rotateaxis.test.d.ts.map +1 -0
  36. package/dist/__tests__/quat.test-d.d.ts +2 -0
  37. package/dist/__tests__/quat.test-d.d.ts.map +1 -0
  38. package/dist/__tests__/ray.property.test.d.ts +2 -0
  39. package/dist/__tests__/ray.property.test.d.ts.map +1 -0
  40. package/dist/__tests__/ray.test.d.ts +2 -0
  41. package/dist/__tests__/ray.test.d.ts.map +1 -0
  42. package/dist/__tests__/types.test-d.d.ts +2 -0
  43. package/dist/__tests__/types.test-d.d.ts.map +1 -0
  44. package/dist/__tests__/vec-catmull-rom.test.d.ts +2 -0
  45. package/dist/__tests__/vec-catmull-rom.test.d.ts.map +1 -0
  46. package/dist/__tests__/vec-smooth-damp.test.d.ts +2 -0
  47. package/dist/__tests__/vec-smooth-damp.test.d.ts.map +1 -0
  48. package/dist/__tests__/vec2.test-d.d.ts +2 -0
  49. package/dist/__tests__/vec2.test-d.d.ts.map +1 -0
  50. package/dist/__tests__/vec3.property.test.d.ts +2 -0
  51. package/dist/__tests__/vec3.property.test.d.ts.map +1 -0
  52. package/dist/__tests__/vec3.test-d.d.ts +2 -0
  53. package/dist/__tests__/vec3.test-d.d.ts.map +1 -0
  54. package/dist/__tests__/vec4.test-d.d.ts +2 -0
  55. package/dist/__tests__/vec4.test-d.d.ts.map +1 -0
  56. package/dist/_internal/epsilon.d.ts +9 -0
  57. package/dist/_internal/epsilon.d.ts.map +1 -0
  58. package/dist/_internal/scalar.d.ts +40 -0
  59. package/dist/_internal/scalar.d.ts.map +1 -0
  60. package/dist/box2.d.ts +35 -0
  61. package/dist/box2.d.ts.map +1 -0
  62. package/dist/box3.d.ts +76 -0
  63. package/dist/box3.d.ts.map +1 -0
  64. package/dist/circle2.d.ts +29 -0
  65. package/dist/circle2.d.ts.map +1 -0
  66. package/dist/color.d.ts +64 -0
  67. package/dist/color.d.ts.map +1 -0
  68. package/dist/easing.d.ts +18 -0
  69. package/dist/easing.d.ts.map +1 -0
  70. package/dist/euler.d.ts +51 -0
  71. package/dist/euler.d.ts.map +1 -0
  72. package/dist/f32-to-f16-bytes.d.ts +10 -0
  73. package/dist/f32-to-f16-bytes.d.ts.map +1 -0
  74. package/dist/frustum.d.ts +49 -0
  75. package/dist/frustum.d.ts.map +1 -0
  76. package/dist/index.d.ts +20 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.mjs +3688 -0
  79. package/dist/index.mjs.map +1 -0
  80. package/dist/mat3.d.ts +70 -0
  81. package/dist/mat3.d.ts.map +1 -0
  82. package/dist/mat4.d.ts +329 -0
  83. package/dist/mat4.d.ts.map +1 -0
  84. package/dist/noise.d.ts +12 -0
  85. package/dist/noise.d.ts.map +1 -0
  86. package/dist/quat.d.ts +312 -0
  87. package/dist/quat.d.ts.map +1 -0
  88. package/dist/ray.d.ts +144 -0
  89. package/dist/ray.d.ts.map +1 -0
  90. package/dist/ray2.d.ts +38 -0
  91. package/dist/ray2.d.ts.map +1 -0
  92. package/dist/sphere.d.ts +41 -0
  93. package/dist/sphere.d.ts.map +1 -0
  94. package/dist/types.d.ts +57 -0
  95. package/dist/types.d.ts.map +1 -0
  96. package/dist/vec2.d.ts +76 -0
  97. package/dist/vec2.d.ts.map +1 -0
  98. package/dist/vec3.d.ts +82 -0
  99. package/dist/vec3.d.ts.map +1 -0
  100. package/dist/vec4.d.ts +61 -0
  101. package/dist/vec4.d.ts.map +1 -0
  102. package/package.json +57 -0
  103. package/src/__tests__/_arbs.ts +149 -0
  104. package/src/__tests__/_fixtures.ts +118 -0
  105. package/src/__tests__/bounds2.test.ts +146 -0
  106. package/src/__tests__/box3.test.ts +277 -0
  107. package/src/__tests__/easing.test.ts +109 -0
  108. package/src/__tests__/euler.test-d.ts +63 -0
  109. package/src/__tests__/mat3.test-d.ts +47 -0
  110. package/src/__tests__/mat4.property.test.ts +256 -0
  111. package/src/__tests__/mat4.test-d.ts +130 -0
  112. package/src/__tests__/mat4.test.ts +162 -0
  113. package/src/__tests__/noise.test.ts +75 -0
  114. package/src/__tests__/quat.basis.test.ts +134 -0
  115. package/src/__tests__/quat.interpolation.test.ts +45 -0
  116. package/src/__tests__/quat.lookat.test.ts +103 -0
  117. package/src/__tests__/quat.property.test.ts +150 -0
  118. package/src/__tests__/quat.rotateaxis.test.ts +149 -0
  119. package/src/__tests__/quat.test-d.ts +138 -0
  120. package/src/__tests__/ray.property.test.ts +105 -0
  121. package/src/__tests__/ray.test.ts +539 -0
  122. package/src/__tests__/types.test-d.ts +64 -0
  123. package/src/__tests__/vec-catmull-rom.test.ts +125 -0
  124. package/src/__tests__/vec-smooth-damp.test.ts +167 -0
  125. package/src/__tests__/vec2.test-d.ts +59 -0
  126. package/src/__tests__/vec3.property.test.ts +72 -0
  127. package/src/__tests__/vec3.test-d.ts +65 -0
  128. package/src/__tests__/vec4.test-d.ts +61 -0
  129. package/src/_internal/epsilon.ts +29 -0
  130. package/src/_internal/scalar.ts +90 -0
  131. package/src/box2.ts +168 -0
  132. package/src/box3.ts +372 -0
  133. package/src/circle2.ts +134 -0
  134. package/src/color.ts +177 -0
  135. package/src/easing.ts +49 -0
  136. package/src/euler.ts +239 -0
  137. package/src/f32-to-f16-bytes.ts +71 -0
  138. package/src/frustum.ts +240 -0
  139. package/src/index.ts +65 -0
  140. package/src/mat3.ts +286 -0
  141. package/src/mat4.ts +1334 -0
  142. package/src/noise.ts +78 -0
  143. package/src/quat.ts +847 -0
  144. package/src/ray.ts +575 -0
  145. package/src/ray2.ts +198 -0
  146. package/src/sphere.ts +138 -0
  147. package/src/types.ts +78 -0
  148. package/src/vec2.ts +229 -0
  149. package/src/vec3.ts +294 -0
  150. package/src/vec4.ts +235 -0
package/src/color.ts ADDED
@@ -0,0 +1,177 @@
1
+ // color.ts — RGBA color namespace (M5 / T-032)
2
+ //
3
+ // 6-function surface: create / clone / srgbToLinear / linearToSrgb / fromHex / toHex
4
+ //
5
+ // Design anchors:
6
+ // - branded Float32Array length 4 [r, g, b, a]; `as Color` casts are funneled inside factories (D-P15)
7
+ // - sRGB ↔ linear: IEC 61966-2-1 piecewise gamma; RGB channels only, alpha pass-through
8
+ // - srgbToLinear: cutoff 0.04045 / linear segment v/12.92 / power segment ((v+0.055)/1.055)^2.4
9
+ // - linearToSrgb: cutoff 0.0031308 / linear segment 12.92*v / power segment 1.055*v^(1/2.4) - 0.055
10
+ // - negatives / NaN returned verbatim (HDR-friendly + IEEE-754 NaN propagation)
11
+ // - fromHex (D-P7): only `#RRGGBB` (7 chars) and `#RRGGBBAA` (9 chars); illegal inputs silently fall
12
+ // back to (0, 0, 0, 1) without throwing (D-P12 degenerate family / AC-06)
13
+ // - #RGB / #RGBA short forms are not supported (stricter than bevy_color::Srgba::hex; less ambiguous
14
+ // in practice; wiki/sources/2026-05-05-bevy-0-19-math-transform-color §Srgba::hex mentions short
15
+ // forms but D-P7 actively tightens the contract)
16
+ // - toHex: alpha=1 → `#rrggbb`; alpha<1 → `#rrggbbaa`; components are clamped to [0, 1] then
17
+ // multiplied by 255 and rounded; output is lowercase, symmetric with fromHex's tolerant input
18
+ //
19
+ // Related: requirements §Surface color lower bound 6 + AC-06 silent fall-back (never raises);
20
+ // plan-strategy D-P7 / D-P12 / §appendix A degenerate registry #14-#16;
21
+ // wiki/sources/2026-05-05-bevy-0-19-math-transform-color §sRGB piecewise gamma;
22
+ // wiki/glam-rs-overview §LinearRgba.
23
+
24
+ import type { Color, ColorLike } from './types';
25
+
26
+ export type { Color, ColorLike };
27
+
28
+ // === create / copy ===
29
+
30
+ /** Create a Color (default RGBA = (0, 0, 0, 1) = opaque black). */
31
+ export function create(r = 0, g = 0, b = 0, a = 1): Color {
32
+ return Float32Array.of(r, g, b, a) as Color;
33
+ }
34
+
35
+ /** Allocate a new Color copy. */
36
+ export function clone(c: ColorLike): Color {
37
+ return Float32Array.of(c[0] as number, c[1] as number, c[2] as number, c[3] as number) as Color;
38
+ }
39
+
40
+ // === sRGB ↔ linear (IEC 61966-2-1 piecewise gamma) ===
41
+
42
+ /**
43
+ * Single-channel sRGB → linear (component-level helper).
44
+ *
45
+ * @degrade NaN input → NaN output (IEEE-754 arithmetic propagation); negatives returned verbatim
46
+ * to preserve HDR / specialized rendering needs (same convention as bevy_color::gamma_function;
47
+ * wiki/sources bevy-0-19-color §gamma_function).
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * srgbChannelToLinear(NaN); // → NaN (IEEE-754 propagation)
52
+ * srgbChannelToLinear(-0.1); // → -0.1 (HDR / negative kept verbatim)
53
+ * ```
54
+ */
55
+ function srgbChannelToLinear(v: number): number {
56
+ if (Number.isNaN(v)) return Number.NaN;
57
+ if (v <= 0) return v;
58
+ if (v <= 0.04045) return v / 12.92;
59
+ return ((v + 0.055) / 1.055) ** 2.4;
60
+ }
61
+
62
+ /** Single-channel linear → sRGB (component-level helper). */
63
+ function linearChannelToSrgb(v: number): number {
64
+ if (Number.isNaN(v)) return Number.NaN;
65
+ if (v <= 0) return v;
66
+ if (v <= 0.0031308) return v * 12.92;
67
+ return 1.055 * v ** (1 / 2.4) - 0.055;
68
+ }
69
+
70
+ /**
71
+ * out = sRGB → linear conversion (RGB channels only; alpha passed through).
72
+ *
73
+ * @degrade NaN propagation / negatives returned verbatim (HDR-friendly); alpha is strictly untouched.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * color.srgbToLinear(out, color.create(NaN, -0.1, 0.5, 0.8));
78
+ * // → out = (NaN, -0.1, ~0.214, 0.8); alpha 0.8 passed through unchanged.
79
+ * ```
80
+ */
81
+ export function srgbToLinear(out: Color, c: ColorLike): Color {
82
+ out[0] = srgbChannelToLinear(c[0] as number);
83
+ out[1] = srgbChannelToLinear(c[1] as number);
84
+ out[2] = srgbChannelToLinear(c[2] as number);
85
+ out[3] = c[3] as number;
86
+ return out;
87
+ }
88
+
89
+ /**
90
+ * out = linear → sRGB conversion (RGB channels only; alpha passed through).
91
+ *
92
+ * @degrade NaN propagation / negatives returned verbatim; alpha is strictly untouched.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * color.linearToSrgb(out, color.create(NaN, -0.05, 0.5, 1));
97
+ * // → out = (NaN, -0.05, ~0.735, 1); alpha is strictly untouched.
98
+ * ```
99
+ */
100
+ export function linearToSrgb(out: Color, c: ColorLike): Color {
101
+ out[0] = linearChannelToSrgb(c[0] as number);
102
+ out[1] = linearChannelToSrgb(c[1] as number);
103
+ out[2] = linearChannelToSrgb(c[2] as number);
104
+ out[3] = c[3] as number;
105
+ return out;
106
+ }
107
+
108
+ // === Hex parse / serialize ===
109
+
110
+ const HEX_PATTERN = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})?$/;
111
+
112
+ /**
113
+ * Write a Color from a hex string.
114
+ *
115
+ * Only supports the two forms `#RRGGBB` (7 chars) and `#RRGGBBAA` (9 chars) (D-P7);
116
+ * the CSS-style `#RGB` / `#RGBA` short forms are not supported.
117
+ *
118
+ * @degrade Any illegal input (short form / non-hex chars / missing # / wrong length / empty string,
119
+ * etc.) silently falls back to (0, 0, 0, 1); never throws (D-P12 / AC-06).
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * color.fromHex(out, '#ff8000'); // → (1, 0.502, 0, 1)
124
+ * color.fromHex(out, '#ff800080'); // → (1, 0.502, 0, 0.502)
125
+ * color.fromHex(out, '#fff'); // → (0, 0, 0, 1) short form unsupported → silent fall-back
126
+ * ```
127
+ */
128
+ export function fromHex(out: Color, hex: string): Color {
129
+ const match = typeof hex === 'string' ? HEX_PATTERN.exec(hex) : null;
130
+ if (match === null) {
131
+ out[0] = 0;
132
+ out[1] = 0;
133
+ out[2] = 0;
134
+ out[3] = 1;
135
+ return out;
136
+ }
137
+ out[0] = Number.parseInt(match[1] as string, 16) / 255;
138
+ out[1] = Number.parseInt(match[2] as string, 16) / 255;
139
+ out[2] = Number.parseInt(match[3] as string, 16) / 255;
140
+ out[3] = match[4] !== undefined ? Number.parseInt(match[4], 16) / 255 : 1;
141
+ return out;
142
+ }
143
+
144
+ /** Per-component [0, 1] clamp + ×255 + round + 2-digit hex (lowercase). */
145
+ function toHexByte(v: number): string {
146
+ if (Number.isNaN(v)) return '00';
147
+ const clamped = Math.max(0, Math.min(1, v));
148
+ const byte = Math.round(clamped * 255);
149
+ return byte.toString(16).padStart(2, '0');
150
+ }
151
+
152
+ /**
153
+ * Serialize as hex string: alpha=1 → `#rrggbb`; alpha<1 → `#rrggbbaa`.
154
+ *
155
+ * @degrade Out-of-range components (HDR > 1 / negatives) are clamped to [0, 1] then rounded;
156
+ * **truncated** rather than throwing (symmetric with fromHex's silent fall-back). NaN components →
157
+ * literal '00' (same byte as 0.0; prevents the hex-string builder from throwing; alpha=NaN takes the
158
+ * alpha=1 path, emitting the short `#rrggbb` form).
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * color.toHex(color.create(2, -0.1, 0.5, 1)); // → '#ff0080' (HDR/negative clamp)
163
+ * color.toHex(color.create(1, 0, 0, 0.5)); // → '#ff000080'
164
+ * color.toHex(color.create(NaN, 0, 0, NaN)); // → '#000000' (NaN→'00', alpha=NaN takes short form)
165
+ * ```
166
+ */
167
+ export function toHex(c: ColorLike): string {
168
+ const r = toHexByte(c[0] as number);
169
+ const g = toHexByte(c[1] as number);
170
+ const b = toHexByte(c[2] as number);
171
+ const aValue = c[3] as number;
172
+ if (aValue >= 1 || Number.isNaN(aValue)) {
173
+ return `#${r}${g}${b}`;
174
+ }
175
+ const a = toHexByte(aValue);
176
+ return `#${r}${g}${b}${a}`;
177
+ }
package/src/easing.ts ADDED
@@ -0,0 +1,49 @@
1
+ // easing.ts — easing-function namespace (solo round 20260713-233409)
2
+ //
3
+ // 4-function surface: cubicInOut / smoothstep / smootherstep / elasticInOut
4
+ // (GLSL `smoothstep`, Perlin's smootherstep; Bevy `EaseFunction::SmoothStep`/`SmootherStep`).
5
+ // Scalar time-remaps t → number: take a normalized parameter and return an eased value. The
6
+ // growable home for Bevy's `EaseFunction` family — further variants (sine / quad / bounce /
7
+ // steps) land here add-only.
8
+ //
9
+ // Every function clamps the input to [0, 1] first (GLSL / Bevy semantics), so out-of-range t
10
+ // saturates to the endpoints rather than extrapolating the curve.
11
+
12
+ import { clamp } from './_internal/scalar';
13
+
14
+ /** Cubic ease-in-out: Bevy `EaseFunction::CubicInOut`, clamped to [0, 1]. */
15
+ export function cubicInOut(t: number): number {
16
+ const x = clamp(t, 0, 1);
17
+ return x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2;
18
+ }
19
+
20
+ /**
21
+ * Smoothstep S-curve: `3t² − 2t³` on the clamped input. GLSL `smoothstep` (with edges 0/1),
22
+ * Bevy `EaseFunction::SmoothStep`. f(0)=0, f(1)=1, f′(0)=f′(1)=0 (slow-in / slow-out).
23
+ * Input clamped to [0, 1]. Use to ease a normalized time / lerp factor instead of a linear ramp.
24
+ */
25
+ export function smoothstep(t: number): number {
26
+ const x = clamp(t, 0, 1);
27
+ return x * x * (3 - 2 * x);
28
+ }
29
+
30
+ /**
31
+ * Smootherstep (Perlin) S-curve: `6t⁵ − 15t⁴ + 10t³` on the clamped input. Bevy
32
+ * `EaseFunction::SmootherStep`. Like {@link smoothstep} but ALSO has zero 2nd derivatives at
33
+ * the endpoints (f″(0)=f″(1)=0), so acceleration is continuous — a gentler, more natural ease.
34
+ * Input clamped to [0, 1].
35
+ */
36
+ export function smootherstep(t: number): number {
37
+ const x = clamp(t, 0, 1);
38
+ return x * x * x * (x * (x * 6 - 15) + 10);
39
+ }
40
+
41
+ /** Elastic ease-in-out: Bevy `EaseFunction::ElasticInOut`, clamped to [0, 1]. */
42
+ export function elasticInOut(t: number): number {
43
+ const x = clamp(t, 0, 1);
44
+ if (x === 0 || x === 1) return x;
45
+ const c5 = (2 * Math.PI) / 4.5;
46
+ return x < 0.5
47
+ ? -(2 ** (20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2
48
+ : (2 ** (-20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + 1;
49
+ }
package/src/euler.ts ADDED
@@ -0,0 +1,239 @@
1
+ // euler.ts — Euler angle namespace (M4 / T-028)
2
+ //
3
+ // 6-function surface (≥ 6 lower bound):
4
+ // create / clone / set / fromQuat / toQuat / fromRotationMatrix
5
+ //
6
+ // Memory layout: plain object `{ x: number, y: number, z: number, order: EulerOrder }`
7
+ // (not a Float32Array). Rationale:
8
+ // - need to store length=3 plus a string order; strings cannot live inside a TypedArray
9
+ // - performance-insensitive (euler is only used at the editor/IO boundary; runtime always converts to Quat)
10
+ //
11
+ // 6 orders (intrinsic rotation; literal order x → y → z):
12
+ // XYZ / YXZ / ZXY / ZYX / YZX / XZY
13
+ //
14
+ // Degenerate convention (plan-strategy §appendix A degenerate registry #16):
15
+ // - fromQuat(q, order) gimbal-lock (pitch ≈ ±π/2) → pick an equivalent branch; never throws
16
+ // - fromRotationMatrix follows the same convention
17
+ //
18
+ // Related: requirements §Surface euler lower bound 6 + 6-order full support;
19
+ // plan-strategy §1.1 euler.ts + §appendix A degenerate registry #16 + D-P19 EulerOrder union;
20
+ // wiki/glam-rs-overview Hamilton convention +
21
+ // Three.js Euler.setFromQuaternion / setFromRotationMatrix 6-order formulas.
22
+ //
23
+ // Degenerate-semantics registry (plan-strategy.md §appendix A; D-P16 dual promise:
24
+ // runtime silent + JSDoc `@degrade` + `@example` guard pattern landing together):
25
+ // #16 euler.fromQuat(q, order) gimbal-lock → pick equivalent branch (no throw)
26
+ // #17 euler.fromRotationMatrix gimbal-lock → same as above
27
+
28
+ import * as quat from './quat';
29
+ import type { Euler, EulerOrder, Mat3Like, Quat, QuatLike } from './types';
30
+
31
+ export type { Euler, EulerOrder };
32
+
33
+ /** Create an Euler, defaulting to (0, 0, 0, 'XYZ'). */
34
+ export function create(): Euler {
35
+ return { x: 0, y: 0, z: 0, order: 'XYZ' };
36
+ }
37
+
38
+ /** Allocate a new Euler copy. */
39
+ export function clone(a: Euler): Euler {
40
+ return { x: a.x, y: a.y, z: a.z, order: a.order };
41
+ }
42
+
43
+ /** out.x/y/z/order = inputs; returns out (in-place). */
44
+ export function set(out: Euler, x: number, y: number, z: number, order: EulerOrder): Euler {
45
+ out.x = x;
46
+ out.y = y;
47
+ out.z = z;
48
+ out.order = order;
49
+ return out;
50
+ }
51
+
52
+ /**
53
+ * out (Quat) = quaternion from euler. Returns out.
54
+ *
55
+ * Routes directly to quat.fromEuler (D-P2 + 6-order coverage already implemented inside quat).
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * euler.toQuat(quatBuf, eulerInstance);
60
+ * ```
61
+ */
62
+ export function toQuat(out: Quat, e: Euler): Quat {
63
+ return quat.fromEuler(out, e.x, e.y, e.z, e.order);
64
+ }
65
+
66
+ /**
67
+ * out (Euler) = euler angles from quaternion in given order. Returns out.
68
+ *
69
+ * Implementation path: quat → 3x3 column-major rotation matrix → fromRotationMatrix(order).
70
+ * This concentrates the 6-order formulas in fromRotationMatrix and avoids dual maintenance.
71
+ *
72
+ * @degrade gimbal lock (pitch ≈ ±π/2, sin/cos critical) → set yaw to 0 and let roll absorb the
73
+ * full rotation (registry #16; same equivalent-branch semantics as Three.js; no throw).
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * euler.fromQuat(out, q, 'XYZ');
78
+ * ```
79
+ */
80
+ export function fromQuat(out: Euler, q: QuatLike, order: EulerOrder): Euler {
81
+ // Expand q into a 3x3 column-major rotation matrix (matching the index convention of quat.fromRotationMatrix)
82
+ const x = q[0] as number;
83
+ const y = q[1] as number;
84
+ const z = q[2] as number;
85
+ const w = q[3] as number;
86
+ const xx = x * x;
87
+ const xy = x * y;
88
+ const xz = x * z;
89
+ const yy = y * y;
90
+ const yz = y * z;
91
+ const zz = z * z;
92
+ const wx = w * x;
93
+ const wy = w * y;
94
+ const wz = w * z;
95
+
96
+ // mat3 column-major: m[col*3+row] → m00=col0row0, m01=col0row1, m10=col1row0...
97
+ // R[row][col] maps to m[col*3+row]:
98
+ // R[0][0]=m00,R[0][1]=m10,R[0][2]=m20
99
+ // R[1][0]=m01,R[1][1]=m11,R[1][2]=m21
100
+ // R[2][0]=m02,R[2][1]=m12,R[2][2]=m22
101
+ const m = new Float32Array(9);
102
+ m[0] = 1 - 2 * (yy + zz);
103
+ m[1] = 2 * (xy + wz);
104
+ m[2] = 2 * (xz - wy);
105
+ m[3] = 2 * (xy - wz);
106
+ m[4] = 1 - 2 * (xx + zz);
107
+ m[5] = 2 * (yz + wx);
108
+ m[6] = 2 * (xz + wy);
109
+ m[7] = 2 * (yz - wx);
110
+ m[8] = 1 - 2 * (xx + yy);
111
+
112
+ return fromRotationMatrix(out, m, order);
113
+ }
114
+
115
+ /**
116
+ * out (Euler) = euler from a 3x3 rotation matrix m (column-major, length 9) in `order`. Returns out.
117
+ *
118
+ * Uses the Three.js Euler.setFromRotationMatrix formulas (intrinsic rotation + Hamilton convention).
119
+ * Each of the 6 orders has an analytic formula; near gimbal-lock (middle axis sin/cos near ±1)
120
+ * an equivalent branch is selected.
121
+ *
122
+ * @degrade gimbal lock → middle axis is fixed at ±π/2; the remaining two axes degenerate
123
+ * (one is set to 0; the other absorbs the rotation).
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * euler.fromRotationMatrix(out, mat3InstanceColumnMajor, 'XYZ');
128
+ * ```
129
+ */
130
+ export function fromRotationMatrix(out: Euler, m: Mat3Like, order: EulerOrder): Euler {
131
+ // Column-major indexing (matches quat.fromRotationMatrix):
132
+ // m00 m01 m02 = column 0 (col=0)'s row=0/1/2
133
+ // m10 m11 m12 = column 1 (col=1)'s row=0/1/2
134
+ // m20 m21 m22 = column 2 (col=2)'s row=0/1/2
135
+ // Mathematically R[row][col] = m[c*3 + r].
136
+ // Thus, in the Three.js formulas, m11 (R[0][0]) corresponds to our m[0],
137
+ // m12 (R[0][1]) corresponds to m[3],
138
+ // m13 (R[0][2]) corresponds to m[6],
139
+ // m21 (R[1][0]) corresponds to m[1], etc.
140
+ // For readability and alignment with Three.js naming, alias (_ij denotes R[i-1][j-1]):
141
+ const _11 = m[0] as number;
142
+ const _21 = m[1] as number;
143
+ const _31 = m[2] as number;
144
+ const _12 = m[3] as number;
145
+ const _22 = m[4] as number;
146
+ const _32 = m[5] as number;
147
+ const _13 = m[6] as number;
148
+ const _23 = m[7] as number;
149
+ const _33 = m[8] as number;
150
+
151
+ out.order = order;
152
+ switch (order) {
153
+ case 'XYZ':
154
+ out.y = Math.asin(clamp11(_13));
155
+ if (Math.abs(_13) < 1 - 1e-7) {
156
+ out.x = Math.atan2(-_23, _33);
157
+ out.z = Math.atan2(-_12, _11);
158
+ } else {
159
+ // gimbal lock
160
+ out.x = Math.atan2(_32, _22);
161
+ out.z = 0;
162
+ }
163
+ break;
164
+ case 'YXZ':
165
+ out.x = Math.asin(-clamp11(_23));
166
+ if (Math.abs(_23) < 1 - 1e-7) {
167
+ out.y = Math.atan2(_13, _33);
168
+ out.z = Math.atan2(_21, _22);
169
+ } else {
170
+ out.y = Math.atan2(-_31, _11);
171
+ out.z = 0;
172
+ }
173
+ break;
174
+ case 'ZXY':
175
+ out.x = Math.asin(clamp11(_32));
176
+ if (Math.abs(_32) < 1 - 1e-7) {
177
+ out.y = Math.atan2(-_31, _33);
178
+ out.z = Math.atan2(-_12, _22);
179
+ } else {
180
+ out.y = 0;
181
+ out.z = Math.atan2(_21, _11);
182
+ }
183
+ break;
184
+ case 'ZYX':
185
+ out.y = Math.asin(-clamp11(_31));
186
+ if (Math.abs(_31) < 1 - 1e-7) {
187
+ out.x = Math.atan2(_32, _33);
188
+ out.z = Math.atan2(_21, _11);
189
+ } else {
190
+ out.x = 0;
191
+ out.z = Math.atan2(-_12, _22);
192
+ }
193
+ break;
194
+ case 'YZX':
195
+ out.z = Math.asin(clamp11(_21));
196
+ if (Math.abs(_21) < 1 - 1e-7) {
197
+ out.x = Math.atan2(-_23, _22);
198
+ out.y = Math.atan2(-_31, _11);
199
+ } else {
200
+ out.x = 0;
201
+ out.y = Math.atan2(_13, _33);
202
+ }
203
+ break;
204
+ case 'XZY':
205
+ out.z = Math.asin(-clamp11(_12));
206
+ if (Math.abs(_12) < 1 - 1e-7) {
207
+ out.x = Math.atan2(_32, _22);
208
+ out.y = Math.atan2(_13, _11);
209
+ } else {
210
+ out.x = Math.atan2(-_23, _33);
211
+ out.y = 0;
212
+ }
213
+ break;
214
+ default:
215
+ // Silent fallback to 'XYZ' (same convention as quat.fromEuler D-P2)
216
+ out.y = Math.asin(clamp11(_13));
217
+ if (Math.abs(_13) < 1 - 1e-7) {
218
+ out.x = Math.atan2(-_23, _33);
219
+ out.z = Math.atan2(-_12, _11);
220
+ } else {
221
+ out.x = Math.atan2(_32, _22);
222
+ out.z = 0;
223
+ }
224
+ out.order = 'XYZ';
225
+ break;
226
+ }
227
+
228
+ return out;
229
+ }
230
+
231
+ /**
232
+ * Internal: clamp to [-1, 1] to keep asin from receiving out-of-range inputs (floating-point error
233
+ * may make |x| slightly > 1; same guard as Three.js; not counted in the surface).
234
+ */
235
+ function clamp11(v: number): number {
236
+ if (v < -1) return -1;
237
+ if (v > 1) return 1;
238
+ return v;
239
+ }
@@ -0,0 +1,71 @@
1
+ // f32-to-f16-bytes.ts — IEEE 754 binary16 half-float conversion.
2
+ //
3
+ // Pure arithmetic codec extracted from runtime/render-resource-table.ts (D-3)
4
+ // so build-time image-importer and runtime uploadCubemapFromEquirect share
5
+ // a single SSOT. Takes a Uint8Array view over packed Float32 RGBA pixels
6
+ // and returns a Uint8Array with half the byte length of the equivalent
7
+ // binary16 interleaved pixels (little-endian).
8
+ //
9
+ // Out-of-range f32 values saturate to +/-inf, NaN propagates, and subnormals
10
+ // round-to-zero (industry-standard f32->f16 path, semantic-preserving
11
+ // extraction).
12
+ //
13
+ // Related: plan-strategy §2 D-3; research Finding 5 + C3; requirements C3;
14
+ // w1 test file: packages/math/src/__tests__/f32-to-f16-bytes.test.ts.
15
+
16
+ /**
17
+ * Convert a packed float32 RGBA byte buffer into the equivalent float16 RGBA
18
+ * byte buffer (IEEE 754 binary16, little-endian). The output length is
19
+ * `src.byteLength / 2`.
20
+ *
21
+ * @param src A Uint8Array whose underlying buffer is an interleaved Float32Array.
22
+ * @returns A Uint8Array of packed binary16 pixels, exactly half the length.
23
+ */
24
+ export function f32ToF16Bytes(src: Uint8Array): Uint8Array {
25
+ const f32 = new Float32Array(src.buffer, src.byteOffset, src.byteLength / 4);
26
+ const out = new Uint8Array(f32.length * 2);
27
+ const view = new DataView(out.buffer);
28
+ const scratch = new ArrayBuffer(4);
29
+ const scratchF = new Float32Array(scratch);
30
+ const scratchU = new Uint32Array(scratch);
31
+ for (let i = 0; i < f32.length; i++) {
32
+ scratchF[0] = f32[i] ?? 0;
33
+ const bits = scratchU[0] ?? 0;
34
+ const sign = (bits >>> 31) & 0x1;
35
+ const exp = (bits >>> 23) & 0xff;
36
+ let mant = bits & 0x7fffff;
37
+ let half: number;
38
+ if (exp === 0xff) {
39
+ half = (sign << 15) | 0x7c00 | (mant ? 0x200 : 0);
40
+ } else if (exp === 0) {
41
+ half = sign << 15;
42
+ } else {
43
+ const e = exp - 127 + 15;
44
+ if (e >= 0x1f) {
45
+ half = (sign << 15) | 0x7c00;
46
+ } else if (e <= 0) {
47
+ if (e < -10) {
48
+ half = sign << 15;
49
+ } else {
50
+ mant = (mant | 0x800000) >> (1 - e);
51
+ if (mant & 0x1000) mant += 0x2000;
52
+ half = (sign << 15) | (mant >> 13);
53
+ }
54
+ } else {
55
+ if (mant & 0x1000) {
56
+ mant += 0x2000;
57
+ if (mant & 0x800000) {
58
+ mant = 0;
59
+ half = (sign << 15) | ((e + 1) << 10);
60
+ } else {
61
+ half = (sign << 15) | (e << 10) | (mant >> 13);
62
+ }
63
+ } else {
64
+ half = (sign << 15) | (e << 10) | (mant >> 13);
65
+ }
66
+ }
67
+ }
68
+ view.setUint16(i * 2, half & 0xffff, true);
69
+ }
70
+ return out;
71
+ }