lecodes-sdk 2.4.10 → 2.5.1

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 (76) hide show
  1. package/dist/global.d.ts +35 -0
  2. package/dist/types/core/env.d.ts +25 -0
  3. package/dist/types/core/particlesConfig.d.ts +196 -0
  4. package/dist/types/g2/Font2D.d.ts +19 -0
  5. package/dist/types/g2/Material2D.d.ts +24 -0
  6. package/dist/types/g2/Node2D.d.ts +6 -5
  7. package/dist/types/g2/Particles2D.d.ts +169 -0
  8. package/dist/types/g2/Path2D.d.ts +95 -0
  9. package/dist/types/g2/Physics2D.d.ts +1 -1
  10. package/dist/types/g2/Scene2D.d.ts +15 -17
  11. package/dist/types/g2/Sprite.d.ts +12 -1
  12. package/dist/types/g2/SpriteSheet.d.ts +4 -1
  13. package/dist/types/g2/Text2D.d.ts +96 -0
  14. package/dist/types/g2/Tilemap.d.ts +8 -0
  15. package/dist/types/g2/defineScene2d.d.ts +69 -11
  16. package/dist/types/gl/Foliage.d.ts +0 -7
  17. package/dist/types/gl/Lightmap.d.ts +101 -2
  18. package/dist/types/gl/Material.d.ts +21 -15
  19. package/dist/types/gl/Model.d.ts +8 -3
  20. package/dist/types/gl/Particles.d.ts +3 -135
  21. package/dist/types/gl/Statics.d.ts +56 -0
  22. package/dist/types/gl/staticsFormat.d.ts +101 -0
  23. package/dist/types/inject.d.ts +11 -3
  24. package/dist/types/runtime/env.d.ts +10 -0
  25. package/dist/types/runtime/envValues.d.ts +8 -0
  26. package/dist/types/scene/defineScene.d.ts +17 -12
  27. package/dist/types/server/env.d.ts +14 -0
  28. package/dist/types/server/inject.d.ts +1 -0
  29. package/dist/types/ui/fonts.d.ts +4 -0
  30. package/dist/types/version.d.ts +1 -1
  31. package/dist/types.json +1 -1
  32. package/package.json +1 -1
  33. package/src/bridges/2d.d.ts +65 -5
  34. package/src/bridges/gl.d.ts +34 -15
  35. package/src/compile/bakeMacro.ts +3 -0
  36. package/src/compile/compileProject.ts +25 -4
  37. package/src/compile/envCompile.ts +74 -0
  38. package/src/compile/fontMacro.ts +11 -6
  39. package/src/compile/index.ts +6 -0
  40. package/src/compile/resourceSlots.ts +1 -0
  41. package/src/compile/serverSplit.ts +47 -4
  42. package/src/core/env.ts +47 -0
  43. package/src/core/particlesConfig.ts +422 -0
  44. package/src/g2/Font2D.ts +87 -0
  45. package/src/g2/Material2D.ts +64 -0
  46. package/src/g2/Node2D.ts +10 -6
  47. package/src/g2/Particles2D.ts +337 -0
  48. package/src/g2/Path2D.ts +215 -0
  49. package/src/g2/Physics2D.ts +1 -1
  50. package/src/g2/Scene2D.ts +16 -22
  51. package/src/g2/Sprite.ts +23 -4
  52. package/src/g2/SpriteSheet.ts +9 -5
  53. package/src/g2/Text2D.ts +178 -0
  54. package/src/g2/Tilemap.ts +10 -0
  55. package/src/g2/defineScene2d.ts +108 -21
  56. package/src/g2/groups2d.ts +2 -2
  57. package/src/gl/Foliage.ts +1 -16
  58. package/src/gl/Lightmap.ts +70 -26
  59. package/src/gl/Material.ts +51 -21
  60. package/src/gl/Model.ts +19 -5
  61. package/src/gl/Particles.ts +22 -361
  62. package/src/gl/Statics.ts +301 -0
  63. package/src/gl/nav/NavMesh.ts +4 -1
  64. package/src/gl/staticsFormat.ts +240 -0
  65. package/src/inject.ts +18 -3
  66. package/src/runtime/env.ts +13 -0
  67. package/src/runtime/envValues.ts +8 -0
  68. package/src/runtime/fetch.ts +18 -0
  69. package/src/scene/defineScene.ts +107 -27
  70. package/src/server/certs/russian-trusted-ca.pem +81 -0
  71. package/src/server/env.ts +25 -0
  72. package/src/server/host.ts +7 -0
  73. package/src/server/inject.ts +2 -1
  74. package/src/server/trustedCa.ts +23 -0
  75. package/src/ui/fonts.ts +46 -0
  76. package/src/version.ts +1 -1
