lecodes-sdk 1.0.0 → 1.2.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 (94) hide show
  1. package/dist/global.d.ts +18 -4
  2. package/dist/inject.js +260 -361
  3. package/dist/types/animate/tween/Animation.d.ts +69 -0
  4. package/dist/types/animate/tween/Timeline.d.ts +55 -0
  5. package/dist/types/animate/tween/animateValue.d.ts +27 -0
  6. package/dist/types/animate/tween/easing.d.ts +29 -0
  7. package/dist/types/animate/tween/spec.d.ts +178 -0
  8. package/dist/types/g2/Node2D.d.ts +16 -0
  9. package/dist/types/g2/Sprite.d.ts +11 -1
  10. package/dist/types/gl/Camera.d.ts +15 -1
  11. package/dist/types/gl/Foliage.d.ts +47 -0
  12. package/dist/types/gl/Geometry.d.ts +24 -0
  13. package/dist/types/gl/Light.d.ts +28 -7
  14. package/dist/types/gl/Lightmap.d.ts +90 -60
  15. package/dist/types/gl/Material.d.ts +28 -20
  16. package/dist/types/gl/Model.d.ts +7 -5
  17. package/dist/types/gl/Node.d.ts +18 -0
  18. package/dist/types/gl/Particles.d.ts +55 -1
  19. package/dist/types/gl/Scene.d.ts +20 -0
  20. package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
  21. package/dist/types/gl/animation/Animator.d.ts +27 -0
  22. package/dist/types/gl/animation/DynamicBone.d.ts +19 -8
  23. package/dist/types/gl/animation/IK.d.ts +86 -30
  24. package/dist/types/gl/animation/Locomotion.d.ts +52 -3
  25. package/dist/types/gl/animation/Warp.d.ts +2 -1
  26. package/dist/types/gl/animation/core.d.ts +35 -4
  27. package/dist/types/gl/physics/Ragdoll.d.ts +87 -12
  28. package/dist/types/gl/terrain/Terrain.d.ts +12 -2
  29. package/dist/types/inject.d.ts +8 -2
  30. package/dist/types/runtime/input.d.ts +7 -0
  31. package/dist/types/scene/defineScene.d.ts +44 -32
  32. package/dist/types/ui/UIButton.d.ts +3 -1
  33. package/dist/types/ui/UIInput.d.ts +5 -1
  34. package/dist/types/ui/UINode.d.ts +24 -24
  35. package/dist/types.json +1 -1
  36. package/package.json +1 -1
  37. package/prompts/README.md +142 -142
  38. package/prompts/core-design.md +27 -4
  39. package/prompts/core.md +35 -6
  40. package/prompts/dist/2d-game.md +197 -408
  41. package/prompts/dist/3d-app.md +166 -491
  42. package/prompts/dist/ar-app.md +163 -373
  43. package/prompts/dist/design.md +87 -83
  44. package/prompts/dist/ui-app.md +136 -325
  45. package/prompts/select.ts +19 -4
  46. package/src/animate/tween/Animation.ts +378 -0
  47. package/src/animate/tween/Timeline.ts +175 -0
  48. package/src/animate/tween/animateValue.ts +100 -0
  49. package/src/animate/tween/easing.ts +172 -0
  50. package/src/animate/tween/spec.ts +479 -0
  51. package/src/audio/audio.ts +161 -161
  52. package/src/bridges.d.ts +235 -65
  53. package/src/compile/__tests__/assetMacro.test.ts +26 -0
  54. package/src/compile/__tests__/detectEntry.test.ts +19 -0
  55. package/src/compile/__tests__/serverSplit.test.ts +27 -0
  56. package/src/compile/bundler.ts +34 -4
  57. package/src/compile/compileProject.ts +31 -1
  58. package/src/compile/detectEntry.ts +8 -3
  59. package/src/compile/index.ts +2 -0
  60. package/src/compile/serverSplit.ts +9 -3
  61. package/src/g2/Node2D.ts +38 -0
  62. package/src/g2/Sprite.ts +20 -1
  63. package/src/gl/Camera.ts +34 -1
  64. package/src/gl/CameraPlace.ts +52 -52
  65. package/src/gl/Foliage.ts +102 -0
  66. package/src/gl/Geometry.ts +393 -348
  67. package/src/gl/Light.ts +49 -16
  68. package/src/gl/Lightmap.ts +439 -275
  69. package/src/gl/Material.ts +59 -47
  70. package/src/gl/Mesh.ts +120 -120
  71. package/src/gl/Model.ts +23 -12
  72. package/src/gl/Node.ts +39 -0
  73. package/src/gl/Particles.ts +80 -2
  74. package/src/gl/Scene.ts +34 -1
  75. package/src/gl/animation/AnimationClip.ts +52 -0
  76. package/src/gl/animation/Animator.ts +42 -2
  77. package/src/gl/animation/DynamicBone.ts +35 -12
  78. package/src/gl/animation/IK.ts +173 -152
  79. package/src/gl/animation/Locomotion.ts +72 -8
  80. package/src/gl/animation/Playback.ts +5 -4
  81. package/src/gl/animation/Warp.ts +5 -2
  82. package/src/gl/animation/core.ts +65 -4
  83. package/src/gl/physics/Ragdoll.ts +451 -272
  84. package/src/gl/scenarios.ts +291 -291
  85. package/src/gl/terrain/Terrain.ts +33 -2
  86. package/src/inject.ts +236 -226
  87. package/src/runtime/input.ts +11 -0
  88. package/src/scene/defineScene.ts +72 -62
  89. package/src/scene/gizmos.ts +148 -148
  90. package/src/ui/UIButton.ts +2 -2
  91. package/src/ui/UIInput.ts +3 -3
  92. package/src/ui/UINode.ts +61 -36
  93. package/dist/types/animate/animate.d.ts +0 -20
  94. package/src/animate/animate.ts +0 -238
