lecodes-cli 0.1.6 → 0.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 (73) hide show
  1. package/README.md +7 -1
  2. package/dist/index.js +202 -84
  3. package/package.json +4 -4
  4. package/runtime/sdk/animate/animate.ts +238 -0
  5. package/runtime/sdk/animate/bezier.ts +138 -0
  6. package/runtime/sdk/animate/easings.ts +126 -0
  7. package/runtime/sdk/core/Aspect.ts +151 -0
  8. package/runtime/sdk/core/color.ts +66 -0
  9. package/runtime/sdk/core/events.ts +38 -0
  10. package/runtime/sdk/core/registry.ts +23 -0
  11. package/runtime/sdk/g2/Camera2D.ts +33 -0
  12. package/runtime/sdk/g2/CharacterController2D.ts +75 -0
  13. package/runtime/sdk/g2/Node2D.ts +194 -0
  14. package/runtime/sdk/g2/Physics2D.ts +126 -0
  15. package/runtime/sdk/g2/Scene2D.ts +101 -0
  16. package/runtime/sdk/g2/Shape.ts +63 -0
  17. package/runtime/sdk/g2/Sprite.ts +79 -0
  18. package/runtime/sdk/g2/SpriteAnimation.ts +90 -0
  19. package/runtime/sdk/g2/Texture2D.ts +37 -0
  20. package/runtime/sdk/g2/Tilemap.ts +41 -0
  21. package/runtime/sdk/g2/Trigger.ts +47 -0
  22. package/runtime/sdk/g2/loop.ts +39 -0
  23. package/runtime/sdk/g2/touch.ts +83 -0
  24. package/runtime/sdk/gl/Camera.ts +40 -0
  25. package/runtime/sdk/gl/Collider.ts +29 -0
  26. package/runtime/sdk/gl/Geometry.ts +218 -0
  27. package/runtime/sdk/gl/Light.ts +37 -0
  28. package/runtime/sdk/gl/Material.ts +124 -0
  29. package/runtime/sdk/gl/Mesh.ts +100 -0
  30. package/runtime/sdk/gl/Model.ts +40 -0
  31. package/runtime/sdk/gl/ModelAnimation.ts +64 -0
  32. package/runtime/sdk/gl/Node.ts +234 -0
  33. package/runtime/sdk/gl/Noise.ts +30 -0
  34. package/runtime/sdk/gl/Particles.ts +185 -0
  35. package/runtime/sdk/gl/Physics.ts +95 -0
  36. package/runtime/sdk/gl/Plane.ts +53 -0
  37. package/runtime/sdk/gl/Ray.ts +16 -0
  38. package/runtime/sdk/gl/Scene.ts +179 -0
  39. package/runtime/sdk/gl/Texture.ts +35 -0
  40. package/runtime/sdk/gl/controls.ts +95 -0
  41. package/runtime/sdk/gl/state.ts +6 -0
  42. package/runtime/sdk/gl/touch.ts +56 -0
  43. package/runtime/sdk/inject.ts +85 -0
  44. package/runtime/sdk/math/Mathf.ts +118 -0
  45. package/runtime/sdk/math/mat4.ts +278 -0
  46. package/runtime/sdk/math/quat.ts +232 -0
  47. package/runtime/sdk/math/vec.ts +255 -0
  48. package/runtime/sdk/plugins/qr.ts +36 -0
  49. package/runtime/sdk/runtime/device.ts +33 -0
  50. package/runtime/sdk/runtime/fetch.ts +71 -0
  51. package/runtime/sdk/runtime/files.ts +23 -0
  52. package/runtime/sdk/runtime/input.ts +12 -0
  53. package/runtime/sdk/runtime/media.ts +111 -0
  54. package/runtime/sdk/runtime/misc.ts +12 -0
  55. package/runtime/sdk/runtime/net.ts +36 -0
  56. package/runtime/sdk/runtime/storage.ts +13 -0
  57. package/runtime/sdk/runtime/touch.ts +50 -0
  58. package/runtime/sdk/ui/UI.ts +19 -0
  59. package/runtime/sdk/ui/UIButton.ts +84 -0
  60. package/runtime/sdk/ui/UIContainer.ts +100 -0
  61. package/runtime/sdk/ui/UIImage.ts +68 -0
  62. package/runtime/sdk/ui/UIInput.ts +78 -0
  63. package/runtime/sdk/ui/UINode.ts +342 -0
  64. package/runtime/sdk/ui/UIScreen.ts +122 -0
  65. package/runtime/sdk/ui/UIScrollable.ts +70 -0
  66. package/runtime/sdk/ui/UISpacer.ts +14 -0
  67. package/runtime/sdk/ui/UIText.ts +40 -0
  68. package/runtime/sdk/ui/UIVideo.ts +38 -0
  69. package/runtime/sdk/ui/UIVirtualizedList.ts +222 -0
  70. package/runtime/sdk/ui/UIWidget.ts +97 -0
  71. package/runtime/sdk/ui/fonts.ts +10 -0
  72. package/runtime/sdk/ui/router.ts +73 -0
  73. package/runtime/inject.js +0 -4629