@@ -0,0 +1,301 @@
1
+ // THE STATIC LEVEL CACHE — the SDK half of creator-gl/src/statics.cpp (the file format: ./staticsFormat.ts).
2
+ //
3
+ // A level's STATICS (the nodes that never move once loaded: what the bake sees) are built the slow way exactly
4
+ // once — a JS node per placement, a clone per node, a bake record bound per primitive — and then SNAPSHOTTED
5
+ // into one binary beside the level's `.bake` (`<level>.static`, written by `lecodes level build`). The next
6
+ // `load()` of the same level fetches that file, checks its digests against the ones it derives from the scene's
7
+ // defs and the bake, and rebuilds the statics natively: the JS loads ONE template per kind and the engine clones
8
+ // it at every placement, binds the records and creates the static bodies — no node, no aspect, no path string
9
+ // per placement in JS. The dynamic nodes of the level (the spawn, the lamps with behaviour, the terrain) build
10
+ // as they always did, beside the one JS node each covered root keeps.
11
+ //
12
+ // WHAT IS COVERED. `coveredRoots` picks the maximal subtrees whose every node is static data: a `model` or a
13
+ // plain node, a `prefab` whose file is all such nodes, with nothing but `Shape` / `Physics { motion: 'static' }`
14
+ // aspects, no `make`, no `mesh`, no `terrain`, no `light`, no model `overrides`, no foliage fade. `static: false`
15
+ // anywhere keeps the node — and the subtree above it — out. The search descends through `children` and through
16
+ // a prefab's own nodes (an imported scene is one prefab holding its terrain beside the groups of placements: the
17
+ // groups are the roots), so a level has as many roots as it has such subtrees; the cache keeps them all, in order.
18
+ //
19
+ // THE DIGESTS: `defs` (two 32-bit hashes over a canonical JSON of the covered defs — a placement moved, a kind
20
+ // swapped) and `bake` (the bake's identity — a rebake). Either mismatch declines the cache with a log line and the
21
+ // level loads the slow way until `lecodes level build` runs again. Assets' CONTENTS are not in them (a GLB
22
+ // re-exported under the same name): that is the compiler's digest, a later step. Kinds are keyed by their STABLE
23
+ // asset name (runtime/fetch.ts assetNameOf): an "id:" url is this run's only.
24
+
25
+ import { fetch, assetNameOf, assetUrlOf, type FetchResponse } from "../runtime/fetch"
26
+ import { Model, foliageKindOf, type FoliageKind } from "./Model"
27
+ import { Node } from "./Node"
28
+ import { Scene } from "./Scene"
29
+ import { Physics } from "./physics/Physics"
30
+ import { Shape } from "./physics/Shape"
31
+ import { CharacterController } from "./physics/CharacterController"
32
+ import type { SceneDef, SceneNodeDef } from "../scene/defineScene"
33
+ import type { AspectEntry } from "../scene/grammar"
34
+ import type { BakeData } from "./Lightmap"
35
+ import { ShapeKind, NONE, bakeDigest, decode, encode, hash64, readHeader, recordsByPath, type Digest, type StaticKind, type StaticPlacement, type StaticRecord } from "./staticsFormat"
36
+
37
+ export { bakeDigest, type Digest } from "./staticsFormat"
38
+
39
+ /** `lecodes level build` runs the app with this on `globalThis.__lecodesStatic`: the project root the snapshot is written under. */
40
+ export type StaticsConfig = { root: string }
41
+ export const staticsConfig = (): StaticsConfig | null => {
42
+ const g = (globalThis as unknown as { __lecodesStatic?: StaticsConfig }).__lecodesStatic
43
+ return g && typeof g.root === "string" ? g : null
44
+ }
45
+ /** True under any of the CLI's tool runs that must see the level the slow, complete way (the bakes, the snapshot). */
46
+ export const toolRun = (): boolean => {
47
+ const g = globalThis as unknown as Record<string, unknown>
48
+ return !!g.__lecodesLightmap || !!g.__lecodesNavmesh || !!g.__lecodesStatic
49
+ }
50
+
51
+ export type CoveredRoot = { path: string, def: SceneNodeDef }
52
+
53
+ // ---- the rule ---------------------------------------------------------------------------------------------
54
+
55
+ const movesByPhysics = (def: SceneNodeDef): boolean =>
56
+ (def.aspects ?? []).some((e) =>
57
+ ((e.ctor as unknown) === Physics && ((e.props as { motion?: string } | undefined)?.motion ?? "dynamic") !== "static")
58
+ || (e.ctor as unknown) === CharacterController)
59
+
60
+ const dataAspect = (e: AspectEntry<any>): boolean => {
61
+ const c = e.ctor as unknown
62
+ if (c === Shape) return true
63
+ if (c === Physics) return ((e.props as { motion?: string } | undefined)?.motion ?? "dynamic") === "static"
64
+ return false
65
+ }
66
+
67
+ const cacheable = (def: SceneNodeDef, stack: Set<SceneDef>): boolean => {
68
+ if (def.static !== undefined ? !def.static : movesByPhysics(def)) return false
69
+ if (def.mesh !== undefined || def.terrain !== undefined || def.light !== undefined || def.camera !== undefined || def.make !== undefined) return false
70
+ if (def.overrides !== undefined) return false
71
+ if (typeof def.foliage === "object") return false
72
+ if ((def.aspects ?? []).some((e) => !dataAspect(e))) return false
73
+ if (def.prefab !== undefined) {
74
+ const pdef = (def.prefab as { def?: SceneDef }).def
75
+ if (!pdef || typeof pdef !== "object" || stack.has(pdef)) return false
76
+ const inner = new Set(stack).add(pdef)
77
+ for (const nd of Object.values(pdef.nodes ?? {})) if (!cacheable(nd, inner)) return false
78
+ }
79
+ for (const nd of Object.values(def.children ?? {})) if (!cacheable(nd, stack)) return false
80
+ return true
81
+ }
82
+
83
+ /** The maximal cacheable subtrees among `defs`, depth first: through `children`, and through a prefab's nodes
84
+ * (paths run through the prefab's path, as the bake's keys do). */
85
+ export const coveredRoots = (defs: Record<string, SceneNodeDef> | undefined, scene: SceneDef): CoveredRoot[] => {
86
+ const out: CoveredRoot[] = []
87
+ const walk = (ds: Record<string, SceneNodeDef> | undefined, prefix: string, stack: Set<SceneDef>): void => {
88
+ for (const [ name, nd ] of Object.entries(ds ?? {})) {
89
+ const path = prefix === "" ? name : `${prefix}/${name}`
90
+ if (cacheable(nd, stack)) { out.push({ path, def: nd }); continue }
91
+ const pdef = nd.prefab !== undefined ? (nd.prefab as { def?: SceneDef }).def : undefined
92
+ if (pdef && !stack.has(pdef)) walk(pdef.nodes, path, new Set(stack).add(pdef))
93
+ walk(nd.children, path, stack)
94
+ }
95
+ }
96
+ walk(defs, "", new Set<SceneDef>([ scene ]))
97
+ return out
98
+ }
99
+
100
+ type ModelPartRow = { path: string, node: Node }
101
+
102
+ /** The live node at a full path: a top-level entry of the record, or a prefab internal reached through its
103
+ * wrapper's part rows (the record only keys the top level; internals live in the wrapper's `_prefabParts`). */
104
+ export const findNode = (nodes: Record<string, Node>, path: string): Node | null => {
105
+ const direct = nodes[path]
106
+ if (direct) return direct
107
+ for (let cut = path.lastIndexOf("/"); cut > 0; cut = path.lastIndexOf("/", cut - 1)) {
108
+ const wrapper = nodes[path.slice(0, cut)]
109
+ if (!wrapper) continue
110
+ const rows = (wrapper as { _prefabParts?: ModelPartRow[] })._prefabParts
111
+ if (!rows) return null
112
+ const rel = path.slice(cut + 1)
113
+ const row = rows.find((r) => r.path === rel)
114
+ return row ? row.node : null
115
+ }
116
+ return null
117
+ }
118
+
119
+ // ---- the defs digest --------------------------------------------------------------------------------------
120
+
121
+ const canonical = (def: SceneNodeDef, stack: Set<SceneDef>): unknown => {
122
+ const o: Record<string, unknown> = {}
123
+ if (def.model !== undefined) o.m = assetNameOf(String(def.model)) // the staged name: an "id:" url is this run's only
124
+ if (def.position !== undefined) o.p = def.position
125
+ if (def.eulerAngles !== undefined) o.e = def.eulerAngles
126
+ if (def.scale !== undefined) o.s = def.scale
127
+ if (def.visible !== undefined) o.v = def.visible
128
+ if (def.castShadows !== undefined) o.c = def.castShadows
129
+ if (def.receiveShadows !== undefined) o.r = def.receiveShadows
130
+ if (def.foliage !== undefined) o.f = def.foliage
131
+ if (def.lightmap !== undefined) o.l = def.lightmap
132
+ if (def.static !== undefined) o.st = def.static
133
+ if (def.aspects?.length) o.a = def.aspects.map((e) => [ (e.ctor as { name?: string }).name ?? "?", e.props ?? null ])
134
+ if (def.prefab !== undefined) {
135
+ const pdef = (def.prefab as { def?: SceneDef }).def
136
+ if (pdef && !stack.has(pdef)) {
137
+ const inner = new Set(stack).add(pdef)
138
+ o.pf = Object.fromEntries(Object.entries(pdef.nodes ?? {}).map(([ k, v ]) => [ k, canonical(v, inner) ]))
139
+ }
140
+ }
141
+ if (def.children) o.ch = Object.fromEntries(Object.entries(def.children).map(([ k, v ]) => [ k, canonical(v, stack) ]))
142
+ return o
143
+ }
144
+
145
+ /** The digest of the covered defs — what a snapshot's placements were made from. */
146
+ export const defsDigest = (roots: CoveredRoot[], scene: SceneDef): Digest => {
147
+ const stack = new Set<SceneDef>([ scene ])
148
+ return hash64(JSON.stringify(roots.map((r) => [ r.path, canonical(r.def, stack) ])))
149
+ }
150
+
151
+ // ---- the snapshot -----------------------------------------------------------------------------------------
152
+
153
+ /** Every def node under a covered root with its live node: `children` and prefab internals alike (the wrapper's
154
+ * `_prefabParts` rows carry the internals' nodes; their defs come from the prefab's file, by the same path). */
155
+ const collect = (root: CoveredRoot, rootNode: Node, scene: SceneDef, out: { path: string, def: SceneNodeDef, node: Node }[]): void => {
156
+ const defsOf = (defs: Record<string, SceneNodeDef> | undefined, prefix: string, stack: Set<SceneDef>, into: Map<string, SceneNodeDef>): void => {
157
+ for (const [ name, nd ] of Object.entries(defs ?? {})) {
158
+ const path = prefix === "" ? name : `${prefix}/${name}`
159
+ into.set(path, nd)
160
+ defsOf(nd.children, path, stack, into)
161
+ const pdef = nd.prefab !== undefined ? (nd.prefab as { def?: SceneDef }).def : undefined
162
+ if (pdef && !stack.has(pdef)) defsOf(pdef.nodes, path, new Set(stack).add(pdef), into)
163
+ }
164
+ }
165
+ const walk = (path: string, def: SceneNodeDef, node: Node, stack: Set<SceneDef>): void => {
166
+ out.push({ path, def, node })
167
+ if (def.prefab !== undefined) {
168
+ const rows = (node as { _prefabParts?: ModelPartRow[] })._prefabParts ?? []
169
+ const pdef = (def.prefab as { def?: SceneDef }).def
170
+ const defs = new Map<string, SceneNodeDef>()
171
+ if (pdef) defsOf(pdef.nodes, "", new Set(stack).add(pdef), defs)
172
+ for (const r of rows) {
173
+ const d = defs.get(r.path)
174
+ if (d) out.push({ path: `${path}/${r.path}`, def: d, node: r.node }) // a GLB's internal parts have no def: skipped
175
+ }
176
+ return
177
+ }
178
+ // the live children by name ONCE per parent: `node.children` and `.name` are bridge calls each, and a root of
179
+ // two thousand placements looked each child up by scanning them all (Chernobyl's snapshot: 9 minutes, 2026-11-03)
180
+ const byName = new Map<string, Node>()
181
+ for (const c of node.children) byName.set(c.name, c)
182
+ for (const [ name, nd ] of Object.entries(def.children ?? {})) {
183
+ const child = byName.get(name)
184
+ if (child) walk(`${path}/${name}`, nd, child, stack)
185
+ }
186
+ }
187
+ walk(root.path, root.def, rootNode, new Set([ scene ]))
188
+ }
189
+
190
+ const shapeOf = (def: SceneNodeDef): { kind: ShapeKind, params: number[] } => {
191
+ let shape: AspectEntry<any> | undefined, body: AspectEntry<any> | undefined
192
+ for (const e of def.aspects ?? []) {
193
+ if ((e.ctor as unknown) === Shape) shape = e
194
+ else if ((e.ctor as unknown) === Physics) body = e
195
+ }
196
+ if (!shape || !body) return { kind: ShapeKind.None, params: [] }
197
+ const p = (shape.props ?? {}) as { mesh?: boolean | "convex", box?: number[], sphere?: number, cylinder?: { halfHeight: number, radius: number }, capsule?: { halfHeight: number, radius: number }, heightfield?: boolean, origin?: number[] }
198
+ if (p.heightfield) return { kind: ShapeKind.None, params: [] }
199
+ const origin = p.origin ?? [ 0, 0, 0 ]
200
+ if (p.mesh === "convex") return { kind: ShapeKind.Convex, params: [ ...origin ] }
201
+ if (p.mesh) return { kind: ShapeKind.Mesh, params: [ ...origin ] }
202
+ if (p.sphere !== undefined) return { kind: ShapeKind.Sphere, params: [ p.sphere, ...origin ] }
203
+ if (p.cylinder) return { kind: ShapeKind.Cylinder, params: [ p.cylinder.halfHeight, p.cylinder.radius, ...origin ] }
204
+ if (p.capsule) return { kind: ShapeKind.Capsule, params: [ p.capsule.halfHeight, p.capsule.radius, ...origin ] }
205
+ if (p.box) return { kind: ShapeKind.Box, params: [ p.box[0], p.box[1], p.box[2], ...origin ] }
206
+ return { kind: ShapeKind.AutoBox, params: [ ...origin ] }
207
+ }
208
+
209
+ /** The snapshot of the covered roots' subtrees as they stand: the bytes `lecodes level build` writes. `bake` = the
210
+ * data the level is bound to (its records go in, joined by path), null = an unbaked level (placements and bodies
211
+ * only; the CLI stitches the records in after the bake). */
212
+ export const snapshot = (roots: { root: CoveredRoot, node: Node }[], scene: SceneDef, bake: BakeData | null, defs: Digest): { bytes: Uint8Array, kinds: number, placements: number, records: number } => {
213
+ const byPath = bake ? recordsByPath(bake.instances) : new Map<string, StaticRecord[]>()
214
+ const kinds: StaticKind[] = []
215
+ const kindIndex = new Map<string, number>()
216
+ const placements: StaticPlacement[] = []
217
+ roots.forEach(({ root, node }, rootIndex) => {
218
+ const entries: { path: string, def: SceneNodeDef, node: Node }[] = []
219
+ collect(root, node, scene, entries)
220
+ const rootInv = node.worldMatrix.invert()
221
+ for (const e of entries) {
222
+ const def = e.def // the root itself is a placement too when it carries a model (at identity under its JS node)
223
+ let kind = NONE
224
+ if (def.model !== undefined) {
225
+ const name = assetNameOf(String(def.model))
226
+ const foliage = foliageKindOf(def.foliage) // a covered model is a lightmap static unless it is foliage (the foliage tier)
227
+ const key = `${foliage || "l"}|${name}`
228
+ let k = kindIndex.get(key)
229
+ if (k === undefined) { k = kinds.length; kinds.push({ name, lightmap: !foliage, foliage }); kindIndex.set(key, k) }
230
+ kind = k
231
+ }
232
+ const sh = shapeOf(def)
233
+ if (kind === NONE && sh.kind === ShapeKind.None) continue // a bare wrapper: nothing to rebuild
234
+ const { position, rotation, scale } = rootInv.mul(e.node.worldMatrix).decompose()
235
+ placements.push({
236
+ kind, path: e.path, root: rootIndex,
237
+ pos: [ position.x, position.y, position.z ], quat: [ rotation.x, rotation.y, rotation.z, rotation.w ], scale: [ scale.x, scale.y, scale.z ],
238
+ castShadows: def.castShadows ?? true, receiveShadows: def.receiveShadows ?? true,
239
+ shape: sh.kind, params: sh.params,
240
+ records: byPath.get(e.path) ?? [],
241
+ })
242
+ }
243
+ })
244
+ const bytes = encode({ defs, bake: bakeDigest(bake), kinds, placements })
245
+ return { bytes, kinds: kinds.length, placements: placements.length, records: placements.reduce((n, p) => n + p.records.length, 0) }
246
+ }
247
+
248
+ // ---- the cached load --------------------------------------------------------------------------------------
249
+
250
+ export type OpenedCache = { resp: FetchResponse, kinds: { url: string, lightmap: boolean, foliage: FoliageKind }[], placements: number, roots: number }
251
+
252
+ /** Fetch the level's cache and check it against the digests; null = absent, not a cache, or stale (the why logged). */
253
+ export const open = async (url: string, defs: Digest, bake: Digest): Promise<OpenedCache | null> => {
254
+ if (!_creator.staticsBegin) return null
255
+ let resp: FetchResponse
256
+ try {
257
+ resp = await fetch(url, { useOnce: true })
258
+ } catch (e) {
259
+ console.warn(`[static] could not read ${url}: ${String(e)} — building the level the slow way`)
260
+ return null
261
+ }
262
+ if (resp.status >= 400) { console.log(`[static] no cache at ${url} (HTTP ${resp.status}) — building the level the slow way; \`lecodes level build\` writes one`); return null }
263
+ const bytes = resp.bytes()
264
+ const h = readHeader(bytes)
265
+ if (!h) { console.warn(`[static] ${url} is not a level cache (or an older one) — building the level the slow way; rerun \`lecodes level build\``); return null }
266
+ if (h.defs[0] !== defs[0] || h.defs[1] !== defs[1]) {
267
+ console.log(`[static] ${url} is stale (the scene's statics changed since \`lecodes level build\`) — building the level the slow way`)
268
+ return null
269
+ }
270
+ if (h.bake[0] !== bake[0] || h.bake[1] !== bake[1]) {
271
+ console.log(`[static] ${url} is stale (the bake changed since \`lecodes level build\`) — building the level the slow way`)
272
+ return null
273
+ }
274
+ const file = decode(bytes)
275
+ if (!file) { console.warn(`[static] ${url} is cut short — building the level the slow way; rerun \`lecodes level build\``); return null }
276
+ let roots = 0
277
+ for (const p of file.placements) roots = Math.max(roots, p.root + 1)
278
+ return { resp, kinds: file.kinds.map((k) => ({ url: assetUrlOf(k.name), lightmap: k.lightmap, foliage: k.foliage })), placements: file.placements.length, roots }
279
+ }
280
+
281
+ /** The kinds' templates, kept for the level's life (a template is the asset every clone shares). */
282
+ const templates: Model[] = []
283
+
284
+ /** Build the opened cache under `roots` (the covered roots' JS nodes, in the snapshot's order) in `scene`: the
285
+ * templates load through `template` (one per kind, as the level's slow path would load them), the engine clones
286
+ * each at its placements as it resolves. `pages` = the bake's light/aux texture ids interleaved (empty = no bake
287
+ * bound). Resolves to the placements made. */
288
+ export const build = async (
289
+ opened: OpenedCache, scene: Scene, roots: Node[], pages: Uint32Array,
290
+ template: (url: string, lightmap: boolean, foliage: FoliageKind) => Promise<Model>,
291
+ ): Promise<number> => {
292
+ const sceneId = (scene as unknown as { _id: number })._id
293
+ const kinds = _creator.staticsBegin!(sceneId, Uint32Array.from(roots.map((r) => r.id)), (opened.resp as unknown as { _h: { id: number } })._h.id)
294
+ if (kinds !== opened.kinds.length) { console.warn(`[static] the engine declined the cache (${kinds} of ${opened.kinds.length} kinds)`); _creator.staticsEnd!(); return 0 }
295
+ await Promise.all(opened.kinds.map(async (k, i) => {
296
+ const t = await template(k.url, k.lightmap, k.foliage)
297
+ templates.push(t)
298
+ _creator.staticsBuildKind!(i, t.id, pages)
299
+ }))
300
+ return _creator.staticsEnd!() // every placement: the kinds' clones and the body-only ones staticsBegin built
301
+ }
@@ -19,6 +19,7 @@
19
19
  // fixed step for every agent in the scene (`NavCrowd`, a system the mesh attaches to its scene).
