lecodes-cli 0.1.6 → 0.1.7

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 (62) hide show
  1. package/dist/index.js +166 -14
  2. package/package.json +3 -2
  3. package/runtime/sdk/animate/animate.ts +238 -0
  4. package/runtime/sdk/animate/bezier.ts +138 -0
  5. package/runtime/sdk/animate/easings.ts +126 -0
  6. package/runtime/sdk/core/color.ts +66 -0
  7. package/runtime/sdk/core/events.ts +38 -0
  8. package/runtime/sdk/core/registry.ts +23 -0
  9. package/runtime/sdk/g2/Camera2D.ts +73 -0
  10. package/runtime/sdk/g2/Node2D.ts +104 -0
  11. package/runtime/sdk/g2/Scene2D.ts +73 -0
  12. package/runtime/sdk/g2/Sprite.ts +164 -0
  13. package/runtime/sdk/g2/Texture2D.ts +37 -0
  14. package/runtime/sdk/g2/Tilemap.ts +41 -0
  15. package/runtime/sdk/g2/loop.ts +22 -0
  16. package/runtime/sdk/gl/Body.ts +101 -0
  17. package/runtime/sdk/gl/Camera.ts +40 -0
  18. package/runtime/sdk/gl/Geometry.ts +218 -0
  19. package/runtime/sdk/gl/Light.ts +37 -0
  20. package/runtime/sdk/gl/Material.ts +124 -0
  21. package/runtime/sdk/gl/Mesh.ts +123 -0
  22. package/runtime/sdk/gl/Node.ts +307 -0
  23. package/runtime/sdk/gl/Noise.ts +30 -0
  24. package/runtime/sdk/gl/Particles.ts +185 -0
  25. package/runtime/sdk/gl/Plane.ts +53 -0
  26. package/runtime/sdk/gl/Ray.ts +16 -0
  27. package/runtime/sdk/gl/Scene.ts +179 -0
  28. package/runtime/sdk/gl/Texture.ts +35 -0
  29. package/runtime/sdk/gl/controls.ts +95 -0
  30. package/runtime/sdk/gl/state.ts +6 -0
  31. package/runtime/sdk/gl/touch.ts +56 -0
  32. package/runtime/sdk/inject.ts +72 -0
  33. package/runtime/sdk/math/Mathf.ts +118 -0
  34. package/runtime/sdk/math/mat4.ts +278 -0
  35. package/runtime/sdk/math/quat.ts +232 -0
  36. package/runtime/sdk/math/vec.ts +255 -0
  37. package/runtime/sdk/plugins/qr.ts +36 -0
  38. package/runtime/sdk/runtime/device.ts +33 -0
  39. package/runtime/sdk/runtime/fetch.ts +71 -0
  40. package/runtime/sdk/runtime/files.ts +23 -0
  41. package/runtime/sdk/runtime/input.ts +12 -0
  42. package/runtime/sdk/runtime/media.ts +111 -0
  43. package/runtime/sdk/runtime/misc.ts +12 -0
  44. package/runtime/sdk/runtime/net.ts +36 -0
  45. package/runtime/sdk/runtime/storage.ts +13 -0
  46. package/runtime/sdk/runtime/touch.ts +44 -0
  47. package/runtime/sdk/ui/UI.ts +19 -0
  48. package/runtime/sdk/ui/UIButton.ts +84 -0
  49. package/runtime/sdk/ui/UIContainer.ts +100 -0
  50. package/runtime/sdk/ui/UIImage.ts +68 -0
  51. package/runtime/sdk/ui/UIInput.ts +78 -0
  52. package/runtime/sdk/ui/UINode.ts +342 -0
  53. package/runtime/sdk/ui/UIScreen.ts +122 -0
  54. package/runtime/sdk/ui/UIScrollable.ts +70 -0
  55. package/runtime/sdk/ui/UISpacer.ts +14 -0
  56. package/runtime/sdk/ui/UIText.ts +40 -0
  57. package/runtime/sdk/ui/UIVideo.ts +38 -0
  58. package/runtime/sdk/ui/UIVirtualizedList.ts +222 -0
  59. package/runtime/sdk/ui/UIWidget.ts +97 -0
  60. package/runtime/sdk/ui/fonts.ts +10 -0
  61. package/runtime/sdk/ui/router.ts +73 -0
  62. package/runtime/inject.js +0 -4629
