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.
- package/dist/global.d.ts +18 -4
- package/dist/inject.js +260 -361
- package/dist/types/animate/tween/Animation.d.ts +69 -0
- package/dist/types/animate/tween/Timeline.d.ts +55 -0
- package/dist/types/animate/tween/animateValue.d.ts +27 -0
- package/dist/types/animate/tween/easing.d.ts +29 -0
- package/dist/types/animate/tween/spec.d.ts +178 -0
- package/dist/types/g2/Node2D.d.ts +16 -0
- package/dist/types/g2/Sprite.d.ts +11 -1
- package/dist/types/gl/Camera.d.ts +15 -1
- package/dist/types/gl/Foliage.d.ts +47 -0
- package/dist/types/gl/Geometry.d.ts +24 -0
- package/dist/types/gl/Light.d.ts +28 -7
- package/dist/types/gl/Lightmap.d.ts +90 -60
- package/dist/types/gl/Material.d.ts +28 -20
- package/dist/types/gl/Model.d.ts +7 -5
- package/dist/types/gl/Node.d.ts +18 -0
- package/dist/types/gl/Particles.d.ts +55 -1
- package/dist/types/gl/Scene.d.ts +20 -0
- package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
- package/dist/types/gl/animation/Animator.d.ts +27 -0
- package/dist/types/gl/animation/DynamicBone.d.ts +19 -8
- package/dist/types/gl/animation/IK.d.ts +86 -30
- package/dist/types/gl/animation/Locomotion.d.ts +52 -3
- package/dist/types/gl/animation/Warp.d.ts +2 -1
- package/dist/types/gl/animation/core.d.ts +35 -4
- package/dist/types/gl/physics/Ragdoll.d.ts +87 -12
- package/dist/types/gl/terrain/Terrain.d.ts +12 -2
- package/dist/types/inject.d.ts +8 -2
- package/dist/types/runtime/input.d.ts +7 -0
- package/dist/types/scene/defineScene.d.ts +44 -32
- package/dist/types/ui/UIButton.d.ts +3 -1
- package/dist/types/ui/UIInput.d.ts +5 -1
- package/dist/types/ui/UINode.d.ts +24 -24
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/prompts/core-design.md +27 -4
- package/prompts/core.md +35 -6
- package/prompts/dist/2d-game.md +197 -408
- package/prompts/dist/3d-app.md +166 -491
- package/prompts/dist/ar-app.md +163 -373
- package/prompts/dist/design.md +87 -83
- package/prompts/dist/ui-app.md +136 -325
- package/prompts/select.ts +19 -4
- package/src/animate/tween/Animation.ts +378 -0
- package/src/animate/tween/Timeline.ts +175 -0
- package/src/animate/tween/animateValue.ts +100 -0
- package/src/animate/tween/easing.ts +172 -0
- package/src/animate/tween/spec.ts +479 -0
- package/src/audio/audio.ts +161 -161
- package/src/bridges.d.ts +235 -65
- package/src/compile/__tests__/assetMacro.test.ts +26 -0
- package/src/compile/__tests__/detectEntry.test.ts +19 -0
- package/src/compile/__tests__/serverSplit.test.ts +27 -0
- package/src/compile/bundler.ts +34 -4
- package/src/compile/compileProject.ts +31 -1
- package/src/compile/detectEntry.ts +8 -3
- package/src/compile/index.ts +2 -0
- package/src/compile/serverSplit.ts +9 -3
- package/src/g2/Node2D.ts +38 -0
- package/src/g2/Sprite.ts +20 -1
- package/src/gl/Camera.ts +34 -1
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Foliage.ts +102 -0
- package/src/gl/Geometry.ts +393 -348
- package/src/gl/Light.ts +49 -16
- package/src/gl/Lightmap.ts +439 -275
- package/src/gl/Material.ts +59 -47
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Model.ts +23 -12
- package/src/gl/Node.ts +39 -0
- package/src/gl/Particles.ts +80 -2
- package/src/gl/Scene.ts +34 -1
- package/src/gl/animation/AnimationClip.ts +52 -0
- package/src/gl/animation/Animator.ts +42 -2
- package/src/gl/animation/DynamicBone.ts +35 -12
- package/src/gl/animation/IK.ts +173 -152
- package/src/gl/animation/Locomotion.ts +72 -8
- package/src/gl/animation/Playback.ts +5 -4
- package/src/gl/animation/Warp.ts +5 -2
- package/src/gl/animation/core.ts +65 -4
- package/src/gl/physics/Ragdoll.ts +451 -272
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +33 -2
- package/src/inject.ts +236 -226
- package/src/runtime/input.ts +11 -0
- package/src/scene/defineScene.ts +72 -62
- package/src/scene/gizmos.ts +148 -148
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIInput.ts +3 -3
- package/src/ui/UINode.ts +61 -36
- package/dist/types/animate/animate.d.ts +0 -20
- package/src/animate/animate.ts +0 -238
package/src/gl/Geometry.ts
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
vertices.set([
|
|
232
|
-
normals.set([
|
|
233
|
-
uvs.set([ u,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
+
}
|