20
20
 
21
21
  import { fetch } from "../../runtime/fetch"
22
+ import { assetNameOf } from "../../runtime/fetch"
22
23
  import { System } from "../../core/Aspect"
23
24
  import { Vec3, cx, cy, cz, type Vec3Like } from "../../math/vec"
24
25
  import { Geometry } from "../Geometry"
@@ -390,7 +391,9 @@ export class NavMesh {
390
391
  if (n === 0) { console.error("[navmesh] error: no static collision geometry — agents walk on STATIC Physics bodies (Shape + Physics { motion: 'static' })"); return }
391
392
  const agent = { radius: opts.agent?.radius ?? 0.4, height: opts.agent?.height ?? 1.8, climb: opts.agent?.climb ?? 0.4, slope: opts.agent?.slope ?? 50 }
392
393
  console.log(`[navmesh] geometry ${n} triangles → ${path}`)
393
- console.log(`[navmesh] config ${JSON.stringify({ agent, areas: areaIds, costs: opts.areas ?? {}, data: opts.data })}`)
394
+ // `data` by its STABLE name (runtime/fetch.ts): an "id:" url is this run's; `lecodes level build` writes the mesh where the
395
+ // scene's own file is
396
+ console.log(`[navmesh] config ${JSON.stringify({ agent, areas: areaIds, costs: opts.areas ?? {}, data: opts.data ? assetNameOf(opts.data) : undefined })}`)
394
397
  console.log("[navmesh] done")
395
398
  }