@@ -1,348 +1,393 @@
1
- // Mesh geometry: vertex / normal / index / uv buffers, plus the primitive builders. `Geometry.box()`
2
- // etc. return a Geometry you can hand to Mesh.from(); Mesh.box() is sugar over them. Ported from
3
- // worker/src/primitives/*.
4
-
5
- import { cx, cy, cz, type Vec3Like } from "../math/vec"
6
-
7
- export type MeshKind = "triangles" | "vertices" | "edges"
8
-
9
- // local tuple helpers — geometry building works on raw Float32Array buffers, so it stays out of the
10
- // Vec3 wrapper (no per-vertex allocation).
11
- type Tup3 = [number, number, number]
12
- const cross = (a: Tup3, b: Tup3): Tup3 => [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]
13
- const normalize = (a: Tup3): Tup3 => {
14
- const l = Math.hypot(a[0], a[1], a[2])
15
- return l === 0 ? [ 0, 0, 0 ] : [ a[0] / l, a[1] / l, a[2] / l ]
16
- }
17
-
18
- export class Geometry {
19
- vertices: Float32Array
20
- normals: Float32Array
21
- indices: Uint16Array
22
- uv: Float32Array
23
- /** Lightmap UV set (2 floats per vertex, non-overlapping, inside the unit square) — the
24
- * `Lightmap` atlas samples a Mesh through it. The box and cylinder builders fill it (their `uv`
25
- * tiles per face, which would fold every face onto the same texels); absent = the host reuses
26
- * `uv`, which is right for a plane. */
27
- uv1?: Float32Array
28
- /** Per-vertex COLOURS: 4 bytes (r, g, b, a) per vertex, 0..255. A material that declares
29
- * `requires: [color]` reads them through `getColor()` — one mesh, many colours, no material per
30
- * shade (a debug drawer, a gradient along a curve). Absent = every vertex white, which is what
31
- * the built-in materials expect. */
32
- colors?: Uint8Array
33
- /** @internal native mesh-type enum (0 triangles / 1 edges / 2 vertices). */
34
- _meshKind = 0
35
-
36
- constructor(vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array) {
37
- this.vertices = vertices
38
- this.normals = normals
39
- this.indices = indices
40
- this.uv = uv
41
- }
42
-
43
- translate(x: number, y: number, z: number): this {
44
- for (let i = 0; i < this.vertices.length; i += 3) {
45
- this.vertices[i] += x; this.vertices[i + 1] += y; this.vertices[i + 2] += z
46
- }
47
- return this
48
- }
49
- scale(x: number, y: number, z: number): this {
50
- for (let i = 0; i < this.vertices.length; i += 3) {
51
- this.vertices[i] *= x; this.vertices[i + 1] *= y; this.vertices[i + 2] *= z
52
- }
53
- return this
54
- }
55
- scaleUV(x: number, y: number): this {
56
- for (let i = 0; i < this.uv.length; i += 2) { this.uv[i] *= x; this.uv[i + 1] *= y }
57
- return this
58
- }
59
- set kind(kind: MeshKind) {
60
- this._meshKind = kind === "edges" ? 1 : kind === "vertices" ? 2 : 0
61
- }
62
-
63
- /** A box; `size` (default 1) scales the vertices AND lays the lightmap chart out in proportion to
64
- * the faces' areas (a floor's top face gets the texels, not its 1 m side strips). */
65
- static box(size: Vec3Like | number = 1): Geometry { return createCube(size) }
66
- static sphere(options?: SphereOptions): Geometry { return createSphere(options) }
67
- static cylinder(options?: CylinderOptions): Geometry { return createCylinder(options) }
68
- /** A capsule along Y, centred at the origin: a cylinder `length` long (between the two cap centres)
69
- * with a hemisphere of `radius` on each end — one closed surface, smooth across the seams. */
70
- static capsule(options?: CapsuleOptions): Geometry { return createCapsule(options) }
71
- static plane(options?: PlaneOptions): Geometry { return createPlane(options) }
72
- }
73
-
74
- // Gutter between lightmap charts (fraction of the chart square): the bake dilates each rect by a
75
- // few texels and the shader samples bilinearly, so neighbouring faces must not touch in uv1.
76
- const LM_GUTTER = 0.03
77
-
78
- type Rect = { x: number, y: number, w: number, h: number }
79
-
80
- /** Shelf-pack rectangles (metres) into the unit square at the largest uniform scale — every face
81
- * gets texels in proportion to its area, so a 64×1×64 floor's top face isn't squeezed into the
82
- * same cell as its 1 m side strips. Returns rects in input order. */
83
- const packCharts = (dims: [number, number][]): Rect[] => {
84
- const order = dims.map((_, i) => i).sort((a, b) => dims[b][1] - dims[a][1])
85
- const attempt = (scale: number): Rect[] | null => {
86
- const out: Rect[] = new Array(dims.length)
87
- let x = 0, y = 0, rowH = 0
88
- for (const i of order) {
89
- const w = dims[i][0] * scale, h = dims[i][1] * scale
90
- if (x > 0 && x + w > 1) { y += rowH + LM_GUTTER; x = 0; rowH = 0 }
91
- if (w > 1 || y + h > 1) return null
92
- out[i] = { x, y, w, h }
93
- x += w + LM_GUTTER
94
- rowH = Math.max(rowH, h)
95
- }
96
- return out
97
- }
98
- let lo = 0, hi = 1 / Math.max(...dims.map((d) => Math.max(d[0], d[1])))
99
- let best = attempt(hi)
100
- if (!best) {
101
- for (let it = 0; it < 40; it++) {
102
- const mid = (lo + hi) / 2
103
- const r = attempt(mid)
104
- if (r) { best = r; lo = mid } else hi = mid
105
- }
106
- }
107
- return best ?? dims.map(() => ({ x: 0, y: 0, w: 1, h: 1 }))
108
- }
109
-
110
- // --- cube ---
111
- const createCube = (size: Vec3Like | number = 1): Geometry => {
112
- const sx = typeof size === "number" ? size : cx(size)
113
- const sy = typeof size === "number" ? size : cy(size)
114
- const sz = typeof size === "number" ? size : cz(size)
115
- const vertices = new Float32Array(6 * 4 * 3)
116
- const normals = new Float32Array(6 * 4 * 3)
117
- const uv = new Float32Array(6 * 4 * 2)
118
- const uv1 = new Float32Array(6 * 4 * 2)
119
- const indices = new Uint16Array(6 * 6)
120
- let cnt = 0, indicesCnt = 0
121
- const sides = [ -0.5, 0.5 ]
122
- // face extents along the (a, b) parameters below: ±x faces span (z, y), ±y faces (x, z), ±z faces (x, y)
123
- const charts = packCharts([ [ sz, sy ], [ sz, sy ], [ sx, sz ], [ sx, sz ], [ sx, sy ], [ sx, sy ] ])
124
- for (let i = 0; i < 6; i++) {
125
- const s = cnt
126
- const side = i % 2 === 0 ? -0.5 : 0.5
127
- const c = charts[i]
128
- for (const a of sides) {
129
- for (const b of sides) {
130
- uv.set([ a + 0.5, b + 0.5 ], cnt * 2)
131
- uv1.set([ c.x + (a + 0.5) * c.w, c.y + (b + 0.5) * c.h ], cnt * 2)
132
- if (i === 0 || i === 1) { vertices.set([ side * sx, b * sy, -a * sz ], cnt * 3); normals.set([ side * 2, 0, 0 ], cnt * 3) }
133
- if (i === 2 || i === 3) { vertices.set([ a * sx, side * sy, -b * sz ], cnt * 3); normals.set([ 0, side * 2, 0 ], cnt * 3) }
134
- if (i === 4 || i === 5) { vertices.set([ a * sx, b * sy, side * sz ], cnt * 3); normals.set([ 0, 0, side * 2 ], cnt * 3) }
135
- cnt++
136
- }
137
- }
138
- const s1 = s + 1, s2 = s + 2, s3 = s + 3
139
- if (i % 2 === 0) indices.set([ s, s1, s2, s3, s2, s1 ], indicesCnt)
140
- else indices.set([ s2, s1, s, s1, s2, s3 ], indicesCnt)
141
- indicesCnt += 6
142
- }
143
- const g = new Geometry(vertices, normals, indices, uv)
144
- g.uv1 = uv1
145
- return g
146
- }
147
-
148
- // --- sphere ---
149
- export type SphereOptions = { radius?: number, widthSegments?: number, heightSegments?: number }
150
- const createSphere = (options: SphereOptions = {}): Geometry => {
151
- const { radius = 0.5, widthSegments = 32, heightSegments = 32 } = options
152
- const verticesCount = (widthSegments + 1) * (heightSegments - 1) + widthSegments * 2
153
- const vertices = new Float32Array(verticesCount * 3)
154
- const normals = new Float32Array(verticesCount * 3)
155
- const uvs = new Float32Array(verticesCount * 2)
156
- const indices = new Uint16Array(widthSegments * (heightSegments - 2) * 6 + widthSegments * 3 * 2)
157
- const phiLength = Math.PI * 2, thetaLength = Math.PI
158
- let vertexCnt = 0, indicesCnt = 0
159
- for (let iy = 1; iy < heightSegments; iy++) {
160
- const v = iy / heightSegments
161
- for (let ix = 0; ix <= widthSegments; ix++) {
162
- const u = ix / widthSegments
163
- const point: [number, number, number] = [
164
- -Math.cos(u * phiLength) * Math.sin(v * thetaLength),
165
- Math.cos(v * thetaLength),
166
- Math.sin(u * phiLength) * Math.sin(v * thetaLength),
167
- ]
168
- vertices.set([ point[0] * radius, point[1] * radius, point[2] * radius ], vertexCnt * 3)
169
- normals.set(point, vertexCnt * 3)
170
- uvs.set([ u, 1 - v ], vertexCnt * 2)
171
- if (iy > 1 && ix > 0) {
172
- const s = vertexCnt - 1
173
- const s1 = vertexCnt - (widthSegments + 1) - 1
174
- indices.set([ s + 1, s1, s, s1, s + 1, s1 + 1 ], indicesCnt)
175
- indicesCnt += 6
176
- }
177
- vertexCnt++
178
- }
179
- }
180
- const sides = [ -radius, radius ]
181
- const circleStart2 = vertexCnt - (widthSegments + 1)
182
- for (const side of sides) {
183
- const circleStart = side > 0 ? 0 : circleStart2
184
- for (let ix = 0; ix < widthSegments; ix++) {
185
- const u = (ix + 0.5) / widthSegments
186
- vertices.set([ 0, side, 0 ], vertexCnt * 3)
187
- normals.set([ 0, side < 0 ? -1 : 1, 0 ], vertexCnt * 3)
188
- uvs.set([ u, side < 0 ? 0 : 1 ], vertexCnt * 2)
189
- const s = circleStart + ix
190
- if (side > 0) indices.set([ s, s + 1, vertexCnt ], indicesCnt)
191
- else indices.set([ vertexCnt, s + 1, s ], indicesCnt)
192
- indicesCnt += 3
193
- vertexCnt++
194
- }
195
- }
196
- return new Geometry(vertices, normals, indices, uvs)
197
- }
198
-
199
- // --- capsule ---
200
- export type CapsuleOptions = {
201
- radius?: number
202
- /** Distance between the two cap centres (the straight part); 0 = a sphere. Default 1. */
203
- length?: number
204
- /** Segments around the axis. */
205
- widthSegments?: number
206
- /** Rings per hemisphere (pole to equator). */
207
- capSegments?: number
208
- }
209
- const createCapsule = (options: CapsuleOptions = {}): Geometry => {
210
- const { radius = 0.5, length = 1, widthSegments = 32, capSegments = 8 } = options
211
- const W = widthSegments, half = length / 2
212
- // rings from the top cap's first ring down to the bottom cap's last: the equator appears twice (once per
213
- // cap, `length` apart) — the band between those two rings is the cylinder; the poles are fans, like the sphere
214
- const R = capSegments * 2
215
- const verticesCount = (W + 1) * R + W * 2
216
- const vertices = new Float32Array(verticesCount * 3)
217
- const normals = new Float32Array(verticesCount * 3)
218
- const uvs = new Float32Array(verticesCount * 2)
219
- const indices = new Uint16Array(W * (R - 1) * 6 + W * 3 * 2)
220
- const total = Math.PI * radius + length // arc from pole to pole, for v
221
- let vertexCnt = 0, indicesCnt = 0
222
- for (let j = 0; j < R; j++) {
223
- const top = j < capSegments
224
- const theta = (top ? j + 1 : j) / capSegments * (Math.PI / 2) // 0 = the top pole, π = the bottom
225
- const yOff = top ? half : -half
226
- const sinT = Math.sin(theta), cosT = Math.cos(theta)
227
- const v = ((Math.PI - theta) * radius + (top ? length : 0)) / total
228
- for (let ix = 0; ix <= W; ix++) {
229
- const u = ix / W
230
- const nx = -Math.cos(u * Math.PI * 2) * sinT, nz = Math.sin(u * Math.PI * 2) * sinT
231
- vertices.set([ nx * radius, cosT * radius + yOff, nz * radius ], vertexCnt * 3)
232
- normals.set([ nx, cosT, nz ], vertexCnt * 3)
233
- uvs.set([ u, v ], vertexCnt * 2)
234
- if (j > 0 && ix > 0) {
235
- const s = vertexCnt - 1, s1 = vertexCnt - (W + 1) - 1
236
- indices.set([ s + 1, s1, s, s1, s + 1, s1 + 1 ], indicesCnt)
237
- indicesCnt += 6
238
- }
239
- vertexCnt++
240
- }
241
- }
242
- const lastRing = vertexCnt - (W + 1)
243
- for (const side of [ 1, -1 ]) {
244
- const circleStart = side > 0 ? 0 : lastRing
245
- for (let ix = 0; ix < W; ix++) {
246
- const u = (ix + 0.5) / W
247
- vertices.set([ 0, side * (radius + half), 0 ], vertexCnt * 3)
248
- normals.set([ 0, side, 0 ], vertexCnt * 3)
249
- uvs.set([ u, side > 0 ? 1 : 0 ], vertexCnt * 2)
250
- const s = circleStart + ix
251
- if (side > 0) indices.set([ s, s + 1, vertexCnt ], indicesCnt)
252
- else indices.set([ vertexCnt, s + 1, s ], indicesCnt)
253
- indicesCnt += 3
254
- vertexCnt++
255
- }
256
- }
257
- return new Geometry(vertices, normals, indices, uvs)
258
- }
259
-
260
- // --- cylinder ---
261
- export type CylinderOptions = { radius?: number, radiusTop?: number, radiusBottom?: number, edges?: number, smooth?: boolean }
262
- const createCylinder = (options: CylinderOptions = {}): Geometry => {
263
- const { edges = 32, smooth = true, radius = 0.5, radiusTop = radius, radiusBottom = radius } = options
264
- const verticesCount = edges * 4 + (edges + 1) * 2
265
- const vertices = new Float32Array(verticesCount * 3)
266
- const normals = new Float32Array(verticesCount * 3)
267
- const uv = new Float32Array(verticesCount * 2)
268
- // lightmap chart: the side strip across the upper half, the two caps as discs in the lower half
269
- const uv1 = new Float32Array(verticesCount * 2)
270
- const indices = new Uint16Array(edges * 6 + edges * 6)
271
- const sides = [ -0.5, 0.5 ]
272
- let cnt = 0, indicesCnt = 0
273
- for (let i = 0; i < edges; i++) {
274
- const s = cnt
275
- const normalX = Math.cos((i + 0.5) / edges * Math.PI * 2)
276
- const normalY = Math.sin((i + 0.5) / edges * Math.PI * 2)
277
- for (let j = 0; j < 2; j++) {
278
- const x = Math.cos((i + j) / edges * Math.PI * 2)
279
- const y = Math.sin((i + j) / edges * Math.PI * 2)
280
- for (const side of sides) {
281
- if (side > 0) vertices.set([ x * radiusTop, side, y * radiusTop ], cnt * 3)
282
- else vertices.set([ x * radiusBottom, side, y * radiusBottom ], cnt * 3)
283
- if (smooth) normals.set([ x, 0, y ], cnt * 3)
284
- else normals.set([ normalX, 0, normalY ], cnt * 3)
285
- uv.set([ (i + j) / edges, side + 0.5 ], cnt * 2)
286
- uv1.set([ (i + j) / edges, 0.55 + (side + 0.5) * 0.43 ], cnt * 2)
287
- cnt++
288
- }
289
- }
290
- const s1 = s + 1, s2 = (s + 2) % (edges * 4), s3 = (s + 3) % (edges * 4)
291
- indices.set([ s, s1, s2, s3, s2, s1 ], indicesCnt)
292
- indicesCnt += 6
293
- }
294
- for (const side of sides) {
295
- const center = cnt
296
- const capU = side > 0 ? 0.75 : 0.25 // lightmap disc centre per cap
297
- vertices.set([ 0, side, 0 ], cnt * 3); normals.set([ 0, side * 2, 0 ], cnt * 3)
298
- uv1.set([ capU, 0.25 ], cnt * 2); cnt++
299
- for (let i = 0; i < edges; i++) {
300
- const x = Math.cos(i / edges * Math.PI * 2), y = Math.sin(i / edges * Math.PI * 2)
301
- if (side > 0) vertices.set([ x * radiusTop, side, y * radiusTop ], cnt * 3)
302
- else vertices.set([ x * radiusBottom, side, y * radiusBottom ], cnt * 3)
303
- normals.set([ 0, side * 2, 0 ], cnt * 3)
304
- uv1.set([ capU + x * 0.22, 0.25 + y * 0.22 ], cnt * 2)
305
- if (side < 0) indices.set([ center, center + i + 1, center + 1 + ((i + 1) % edges) ], indicesCnt)
306
- else indices.set([ center + 1 + ((i + 1) % edges), center + i + 1, center ], indicesCnt)
307
- indicesCnt += 3
308
- cnt++
309
- }
310
- }
311
- const g = new Geometry(vertices, normals, indices, uv)
312
- g.uv1 = uv1
313
- return g
314
- }
315
-
316
- // --- plane ---
317
- export type PlaneOptions = { normal?: Vec3Like }
318
- const createPlane = (options: PlaneOptions = {}): Geometry => {
319
- const normal: Tup3 = options.normal ? [ cx(options.normal), cy(options.normal), cz(options.normal) ] : [ 0, 0, 1 ]
320
- const vertices = new Float32Array(4 * 3)
321
- const normals = new Float32Array(4 * 3)
322
- const uvs = new Float32Array(4 * 2)
323
- const sides = [ -0.5, 0.5 ]
324
- let cnt = 0
325
- let upVector: Tup3, rightVec: Tup3
326
- if (normal[1] === 0 && normal[2] === 0) { upVector = [ 0, 1, 0 ]; rightVec = [ 0, 0, -normal[0] ] }
327
- else if (normal[0] === 0 && normal[2] === 0) { upVector = [ 0, 0, 1 ]; rightVec = [ -normal[1], 0, 0 ] }
328
- else if (normal[0] === 0 && normal[1] === 0) { upVector = [ 0, 1, 0 ]; rightVec = [ normal[2], 0, 0 ] } // тут было верно
329
- else {
330
- const temp: Tup3 = Math.abs(normal[0]) < 0.5 ? [ 1, 0, 0 ] : [ 0, 0, 1 ]
331
- upVector = normalize(cross(temp, normal))
332
- rightVec = normalize(cross(upVector, normal))
333
- }
334
- for (const a of sides) {
335
- for (const b of sides) {
336
- normals.set(normal, cnt * 3)
337
- uvs.set([ a + 0.5, b + 0.5 ], cnt * 2)
338
- vertices.set([
339
- rightVec[0] * a + upVector[0] * b,
340
- rightVec[1] * a + upVector[1] * b,
341
- rightVec[2] * a + upVector[2] * b,
342
- ], cnt * 3)
343
- cnt++
344
- }
345
- }
346
- const indices = new Uint16Array([ 2, 1, 0, 1, 2, 3 ])
347
- return new Geometry(vertices, normals, indices, uvs)
348
- }
1
+ // Mesh geometry: vertex / normal / index / uv buffers, plus the primitive builders. `Geometry.box()`
2
+ // etc. return a Geometry you can hand to Mesh.from(); Mesh.box() is sugar over them. Ported from
3
+ // worker/src/primitives/*.
4
+
5
+ import { cx, cy, cz, type Vec3Like } from "../math/vec"
6
+ import { fetch } from "../runtime/fetch"
7
+
8
+ export type MeshKind = "triangles" | "vertices" | "edges"
9
+
10
+ /** A geometry file — what `lecodes assets geometry <model.glb>` writes (`<model>.geometry.json`):
11
+ * one merged, world-space triangle buffer. `Geometry.load(asset('./casing.geometry.json'))`. */
12
+ export type GeometryData = {
13
+ format?: "lecodes-geometry"
14
+ version?: number
15
+ /** xyz per vertex. */
16
+ vertices: ArrayLike<number>
17
+ /** xyz per vertex (unit length). */
18
+ normals: ArrayLike<number>
19
+ /** uv per vertex. */
20
+ uv: ArrayLike<number>
21
+ /** Triangle list, ≤ 65535 vertices addressed. */
22
+ indices: ArrayLike<number>
23
+ }
24
+
25
+ // local tuple helpers — geometry building works on raw Float32Array buffers, so it stays out of the
26
+ // Vec3 wrapper (no per-vertex allocation).
27
+ type Tup3 = [number, number, number]
28
+ const cross = (a: Tup3, b: Tup3): Tup3 => [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]
29
+ const normalize = (a: Tup3): Tup3 => {
30
+ const l = Math.hypot(a[0], a[1], a[2])
31
+ return l === 0 ? [ 0, 0, 0 ] : [ a[0] / l, a[1] / l, a[2] / l ]
32
+ }
33
+
34
+ export class Geometry {
35
+ vertices: Float32Array
36
+ normals: Float32Array
37
+ indices: Uint16Array
38
+ uv: Float32Array
39
+ /** Lightmap UV set (2 floats per vertex, non-overlapping, inside the unit square) — the
40
+ * `Lightmap` atlas samples a Mesh through it. The box and cylinder builders fill it (their `uv`
41
+ * tiles per face, which would fold every face onto the same texels); absent = the host reuses
42
+ * `uv`, which is right for a plane. */
43
+ uv1?: Float32Array
44
+ /** Per-vertex COLOURS: 4 bytes (r, g, b, a) per vertex, 0..255. A material that declares
45
+ * `requires: [color]` reads them through `getColor()` — one mesh, many colours, no material per
46
+ * shade (a debug drawer, a gradient along a curve). Absent = every vertex white, which is what
47
+ * the built-in materials expect. */
48
+ colors?: Uint8Array
49
+ /** @internal native mesh-type enum (0 triangles / 1 edges / 2 vertices). */
50
+ _meshKind = 0
51
+
52
+ constructor(vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array) {
53
+ this.vertices = vertices
54
+ this.normals = normals
55
+ this.indices = indices
56
+ this.uv = uv
57
+ }
58
+
59
+ translate(x: number, y: number, z: number): this {
60
+ for (let i = 0; i < this.vertices.length; i += 3) {
61
+ this.vertices[i] += x; this.vertices[i + 1] += y; this.vertices[i + 2] += z
62
+ }
63
+ return this
64
+ }
65
+ scale(x: number, y: number, z: number): this {
66
+ for (let i = 0; i < this.vertices.length; i += 3) {
67
+ this.vertices[i] *= x; this.vertices[i + 1] *= y; this.vertices[i + 2] *= z
68
+ }
69
+ return this
70
+ }
71
+ scaleUV(x: number, y: number): this {
72
+ for (let i = 0; i < this.uv.length; i += 2) { this.uv[i] *= x; this.uv[i + 1] *= y }
73
+ return this
74
+ }
75
+ set kind(kind: MeshKind) {
76
+ this._meshKind = kind === "edges" ? 1 : kind === "vertices" ? 2 : 0
77
+ }
78
+
79
+ /** A box; `size` (default 1) scales the vertices AND lays the lightmap chart out in proportion to
80
+ * the faces' areas (a floor's top face gets the texels, not its 1 m side strips). */
81
+ static box(size: Vec3Like | number = 1): Geometry { return createCube(size) }
82
+ static sphere(options?: SphereOptions): Geometry { return createSphere(options) }
83
+ static cylinder(options?: CylinderOptions): Geometry { return createCylinder(options) }
84
+ /** A capsule along Y, centred at the origin: a cylinder `length` long (between the two cap centres)
85
+ * with a hemisphere of `radius` on each end — one closed surface, smooth across the seams. */
86
+ static capsule(options?: CapsuleOptions): Geometry { return createCapsule(options) }
87
+ static plane(options?: PlaneOptions): Geometry { return createPlane(options) }
88
+
89
+ /** A Geometry from a geometry file's contents (`GeometryData` — see `Geometry.load`). The arrays
90
+ * are copied into typed buffers, so the source object can be dropped. */
91
+ static fromData(data: GeometryData): Geometry {
92
+ if (!data || !data.vertices || !data.indices) throw new Error("Geometry.fromData: not a geometry file (no vertices/indices)")
93
+ if (data.format !== undefined && data.format !== "lecodes-geometry") throw new Error(`Geometry.fromData: unknown format "${data.format}"`)
94
+ const nv = data.vertices.length / 3
95
+ if (nv > 65535) throw new Error(`Geometry.fromData: ${nv} vertices — at most 65535 (uint16 indices)`)
96
+ const vertices = Float32Array.from(data.vertices)
97
+ const normals = data.normals && data.normals.length === vertices.length ? Float32Array.from(data.normals) : new Float32Array(vertices.length)
98
+ const uv = data.uv && data.uv.length === nv * 2 ? Float32Array.from(data.uv) : new Float32Array(nv * 2)
99
+ return new Geometry(vertices, normals, Uint16Array.from(data.indices), uv)
100
+ }
101
+
102
+ /** Load a geometry file: `lecodes assets geometry casing.glb` → `casing.geometry.json` →
103
+ * `await Geometry.load(asset('./casing.geometry.json'))`. A GLB's triangles as a plain buffer,
104
+ * for `Particles({ mesh })` debris (shell casings, rubble) and `Mesh.from`.
105
+ * A `.json` project file is INLINED by the compiler — `asset('./x.json')` is the parsed data, not
106
+ * a URL — so that form (and `import data from './x.json'`) is read directly, no fetch; a string
107
+ * (a URL, a non-`.json` asset such as `.geo`) is fetched. */
108
+ static async load(source: string | GeometryData): Promise<Geometry> {
109
+ if (typeof source !== "string") return Geometry.fromData(source)
110
+ const url = source
111
+ const resp = await fetch(url, { useOnce: true })
112
+ if (resp.status >= 400) { resp.dispose(); throw new Error(`Geometry.load: ${url} → HTTP ${resp.status}`) }
113
+ const data = resp.json<GeometryData>()
114
+ resp.dispose()
115
+ return Geometry.fromData(data)
116
+ }
117
+ }
118
+
119
+ // Gutter between lightmap charts (fraction of the chart square): the bake dilates each rect by a
120
+ // few texels and the shader samples bilinearly, so neighbouring faces must not touch in uv1.
121
+ const LM_GUTTER = 0.03
122
+
123
+ type Rect = { x: number, y: number, w: number, h: number }
124
+
125
+ /** Shelf-pack rectangles (metres) into the unit square at the largest uniform scale — every face
126
+ * gets texels in proportion to its area, so a 64×1×64 floor's top face isn't squeezed into the
127
+ * same cell as its 1 m side strips. Returns rects in input order. */
128
+ const packCharts = (dims: [number, number][]): Rect[] => {
129
+ const order = dims.map((_, i) => i).sort((a, b) => dims[b][1] - dims[a][1])
130
+ const attempt = (scale: number): Rect[] | null => {
131
+ const out: Rect[] = new Array(dims.length)
132
+ let x = 0, y = 0, rowH = 0
133
+ for (const i of order) {
134
+ const w = dims[i][0] * scale, h = dims[i][1] * scale
135
+ if (x > 0 && x + w > 1) { y += rowH + LM_GUTTER; x = 0; rowH = 0 }
136
+ if (w > 1 || y + h > 1) return null
137
+ out[i] = { x, y, w, h }
138
+ x += w + LM_GUTTER
139
+ rowH = Math.max(rowH, h)
140
+ }
141
+ return out
142
+ }
143
+ let lo = 0, hi = 1 / Math.max(...dims.map((d) => Math.max(d[0], d[1])))
144
+ let best = attempt(hi)
145
+ if (!best) {
146
+ for (let it = 0; it < 40; it++) {
147
+ const mid = (lo + hi) / 2
148
+ const r = attempt(mid)
149
+ if (r) { best = r; lo = mid } else hi = mid
150
+ }
151
+ }
152
+ return best ?? dims.map(() => ({ x: 0, y: 0, w: 1, h: 1 }))
153
+ }
154
+
155
+ // --- cube ---
156
+ const createCube = (size: Vec3Like | number = 1): Geometry => {
157
+ const sx = typeof size === "number" ? size : cx(size)
158
+ const sy = typeof size === "number" ? size : cy(size)
159
+ const sz = typeof size === "number" ? size : cz(size)
160
+ const vertices = new Float32Array(6 * 4 * 3)
161
+ const normals = new Float32Array(6 * 4 * 3)
162
+ const uv = new Float32Array(6 * 4 * 2)
163
+ const uv1 = new Float32Array(6 * 4 * 2)
164
+ const indices = new Uint16Array(6 * 6)
165
+ let cnt = 0, indicesCnt = 0
166
+ const sides = [ -0.5, 0.5 ]
167
+ // face extents along the (a, b) parameters below: ±x faces span (z, y), ±y faces (x, z), ±z faces (x, y)
168
+ const charts = packCharts([ [ sz, sy ], [ sz, sy ], [ sx, sz ], [ sx, sz ], [ sx, sy ], [ sx, sy ] ])
169
+ for (let i = 0; i < 6; i++) {
170
+ const s = cnt
171
+ const side = i % 2 === 0 ? -0.5 : 0.5
172
+ const c = charts[i]
173
+ for (const a of sides) {
174
+ for (const b of sides) {
175
+ uv.set([ a + 0.5, b + 0.5 ], cnt * 2)
176
+ uv1.set([ c.x + (a + 0.5) * c.w, c.y + (b + 0.5) * c.h ], cnt * 2)
177
+ if (i === 0 || i === 1) { vertices.set([ side * sx, b * sy, -a * sz ], cnt * 3); normals.set([ side * 2, 0, 0 ], cnt * 3) }
178
+ if (i === 2 || i === 3) { vertices.set([ a * sx, side * sy, -b * sz ], cnt * 3); normals.set([ 0, side * 2, 0 ], cnt * 3) }
179
+ if (i === 4 || i === 5) { vertices.set([ a * sx, b * sy, side * sz ], cnt * 3); normals.set([ 0, 0, side * 2 ], cnt * 3) }
180
+ cnt++
181
+ }
182
+ }
183
+ const s1 = s + 1, s2 = s + 2, s3 = s + 3
184
+ if (i % 2 === 0) indices.set([ s, s1, s2, s3, s2, s1 ], indicesCnt)
185
+ else indices.set([ s2, s1, s, s1, s2, s3 ], indicesCnt)
186
+ indicesCnt += 6
187
+ }
188
+ const g = new Geometry(vertices, normals, indices, uv)
189
+ g.uv1 = uv1
190
+ return g
191
+ }
192
+
193
+ // --- sphere ---
194
+ export type SphereOptions = { radius?: number, widthSegments?: number, heightSegments?: number }
195
+ const createSphere = (options: SphereOptions = {}): Geometry => {
196
+ const { radius = 0.5, widthSegments = 32, heightSegments = 32 } = options
197
+ const verticesCount = (widthSegments + 1) * (heightSegments - 1) + widthSegments * 2
198
+ const vertices = new Float32Array(verticesCount * 3)
199
+ const normals = new Float32Array(verticesCount * 3)
200
+ const uvs = new Float32Array(verticesCount * 2)
201
+ const indices = new Uint16Array(widthSegments * (heightSegments - 2) * 6 + widthSegments * 3 * 2)
202
+ const phiLength = Math.PI * 2, thetaLength = Math.PI
203
+ let vertexCnt = 0, indicesCnt = 0
204
+ for (let iy = 1; iy < heightSegments; iy++) {
205
+ const v = iy / heightSegments
206
+ for (let ix = 0; ix <= widthSegments; ix++) {
207
+ const u = ix / widthSegments
208
+ const point: [number, number, number] = [
209
+ -Math.cos(u * phiLength) * Math.sin(v * thetaLength),
210
+ Math.cos(v * thetaLength),
211
+ Math.sin(u * phiLength) * Math.sin(v * thetaLength),
212
+ ]
213
+ vertices.set([ point[0] * radius, point[1] * radius, point[2] * radius ], vertexCnt * 3)
214
+ normals.set(point, vertexCnt * 3)
215
+ uvs.set([ u, 1 - v ], vertexCnt * 2)
216
+ if (iy > 1 && ix > 0) {
217
+ const s = vertexCnt - 1
218
+ const s1 = vertexCnt - (widthSegments + 1) - 1
219
+ indices.set([ s + 1, s1, s, s1, s + 1, s1 + 1 ], indicesCnt)
220
+ indicesCnt += 6
221
+ }
222
+ vertexCnt++
223
+ }
224
+ }
225
+ const sides = [ -radius, radius ]
226
+ const circleStart2 = vertexCnt - (widthSegments + 1)
227
+ for (const side of sides) {
228
+ const circleStart = side > 0 ? 0 : circleStart2
229
+ for (let ix = 0; ix < widthSegments; ix++) {
230
+ const u = (ix + 0.5) / widthSegments
231
+ vertices.set([ 0, side, 0 ], vertexCnt * 3)
232
+ normals.set([ 0, side < 0 ? -1 : 1, 0 ], vertexCnt * 3)
233
+ uvs.set([ u, side < 0 ? 0 : 1 ], vertexCnt * 2)
234
+ const s = circleStart + ix
235
+ if (side > 0) indices.set([ s, s + 1, vertexCnt ], indicesCnt)
236
+ else indices.set([ vertexCnt, s + 1, s ], indicesCnt)
237
+ indicesCnt += 3
238
+ vertexCnt++
239
+ }
240
+ }
241
+ return new Geometry(vertices, normals, indices, uvs)
242
+ }
243
+
244
+ // --- capsule ---
245
+ export type CapsuleOptions = {
246
+ radius?: number
247
+ /** Distance between the two cap centres (the straight part); 0 = a sphere. Default 1. */
248
+ length?: number
249
+ /** Segments around the axis. */
250
+ widthSegments?: number
251
+ /** Rings per hemisphere (pole to equator). */
252
+ capSegments?: number
253
+ }
254
+ const createCapsule = (options: CapsuleOptions = {}): Geometry => {
255
+ const { radius = 0.5, length = 1, widthSegments = 32, capSegments = 8 } = options
256
+ const W = widthSegments, half = length / 2
257
+ // rings from the top cap's first ring down to the bottom cap's last: the equator appears twice (once per
258
+ // cap, `length` apart) — the band between those two rings is the cylinder; the poles are fans, like the sphere
259
+ const R = capSegments * 2
260
+ const verticesCount = (W + 1) * R + W * 2
261
+ const vertices = new Float32Array(verticesCount * 3)
262
+ const normals = new Float32Array(verticesCount * 3)
263
+ const uvs = new Float32Array(verticesCount * 2)
264
+ const indices = new Uint16Array(W * (R - 1) * 6 + W * 3 * 2)
265
+ const total = Math.PI * radius + length // arc from pole to pole, for v
266
+ let vertexCnt = 0, indicesCnt = 0
267
+ for (let j = 0; j < R; j++) {
268
+ const top = j < capSegments
269
+ const theta = (top ? j + 1 : j) / capSegments * (Math.PI / 2) // 0 = the top pole, π = the bottom
270
+ const yOff = top ? half : -half
271
+ const sinT = Math.sin(theta), cosT = Math.cos(theta)
272
+ const v = ((Math.PI - theta) * radius + (top ? length : 0)) / total
273
+ for (let ix = 0; ix <= W; ix++) {
274
+ const u = ix / W
275
+ const nx = -Math.cos(u * Math.PI * 2) * sinT, nz = Math.sin(u * Math.PI * 2) * sinT
276
+ vertices.set([ nx * radius, cosT * radius + yOff, nz * radius ], vertexCnt * 3)
277
+ normals.set([ nx, cosT, nz ], vertexCnt * 3)
278
+ uvs.set([ u, v ], vertexCnt * 2)
279
+ if (j > 0 && ix > 0) {
280
+ const s = vertexCnt - 1, s1 = vertexCnt - (W + 1) - 1
281
+ indices.set([ s + 1, s1, s, s1, s + 1, s1 + 1 ], indicesCnt)
282
+ indicesCnt += 6
283
+ }
284
+ vertexCnt++
285
+ }
286
+ }
287
+ const lastRing = vertexCnt - (W + 1)
288
+ for (const side of [ 1, -1 ]) {
289
+ const circleStart = side > 0 ? 0 : lastRing
290
+ for (let ix = 0; ix < W; ix++) {
291
+ const u = (ix + 0.5) / W
292
+ vertices.set([ 0, side * (radius + half), 0 ], vertexCnt * 3)
293
+ normals.set([ 0, side, 0 ], vertexCnt * 3)
294
+ uvs.set([ u, side > 0 ? 1 : 0 ], vertexCnt * 2)
295
+ const s = circleStart + ix
296
+ if (side > 0) indices.set([ s, s + 1, vertexCnt ], indicesCnt)
297
+ else indices.set([ vertexCnt, s + 1, s ], indicesCnt)
298
+ indicesCnt += 3
299
+ vertexCnt++
300
+ }
301
+ }
302
+ return new Geometry(vertices, normals, indices, uvs)
303
+ }
304
+
305
+ // --- cylinder ---
306
+ export type CylinderOptions = { radius?: number, radiusTop?: number, radiusBottom?: number, edges?: number, smooth?: boolean }
307
+ const createCylinder = (options: CylinderOptions = {}): Geometry => {
308
+ const { edges = 32, smooth = true, radius = 0.5, radiusTop = radius, radiusBottom = radius } = options
309
+ const verticesCount = edges * 4 + (edges + 1) * 2
310
+ const vertices = new Float32Array(verticesCount * 3)
311
+ const normals = new Float32Array(verticesCount * 3)
312
+ const uv = new Float32Array(verticesCount * 2)
313
+ // lightmap chart: the side strip across the upper half, the two caps as discs in the lower half
314
+ const uv1 = new Float32Array(verticesCount * 2)
315
+ const indices = new Uint16Array(edges * 6 + edges * 6)
316
+ const sides = [ -0.5, 0.5 ]
317
+ let cnt = 0, indicesCnt = 0
318
+ for (let i = 0; i < edges; i++) {
319
+ const s = cnt
320
+ const normalX = Math.cos((i + 0.5) / edges * Math.PI * 2)
321
+ const normalY = Math.sin((i + 0.5) / edges * Math.PI * 2)
322
+ for (let j = 0; j < 2; j++) {
323
+ const x = Math.cos((i + j) / edges * Math.PI * 2)
324
+ const y = Math.sin((i + j) / edges * Math.PI * 2)
325
+ for (const side of sides) {
326
+ if (side > 0) vertices.set([ x * radiusTop, side, y * radiusTop ], cnt * 3)
327
+ else vertices.set([ x * radiusBottom, side, y * radiusBottom ], cnt * 3)
328
+ if (smooth) normals.set([ x, 0, y ], cnt * 3)
329
+ else normals.set([ normalX, 0, normalY ], cnt * 3)
330
+ uv.set([ (i + j) / edges, side + 0.5 ], cnt * 2)
331
+ uv1.set([ (i + j) / edges, 0.55 + (side + 0.5) * 0.43 ], cnt * 2)
332
+ cnt++
333
+ }
334
+ }
335
+ const s1 = s + 1, s2 = (s + 2) % (edges * 4), s3 = (s + 3) % (edges * 4)
336
+ indices.set([ s, s1, s2, s3, s2, s1 ], indicesCnt)
337
+ indicesCnt += 6
338
+ }
339
+ for (const side of sides) {
340
+ const center = cnt
341
+ const capU = side > 0 ? 0.75 : 0.25 // lightmap disc centre per cap
342
+ vertices.set([ 0, side, 0 ], cnt * 3); normals.set([ 0, side * 2, 0 ], cnt * 3)
343
+ uv1.set([ capU, 0.25 ], cnt * 2); cnt++
344
+ for (let i = 0; i < edges; i++) {
345
+ const x = Math.cos(i / edges * Math.PI * 2), y = Math.sin(i / edges * Math.PI * 2)
346
+ if (side > 0) vertices.set([ x * radiusTop, side, y * radiusTop ], cnt * 3)
347
+ else vertices.set([ x * radiusBottom, side, y * radiusBottom ], cnt * 3)
348
+ normals.set([ 0, side * 2, 0 ], cnt * 3)
349
+ uv1.set([ capU + x * 0.22, 0.25 + y * 0.22 ], cnt * 2)
350
+ if (side < 0) indices.set([ center, center + i + 1, center + 1 + ((i + 1) % edges) ], indicesCnt)
351
+ else indices.set([ center + 1 + ((i + 1) % edges), center + i + 1, center ], indicesCnt)
352
+ indicesCnt += 3
353
+ cnt++
354
+ }
355
+ }
356
+ const g = new Geometry(vertices, normals, indices, uv)
357
+ g.uv1 = uv1
358
+ return g
359
+ }
360
+
361
+ // --- plane ---
362
+ export type PlaneOptions = { normal?: Vec3Like }
363
+ const createPlane = (options: PlaneOptions = {}): Geometry => {
364
+ const normal: Tup3 = options.normal ? [ cx(options.normal), cy(options.normal), cz(options.normal) ] : [ 0, 0, 1 ]
365
+ const vertices = new Float32Array(4 * 3)
366
+ const normals = new Float32Array(4 * 3)
367
+ const uvs = new Float32Array(4 * 2)
368
+ const sides = [ -0.5, 0.5 ]
369
+ let cnt = 0
370
+ let upVector: Tup3, rightVec: Tup3
371
+ if (normal[1] === 0 && normal[2] === 0) { upVector = [ 0, 1, 0 ]; rightVec = [ 0, 0, -normal[0] ] }
372
+ else if (normal[0] === 0 && normal[2] === 0) { upVector = [ 0, 0, 1 ]; rightVec = [ -normal[1], 0, 0 ] }
373
+ else if (normal[0] === 0 && normal[1] === 0) { upVector = [ 0, 1, 0 ]; rightVec = [ normal[2], 0, 0 ] } // тут было верно
374
+ else {
375
+ const temp: Tup3 = Math.abs(normal[0]) < 0.5 ? [ 1, 0, 0 ] : [ 0, 0, 1 ]
376
+ upVector = normalize(cross(temp, normal))
377
+ rightVec = normalize(cross(upVector, normal))
378
+ }
379
+ for (const a of sides) {
380
+ for (const b of sides) {
381
+ normals.set(normal, cnt * 3)
382
+ uvs.set([ a + 0.5, b + 0.5 ], cnt * 2)
383
+ vertices.set([
384
+ rightVec[0] * a + upVector[0] * b,
385
+ rightVec[1] * a + upVector[1] * b,
386
+ rightVec[2] * a + upVector[2] * b,
387
+ ], cnt * 3)
388
+ cnt++
389
+ }
390
+ }
391
+ const indices = new Uint16Array([ 2, 1, 0, 1, 2, 3 ])
392
+ return new Geometry(vertices, normals, indices, uvs)
393
+ }