@forgeax/engine-geometry 0.1.19 → 0.1.21

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/src/teapot.ts ADDED
@@ -0,0 +1,263 @@
1
+ // Exact Utah teapot patch tessellation ported from Three.js TeapotGeometry.
2
+ // Source: three.js ad005397bbd15b0a9fcd5159c782eba56e1cba2a, examples/jsm/geometries/TeapotGeometry.js
3
+ // Three.js is MIT licensed; this file preserves the upstream attribution.
4
+
5
+ import type { MeshAsset } from '@forgeax/engine-types';
6
+ import { ASSET_ERROR_HINTS, AssetError, err, ok, type Result } from '@forgeax/engine-types';
7
+ import { FACTORY_FLOATS_PER_VERTEX, meshFromInterleaved } from './box';
8
+
9
+ const PATCHES = new Uint16Array([
10
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 3, 16, 17, 18, 7, 19, 20, 21, 11, 22, 23,
11
+ 24, 15, 25, 26, 27, 18, 28, 29, 30, 21, 31, 32, 33, 24, 34, 35, 36, 27, 37, 38, 39, 30, 40, 41, 0,
12
+ 33, 42, 43, 4, 36, 44, 45, 8, 39, 46, 47, 12, 12, 13, 14, 15, 48, 49, 50, 51, 52, 53, 54, 55, 56,
13
+ 57, 58, 59, 15, 25, 26, 27, 51, 60, 61, 62, 55, 63, 64, 65, 59, 66, 67, 68, 27, 37, 38, 39, 62,
14
+ 69, 70, 71, 65, 72, 73, 74, 68, 75, 76, 77, 39, 46, 47, 12, 71, 78, 79, 48, 74, 80, 81, 52, 77,
15
+ 82, 83, 56, 56, 57, 58, 59, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 59, 66, 67, 68, 87,
16
+ 96, 97, 98, 91, 99, 100, 101, 95, 102, 103, 104, 68, 75, 76, 77, 98, 105, 106, 107, 101, 108, 109,
17
+ 110, 104, 111, 112, 113, 77, 82, 83, 56, 107, 114, 115, 84, 110, 116, 117, 88, 113, 118, 119, 92,
18
+ 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 123, 136, 137,
19
+ 120, 127, 138, 139, 124, 131, 140, 141, 128, 135, 142, 143, 132, 132, 133, 134, 135, 144, 145,
20
+ 146, 147, 148, 149, 150, 151, 68, 152, 153, 154, 135, 142, 143, 132, 147, 155, 156, 144, 151, 157,
21
+ 158, 148, 154, 159, 160, 68, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174,
22
+ 175, 176, 164, 177, 178, 161, 168, 179, 180, 165, 172, 181, 182, 169, 176, 183, 184, 173, 173,
23
+ 174, 175, 176, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 176, 183, 184, 173,
24
+ 188, 197, 198, 185, 192, 199, 200, 189, 196, 201, 202, 193, 203, 203, 203, 203, 204, 205, 206,
25
+ 207, 208, 208, 208, 208, 209, 210, 211, 212, 203, 203, 203, 203, 207, 213, 214, 215, 208, 208,
26
+ 208, 208, 212, 216, 217, 218, 203, 203, 203, 203, 215, 219, 220, 221, 208, 208, 208, 208, 218,
27
+ 222, 223, 224, 203, 203, 203, 203, 221, 225, 226, 204, 208, 208, 208, 208, 224, 227, 228, 209,
28
+ 209, 210, 211, 212, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 212, 216, 217,
29
+ 218, 232, 241, 242, 243, 236, 244, 245, 246, 240, 247, 248, 249, 218, 222, 223, 224, 243, 250,
30
+ 251, 252, 246, 253, 254, 255, 249, 256, 257, 258, 224, 227, 228, 209, 252, 259, 260, 229, 255,
31
+ 261, 262, 233, 258, 263, 264, 237, 265, 265, 265, 265, 266, 267, 268, 269, 270, 271, 272, 273, 92,
32
+ 119, 118, 113, 265, 265, 265, 265, 269, 274, 275, 276, 273, 277, 278, 279, 113, 112, 111, 104,
33
+ 265, 265, 265, 265, 276, 280, 281, 282, 279, 283, 284, 285, 104, 103, 102, 95, 265, 265, 265, 265,
34
+ 282, 286, 287, 266, 285, 288, 289, 270, 95, 94, 93, 92,
35
+ ]);
36
+ const CONTROL_POINTS = new Float32Array([
37
+ 1.4, 0, 2.4, 1.4, -0.784, 2.4, 0.784, -1.4, 2.4, 0, -1.4, 2.4, 1.3375, 0, 2.53125, 1.3375, -0.749,
38
+ 2.53125, 0.749, -1.3375, 2.53125, 0, -1.3375, 2.53125, 1.4375, 0, 2.53125, 1.4375, -0.805,
39
+ 2.53125, 0.805, -1.4375, 2.53125, 0, -1.4375, 2.53125, 1.5, 0, 2.4, 1.5, -0.84, 2.4, 0.84, -1.5,
40
+ 2.4, 0, -1.5, 2.4, -0.784, -1.4, 2.4, -1.4, -0.784, 2.4, -1.4, 0, 2.4, -0.749, -1.3375, 2.53125,
41
+ -1.3375, -0.749, 2.53125, -1.3375, 0, 2.53125, -0.805, -1.4375, 2.53125, -1.4375, -0.805, 2.53125,
42
+ -1.4375, 0, 2.53125, -0.84, -1.5, 2.4, -1.5, -0.84, 2.4, -1.5, 0, 2.4, -1.4, 0.784, 2.4, -0.784,
43
+ 1.4, 2.4, 0, 1.4, 2.4, -1.3375, 0.749, 2.53125, -0.749, 1.3375, 2.53125, 0, 1.3375, 2.53125,
44
+ -1.4375, 0.805, 2.53125, -0.805, 1.4375, 2.53125, 0, 1.4375, 2.53125, -1.5, 0.84, 2.4, -0.84, 1.5,
45
+ 2.4, 0, 1.5, 2.4, 0.784, 1.4, 2.4, 1.4, 0.784, 2.4, 0.749, 1.3375, 2.53125, 1.3375, 0.749,
46
+ 2.53125, 0.805, 1.4375, 2.53125, 1.4375, 0.805, 2.53125, 0.84, 1.5, 2.4, 1.5, 0.84, 2.4, 1.75, 0,
47
+ 1.875, 1.75, -0.98, 1.875, 0.98, -1.75, 1.875, 0, -1.75, 1.875, 2, 0, 1.35, 2, -1.12, 1.35, 1.12,
48
+ -2, 1.35, 0, -2, 1.35, 2, 0, 0.9, 2, -1.12, 0.9, 1.12, -2, 0.9, 0, -2, 0.9, -0.98, -1.75, 1.875,
49
+ -1.75, -0.98, 1.875, -1.75, 0, 1.875, -1.12, -2, 1.35, -2, -1.12, 1.35, -2, 0, 1.35, -1.12, -2,
50
+ 0.9, -2, -1.12, 0.9, -2, 0, 0.9, -1.75, 0.98, 1.875, -0.98, 1.75, 1.875, 0, 1.75, 1.875, -2, 1.12,
51
+ 1.35, -1.12, 2, 1.35, 0, 2, 1.35, -2, 1.12, 0.9, -1.12, 2, 0.9, 0, 2, 0.9, 0.98, 1.75, 1.875,
52
+ 1.75, 0.98, 1.875, 1.12, 2, 1.35, 2, 1.12, 1.35, 1.12, 2, 0.9, 2, 1.12, 0.9, 2, 0, 0.45, 2, -1.12,
53
+ 0.45, 1.12, -2, 0.45, 0, -2, 0.45, 1.5, 0, 0.225, 1.5, -0.84, 0.225, 0.84, -1.5, 0.225, 0, -1.5,
54
+ 0.225, 1.5, 0, 0.15, 1.5, -0.84, 0.15, 0.84, -1.5, 0.15, 0, -1.5, 0.15, -1.12, -2, 0.45, -2,
55
+ -1.12, 0.45, -2, 0, 0.45, -0.84, -1.5, 0.225, -1.5, -0.84, 0.225, -1.5, 0, 0.225, -0.84, -1.5,
56
+ 0.15, -1.5, -0.84, 0.15, -1.5, 0, 0.15, -2, 1.12, 0.45, -1.12, 2, 0.45, 0, 2, 0.45, -1.5, 0.84,
57
+ 0.225, -0.84, 1.5, 0.225, 0, 1.5, 0.225, -1.5, 0.84, 0.15, -0.84, 1.5, 0.15, 0, 1.5, 0.15, 1.12,
58
+ 2, 0.45, 2, 1.12, 0.45, 0.84, 1.5, 0.225, 1.5, 0.84, 0.225, 0.84, 1.5, 0.15, 1.5, 0.84, 0.15,
59
+ -1.6, 0, 2.025, -1.6, -0.3, 2.025, -1.5, -0.3, 2.25, -1.5, 0, 2.25, -2.3, 0, 2.025, -2.3, -0.3,
60
+ 2.025, -2.5, -0.3, 2.25, -2.5, 0, 2.25, -2.7, 0, 2.025, -2.7, -0.3, 2.025, -3, -0.3, 2.25, -3, 0,
61
+ 2.25, -2.7, 0, 1.8, -2.7, -0.3, 1.8, -3, -0.3, 1.8, -3, 0, 1.8, -1.5, 0.3, 2.25, -1.6, 0.3, 2.025,
62
+ -2.5, 0.3, 2.25, -2.3, 0.3, 2.025, -3, 0.3, 2.25, -2.7, 0.3, 2.025, -3, 0.3, 1.8, -2.7, 0.3, 1.8,
63
+ -2.7, 0, 1.575, -2.7, -0.3, 1.575, -3, -0.3, 1.35, -3, 0, 1.35, -2.5, 0, 1.125, -2.5, -0.3, 1.125,
64
+ -2.65, -0.3, 0.9375, -2.65, 0, 0.9375, -2, -0.3, 0.9, -1.9, -0.3, 0.6, -1.9, 0, 0.6, -3, 0.3,
65
+ 1.35, -2.7, 0.3, 1.575, -2.65, 0.3, 0.9375, -2.5, 0.3, 1.125, -1.9, 0.3, 0.6, -2, 0.3, 0.9, 1.7,
66
+ 0, 1.425, 1.7, -0.66, 1.425, 1.7, -0.66, 0.6, 1.7, 0, 0.6, 2.6, 0, 1.425, 2.6, -0.66, 1.425, 3.1,
67
+ -0.66, 0.825, 3.1, 0, 0.825, 2.3, 0, 2.1, 2.3, -0.25, 2.1, 2.4, -0.25, 2.025, 2.4, 0, 2.025, 2.7,
68
+ 0, 2.4, 2.7, -0.25, 2.4, 3.3, -0.25, 2.4, 3.3, 0, 2.4, 1.7, 0.66, 0.6, 1.7, 0.66, 1.425, 3.1,
69
+ 0.66, 0.825, 2.6, 0.66, 1.425, 2.4, 0.25, 2.025, 2.3, 0.25, 2.1, 3.3, 0.25, 2.4, 2.7, 0.25, 2.4,
70
+ 2.8, 0, 2.475, 2.8, -0.25, 2.475, 3.525, -0.25, 2.49375, 3.525, 0, 2.49375, 2.9, 0, 2.475, 2.9,
71
+ -0.15, 2.475, 3.45, -0.15, 2.5125, 3.45, 0, 2.5125, 2.8, 0, 2.4, 2.8, -0.15, 2.4, 3.2, -0.15, 2.4,
72
+ 3.2, 0, 2.4, 3.525, 0.25, 2.49375, 2.8, 0.25, 2.475, 3.45, 0.15, 2.5125, 2.9, 0.15, 2.475, 3.2,
73
+ 0.15, 2.4, 2.8, 0.15, 2.4, 0, 0, 3.15, 0.8, 0, 3.15, 0.8, -0.45, 3.15, 0.45, -0.8, 3.15, 0, -0.8,
74
+ 3.15, 0, 0, 2.85, 0.2, 0, 2.7, 0.2, -0.112, 2.7, 0.112, -0.2, 2.7, 0, -0.2, 2.7, -0.45, -0.8,
75
+ 3.15, -0.8, -0.45, 3.15, -0.8, 0, 3.15, -0.112, -0.2, 2.7, -0.2, -0.112, 2.7, -0.2, 0, 2.7, -0.8,
76
+ 0.45, 3.15, -0.45, 0.8, 3.15, 0, 0.8, 3.15, -0.2, 0.112, 2.7, -0.112, 0.2, 2.7, 0, 0.2, 2.7, 0.45,
77
+ 0.8, 3.15, 0.8, 0.45, 3.15, 0.112, 0.2, 2.7, 0.2, 0.112, 2.7, 0.4, 0, 2.55, 0.4, -0.224, 2.55,
78
+ 0.224, -0.4, 2.55, 0, -0.4, 2.55, 1.3, 0, 2.55, 1.3, -0.728, 2.55, 0.728, -1.3, 2.55, 0, -1.3,
79
+ 2.55, 1.3, 0, 2.4, 1.3, -0.728, 2.4, 0.728, -1.3, 2.4, 0, -1.3, 2.4, -0.224, -0.4, 2.55, -0.4,
80
+ -0.224, 2.55, -0.4, 0, 2.55, -0.728, -1.3, 2.55, -1.3, -0.728, 2.55, -1.3, 0, 2.55, -0.728, -1.3,
81
+ 2.4, -1.3, -0.728, 2.4, -1.3, 0, 2.4, -0.4, 0.224, 2.55, -0.224, 0.4, 2.55, 0, 0.4, 2.55, -1.3,
82
+ 0.728, 2.55, -0.728, 1.3, 2.55, 0, 1.3, 2.55, -1.3, 0.728, 2.4, -0.728, 1.3, 2.4, 0, 1.3, 2.4,
83
+ 0.224, 0.4, 2.55, 0.4, 0.224, 2.55, 0.728, 1.3, 2.55, 1.3, 0.728, 2.55, 0.728, 1.3, 2.4, 1.3,
84
+ 0.728, 2.4, 0, 0, 0, 1.425, 0, 0, 1.425, 0.798, 0, 0.798, 1.425, 0, 0, 1.425, 0, 1.5, 0, 0.075,
85
+ 1.5, 0.84, 0.075, 0.84, 1.5, 0.075, 0, 1.5, 0.075, -0.798, 1.425, 0, -1.425, 0.798, 0, -1.425, 0,
86
+ 0, -0.84, 1.5, 0.075, -1.5, 0.84, 0.075, -1.5, 0, 0.075, -1.425, -0.798, 0, -0.798, -1.425, 0, 0,
87
+ -1.425, 0, -1.5, -0.84, 0.075, -0.84, -1.5, 0.075, 0, -1.5, 0.075, 0.798, -1.425, 0, 1.425,
88
+ -0.798, 0, 0.84, -1.5, 0.075, 1.5, -0.84, 0.075,
89
+ ]);
90
+
91
+ export interface TeapotProvenance {
92
+ readonly source: 'three.js';
93
+ readonly commit: 'ad005397bbd15b0a9fcd5159c782eba56e1cba2a';
94
+ readonly path: 'examples/jsm/geometries/TeapotGeometry.js';
95
+ readonly license: 'MIT';
96
+ }
97
+
98
+ export type TeapotMeshAsset = MeshAsset & { readonly provenance: TeapotProvenance };
99
+
100
+ function basis(value: number): [number, number, number, number] {
101
+ const inverse = 1 - value;
102
+ return [
103
+ inverse * inverse * inverse,
104
+ 3 * value * inverse * inverse,
105
+ 3 * value * value * inverse,
106
+ value * value * value,
107
+ ];
108
+ }
109
+
110
+ function derivativeBasis(value: number): [number, number, number, number] {
111
+ const inverse = 1 - value;
112
+ return [
113
+ -3 * inverse * inverse,
114
+ 3 * inverse * inverse - 6 * value * inverse,
115
+ 6 * value * inverse - 3 * value * value,
116
+ 3 * value * value,
117
+ ];
118
+ }
119
+
120
+ function evaluatePatch(
121
+ surface: number,
122
+ s: number,
123
+ t: number,
124
+ fitLid: boolean,
125
+ blinn: boolean,
126
+ ): {
127
+ point: [number, number, number];
128
+ ds: [number, number, number];
129
+ dt: [number, number, number];
130
+ } {
131
+ const sb = basis(s);
132
+ const tb = basis(t);
133
+ const dsb = derivativeBasis(s);
134
+ const dtb = derivativeBasis(t);
135
+ const point: [number, number, number] = [0, 0, 0];
136
+ const ds: [number, number, number] = [0, 0, 0];
137
+ const dt: [number, number, number] = [0, 0, 0];
138
+ for (let row = 0; row < 4; row++) {
139
+ for (let column = 0; column < 4; column++) {
140
+ const control = PATCHES[surface * 16 + row * 4 + column] ?? 0;
141
+ const source = control * 3;
142
+ for (let axis = 0; axis < 3; axis++) {
143
+ const lidScale = fitLid && surface >= 20 && surface < 28 && axis !== 2 ? 1.077 : 1;
144
+ let value = (CONTROL_POINTS[source + axis] ?? 0) * lidScale;
145
+ if (!blinn && axis === 2) value *= 1.3;
146
+ const pointWeight = value * (sb[row] ?? 0) * (tb[column] ?? 0);
147
+ const dsWeight = value * (dsb[row] ?? 0) * (tb[column] ?? 0);
148
+ const dtWeight = value * (sb[row] ?? 0) * (dtb[column] ?? 0);
149
+ point[axis] = (point[axis] ?? 0) + pointWeight;
150
+ ds[axis] = (ds[axis] ?? 0) + dsWeight;
151
+ dt[axis] = (dt[axis] ?? 0) + dtWeight;
152
+ }
153
+ }
154
+ }
155
+ return { point, ds, dt };
156
+ }
157
+
158
+ function cross(
159
+ a: readonly [number, number, number],
160
+ b: readonly [number, number, number],
161
+ ): [number, number, number] {
162
+ return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
163
+ }
164
+
165
+ function normalize(value: [number, number, number]): [number, number, number] {
166
+ const length = Math.hypot(value[0], value[1], value[2]);
167
+ return length > 0 ? [value[0] / length, value[1] / length, value[2] / length] : [0, 1, 0];
168
+ }
169
+
170
+ /** Build an exact Three.js TeapotGeometry-compatible Utah teapot mesh. */
171
+ export function createTeapotGeometry(
172
+ size = 0.8,
173
+ segments = 18,
174
+ bottom = true,
175
+ lid = true,
176
+ body = true,
177
+ fitLid = true,
178
+ blinn = true,
179
+ ): Result<TeapotMeshAsset, AssetError> {
180
+ if (!Number.isFinite(size) || size <= 0 || !Number.isFinite(segments) || segments < 2) {
181
+ return err(
182
+ new AssetError({
183
+ code: 'asset-parse-failed',
184
+ expected: 'finite positive size and segments >= 2',
185
+ hint: ASSET_ERROR_HINTS['asset-parse-failed'],
186
+ detail: { field: 'parameters', value: size, reason: `segments=${segments}` },
187
+ }),
188
+ );
189
+ }
190
+ const steps = Math.max(2, Math.floor(segments));
191
+ const maxHeight = 3.15 * (blinn ? 1 : 1.3);
192
+ const maxHeight2 = maxHeight / 2;
193
+ const trueSize = size / maxHeight2;
194
+ const firstSurface = body ? 0 : 20;
195
+ const lastSurface = bottom ? 32 : 28;
196
+ const activeSurfaces: number[] = [];
197
+ for (let surface = firstSurface; surface < lastSurface; surface++) {
198
+ if (lid || surface < 20 || surface >= 28) activeSurfaces.push(surface);
199
+ }
200
+ const vertices = new Float32Array(
201
+ activeSurfaces.length * (steps + 1) * (steps + 1) * FACTORY_FLOATS_PER_VERTEX,
202
+ );
203
+ const indices: number[] = [];
204
+ const stride = steps + 1;
205
+ let vertex = 0;
206
+ const positions: Array<[number, number, number]> = [];
207
+ for (const surface of activeSurfaces) {
208
+ const surfaceStart = vertex;
209
+ for (let sStep = 0; sStep <= steps; sStep++) {
210
+ const s = sStep / steps;
211
+ for (let tStep = 0; tStep <= steps; tStep++) {
212
+ const t = tStep / steps;
213
+ const evaluated = evaluatePatch(surface, s, t, fitLid, blinn);
214
+ const normalRaw = cross(evaluated.dt, evaluated.ds);
215
+ const normal =
216
+ evaluated.point[0] === 0 && evaluated.point[1] === 0
217
+ ? ([0, evaluated.point[2] > maxHeight2 ? 1 : -1, 0] as [number, number, number])
218
+ : normalize([normalRaw[0], normalRaw[2], -normalRaw[1]]);
219
+ const position: [number, number, number] = [
220
+ Math.fround(trueSize * evaluated.point[0]),
221
+ Math.fround(trueSize * (evaluated.point[2] - maxHeight2)),
222
+ Math.fround(-trueSize * evaluated.point[1]),
223
+ ];
224
+ const base = vertex * FACTORY_FLOATS_PER_VERTEX;
225
+ vertices[base] = position[0];
226
+ vertices[base + 1] = position[1];
227
+ vertices[base + 2] = position[2];
228
+ vertices[base + 3] = normal[0];
229
+ vertices[base + 4] = normal[1];
230
+ vertices[base + 5] = normal[2];
231
+ vertices[base + 6] = 1 - t;
232
+ vertices[base + 7] = 1 - s;
233
+ positions.push(position);
234
+ vertex++;
235
+ }
236
+ }
237
+ for (let sStep = 0; sStep < steps; sStep++) {
238
+ for (let tStep = 0; tStep < steps; tStep++) {
239
+ const v1 = surfaceStart + sStep * stride + tStep;
240
+ const v2 = v1 + 1;
241
+ const v3 = v2 + stride;
242
+ const v4 = v1 + stride;
243
+ const same = (a: number, b: number) =>
244
+ positions[a]?.[0] === positions[b]?.[0] &&
245
+ positions[a]?.[1] === positions[b]?.[1] &&
246
+ positions[a]?.[2] === positions[b]?.[2];
247
+ if (!same(v1, v2) && !same(v1, v3) && !same(v2, v3)) indices.push(v1, v2, v3);
248
+ if (!same(v1, v3) && !same(v1, v4) && !same(v3, v4)) indices.push(v1, v3, v4);
249
+ }
250
+ }
251
+ }
252
+ const mesh = meshFromInterleaved(vertices, new Uint32Array(indices));
253
+ if (!mesh.ok) return mesh;
254
+ return ok({
255
+ ...mesh.value,
256
+ provenance: {
257
+ source: 'three.js',
258
+ commit: 'ad005397bbd15b0a9fcd5159c782eba56e1cba2a',
259
+ path: 'examples/jsm/geometries/TeapotGeometry.js',
260
+ license: 'MIT',
261
+ },
262
+ });
263
+ }