396
399
  }
@@ -0,0 +1,240 @@
1
+ // THE STATIC LEVEL CACHE'S FILE FORMAT — pure data, no runtime: the SDK writes and reads it in the app (gl/Statics.ts),
2
+ // the CLI reads and rewrites it in Bun (`lecodes level build` stitches a fresh bake's records in without a host run).
3
+ // creator-gl/src/statics.cpp is the third reader; its header comment is the same spec.
4
+ //
5
+ // header magic 'LCST', version 2, defs digest lo/hi, bake digest lo/hi, kinds, placements, records, floats,
6
+ // string bytes (11 words)
7
+ // kinds [nameStr, flags, 0, 0] flags: bit0 lightmapped, bit1 foliage, bit2 grass (the
8
+ // foliage kind: ground cover; else a crown)
9
+ // placements [kind | NONE, pathStr, pos xyz, quat xyzw, scale xyz, flags, shapeKind, shapeOff, recFirst, recCount,
10
+ // root, 0, 0] flags: bit0 castShadows, bit1 receiveShadows
11
+ // records [ordinal, page | OBJECT, st0, st1, st2, st3] OBJECT: st0 = floats offset, st1 = float count
12
+ // floats shape parameters (by shapeKind, then origin xyz), object-light slabs
13
+ // strings NUL-terminated UTF-8, indexed by byte offset
14
+ //
15
+ // Every word is 32 bits, floats bit-cast, little-endian. The two digests are what makes the file safe to trust:
16
+ // `defs` covers the scene's covered defs (a placement moved, a kind swapped), `bake` the bake the records came from
17
+ // (a rebake); the app compares both with what it derives and declines a mismatch. The CLI restamps `bake` when it
18
+ // stitches records, and leaves `defs` alone: a snapshot's placements are only ever written by the app.
19
+
20
+ import type { FoliageKind } from "./Model"
21
+
22
+ export const MAGIC = 0x5453434C, VERSION = 2, NONE = 0xFFFFFFFF
23
+ export const HEADER = 11, KIND = 4, PLACEMENT = 20, RECORD = 6
24
+ export const ShapeKind = { None: 0, Mesh: 1, Convex: 2, Box: 3, Sphere: 4, Cylinder: 5, Capsule: 6, AutoBox: 7 } as const
25
+ export type ShapeKind = number
26
+
27
+ export type Digest = [number, number]
28
+ export type StaticKind = { name: string, lightmap: boolean, foliage: FoliageKind }
29
+ /** A record: a rect (`page` + `st`) or an object light (`page === NONE`, `slabs` = the floats lightmapObjectApply takes). */
30
+ export type StaticRecord = { ordinal: number, page: number, st?: number[], slabs?: number[] }
31
+ export type StaticPlacement = {
32
+ kind: number, path: string, root: number
33
+ pos: number[], quat: number[], scale: number[]
34
+ castShadows: boolean, receiveShadows: boolean
35
+ shape: ShapeKind, params: number[]
36
+ records: StaticRecord[]
37
+ }
38
+ export type StaticsFile = { defs: Digest, bake: Digest, kinds: StaticKind[], placements: StaticPlacement[] }
39
+
40
+ /** Two independent 32-bit hashes over `s` (FNV-1a and a multiplicative xor mix), chained from `seed`: 64 bits of
41
+ * "did this change", which is all a header needs. */
42
+ export const hash64 = (s: string, seed: Digest = [ 0x811c9dc5, 0x9747b28c ]): Digest => {
43
+ let a = seed[0] >>> 0, b = seed[1] >>> 0
44
+ for (let i = 0; i < s.length; i++) {
45
+ const c = s.charCodeAt(i)
46
+ a = Math.imul(a ^ c, 0x01000193) >>> 0
47
+ b = (Math.imul(b, 0x5bd1e995) ^ c ^ (b >>> 13)) >>> 0
48
+ }
49
+ return [ a, b ]
50
+ }
51
+ export const NO_BAKE: Digest = hash64("none")
52
+
53
+ /** The bake's identity as the cache keys it: what a rebake changes. `bake` is the `.bake` JSON. */
54
+ export const bakeDigest = (bake: { size: number, pages: number, texel: number, texels?: number, instances: unknown[], uv1?: string | null } | null): Digest =>
55
+ bake ? hash64(`${bake.size}|${bake.pages}|${bake.texel}|${bake.texels ?? 0}|${bake.instances.length}|${bake.uv1 ?? ""}`) : NO_BAKE
56
+
57
+ // ---- the bake's records for a placement ------------------------------------------------------------------
58
+
59
+ /** What a `.bake` instance carries that the cache keeps (the SDK's BakeInstance, structurally). */
60
+ export type BakeRecordSource = {
61
+ key: string, receiver: boolean, small?: boolean, page?: number, st?: number[]
62
+ light?: { axis: number, from: number, to: number, cubes: number[][][], probes: number[] }
63
+ }
64
+
65
+ const probeId = (k: number): number => k === -2 ? 65534 : k < 0 ? 65535 : k
66
+
67
+ /** The records of every bake instance keyed by node PATH (`<path>@<ordinal>` and whole-node `<path>` alike), in the
68
+ * shape the cache stores — the same join the runtime makes when it binds a bake the slow way. */
69
+ export const recordsByPath = (instances: BakeRecordSource[]): Map<string, StaticRecord[]> => {
70
+ const out = new Map<string, StaticRecord[]>()
71
+ for (const inst of instances) {
72
+ const m = /^(.*)@(\d+)$/.exec(inst.key)
73
+ const path = m ? m[1] : inst.key
74
+ const ordinal = m ? Number(m[2]) : -1
75
+ let rec: StaticRecord | null = null
76
+ if (inst.receiver && inst.st && inst.page !== undefined) rec = { ordinal, page: inst.page, st: inst.st.slice(0, 4) }
77
+ else if (inst.small && inst.light && Array.isArray(inst.light.cubes)) {
78
+ const l = inst.light
79
+ rec = { ordinal, page: NONE, slabs: [ l.axis, l.from, l.to, l.cubes.length, ...l.cubes.flat(2), probeId(l.probes[0]), probeId(l.probes[1]), l.probes[2] ] }
80
+ }
81
+ if (!rec) continue
82
+ let g = out.get(path)
83
+ if (!g) { g = []; out.set(path, g) }
84
+ g.push(rec)
85
+ }
86
+ return out
87
+ }
88
+
89
+ // ---- encode / decode ------------------------------------------------------------------------------------
90
+
91
+ const utf8Length = (s: string): number => {
92
+ let n = 0
93
+ for (let i = 0; i < s.length; i++) {
94
+ const c = s.charCodeAt(i)
95
+ n += c < 0x80 ? 1 : c < 0x800 ? 2 : (c >= 0xd800 && c < 0xdc00) ? (i++, 4) : 3
96
+ }
97
+ return n
98
+ }
99
+ const utf8Write = (b: Uint8Array, at: number, s: string): number => {
100
+ const start = at
101
+ for (let i = 0; i < s.length; i++) {
102
+ let c = s.charCodeAt(i)
103
+ if (c >= 0xd800 && c < 0xdc00 && i + 1 < s.length) c = 0x10000 + ((c - 0xd800) << 10) + (s.charCodeAt(++i) - 0xdc00)
104
+ if (c < 0x80) b[at++] = c
105
+ else if (c < 0x800) { b[at++] = 0xc0 | (c >> 6); b[at++] = 0x80 | (c & 63) }
106
+ else if (c < 0x10000) { b[at++] = 0xe0 | (c >> 12); b[at++] = 0x80 | ((c >> 6) & 63); b[at++] = 0x80 | (c & 63) }
107
+ else { b[at++] = 0xf0 | (c >> 18); b[at++] = 0x80 | ((c >> 12) & 63); b[at++] = 0x80 | ((c >> 6) & 63); b[at++] = 0x80 | (c & 63) }
108
+ }
109
+ return at - start
110
+ }
111
+ const utf8Read = (b: Uint8Array, from: number, to: number): string => {
112
+ let s = ""
113
+ for (let i = from; i < to;) {
114
+ const c = b[i++]
115
+ if (c < 0x80) s += String.fromCharCode(c)
116
+ else if (c < 0xe0) s += String.fromCharCode(((c & 31) << 6) | (b[i++] & 63))
117
+ else if (c < 0xf0) s += String.fromCharCode(((c & 15) << 12) | ((b[i++] & 63) << 6) | (b[i++] & 63))
118
+ else { const cp = ((c & 7) << 18) | ((b[i++] & 63) << 12) | ((b[i++] & 63) << 6) | (b[i++] & 63); s += String.fromCodePoint(cp) }
119
+ }
120
+ return s
121
+ }
122
+
123
+ /** The file's bytes for `f`. */
124
+ export const encode = (f: StaticsFile): Uint8Array => {
125
+ // the strings, interned BEFORE the buffer is sized (the header carries their byte count)
126
+ const offsets = new Map<string, number>()
127
+ const strings: string[] = []
128
+ let stringBytes = 0
129
+ const intern = (s: string): number => {
130
+ let off = offsets.get(s)
131
+ if (off === undefined) { off = stringBytes; offsets.set(s, off); strings.push(s); stringBytes += utf8Length(s) + 1 }
132
+ return off
133
+ }
134
+ for (const k of f.kinds) intern(k.name)
135
+ for (const p of f.placements) intern(p.path)
136
+ // the floats: per placement its shape parameters, then its object-light slabs
137
+ const floats: number[] = []
138
+ const shapeOff: number[] = []
139
+ const slabOff = new Map<StaticRecord, number>()
140
+ for (const p of f.placements) {
141
+ shapeOff.push(floats.length)
142
+ floats.push(...p.params)
143
+ for (const r of p.records) if (r.page === NONE && r.slabs) { slabOff.set(r, floats.length); floats.push(...r.slabs) }
144
+ }
145
+ const recordCount = f.placements.reduce((n, p) => n + p.records.length, 0)
146
+ const stringWords = (stringBytes + 3) >> 2
147
+ const words = HEADER + f.kinds.length * KIND + f.placements.length * PLACEMENT + recordCount * RECORD + floats.length + stringWords
148
+ const buf = new ArrayBuffer(words * 4)
149
+ const u = new Uint32Array(buf), fl = new Float32Array(buf), b = new Uint8Array(buf)
150
+ u[0] = MAGIC; u[1] = VERSION; u[2] = f.defs[0]; u[3] = f.defs[1]; u[4] = f.bake[0]; u[5] = f.bake[1]
151
+ u[6] = f.kinds.length; u[7] = f.placements.length; u[8] = recordCount; u[9] = floats.length; u[10] = stringBytes
152
+ let w = HEADER
153
+ for (const k of f.kinds) { u[w] = intern(k.name); u[w + 1] = (k.lightmap ? 1 : 0) | (k.foliage ? 2 : 0) | (k.foliage === "grass" ? 4 : 0); u[w + 2] = 0; u[w + 3] = 0; w += KIND }
154
+ const recBase = HEADER + f.kinds.length * KIND + f.placements.length * PLACEMENT
155
+ let rec = 0
156
+ f.placements.forEach((p, i) => {
157
+ u[w] = p.kind; u[w + 1] = intern(p.path)
158
+ fl[w + 2] = p.pos[0]; fl[w + 3] = p.pos[1]; fl[w + 4] = p.pos[2]
159
+ fl[w + 5] = p.quat[0]; fl[w + 6] = p.quat[1]; fl[w + 7] = p.quat[2]; fl[w + 8] = p.quat[3]
160
+ fl[w + 9] = p.scale[0]; fl[w + 10] = p.scale[1]; fl[w + 11] = p.scale[2]
161
+ u[w + 12] = (p.castShadows ? 1 : 0) | (p.receiveShadows ? 2 : 0); u[w + 13] = p.shape; u[w + 14] = shapeOff[i]
162
+ u[w + 15] = rec; u[w + 16] = p.records.length; u[w + 17] = p.root; u[w + 18] = 0; u[w + 19] = 0
163
+ for (const r of p.records) {
164
+ const at = recBase + rec * RECORD
165
+ u[at] = r.ordinal >>> 0; u[at + 1] = r.page >>> 0
166
+ if (r.page === NONE) { u[at + 2] = slabOff.get(r) ?? 0; u[at + 3] = r.slabs?.length ?? 0; u[at + 4] = 0; u[at + 5] = 0 }
167
+ else { const st = r.st ?? [ 0, 0, 0, 0 ]; fl[at + 2] = st[0]; fl[at + 3] = st[1]; fl[at + 4] = st[2]; fl[at + 5] = st[3] }
168
+ rec++
169
+ }
170
+ w += PLACEMENT
171
+ })
172
+ const floatBase = recBase + recordCount * RECORD
173
+ for (let i = 0; i < floats.length; i++) fl[floatBase + i] = floats[i]
174
+ let at = (floatBase + floats.length) * 4
175
+ for (const s of strings) { at += utf8Write(b, at, s); b[at++] = 0 }
176
+ return b
177
+ }
178
+
179
+ /** The header alone: null when `bytes` is not a cache of this version. */
180
+ export const readHeader = (bytes: Uint8Array): { defs: Digest, bake: Digest, kinds: number, placements: number, records: number, floats: number, stringBytes: number } | null => {
181
+ if (bytes.length < HEADER * 4 || (bytes.byteOffset & 3) !== 0) return null
182
+ const u = new Uint32Array(bytes.buffer, bytes.byteOffset, HEADER)
183
+ if (u[0] !== MAGIC || u[1] !== VERSION) return null
184
+ return { defs: [ u[2], u[3] ], bake: [ u[4], u[5] ], kinds: u[6], placements: u[7], records: u[8], floats: u[9], stringBytes: u[10] }
185
+ }
186
+
187
+ const shapeParamCount = (kind: ShapeKind): number =>
188
+ kind === ShapeKind.Box ? 6 : kind === ShapeKind.Sphere ? 4 : kind === ShapeKind.Cylinder || kind === ShapeKind.Capsule ? 5 : kind === ShapeKind.None ? 0 : 3
189
+
190
+ /** The whole file as data: null when it is not one. */
191
+ export const decode = (bytes: Uint8Array): StaticsFile | null => {
192
+ const h = readHeader(bytes)
193
+ if (!h) return null
194
+ const words = HEADER + h.kinds * KIND + h.placements * PLACEMENT + h.records * RECORD + h.floats + ((h.stringBytes + 3) >> 2)
195
+ if (bytes.length < words * 4) return null
196
+ const u = new Uint32Array(bytes.buffer, bytes.byteOffset, words), fl = new Float32Array(bytes.buffer, bytes.byteOffset, words)
197
+ const stringBase = (HEADER + h.kinds * KIND + h.placements * PLACEMENT + h.records * RECORD + h.floats) * 4
198
+ const str = (off: number): string => {
199
+ let end = stringBase + off
200
+ while (end < bytes.length && bytes[end] !== 0) end++
201
+ return utf8Read(bytes, stringBase + off, end)
202
+ }
203
+ const floatBase = HEADER + h.kinds * KIND + h.placements * PLACEMENT + h.records * RECORD
204
+ const kinds: StaticKind[] = []
205
+ for (let k = 0; k < h.kinds; k++) {
206
+ const at = HEADER + k * KIND
207
+ kinds.push({ name: str(u[at]), lightmap: (u[at + 1] & 1) !== 0, foliage: (u[at + 1] & 2) === 0 ? false : (u[at + 1] & 4) !== 0 ? "grass" : "tree" })
208
+ }
209
+ const recBase = HEADER + h.kinds * KIND + h.placements * PLACEMENT
210
+ const placements: StaticPlacement[] = []
211
+ for (let i = 0; i < h.placements; i++) {
212
+ const w = HEADER + h.kinds * KIND + i * PLACEMENT
213
+ const shape = u[w + 13]
214
+ const records: StaticRecord[] = []
215
+ for (let r = u[w + 15], end = u[w + 15] + u[w + 16]; r < end; r++) {
216
+ const at = recBase + r * RECORD
217
+ const ordinal = u[at] | 0, page = u[at + 1]
218
+ if (page === NONE) records.push({ ordinal, page, slabs: Array.from(fl.subarray(floatBase + u[at + 2], floatBase + u[at + 2] + u[at + 3])) })
219
+ else records.push({ ordinal, page, st: [ fl[at + 2], fl[at + 3], fl[at + 4], fl[at + 5] ] })
220
+ }
221
+ placements.push({
222
+ kind: u[w], path: str(u[w + 1]), root: u[w + 17],
223
+ pos: [ fl[w + 2], fl[w + 3], fl[w + 4] ], quat: [ fl[w + 5], fl[w + 6], fl[w + 7], fl[w + 8] ], scale: [ fl[w + 9], fl[w + 10], fl[w + 11] ],
224
+ castShadows: (u[w + 12] & 1) !== 0, receiveShadows: (u[w + 12] & 2) !== 0,
225
+ shape, params: Array.from(fl.subarray(floatBase + u[w + 14], floatBase + u[w + 14] + shapeParamCount(shape))),
226
+ records,
227
+ })
228
+ }
229
+ return { defs: h.defs, bake: h.bake, kinds, placements }
230
+ }
231
+
232
+ /** `f` with its records replaced by `bake`'s (joined by placement path) and its bake digest restamped — what
233
+ * `lecodes level build` does after a bake, with no host run. `null` bake = no records, NO_BAKE. */
234
+ export const stitch = (f: StaticsFile, bake: { size: number, pages: number, texel: number, texels?: number, instances: BakeRecordSource[], uv1?: string | null } | null): StaticsFile => {
235
+ const byPath = bake ? recordsByPath(bake.instances) : new Map<string, StaticRecord[]>()
236
+ return {
237
+ defs: f.defs, bake: bakeDigest(bake), kinds: f.kinds,
238
+ placements: f.placements.map((p) => ({ ...p, records: byPath.get(p.path) ?? [] })),
239
+ }
240
+ }
package/src/inject.ts CHANGED
@@ -31,10 +31,11 @@ export { Canvas, Bitmap } from "./canvas/Canvas"
31
31
  // ---- platform runtime (injected globals; the host provides the lower-level _creator* bridges) ----