@@ -0,0 +1,37 @@
1
+ // A GPU texture decoded natively by creator-2d (stb_image). Load from a URL (or an already-fetched
2
+ // response); the raw file bytes are handed to the engine, which decodes + uploads them.
3
+
4
+ // Internal SDK code imports injected globals explicitly (the bare-global inject is only for USER
5
+ // code; here `fetch` would otherwise resolve to the node/bun ambient one).
6
+ import { fetch, type FetchResponse } from "../runtime/fetch"
7
+
8
+ export class Texture2D {
9
+ /** Native texture handle. */
10
+ readonly id: number
11
+ readonly width: number
12
+ readonly height: number
13
+
14
+ constructor(id: number, width: number, height: number) {
15
+ this.id = id
16
+ this.width = width
17
+ this.height = height
18
+ }
19
+
20
+ static load(source: string | FetchResponse): Promise<Texture2D> {
21
+ if (typeof source === "string") {
22
+ return fetch(source, { useOnce: true }).then((resp) => {
23
+ if (resp.status >= 400) {
24
+ return Promise.reject(new Error(`Failed to fetch texture from ${source}. HTTP ${resp.status}`))
25
+ }
26
+ return Texture2D.load(resp)
27
+ })
28
+ }
29
+ return new Promise<Texture2D>((resolve, reject) => {
30
+ _creator2d.createTexture((source as any).id ?? (source as any)._id, (id, w, h) => resolve(new Texture2D(id, w, h)), reject)
31
+ })
32
+ }
33
+
34
+ destroy(): void {
35
+ _creator2d.destroyTexture(this.id)
36
+ }
37
+ }
@@ -0,0 +1,41 @@
1
+ // A tilemap node. The grid is uploaded once and rendered natively with per-frame view culling; the
2
+ // node's position is the bottom-left of the map. Edit cells with setTile.
3
+
4
+ import { Node2D } from "./Node2D"
5
+ import type { Texture2D } from "./Texture2D"
6
+
7
+ export type TilemapOptions = {
8
+ texture: Texture2D
9
+ /** Map grid size (in tiles). */
10
+ cols: number
11
+ rows: number
12
+ /** On-screen cell size in world units — a single number (square) or [w, h]. */
13
+ tile: number | [number, number]
14
+ /** Atlas grid: how the texture is sliced into tiles, [atlasCols, atlasRows]. */
15
+ atlas: [number, number]
16
+ /** Row-major tile indices into the atlas (-1 = empty), length cols*rows. Row 0 is the top row. */
17
+ data: number[] | Int32Array
18
+ }
19
+
20
+ export class Tilemap extends Node2D {
21
+ readonly cols: number
22
+ readonly rows: number
23
+
24
+ constructor(options: TilemapOptions) {
25
+ const data = options.data instanceof Int32Array ? options.data : Int32Array.from(options.data)
26
+ const tileW = typeof options.tile === "number" ? options.tile : options.tile[0]
27
+ const tileH = typeof options.tile === "number" ? options.tile : options.tile[1]
28
+ const [ atlasCols, atlasRows ] = options.atlas
29
+ const id = _creator2d.createTilemap(
30
+ options.texture.id, options.cols, options.rows, tileW, tileH, atlasCols, atlasRows, data,
31
+ )
32
+ super(id) // wrap the native tilemap entity
33
+ this.cols = options.cols
34
+ this.rows = options.rows
35
+ }
36
+
37
+ setTile(x: number, y: number, index: number): this {
38
+ _creator2d.setTile(this.id, x, y, index)
39
+ return this
40
+ }
41
+ }
@@ -0,0 +1,22 @@
1
+ // Internal per-frame plumbing for the 2D engine. NOT user-facing — user game logic uses the host
2
+ // `setLoop` global. `onUpdate` taps the engine's render-synced pre-frame hook (_creator2d.onUpdate),
3
+ // used by camera.follow so the camera moves in lockstep with the native render (no 1-frame lag).
4
+ //
5
+ // Both registrations are lazy (first use) so a 2D project that never follows a camera or animates a
6
+ // sprite pulls none of this in.
7
+
8
+ import { node2dRegistry } from "./Node2D"
9
+
10
+ export const onUpdate = (callback: (dt: number) => void): void => _creator2d.onUpdate(callback)
11
+ export const offUpdate = (callback: (dt: number) => void): void => _creator2d.offUpdate(callback)
12
+
13
+ // Route native animation events (queued in C++, drained once per frame) to the owning node, which
14
+ // translates the native clip id to a clip name and emits 'loopReached' / 'completed'.
15
+ let animEventsRegistered = false
16
+ export const ensureAnimEvents = (): void => {
17
+ if (animEventsRegistered) return
18
+ animEventsRegistered = true
19
+ _creator2d.setOnAnimEvent(({ entityId, clipId, type }) => {
20
+ node2dRegistry.get(entityId)?._dispatchAnimEvent(clipId, type)
21
+ })
22
+ }
@@ -0,0 +1,101 @@
1
+ // Physics handle for a Node, backed by a JoltPhysics body in creator-gl. Created by node.physics();
2
+ // reached as node.body. Ported from worker/src/components/RigidBody.ts.
3
+ //
4
+ // For a `dynamic` body, physics OWNS the transform — do NOT set node.position per frame; drive it
5
+ // via velocity / applyImpulse. Reading node.position is free (native already wrote it). Kinematic
6
+ // bodies are driven with moveTo.
7
+
8
+ import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
9
+ import type { Node } from "./Node"
10
+
11
+ export type MotionType = "static" | "dynamic" | "kinematic"
12
+ const MOTION: Record<MotionType, number> = { static: 0, kinematic: 1, dynamic: 2 }
13
+
14
+ export type BodyShape =
15
+ | { type: "auto" }
16
+ | { type: "box", halfExtents: Vec3Like }
17
+ | { type: "sphere", radius: number }
18
+ | { type: "cylinder", halfHeight: number, radius: number }
19
+
20
+ export type PhysicsOptions = {
21
+ /** Default "dynamic". */
22
+ motion?: MotionType
23
+ /** Mass in kg for dynamic bodies (default 1). Ignored for static/kinematic. */
24
+ mass?: number
25
+ /** Default { type: "auto" } — a box from the mesh AABB × world scale. */
26
+ shape?: BodyShape
27
+ }
28
+
29
+ export class Body {
30
+ private _bodyId = 0
31
+ private readonly _motion: MotionType
32
+
33
+ constructor(node: Node, options: PhysicsOptions = {}) {
34
+ this._motion = options.motion ?? "dynamic"
35
+ const mass = options.mass ?? 1
36
+
37
+ // No physics in this build (CREATOR_GL_PHYSICS=OFF) → bodyId stays 0; all methods no-op.
38
+ if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
39
+
40
+ const motion = MOTION[this._motion]
41
+ const shape = resolveShape(node, options.shape ?? { type: "auto" })
42
+ if (shape.type === "sphere") {
43
+ this._bodyId = _creator.physicsCreateSphere(node.id, shape.radius, motion, mass)
44
+ } else if (shape.type === "cylinder") {
45
+ this._bodyId = _creator.physicsCreateCylinder(node.id, shape.halfHeight, shape.radius, motion, mass)
46
+ } else {
47
+ this._bodyId = _creator.physicsCreateBox(node.id, cx(shape.halfExtents), cy(shape.halfExtents), cz(shape.halfExtents), motion, mass)
48
+ }
49
+ }
50
+
51
+ /** Native Jolt body id. 0 until assigned, or if the build has no physics support. */
52
+ get id(): number { return this._bodyId }
53
+ get motion(): MotionType { return this._motion }
54
+
55
+ /** Linear velocity in world units/second (fresh Vec3 on read). */
56
+ get velocity(): Vec3 {
57
+ const out = new Float32Array(3)
58
+ if (this._bodyId) _creator.physicsGetLinearVelocity(this._bodyId, out)
59
+ return new Vec3(out[0], out[1], out[2])
60
+ }
61
+ set velocity(v: Vec3Like) {
62
+ if (this._bodyId) _creator.physicsSetLinearVelocity(this._bodyId, cx(v), cy(v), cz(v))
63
+ }
64
+
65
+ /** Apply an instantaneous impulse (kg·m/s) and wake the body. */
66
+ applyImpulse(v: Vec3Like): this {
67
+ if (this._bodyId) _creator.physicsApplyImpulse(this._bodyId, cx(v), cy(v), cz(v))
68
+ return this
69
+ }
70
+
71
+ /** Teleport / drive a kinematic (or dynamic) body to a world position. */
72
+ moveTo(p: Vec3Like): this {
73
+ if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p))
74
+ return this
75
+ }
76
+
77
+ /** Remove the body from the simulation. */
78
+ remove(): void {
79
+ if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
80
+ }
81
+ }
82
+
83
+ // Resolve "auto" to a box from the mesh AABB × world scale (so the physical box matches the rendered
84
+ // size). Falls back to a unit box if there is no geometry.
85
+ const resolveShape = (node: Node, shape: BodyShape): Exclude<BodyShape, { type: "auto" }> => {
86
+ if (shape.type !== "auto") return shape
87
+ const scale = node.worldScale
88
+ let hx = 0.5, hy = 0.5, hz = 0.5
89
+ const geom = node.geometry
90
+ if (geom) {
91
+ const v = geom.vertices
92
+ let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity
93
+ for (let i = 0; i + 2 < v.length; i += 3) {
94
+ if (v[i] < minX) minX = v[i]; if (v[i] > maxX) maxX = v[i]
95
+ if (v[i + 1] < minY) minY = v[i + 1]; if (v[i + 1] > maxY) maxY = v[i + 1]
96
+ if (v[i + 2] < minZ) minZ = v[i + 2]; if (v[i + 2] > maxZ) maxZ = v[i + 2]
97
+ }
98
+ hx = (maxX - minX) * 0.5; hy = (maxY - minY) * 0.5; hz = (maxZ - minZ) * 0.5
99
+ }
100
+ return { type: "box", halfExtents: [ hx * scale.x, hy * scale.y, hz * scale.z ] }
101
+ }
@@ -0,0 +1,40 @@
1
+ // The scene camera — a Node (so position / lookAt / transform all work) plus projection reads and
2
+ // screen↔world ray helpers. Created and attached by Scene. Ported from worker/src/components/Camera.ts.
3
+
4
+ import { Vec3 } from "../math/vec"
5
+ import { Node } from "./Node"
6
+ import { Ray } from "./Ray"
7
+
8
+ export class Camera extends Node {
9
+ private _sceneId = -1
10
+ /** Live projection matrix (updated by the host when the viewport/fov changes). */
11
+ readonly projectionMatrix = new Float32Array(16)
12
+
13
+ /** @internal — Scene wires the camera to its native scene id + projection callback. */
14
+ _attach(sceneId: number): void {
15
+ this._sceneId = sceneId
16
+ _creator.attachCameraToAR(sceneId, this.id, (mat) => {
17
+ for (let i = 0; i < 16; i++) this.projectionMatrix[i] = mat[i]
18
+ })
19
+ }
20
+
21
+ get fov(): number { return _creator.getCameraFov(this._sceneId, 0) }
22
+ get horizontalFov(): number { return _creator.getCameraFov(this._sceneId, 1) }
23
+ get displaySize(): [number, number] {
24
+ const s = _creator.getDisplaySize()
25
+ return [ s[0], s[1] ]
26
+ }
27
+
28
+ /** World-space ray direction through a screen pixel. */
29
+ getViewDirection(screenX: number, screenY: number): Vec3 {
30
+ if (this._sceneId < 0) throw new Error("Camera is not attached to a scene")
31
+ const v = new Float32Array(3)
32
+ _creator.getCameraViewDirection(this._sceneId, screenX, screenY, v)
33
+ return new Vec3(v[0], v[1], v[2])
34
+ }
35
+
36
+ /** A pick ray from the camera through a screen pixel. */
37
+ getRay(screenX: number, screenY: number): Ray {
38
+ return new Ray(this.worldPosition, this.getViewDirection(screenX, screenY))
39
+ }
40
+ }
@@ -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
+ }