@@ -0,0 +1,218 @@
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
+ /** @internal native mesh-type enum (0 triangles / 1 edges / 2 vertices). */
24
+ _meshKind = 0
25
+
26
+ constructor(vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array) {
27
+ this.vertices = vertices
28
+ this.normals = normals
29
+ this.indices = indices
30
+ this.uv = uv
31
+ }
32
+
33
+ translate(x: number, y: number, z: number): this {
34
+ for (let i = 0; i < this.vertices.length; i += 3) {
35
+ this.vertices[i] += x; this.vertices[i + 1] += y; this.vertices[i + 2] += z
36
+ }
37
+ return this
38
+ }
39
+ scale(x: number, y: number, z: number): this {
40
+ for (let i = 0; i < this.vertices.length; i += 3) {
41
+ this.vertices[i] *= x; this.vertices[i + 1] *= y; this.vertices[i + 2] *= z
42
+ }
43
+ return this
44
+ }
45
+ scaleUV(x: number, y: number): this {
46
+ for (let i = 0; i < this.uv.length; i += 2) { this.uv[i] *= x; this.uv[i + 1] *= y }
47
+ return this
48
+ }
49
+ set kind(kind: MeshKind) {
50
+ this._meshKind = kind === "edges" ? 1 : kind === "vertices" ? 2 : 0
51
+ }
52
+
53
+ static box(): Geometry { return createCube() }
54
+ static sphere(options?: SphereOptions): Geometry { return createSphere(options) }
55
+ static cylinder(options?: CylinderOptions): Geometry { return createCylinder(options) }
56
+ static plane(options?: PlaneOptions): Geometry { return createPlane(options) }
57
+ }
58
+
59
+ // --- cube ---
60
+ const createCube = (): Geometry => {
61
+ const vertices = new Float32Array(6 * 4 * 3)
62
+ const normals = new Float32Array(6 * 4 * 3)
63
+ const uv = new Float32Array(6 * 4 * 3)
64
+ const indices = new Uint16Array(6 * 6)
65
+ let cnt = 0, indicesCnt = 0
66
+ const sides = [ -0.5, 0.5 ]
67
+ for (let i = 0; i < 6; i++) {
68
+ const s = cnt
69
+ const side = i % 2 === 0 ? -0.5 : 0.5
70
+ for (const a of sides) {
71
+ for (const b of sides) {
72
+ uv.set([ a + 0.5, b + 0.5 ], cnt * 2)
73
+ if (i === 0 || i === 1) { vertices.set([ side, b, -a ], cnt * 3); normals.set([ side * 2, 0, 0 ], cnt * 3) }
74
+ if (i === 2 || i === 3) { vertices.set([ a, side, -b ], cnt * 3); normals.set([ 0, side * 2, 0 ], cnt * 3) }
75
+ if (i === 4 || i === 5) { vertices.set([ a, b, side ], cnt * 3); normals.set([ 0, 0, side * 2 ], cnt * 3) }
76
+ cnt++
77
+ }
78
+ }
79
+ const s1 = s + 1, s2 = s + 2, s3 = s + 3
80
+ if (i % 2 === 0) indices.set([ s, s1, s2, s3, s2, s1 ], indicesCnt)
81
+ else indices.set([ s2, s1, s, s1, s2, s3 ], indicesCnt)
82
+ indicesCnt += 6
83
+ }
84
+ return new Geometry(vertices, normals, indices, uv)
85
+ }
86
+
87
+ // --- sphere ---
88
+ export type SphereOptions = { radius?: number, widthSegments?: number, heightSegments?: number }
89
+ const createSphere = (options: SphereOptions = {}): Geometry => {
90
+ const { radius = 0.5, widthSegments = 32, heightSegments = 32 } = options
91
+ const verticesCount = (widthSegments + 1) * (heightSegments - 1) + widthSegments * 2
92
+ const vertices = new Float32Array(verticesCount * 3)
93
+ const normals = new Float32Array(verticesCount * 3)
94
+ const uvs = new Float32Array(verticesCount * 2)
95
+ const indices = new Uint16Array(widthSegments * (heightSegments - 2) * 6 + widthSegments * 3 * 2)
96
+ const phiLength = Math.PI * 2, thetaLength = Math.PI
97
+ let vertexCnt = 0, indicesCnt = 0
98
+ for (let iy = 1; iy < heightSegments; iy++) {
99
+ const v = iy / heightSegments
100
+ for (let ix = 0; ix <= widthSegments; ix++) {
101
+ const u = ix / widthSegments
102
+ const point: [number, number, number] = [
103
+ -Math.cos(u * phiLength) * Math.sin(v * thetaLength),
104
+ Math.cos(v * thetaLength),
105
+ Math.sin(u * phiLength) * Math.sin(v * thetaLength),
106
+ ]
107
+ vertices.set([ point[0] * radius, point[1] * radius, point[2] * radius ], vertexCnt * 3)
108
+ normals.set(point, vertexCnt * 3)
109
+ uvs.set([ u, 1 - v ], vertexCnt * 2)
110
+ if (iy > 1 && ix > 0) {
111
+ const s = vertexCnt - 1
112
+ const s1 = vertexCnt - (widthSegments + 1) - 1
113
+ indices.set([ s + 1, s1, s, s1, s + 1, s1 + 1 ], indicesCnt)
114
+ indicesCnt += 6
115
+ }
116
+ vertexCnt++
117
+ }
118
+ }
119
+ const sides = [ -radius, radius ]
120
+ const circleStart2 = vertexCnt - (widthSegments + 1)
121
+ for (const side of sides) {
122
+ const circleStart = side > 0 ? 0 : circleStart2
123
+ for (let ix = 0; ix < widthSegments; ix++) {
124
+ const u = (ix + 0.5) / widthSegments
125
+ vertices.set([ 0, side, 0 ], vertexCnt * 3)
126
+ normals.set([ 0, side < 0 ? -1 : 1, 0 ], vertexCnt * 3)
127
+ uvs.set([ u, side < 0 ? 0 : 1 ], vertexCnt * 2)
128
+ const s = circleStart + ix
129
+ if (side > 0) indices.set([ s, s + 1, vertexCnt ], indicesCnt)
130
+ else indices.set([ vertexCnt, s + 1, s ], indicesCnt)
131
+ indicesCnt += 3
132
+ vertexCnt++
133
+ }
134
+ }
135
+ return new Geometry(vertices, normals, indices, uvs)
136
+ }
137
+
138
+ // --- cylinder ---
139
+ export type CylinderOptions = { radius?: number, radiusTop?: number, radiusBottom?: number, edges?: number, smooth?: boolean }
140
+ const createCylinder = (options: CylinderOptions = {}): Geometry => {
141
+ const { edges = 32, smooth = true, radius = 0.5, radiusTop = radius, radiusBottom = radius } = options
142
+ const verticesCount = edges * 4 + (edges + 1) * 2
143
+ const vertices = new Float32Array(verticesCount * 3)
144
+ const normals = new Float32Array(verticesCount * 3)
145
+ const uv = new Float32Array(verticesCount * 2)
146
+ const indices = new Uint16Array(edges * 6 + edges * 6)
147
+ const sides = [ -0.5, 0.5 ]
148
+ let cnt = 0, indicesCnt = 0
149
+ for (let i = 0; i < edges; i++) {
150
+ const s = cnt
151
+ const normalX = Math.cos((i + 0.5) / edges * Math.PI * 2)
152
+ const normalY = Math.sin((i + 0.5) / edges * Math.PI * 2)
153
+ for (let j = 0; j < 2; j++) {
154
+ const x = Math.cos((i + j) / edges * Math.PI * 2)
155
+ const y = Math.sin((i + j) / edges * Math.PI * 2)
156
+ for (const side of sides) {
157
+ if (side > 0) vertices.set([ x * radiusTop, side, y * radiusTop ], cnt * 3)
158
+ else vertices.set([ x * radiusBottom, side, y * radiusBottom ], cnt * 3)
159
+ if (smooth) normals.set([ x, 0, y ], cnt * 3)
160
+ else normals.set([ normalX, 0, normalY ], cnt * 3)
161
+ uv.set([ (i + j) / edges, side + 0.5 ], cnt * 2)
162
+ cnt++
163
+ }
164
+ }
165
+ const s1 = s + 1, s2 = (s + 2) % (edges * 4), s3 = (s + 3) % (edges * 4)
166
+ indices.set([ s, s1, s2, s3, s2, s1 ], indicesCnt)
167
+ indicesCnt += 6
168
+ }
169
+ for (const side of sides) {
170
+ const center = cnt
171
+ vertices.set([ 0, side, 0 ], cnt * 3); normals.set([ 0, side * 2, 0 ], cnt * 3); cnt++
172
+ for (let i = 0; i < edges; i++) {
173
+ const x = Math.cos(i / edges * Math.PI * 2), y = Math.sin(i / edges * Math.PI * 2)
174
+ if (side > 0) vertices.set([ x * radiusTop, side, y * radiusTop ], cnt * 3)
175
+ else vertices.set([ x * radiusBottom, side, y * radiusBottom ], cnt * 3)
176
+ normals.set([ 0, side * 2, 0 ], cnt * 3)
177
+ if (side < 0) indices.set([ center, center + i + 1, center + 1 + ((i + 1) % edges) ], indicesCnt)
178
+ else indices.set([ center + 1 + ((i + 1) % edges), center + i + 1, center ], indicesCnt)
179
+ indicesCnt += 3
180
+ cnt++
181
+ }
182
+ }
183
+ return new Geometry(vertices, normals, indices, uv)
184
+ }
185
+
186
+ // --- plane ---
187
+ export type PlaneOptions = { normal?: Vec3Like }
188
+ const createPlane = (options: PlaneOptions = {}): Geometry => {
189
+ const normal: Tup3 = options.normal ? [ cx(options.normal), cy(options.normal), cz(options.normal) ] : [ 0, 0, 1 ]
190
+ const vertices = new Float32Array(4 * 3)
191
+ const normals = new Float32Array(4 * 3)
192
+ const uvs = new Float32Array(4 * 2)
193
+ const sides = [ -0.5, 0.5 ]
194
+ let cnt = 0
195
+ let upVector: Tup3, rightVec: Tup3
196
+ if (normal[1] === 0 && normal[2] === 0) { upVector = [ 0, 1, 0 ]; rightVec = [ 0, 0, -normal[0] ] }
197
+ else if (normal[0] === 0 && normal[2] === 0) { upVector = [ 0, 0, 1 ]; rightVec = [ -normal[1], 0, 0 ] }
198
+ else if (normal[0] === 0 && normal[1] === 0) { upVector = [ 0, 1, 0 ]; rightVec = [ normal[2], 0, 0 ] } // тут было верно
199
+ else {
200
+ const temp: Tup3 = Math.abs(normal[0]) < 0.5 ? [ 1, 0, 0 ] : [ 0, 0, 1 ]
201
+ upVector = normalize(cross(temp, normal))
202
+ rightVec = normalize(cross(upVector, normal))
203
+ }
204
+ for (const a of sides) {
205
+ for (const b of sides) {
206
+ normals.set(normal, cnt * 3)
207
+ uvs.set([ a + 0.5, b + 0.5 ], cnt * 2)
208
+ vertices.set([
209
+ rightVec[0] * a + upVector[0] * b,
210
+ rightVec[1] * a + upVector[1] * b,
211
+ rightVec[2] * a + upVector[2] * b,
212
+ ], cnt * 3)
213
+ cnt++
214
+ }
215
+ }
216
+ const indices = new Uint16Array([ 2, 1, 0, 1, 2, 3 ])
217
+ return new Geometry(vertices, normals, indices, uvs)
218
+ }
@@ -0,0 +1,37 @@
1
+ // Lights. Currently a directional sun (the engine's primary light). Add it to a scene like any node.
2
+
3
+ import { Color, type ColorInput } from "../core/color"
4
+ import type { Vec3Like } from "../math/vec"
5
+ import { Node } from "./Node"
6
+
7
+ export type SunOptions = {
8
+ /** Light direction (points where the light travels). Defaults to a typical key-light angle. */
9
+ direction?: Vec3Like
10
+ /** Luminous intensity. */
11
+ intensity?: number
12
+ color?: ColorInput
13
+ /**
14
+ * Shadow quality (default 1):
15
+ * 0 - no shadows
16
+ * 1 - standard (1024 shadow map, hard PCF edges)
17
+ * 2 - high (2048 + stable, soft contact-hardening edges)
18
+ * 3 - best (4096 + stable, PCSS soft shadows)
19
+ * Higher values cost more GPU/memory.
20
+ */
21
+ shadowsQuality?: 0 | 1 | 2 | 3
22
+ }
23
+
24
+ export class Light extends Node {
25
+ /** A directional sun light. */
26
+ static sun(options: SunOptions = {}): Light {
27
+ const light = new Light()
28
+ const [ dx, dy, dz ] = options.direction ?? [ 0.548267, -0.473983, -0.689016 ]
29
+ _creator.createSunLight(
30
+ light.id, dx, dy, dz,
31
+ options.intensity ?? 100000,
32
+ Color.toPackedRgb(options.color ?? 0xffffff),
33
+ options.shadowsQuality ?? 1,
34
+ )
35
+ return light
36
+ }
37
+ }
@@ -0,0 +1,124 @@
1
+ // A material instance over creator-gl. Uniforms are set through a Proxy that lowers each JS value to
2
+ // the right native uniform call. The common materials are static factories (Material.lit/unlit/
3
+ // video/shadow); Material.load brings in a custom compiled shader. Ported from worker/src/Material.ts,
4
+ // with the Lit/Unlit/... subclasses collapsed into factories and color accepting any ColorInput.
5
+
6
+ import { Color, type ColorInput } from "../core/color"
7
+ import { fetch, type FetchResponse } from "../runtime/fetch"
8
+ import { Texture } from "./Texture"
9
+
10
+ type UniformValue = string | Texture | number | boolean | number[] | Float32Array | null
11
+
12
+ const applyUniform = (id: number, key: string, value: UniformValue): void => {
13
+ if (typeof value === "string") {
14
+ if (value.length > 7) _creator.setUniformRgba(id, key, parseInt(value.slice(1), 16))
15
+ else _creator.setUniformRgb(id, key, parseInt(value.slice(1), 16))
16
+ } else if (typeof value === "number") {
17
+ _creator.setUniformFloat(id, key, value)
18
+ } else if (typeof value === "boolean") {
19
+ _creator.setUniformBoolean(id, key, value)
20
+ } else if (value instanceof Texture) {
21
+ _creator.setUniformTexture(id, key, value._id, value.wrapS, value.wrapT)
22
+ } else if (value instanceof Float32Array) {
23
+ _creator.setUniformArray(id, key, value)
24
+ } else if (Array.isArray(value)) {
25
+ _creator.setUniformArray(id, key, Float32Array.of(...value))
26
+ } else {
27
+ _creator.setUniformNull(id, key)
28
+ }
29
+ }
30
+
31
+ export type MaterialColorOptions = { color?: ColorInput, map?: Texture | null }
32
+
33
+ export class Material {
34
+ /** @internal native material-instance handle. */
35
+ _id: number
36
+ readonly shader: FetchResponse | "unknown"
37
+ readonly uniforms: Record<string, UniformValue>
38
+
39
+ private _colorKey: string | null = null
40
+ private _useMapFlag = true
41
+
42
+ constructor(shader: FetchResponse | number) {
43
+ if (typeof shader === "number") {
44
+ this.shader = "unknown"
45
+ this._id = shader
46
+ } else {
47
+ this.shader = shader
48
+ this._id = _creator.createMaterial((shader as any)._id)
49
+ }
50
+ const store: Record<string, UniformValue> = {}
51
+ const id = () => this._id
52
+ this.uniforms = new Proxy(store, {
53
+ get: (t, k: string) => t[k],
54
+ set: (t, k: string, v: UniformValue) => { t[k] = v; applyUniform(id(), k, v); return true },
55
+ })
56
+ }
57
+
58
+ /** Set a uniform (chainable). */
59
+ set(key: string, value: UniformValue): this {
60
+ this.uniforms[key] = value
61
+ return this
62
+ }
63
+
64
+ set color(c: ColorInput) {
65
+ if (this._colorKey) this.uniforms[this._colorKey] = Color.toHexString(c)
66
+ }
67
+
68
+ set map(value: Texture | null) {
69
+ if (value === null) {
70
+ if (this._useMapFlag) this.uniforms.useBaseColorMap = false
71
+ } else {
72
+ this.uniforms.baseColorMap = value
73
+ if (this._useMapFlag) this.uniforms.useBaseColorMap = true
74
+ }
75
+ }
76
+
77
+ // --- factories ---
78
+
79
+ /** PBR lit material. */
80
+ static lit(options: MaterialColorOptions = {}): Material {
81
+ // The fetchLocal literal must be inline so the compiler's preload header captures the shader name.
82
+ const m = new Material({ _id: _creatorUtils.fetchLocal("lit.filamat") } as any)
83
+ m._colorKey = "baseColor"
84
+ if (options.color !== undefined) m.color = options.color
85
+ if (options.map) m.map = options.map
86
+ return m
87
+ }
88
+
89
+ /** Unlit (flat) material. */
90
+ static unlit(options: MaterialColorOptions = {}): Material {
91
+ const m = new Material({ _id: _creatorUtils.fetchLocal("unlit.filamat") } as any)
92
+ m._colorKey = "baseColor"
93
+ if (options.color !== undefined) m.color = options.color
94
+ if (options.map) m.map = options.map
95
+ return m
96
+ }
97
+
98
+ /** Material that samples a VideoPlayer's texture. */
99
+ static video(map?: Texture): Material {
100
+ const m = new Material({ _id: _creatorUtils.fetchLocal("video.filamat") } as any)
101
+ m._useMapFlag = false
102
+ if (map) m.uniforms.baseColorMap = map
103
+ return m
104
+ }
105
+
106
+ /** Shadow-catcher material (transparent except where shadows fall). */
107
+ static shadow(color: ColorInput = "#000000aa"): Material {
108
+ const m = new Material({ _id: _creatorUtils.fetchLocal("shadow.filamat") } as any)
109
+ m._colorKey = "color"
110
+ m.color = color
111
+ return m
112
+ }
113
+
114
+ /** Load a custom compiled shader (.mat URL) as a material. */
115
+ static load(url: string): Promise<Material> {
116
+ return fetch(url, { useOnce: true }).then((resp) => {
117
+ if (resp.status >= 400) return Promise.reject(new Error(`Failed to fetch shader from ${url}. HTTP ${resp.status}`))
118
+ return new Material(resp)
119
+ })
120
+ }
121
+
122
+ /** @internal */
123
+ static idOf(material: Material): number { return material._id }
124
+ }
@@ -0,0 +1,100 @@
1
+ // A renderable 3D node from raw geometry — the primitive shapes (box/sphere/cylinder/plane) and
2
+ // Mesh.from for a custom Geometry. A GLB import is a different kind: see Model.load.
3
+
4
+ import { cx, cy, cz, type Vec3Like } from "../math/vec"
5
+ import {
6
+ Geometry,
7
+ type CylinderOptions,
8
+ type PlaneOptions,
9
+ type SphereOptions,
10
+ } from "./Geometry"
11
+ import { Material } from "./Material"
12
+ import { Node } from "./Node"
13
+
14
+ /** Common transform/render options every primitive factory accepts. */
15
+ export type MeshOptions = {
16
+ material?: Material
17
+ position?: Vec3Like
18
+ eulerAngles?: Vec3Like
19
+ scale?: Vec3Like | number
20
+ name?: string
21
+ castShadows?: boolean
22
+ receiveShadows?: boolean
23
+ }
24
+
25
+ export class Mesh extends Node {
26
+ private _geometry?: Geometry
27
+ private _materials: Material[] = []
28
+
29
+ constructor(geometry?: Geometry, material?: Material) {
30
+ super()
31
+ if (geometry) {
32
+ const mat = material ?? Material.unlit()
33
+ this._geometry = geometry
34
+ this._materials = [ mat ]
35
+ _creator.setMesh(this.id, Material.idOf(mat), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, geometry._meshKind)
36
+ }
37
+ }
38
+
39
+ override get geometry(): Geometry | undefined { return this._geometry }
40
+
41
+ get material(): Material { return this._materials[0] }
42
+ set material(m: Material) { this.setMaterial(m, 0) }
43
+
44
+ setMaterial(material: Material, index = 0): this {
45
+ this._materials[index] = material
46
+ _creator.setMaterial(this.id, Material.idOf(material), index)
47
+ return this
48
+ }
49
+ getMaterial(index = 0): Material | null {
50
+ const existing = this._materials[index]
51
+ if (existing) return existing
52
+ const id = _creator.getMaterial(this.id, index)
53
+ if (id === 0) return null
54
+ const mat = new Material(id)
55
+ this._materials[index] = mat
56
+ return mat
57
+ }
58
+
59
+ set culling(v: boolean) { _creator.setCulling(this.id, v) }
60
+ set castShadows(v: boolean) { _creator.setCastShadows(this.id, v) }
61
+ set receiveShadows(v: boolean) { _creator.setReceiveShadows(this.id, v) }
62
+
63
+ // --- factories ---
64
+
65
+ static box(options: MeshOptions & { size?: Vec3Like | number } = {}): Mesh {
66
+ const geo = Geometry.box()
67
+ const [ sx, sy, sz ] = sizeToVec(options.size ?? 1)
68
+ if (sx !== 1 || sy !== 1 || sz !== 1) geo.scale(sx, sy, sz)
69
+ return apply(new Mesh(geo, options.material), options)
70
+ }
71
+
72
+ static sphere(options: MeshOptions & SphereOptions = {}): Mesh {
73
+ return apply(new Mesh(Geometry.sphere(options), options.material), options)
74
+ }
75
+
76
+ static cylinder(options: MeshOptions & CylinderOptions = {}): Mesh {
77
+ return apply(new Mesh(Geometry.cylinder(options), options.material), options)
78
+ }
79
+
80
+ static plane(options: MeshOptions & PlaneOptions = {}): Mesh {
81
+ return apply(new Mesh(Geometry.plane(options), options.material), options)
82
+ }
83
+
84
+ /** Wrap a custom Geometry. */
85
+ static from(geometry: Geometry, options: MeshOptions = {}): Mesh {
86
+ return apply(new Mesh(geometry, options.material), options)
87
+ }
88
+ }
89
+
90
+ const sizeToVec = (s: Vec3Like | number): [number, number, number] => (typeof s === "number" ? [ s, s, s ] : [ cx(s), cy(s), cz(s) ])
91
+
92
+ const apply = (mesh: Mesh, o: MeshOptions): Mesh => {
93
+ if (o.position) mesh.position = o.position
94
+ if (o.eulerAngles) mesh.eulerAngles = o.eulerAngles
95
+ if (o.scale !== undefined) mesh.scale = typeof o.scale === "number" ? [ o.scale, o.scale, o.scale ] : o.scale
96
+ if (o.name !== undefined) mesh.name = o.name
97
+ if (o.castShadows !== undefined) mesh.castShadows = o.castShadows
98
+ if (o.receiveShadows !== undefined) mesh.receiveShadows = o.receiveShadows
99
+ return mesh
100
+ }
@@ -0,0 +1,40 @@
1
+ // A loaded GLB model — its own node kind (a GLB is a node hierarchy with baked animation clips),
2
+ // distinct from Mesh (raw primitives, no animation). Animation is always present, reached as
3
+ // model.anim:
4
+ // const hero = await Model.load(asset('./hero.glb'))
5
+ // hero.anim.play('Run')
6
+
7
+ import { fetch, type FetchResponse } from "../runtime/fetch"
8
+ import { Node } from "./Node"
9
+ import { ModelAnimation } from "./ModelAnimation"
10
+
11
+ export class Model extends Node {
12
+ /** GLB skeletal animation — always present on a Model. */
13
+ declare readonly anim: ModelAnimation
14
+
15
+ constructor(internalId: number) {
16
+ super(internalId)
17
+ this.aspect(ModelAnimation)
18
+ }
19
+
20
+ /** Load a GLB model. Returns its root as a Model; play its baked clips via model.anim. */
21
+ static load(
22
+ source: string | FetchResponse,
23
+ options: { culling?: boolean, onProgress?: (p: { loaded: number, total?: number }) => void } = {},
24
+ ): Promise<Model> {
25
+ if (typeof source === "string") {
26
+ return fetch(source, { useOnce: true, onProgress: options.onProgress }).then((resp) => {
27
+ if (resp.status >= 400) return Promise.reject(new Error(`Failed to load GLB from ${source}. HTTP ${resp.status}`))
28
+ return Model.load(resp, options)
29
+ })
30
+ }
31
+ return new Promise<Model>((resolve, reject) => {
32
+ _creator.createGlb((source as unknown as { _id: number })._id, (entityId: number) => {
33
+ if (entityId === 0) { reject(new Error("Failed to load GLB")); return }
34
+ const model = new Model(entityId)
35
+ if (options.culling !== true) _creator.setGlbCulling(entityId, false)
36
+ resolve(model)
37
+ }, reject)
38
+ })
39
+ }
40
+ }
@@ -0,0 +1,64 @@
1
+ // GLB skeletal animation as an aspect. Auto-attached by Model (a glb always has its clips), so you
2
+ // reach it directly: model.anim.play('Run'). Clips come baked in the glb; play by name or index.
3
+
4
+ import { Aspect } from "../core/Aspect"
5
+ import { nodeRegistry } from "./Node"
6
+ import type { Model } from "./Model"
7
+
8
+ let animatorRegistered = false
9
+ const ensureAnimator = (): void => {
10
+ if (animatorRegistered) return
11
+ animatorRegistered = true
12
+ _creator.setOnAnimationComplete((entityId, clipIndex) => nodeRegistry.get(entityId)?._onAnimationComplete(clipIndex) ?? true)
13
+ }
14
+
15
+ export class ModelAnimation extends Aspect<"anim", Model> {
16
+ static readonly aspect = "anim"
17
+
18
+ private _playing = false
19
+ private _clip = 0
20
+ private _speed = 1
21
+ private _loop = false
22
+
23
+ onAttach(): void {
24
+ this.node._animComplete = (clipIndex) => {
25
+ if (clipIndex !== this._clip) return true
26
+ if (!this._loop) { this._playing = false; this.node._emitAnim("completed", clipIndex) }
27
+ else this.node._emitAnim("loopReached", clipIndex)
28
+ return this._playing
29
+ }
30
+ }
31
+
32
+ /** The clips baked into the glb. */
33
+ get clips(): { name: string, duration: number }[] { return _creator.getGlbAnimations(this.node.id) }
34
+
35
+ get playing(): boolean { return this._playing }
36
+ set playing(value: boolean) {
37
+ this._playing = value
38
+ _creator.setAnimation(this.node.id, value, this._clip, this._speed)
39
+ }
40
+
41
+ /** Play a clip by name or index. Chainable. */
42
+ play(clip?: string | number, options: { loop?: boolean } = {}): this {
43
+ if (typeof clip === "number") this._clip = clip
44
+ else if (typeof clip === "string") {
45
+ const i = this.clips.findIndex((c) => c.name === clip)
46
+ if (i >= 0) this._clip = i
47
+ }
48
+ if (options.loop !== undefined) this._loop = options.loop
49
+ this.playing = true
50
+ ensureAnimator()
51
+ return this
52
+ }
53
+ stop(): this { this.playing = false; return this }
54
+
55
+ get loop(): boolean { return this._loop }
56
+ set loop(v: boolean) { this._loop = v }
57
+ get speed(): number { return this._speed }
58
+ set speed(v: number) {
59
+ this._speed = v
60
+ _creator.setAnimation(this.node.id, this._playing, this._clip, v)
61
+ }
62
+ get time(): number { return _creator.getAnimationTime(this.node.id) }
63
+ set time(v: number) { _creator.setAnimationTime(this.node.id, this._clip, v) }
64
+ }