32
32
  export { toast, openURL, SvgSource } from "./runtime/misc"
33
33
  export { app, type AppState, type OrientationLock } from "./runtime/app"
34
+ export { env } from "./runtime/env"
34
35
  export { clipboard } from "./runtime/clipboard"
35
36
  export { permissions, type PermissionKind, type PermissionState } from "./runtime/permissions"
36
37
  export { date, type DateValue, type DateInput, type Unit, type Locale } from "./runtime/datetime"
37
- export { fetch, fetchLocal, FormData, File, type FetchResponse } from "./runtime/fetch"
38
+ export { fetch, fetchLocal, FormData, File, __localAsset, type FetchResponse } from "./runtime/fetch"
38
39
  export { localStorage } from "./runtime/storage"
39
40
  export { device, type HapticStyle, type MotionOptions } from "./runtime/device"
40
41
  export { Input, InputChannel, type InputKeyEvent, type InputGamepadEvent, type InputEventName, type GamepadState, type GamepadAxisName } from "./runtime/input"
@@ -44,6 +45,8 @@ export { Net, NetPlayer, Replicated, NetEntity, type NetMessage, type NetRole, t
44
45
  // App-backend transport: the compiler-generated stubs of `*.server.ts` modules call these; user code
45
46
  // only ever imports its server functions (docs/backend-plan.md §4).
46
47
  export { __rpc, __channel, __pushChannel, __serverOnly, RpcError } from "./runtime/rpc"
48
+ // The font() compile macro's generated `.__fonts.ts` modules call this per face (UI tree + Font2D).
49
+ export { __registerFont } from "./ui/fonts"
47
50
  // What a screen asks of the server, as reactive state: a request a screen follows, an action with its own busy / error.
48
51
  export { resource, action, dataOf, failureOf, type Loaded, type Failure, type Resource, type Action } from "./runtime/resource"
49
52
  export type { ChannelSubscription } from "./server/channel"
@@ -73,7 +76,17 @@ export { CharacterController2D } from "./g2/CharacterController2D"
73
76
  export { OneWay2D } from "./g2/OneWay2D"
74
77
  export { PhysicsGroup2D } from "./g2/groups2d"
75
78
  export { Tilemap } from "./g2/Tilemap"
79
+ export { Path2D, type Path2DOptions, type Stroke2D, type StrokeJoin2D, type StrokeCap2D } from "./g2/Path2D"
80
+ export { Material2D, type Material2DOptions, type Blend2D, type Filter2D, type Wrap2D } from "./g2/Material2D"
76
81
  export { Texture2D } from "./g2/Texture2D"
82
+ // SDF text in the sprite batch (see src/g2/Text2D.ts)
83
+ export { Font2D, type Font2DOptions } from "./g2/Font2D"
84
+ export { Text2D, type Text2DOptions, type TextAlign2D, type TextOutline2D } from "./g2/Text2D"
85
+ // particles in the sprite batch — the 3D node's simulation and vocabulary (see src/g2/Particles2D.ts)
86
+ export {
87
+ Particles2D, type Particles2DOptions, type ParticleMode2D, type ParticleShape2D, type VelocityValue2D,
88
+ type VelocityOverLife2D,
89
+ } from "./g2/Particles2D"
77
90
  // sprite sheets as data (.sprite.ts files — see src/g2/SpriteSheet.ts)
78
91
  export { SpriteSheet, defineSpriteSheet } from "./g2/SpriteSheet"
79
92
  export type { SpriteSheetDef, SheetSprite, SpriteMakeOptions } from "./g2/SpriteSheet"
@@ -87,7 +100,7 @@ export type {
87
100
  } from "./g2/autotile"
88
101
  // 2D scenes as data (.scene2d.ts files — see src/g2/defineScene2d.ts) + the cells('…') payload
89
102
  export { defineScene2d, Scene2dHandle } from "./g2/defineScene2d"
90
- export type { Scene2dDef, Scene2dNodeDef, Scene2dEnv, SpriteSourceDef, TilemapSourceDef, Camera2dNodeDef, LoadedScene2d } from "./g2/defineScene2d"
103
+ export type { Scene2dDef, Scene2dNodeDef, Scene2dEnv, SpriteSourceDef, TilemapSourceDef, PathSourceDef, TextSourceDef, Particles2DSourceDef, Camera2dNodeDef, LoadedScene2d } from "./g2/defineScene2d"
91
104
  export { cells, encodeCells, type CellsData } from "./g2/cells"
92
105
  // no-code 2D scenario aspects (attachable in scene files — see src/g2/scenarios2d.ts)
93
106
  export { CameraFollow } from "./g2/scenarios2d"
@@ -260,6 +273,8 @@ export type { ReflectionProbeOptions } from "./gl/ReflectionProbe"
260
273
  export { Ray } from "./gl/Ray"
261
274
  export { Plane } from "./gl/Plane"
262
275
  export { Noise } from "./gl/Noise"
263
- export { Particles, Trail, curve, colorCurve } from "./gl/Particles"
276
+ export { Particles, Trail } from "./gl/Particles"
277
+ // the particle curves — one vocabulary for Particles, Trail and Particles2D (src/core/particlesConfig.ts)
278
+ export { curve, colorCurve } from "./core/particlesConfig"
264
279
  export { DecalSet, DecalTrail } from "./gl/DecalSet"
265
280
  export type { DecalOptions, DecalSetOptions, DecalSpawnOptions, DecalPlacement, DecalTrailOptions } from "./gl/DecalSet"
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `env` in the APP — the project's `PUBLIC_*` keys (src/core/env.ts says what `env` is): a maps key
3
+ * restricted to the app's id, an analytics id, a Sentry DSN. The values are baked into the bundle at
4
+ * compile time (./envValues.ts, replaced by the compile), so they are there offline and without a
5
+ * backend. Any other key is a secret: the compile refuses `env.STRIPE_KEY` in app code — read it in a
6
+ * `*.server.ts` file and call an endpoint.
7
+ */
8
+
9
+ import { makeEnv, type Env } from "../core/env"
10
+ import { values } from "./envValues"
11
+
12
+ /** The app's public keys. A `PUBLIC_*` key that is not set throws `EnvError` on read. */
13
+ export const env: Env = makeEnv(() => values)
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The app's keys — the `PUBLIC_*` subset of the project's keys, baked into the bundle.
3
+ *
4
+ * This module is a placeholder the COMPILER REPLACES: `compileProjectWithMap({ env })` puts a module
5
+ * with the real values at this same path in chisel's file map (project files win over SDK files),
6
+ * the way `server/auth/appConfig.ts` carries app.json. Outside a compiled bundle it is empty.
7
+ */
8
+ export const values: Record<string, string> = {}