lecodes-cli 0.18.2 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1240 -170
- package/package.json +3 -2
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/compile/aspectMacro.ts +86 -0
- package/runtime/sdk/compile/assetIconMacro.ts +384 -0
- package/runtime/sdk/compile/assetMacro.ts +146 -0
- package/runtime/sdk/compile/assetName.ts +50 -0
- package/runtime/sdk/compile/bundler.ts +287 -0
- package/runtime/sdk/compile/compileProject.ts +129 -0
- package/runtime/sdk/compile/detectEntry.ts +128 -0
- package/runtime/sdk/compile/fontMacro.ts +459 -0
- package/runtime/sdk/compile/fontRegistry.ts +78 -0
- package/runtime/sdk/compile/header.ts +67 -0
- package/runtime/sdk/compile/index.ts +100 -0
- package/runtime/sdk/compile/libraryImports.ts +52 -0
- package/runtime/sdk/compile/liteMaterial.ts +247 -0
- package/runtime/sdk/compile/sceneEditor.ts +88 -0
- package/runtime/sdk/compile/serverSplit.ts +233 -0
- package/runtime/sdk/compile/serverTypes.ts +227 -0
- package/runtime/sdk/compile/sfnt.ts +98 -0
- package/runtime/sdk/compile/shaderSchema.ts +202 -0
- package/runtime/sdk/compile/shaderTargets.ts +81 -0
- package/runtime/sdk/compile/sourcemap.ts +25 -0
- package/runtime/sdk/core/Aspect.ts +65 -9
- package/runtime/sdk/core/StateMachine.ts +308 -0
- package/runtime/sdk/core/time.ts +3 -0
- package/runtime/sdk/gl/CameraPlace.ts +52 -52
- package/runtime/sdk/gl/Light.ts +12 -4
- package/runtime/sdk/gl/Lightmap.ts +87 -10
- package/runtime/sdk/gl/Material.ts +15 -0
- package/runtime/sdk/gl/Model.ts +68 -8
- package/runtime/sdk/gl/NavAgent.ts +337 -0
- package/runtime/sdk/gl/NavMesh.ts +397 -0
- package/runtime/sdk/gl/Ragdoll.ts +270 -0
- package/runtime/sdk/gl/Scene.ts +7 -0
- package/runtime/sdk/gl/Shape.ts +37 -5
- package/runtime/sdk/gl/Terrain.ts +1092 -0
- package/runtime/sdk/gl/Texture.ts +17 -0
- package/runtime/sdk/gl/Trigger.ts +45 -45
- package/runtime/sdk/gl/animation/Animator.ts +35 -2
- package/runtime/sdk/gl/animation/Layer.ts +3 -1
- package/runtime/sdk/gl/animation/Loop.ts +5 -0
- package/runtime/sdk/gl/animation/Playback.ts +4 -3
- package/runtime/sdk/gl/animation/core.ts +52 -10
- package/runtime/sdk/gl/scenarios.ts +291 -291
- package/runtime/sdk/gl/state.ts +6 -6
- package/runtime/sdk/gl/terrainMesh.ts +219 -0
- package/runtime/sdk/inject.ts +12 -1
- package/runtime/sdk/net/codec.ts +119 -0
- package/runtime/sdk/net/core.ts +384 -0
- package/runtime/sdk/net/index.ts +181 -0
- package/runtime/sdk/net/replication.ts +622 -0
- package/runtime/sdk/scene/defineScene.ts +140 -14
- package/runtime/sdk/scene/editorPlugins.ts +18 -0
- package/runtime/sdk/scene/gizmos.ts +148 -148
- package/runtime/sdk/ui/UINode.ts +4 -0
- package/runtime/sdk-types.json +1 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// .mat source → the shader's PARAMETER SCHEMA: what the scene editor's material card renders and
|
|
2
|
+
// what `lecodes shaders` prints. Filament's material block carries name + type only, so the rest
|
|
3
|
+
// (widget kind, range, default, tooltip) comes from ANNOTATIONS in the comment that follows each
|
|
4
|
+
// parameter entry — the same trailing comments shader authors already write:
|
|
5
|
+
//
|
|
6
|
+
// parameters : [
|
|
7
|
+
// { type : float3, name : tint }, // @color what the coating does to the world behind it
|
|
8
|
+
// { type : float, name : opacity }, // @range(0, 1) @default(0.22) how much the glass darkens
|
|
9
|
+
// { type : sampler2d, name : mask }, // the reticle mask, white on alpha
|
|
10
|
+
// ]
|
|
11
|
+
//
|
|
12
|
+
// Tags: `@color` (float3/float4 = a colour picker; `@vec` forces the plain vector — without either,
|
|
13
|
+
// a float3/float4 whose NAME reads like a colour is one), `@range(min, max)` (slider), `@step(s)`,
|
|
14
|
+
// `@default(v)` (a number, `#hex`, `[x, y, z]`, `true`/`false`). Whatever text is left is the
|
|
15
|
+
// tooltip. A full-line comment directly above the entry counts too. Unknown tags are ignored; a
|
|
16
|
+
// malformed one lands in `errors` and the parameter still lists.
|
|
17
|
+
//
|
|
18
|
+
// Built on liteMaterial's block extraction + relaxed-JSON parser (the ONLY .mat syntax knowledge
|
|
19
|
+
// in the repo — do not fork it). Dependency-free, browser-safe, never throws.
|
|
20
|
+
|
|
21
|
+
import { extractMatBlocks, parseJsonish, type JsonishValue } from "./liteMaterial"
|
|
22
|
+
|
|
23
|
+
export type ShaderParamEditor =
|
|
24
|
+
| "number" | "int" | "switch" | "vec2" | "vec3" | "vec4" | "color" | "texture" | "matrix" | "array"
|
|
25
|
+
|
|
26
|
+
export type ShaderParamValue = number | boolean | string | number[] | null
|
|
27
|
+
|
|
28
|
+
export type ShaderParam = {
|
|
29
|
+
name: string
|
|
30
|
+
/** filament type name, verbatim (float, float3, sampler2d, …) */
|
|
31
|
+
type: string
|
|
32
|
+
/** array parameters ("float[9]") */
|
|
33
|
+
size?: number
|
|
34
|
+
editor: ShaderParamEditor
|
|
35
|
+
/** The value a new material starts with (from `@default`, else the type's zero — white for a colour). */
|
|
36
|
+
default: ShaderParamValue
|
|
37
|
+
min?: number
|
|
38
|
+
max?: number
|
|
39
|
+
step?: number
|
|
40
|
+
tooltip?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ShaderSchema = {
|
|
44
|
+
name: string
|
|
45
|
+
shadingModel: string
|
|
46
|
+
blending: string
|
|
47
|
+
params: ShaderParam[]
|
|
48
|
+
errors: string[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const SAMPLER_TYPES = new Set([ "sampler2d", "samplerExternal", "sampler2dArray", "samplerCubemap", "sampler3d" ])
|
|
52
|
+
const COLOR_NAME = /colou?r|tint|albedo|emissive|glare|glow/i
|
|
53
|
+
|
|
54
|
+
const zeroFor = (type: string, editor: ShaderParamEditor): ShaderParamValue => {
|
|
55
|
+
switch (editor) {
|
|
56
|
+
case "number": return 0
|
|
57
|
+
case "int": return 0
|
|
58
|
+
case "switch": return false
|
|
59
|
+
case "vec2": return [ 0, 0 ]
|
|
60
|
+
case "vec3": return [ 0, 0, 0 ]
|
|
61
|
+
case "vec4": return [ 0, 0, 0, 0 ]
|
|
62
|
+
case "color": return type === "float4" ? "#ffffffff" : "#ffffff"
|
|
63
|
+
case "texture": return null
|
|
64
|
+
default: return null
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Parse one `@default(...)` payload. */
|
|
69
|
+
const parseDefault = (raw: string, editor: ShaderParamEditor): ShaderParamValue | undefined => {
|
|
70
|
+
const s = raw.trim()
|
|
71
|
+
if (s === "true") return true
|
|
72
|
+
if (s === "false") return false
|
|
73
|
+
if (/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(s)) return s.toLowerCase()
|
|
74
|
+
if (/^-?\d+(\.\d+)?$/.test(s)) return editor === "switch" ? Number(s) !== 0 : Number(s)
|
|
75
|
+
const arr = /^\[(.*)\]$/.exec(s)
|
|
76
|
+
if (arr) {
|
|
77
|
+
const nums = arr[1]!.split(",").map((p) => Number(p.trim()))
|
|
78
|
+
if (nums.length > 0 && nums.every((n) => Number.isFinite(n))) return nums
|
|
79
|
+
}
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type Annotation = { color?: boolean, vec?: boolean, range?: [number, number], step?: number, default?: string, tooltip: string, errors: string[] }
|
|
84
|
+
|
|
85
|
+
/** Split a comment into tags + the remaining tooltip text. */
|
|
86
|
+
const parseAnnotation = (comment: string): Annotation => {
|
|
87
|
+
const out: Annotation = { tooltip: "", errors: [] }
|
|
88
|
+
let rest = comment
|
|
89
|
+
rest = rest.replace(/@(color|vec)\b/g, (_m, tag: string) => { out[tag as "color" | "vec"] = true; return " " })
|
|
90
|
+
rest = rest.replace(/@(range|step|default)\s*\(([^)]*)\)/g, (_m, tag: string, body: string) => {
|
|
91
|
+
if (tag === "range") {
|
|
92
|
+
const parts = body.split(",").map((p) => Number(p.trim()))
|
|
93
|
+
if (parts.length === 2 && parts.every((n) => Number.isFinite(n))) out.range = [ parts[0]!, parts[1]! ]
|
|
94
|
+
else out.errors.push(`@range expects two numbers, got "${body.trim()}"`)
|
|
95
|
+
} else if (tag === "step") {
|
|
96
|
+
const n = Number(body.trim())
|
|
97
|
+
if (Number.isFinite(n) && n > 0) out.step = n
|
|
98
|
+
else out.errors.push(`@step expects a positive number, got "${body.trim()}"`)
|
|
99
|
+
} else {
|
|
100
|
+
out.default = body
|
|
101
|
+
}
|
|
102
|
+
return " "
|
|
103
|
+
})
|
|
104
|
+
rest = rest.replace(/@\w+(\([^)]*\))?/g, " ") // unknown tags: dropped
|
|
105
|
+
out.tooltip = rest.replace(/\s+/g, " ").trim()
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The comment attached to each parameter entry, by parameter name: the trailing `//` on the
|
|
110
|
+
* entry's line, else the full-line comment(s) directly above it. Scans the raw block text
|
|
111
|
+
* (the relaxed-JSON parser drops comments). */
|
|
112
|
+
const collectComments = (materialBlock: string): Map<string, string> => {
|
|
113
|
+
const out = new Map<string, string>()
|
|
114
|
+
const start = materialBlock.search(/\bparameters\s*[:=]/)
|
|
115
|
+
if (start < 0) return out
|
|
116
|
+
const lines = materialBlock.slice(start).split("\n")
|
|
117
|
+
let above: string[] = []
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
const trimmed = line.trim()
|
|
120
|
+
const name = /\bname\s*[:=]\s*"?([A-Za-z_]\w*)"?/.exec(line)?.[1]
|
|
121
|
+
if (name) {
|
|
122
|
+
const trailing = /\}[^/\n]*\/\/(.*)$/.exec(line)?.[1] ?? /\/\/(.*)$/.exec(line.slice(line.indexOf("}") + 1))?.[1]
|
|
123
|
+
const text = trailing !== undefined ? trailing : above.join(" ")
|
|
124
|
+
if (text.trim() !== "") out.set(name, text.trim())
|
|
125
|
+
above = []
|
|
126
|
+
} else if (trimmed.startsWith("//")) {
|
|
127
|
+
above.push(trimmed.slice(2).trim())
|
|
128
|
+
} else if (trimmed !== "" && trimmed !== "[" && trimmed !== "],") {
|
|
129
|
+
above = []
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Parse a `.mat` source into its parameter schema. Never throws: an unreadable file yields an
|
|
136
|
+
* empty schema whose `errors` say why. */
|
|
137
|
+
export const parseShaderSchema = (text: string): ShaderSchema => {
|
|
138
|
+
const schema: ShaderSchema = { name: "", shadingModel: "lit", blending: "opaque", params: [], errors: [] }
|
|
139
|
+
let block: string | undefined
|
|
140
|
+
let def: Record<string, JsonishValue>
|
|
141
|
+
try {
|
|
142
|
+
block = extractMatBlocks(text).material
|
|
143
|
+
if (block === undefined) { schema.errors.push("no material block"); return schema }
|
|
144
|
+
def = parseJsonish("{" + block + "}")
|
|
145
|
+
} catch (e) {
|
|
146
|
+
schema.errors.push(e instanceof Error ? e.message : String(e))
|
|
147
|
+
return schema
|
|
148
|
+
}
|
|
149
|
+
schema.name = typeof def.name === "string" ? def.name : ""
|
|
150
|
+
schema.shadingModel = typeof def.shadingModel === "string" ? def.shadingModel : "lit"
|
|
151
|
+
schema.blending = typeof def.blending === "string" ? def.blending : "opaque"
|
|
152
|
+
|
|
153
|
+
const comments = collectComments(block)
|
|
154
|
+
const rawParams = def.parameters === undefined ? [] : Array.isArray(def.parameters) ? def.parameters : [ def.parameters ]
|
|
155
|
+
for (const raw of rawParams) {
|
|
156
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) { schema.errors.push("malformed parameter entry"); continue }
|
|
157
|
+
const entry = raw as Record<string, JsonishValue>
|
|
158
|
+
const name = String(entry.name ?? "")
|
|
159
|
+
const typeRaw = String(entry.type ?? "")
|
|
160
|
+
const typeMatch = /^(\w+)(?:\[(\d+)\])?$/.exec(typeRaw)
|
|
161
|
+
if (!name || !typeMatch) { schema.errors.push(`malformed parameter "${name || typeRaw}"`); continue }
|
|
162
|
+
const type = typeMatch[1]!
|
|
163
|
+
const size = typeMatch[2] ? Number(typeMatch[2]) : undefined
|
|
164
|
+
const ann = parseAnnotation(comments.get(name) ?? "")
|
|
165
|
+
for (const e of ann.errors) schema.errors.push(`${name}: ${e}`)
|
|
166
|
+
|
|
167
|
+
let editor: ShaderParamEditor
|
|
168
|
+
if (size !== undefined) editor = "array"
|
|
169
|
+
else if (SAMPLER_TYPES.has(type)) editor = "texture"
|
|
170
|
+
else if (type === "float") editor = "number"
|
|
171
|
+
else if (type === "int" || type === "uint") editor = "int"
|
|
172
|
+
else if (type === "bool") editor = "switch"
|
|
173
|
+
else if (type === "float2") editor = "vec2"
|
|
174
|
+
else if (type === "float3" || type === "float4") {
|
|
175
|
+
const isColor = ann.color === true || (ann.vec !== true && COLOR_NAME.test(name))
|
|
176
|
+
editor = isColor ? "color" : type === "float3" ? "vec3" : "vec4"
|
|
177
|
+
} else if (/^(int|uint|bool)[234]$/.test(type)) editor = `vec${type.slice(-1)}` as ShaderParamEditor
|
|
178
|
+
else if (/^float[234]x[234]$/.test(type)) editor = "matrix"
|
|
179
|
+
else editor = "array"
|
|
180
|
+
|
|
181
|
+
const param: ShaderParam = { name, type, editor, default: zeroFor(type, editor) }
|
|
182
|
+
if (size !== undefined) param.size = size
|
|
183
|
+
if (ann.range) { param.min = ann.range[0]; param.max = ann.range[1] }
|
|
184
|
+
if (ann.step !== undefined) param.step = ann.step
|
|
185
|
+
if (ann.default !== undefined) {
|
|
186
|
+
const v = parseDefault(ann.default, editor)
|
|
187
|
+
if (v === undefined) schema.errors.push(`${name}: unreadable @default(${ann.default.trim()})`)
|
|
188
|
+
else param.default = v
|
|
189
|
+
}
|
|
190
|
+
if (ann.tooltip) param.tooltip = ann.tooltip
|
|
191
|
+
schema.params.push(param)
|
|
192
|
+
}
|
|
193
|
+
return schema
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The parameter values a NEW material starts with for this shader (the schema defaults, textures
|
|
197
|
+
* left out — an unset sampler stays the engine's white). */
|
|
198
|
+
export const shaderDefaults = (schema: ShaderSchema): Record<string, ShaderParamValue> => {
|
|
199
|
+
const out: Record<string, ShaderParamValue> = {}
|
|
200
|
+
for (const p of schema.params) if (p.editor !== "texture" && p.default !== null) out[p.name] = p.default
|
|
201
|
+
return out
|
|
202
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// The one shader-compile target matrix, shared by the backend's server-side compile
|
|
2
|
+
// (backend/src/utils/compileShader.ts) and the CLI's local compile (lecodes-cli
|
|
3
|
+
// src/compile/shaders.ts). Both must produce the SAME artifact family for a .mat source —
|
|
4
|
+
// the compiled bundle derives per-backend URLs by substituting `_creator.subBackend ??
|
|
5
|
+
// _creator.backend` into the opengl src (compileProject.ts), so the set of names is ABI.
|
|
6
|
+
|
|
7
|
+
export const externalSamplerReg = /type\s*:\s*samplerExternal/g
|
|
8
|
+
|
|
9
|
+
// matc invocation per target. `profile` is matc's -p (mobile = shader model MOBILE / ESSL3,
|
|
10
|
+
// desktop = shader model DESKTOP / GLSL 4.1); `api` is -a. WebGL and the Windows desktop host
|
|
11
|
+
// both run OpenGL without external (OES) sampler support, so they get samplerExternal rewritten
|
|
12
|
+
// to sampler2d. The desktop variant exists because the desktop Filament engine rejects the mobile
|
|
13
|
+
// _opengl shader ("not built for desktop"); _creator.backend returns "desktop" there.
|
|
14
|
+
//
|
|
15
|
+
// These names are opaque identities for a (profile, api, rewrite) triple, not API names — hence
|
|
16
|
+
// "opengl" (mobile profile) and "desktop" (desktop profile) both being OpenGL. Their only job is to
|
|
17
|
+
// be stable and unique: the runtime substitutes one into `<hash>_<name>.filamat`.
|
|
18
|
+
export const PLATFORM_BUILD = {
|
|
19
|
+
opengl: { profile: "mobile", api: "opengl", replaceExternal: false },
|
|
20
|
+
metal: { profile: "mobile", api: "metal", replaceExternal: false },
|
|
21
|
+
webgl: { profile: "mobile", api: "opengl", replaceExternal: true },
|
|
22
|
+
desktop: { profile: "desktop", api: "opengl", replaceExternal: true },
|
|
23
|
+
} as const
|
|
24
|
+
export type MatcPlatform = keyof typeof PLATFORM_BUILD
|
|
25
|
+
|
|
26
|
+
// Targets the CLI compiles LOCALLY and the platform does not. Deliberately a separate table, not
|
|
27
|
+
// another PLATFORM_BUILD entry: `MatcPlatform` feeds the backend's `Platform` type, which is checked
|
|
28
|
+
// against the `shaderPlatform` pg enum — widening it would break the backend's inserts at compile
|
|
29
|
+
// time for a value the server never produces. Keep local-only targets here until the platform grows
|
|
30
|
+
// the matching enum value (docs/desktop-vulkan-release-plan.md §4 "Known limitation").
|
|
31
|
+
//
|
|
32
|
+
// 'desktop-vulkan' = the Windows Vulkan desktop host: same DESKTOP shader model as 'desktop' but
|
|
33
|
+
// SPIR-V, and the two are not interchangeable — Filament rejects a foreign one outright ("not built
|
|
34
|
+
// for any of the OpenGL backend's supported shader languages"). `_creator.backend` returns
|
|
35
|
+
// "desktop-vulkan" on that host.
|
|
36
|
+
export const LOCAL_PLATFORM_BUILD = {
|
|
37
|
+
"desktop-vulkan": { profile: "desktop", api: "vulkan", replaceExternal: true },
|
|
38
|
+
} as const
|
|
39
|
+
export type LocalMatcPlatform = keyof typeof LOCAL_PLATFORM_BUILD
|
|
40
|
+
|
|
41
|
+
/** Every matc target, local-only ones included — the lookup table for matcArgs. */
|
|
42
|
+
export const ALL_PLATFORM_BUILD = { ...PLATFORM_BUILD, ...LOCAL_PLATFORM_BUILD }
|
|
43
|
+
export type AnyMatcPlatform = MatcPlatform | LocalMatcPlatform
|
|
44
|
+
|
|
45
|
+
/** The local-only targets, as a list (order matters where it feeds an artifact family). */
|
|
46
|
+
export const LOCAL_ONLY_PLATFORMS: LocalMatcPlatform[] = ["desktop-vulkan"]
|
|
47
|
+
|
|
48
|
+
// 'webgl-lite' is not a matc target: viewer-lite gets a JSON artifact (parsed .mat — see
|
|
49
|
+
// liteMaterial.ts) that it wraps in its own GLSL template at runtime.
|
|
50
|
+
export type ShaderPlatform = MatcPlatform | "webgl-lite"
|
|
51
|
+
|
|
52
|
+
/** Every platform the LOCAL compile can produce (matc targets + the lite JSON). */
|
|
53
|
+
export type AnyShaderPlatform = AnyMatcPlatform | "webgl-lite"
|
|
54
|
+
|
|
55
|
+
/** Bump when the matc invocation for an EXISTING target name changes (a different -p/-a/rewrite for
|
|
56
|
+
* a name that already exists). The CLI folds this into its artifact cache key, so such a change
|
|
57
|
+
* invalidates caches that would otherwise look complete — adding or removing a target name is
|
|
58
|
+
* already caught by the artifact-family check, but changing the argv behind a name is not.
|
|
59
|
+
* 1 = 'desktop-vulkan' added (2026-08-24). */
|
|
60
|
+
export const TARGET_MATRIX_GENERATION = 1
|
|
61
|
+
|
|
62
|
+
/** The matc platforms a .mat source needs compiled. WebGL is matc-compiled only when the shader
|
|
63
|
+
* has an external sampler: its samplerExternal → sampler2d rewrite makes the output differ from
|
|
64
|
+
* the mobile opengl build. Without one the WebGL artifact is byte-identical to the mobile shader,
|
|
65
|
+
* so callers copy it from the opengl output instead of running matc again. Either way a _webgl
|
|
66
|
+
* artifact must always exist — the web runtime always requests one (via _creator.subBackend).
|
|
67
|
+
*
|
|
68
|
+
* This is the SERVER's contract — it deliberately does not include LOCAL_ONLY_PLATFORMS. The CLI
|
|
69
|
+
* appends those itself (lecodes-cli src/compile/shaders.ts). */
|
|
70
|
+
export const matcPlatformsFor = (text: string): MatcPlatform[] => {
|
|
71
|
+
externalSamplerReg.lastIndex = 0
|
|
72
|
+
return externalSamplerReg.test(text)
|
|
73
|
+
? ["opengl", "metal", "webgl", "desktop"]
|
|
74
|
+
: ["opengl", "metal", "desktop"]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The matc argv (without the executable) for one target. */
|
|
78
|
+
export const matcArgs = (platform: AnyMatcPlatform, outPath: string, inPath: string): string[] => {
|
|
79
|
+
const build = ALL_PLATFORM_BUILD[platform]
|
|
80
|
+
return ["-p", build.profile, "--no-essl1", "-a", build.api, "-o", outPath, inPath]
|
|
81
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Helpers for serving the bundler's source map alongside the bundle. The map's generated positions
|
|
2
|
+
// are relative to the bundle code; when the deploy/preview prepends a comment header, the map must be
|
|
3
|
+
// shifted down by that many lines, then inlined into the bundle as a data URL (the preview executes
|
|
4
|
+
// the bundle via `new Function(string)`, which has no URL identity, so an external `.map` can't be
|
|
5
|
+
// fetched — it must be inline).
|
|
6
|
+
|
|
7
|
+
/** Shift every generated position in `mapJson` down by `lines` (each VLQ line is `;`-separated). */
|
|
8
|
+
export const offsetSourceMap = (mapJson: string, lines: number): string => {
|
|
9
|
+
if (lines <= 0) return mapJson
|
|
10
|
+
const map = JSON.parse(mapJson) as { mappings: string }
|
|
11
|
+
map.mappings = ";".repeat(lines) + map.mappings
|
|
12
|
+
return JSON.stringify(map)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Inline the map into a bundle string under a **non-standard** directive (`lecodesMap`, not
|
|
17
|
+
* `sourceMappingURL`), so the browser does NOT auto-apply it. The bundle runs via `new Function`, and
|
|
18
|
+
* the browser maps the raw stack position *including* the `new Function` wrapper (~2-3 lines) which the
|
|
19
|
+
* map can't subtract — so its native source location would be off by those lines. Our preview mapper
|
|
20
|
+
* reads `lecodesMap` and subtracts the (calibrated) wrapper itself, so its locations are correct.
|
|
21
|
+
*/
|
|
22
|
+
export const inlineSourceMapComment = (mapJson: string): string => {
|
|
23
|
+
const b64 = Buffer.from(mapJson, "utf8").toString("base64")
|
|
24
|
+
return `//# lecodesMap=data:application/json;charset=utf-8;base64,${b64}`
|
|
25
|
+
}
|
|
@@ -79,6 +79,7 @@ interface Updatable {
|
|
|
79
79
|
updateWhilePaused: boolean
|
|
80
80
|
update?(dt: number): void
|
|
81
81
|
updateBefore?(dt: number): void
|
|
82
|
+
updateFixed?(dt: number): void
|
|
82
83
|
onAttach?(): void
|
|
83
84
|
onDetach?(): void
|
|
84
85
|
onReconfigure?(): void
|
|
@@ -90,9 +91,18 @@ type Ctor = Function & { after?: Function[], before?: Function[], aspect?: strin
|
|
|
90
91
|
// A render-synced frame source: given the two phase runners, wire them to the engine and return true;
|
|
91
92
|
// return false if unavailable (→ setLoop fallback). Installed by the engine layers (Scene / Scene2D).
|
|
92
93
|
type FrameInstaller = (early: PhaseFn, late: PhaseFn) => boolean
|
|
94
|
+
// The FIXED-phase source: wire `fixed` to the engine's per-substep hook and return true; false when the
|
|
95
|
+
// host has no hook (older builds, 2D-only engines) -> the dispatcher steps its own 1/60 accumulator.
|
|
96
|
+
type FixedInstaller = (fixed: PhaseFn) => boolean
|
|
93
97
|
let frameInstaller: FrameInstaller | undefined
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
let fixedInstaller: FixedInstaller | undefined
|
|
99
|
+
/** @internal Install a render-synced frame source for aspect update(dt) (+ the fixed-step source). Called from an engine layer. */
|
|
100
|
+
export const _installAspectFrames = (fn: FrameInstaller, fixed?: FixedInstaller): void => { frameInstaller = fn; fixedInstaller = fixed }
|
|
101
|
+
|
|
102
|
+
/** @internal Engine-level phase hooks (the Net system, sdk/src/net): FIRST in the early and fixed
|
|
103
|
+
* phases, LAST in the late one, ahead of / behind every game aspect regardless of ordering
|
|
104
|
+
* constraints. Single slot each; raw dt. */
|
|
105
|
+
export const _phaseHooks: { early: PhaseFn | null; fixed: PhaseFn | null; late: PhaseFn | null } = { early: null, fixed: null, late: null }
|
|
96
106
|
|
|
97
107
|
let attachSeq = 0
|
|
98
108
|
const warned = new Set<string>()
|
|
@@ -107,11 +117,11 @@ class PhaseList {
|
|
|
107
117
|
private dirty = false
|
|
108
118
|
private holes = false
|
|
109
119
|
|
|
110
|
-
add(a: Updatable, phase: "update" | "updateBefore"): void {
|
|
120
|
+
add(a: Updatable, phase: "update" | "updateBefore" | "updateFixed"): void {
|
|
111
121
|
a._phaseSeq = ++attachSeq
|
|
112
122
|
this.items.push(a)
|
|
113
123
|
this.dirty = true
|
|
114
|
-
a._phases |= phase === "update" ? 2 : 1
|
|
124
|
+
a._phases |= phase === "update" ? 2 : phase === "updateFixed" ? 4 : 1
|
|
115
125
|
}
|
|
116
126
|
remove(a: Updatable): boolean {
|
|
117
127
|
const i = this.items.indexOf(a)
|
|
@@ -120,7 +130,7 @@ class PhaseList {
|
|
|
120
130
|
else this.items.splice(i, 1)
|
|
121
131
|
return true
|
|
122
132
|
}
|
|
123
|
-
run(method: "update" | "updateBefore", dt: number): void {
|
|
133
|
+
run(method: "update" | "updateBefore" | "updateFixed", dt: number): void {
|
|
124
134
|
if (this.dirty) this.sort()
|
|
125
135
|
const items = this.items
|
|
126
136
|
const n = items.length // appended during the pass → next frame (attach-order snapshot semantics)
|
|
@@ -194,35 +204,72 @@ const rankConstraints = (items: (Updatable | null)[]): Map<Function, number> =>
|
|
|
194
204
|
|
|
195
205
|
const earlyUpdaters = new PhaseList()
|
|
196
206
|
const lateUpdaters = new PhaseList()
|
|
207
|
+
const fixedUpdaters = new PhaseList()
|
|
197
208
|
let dispatchInstalled = false
|
|
209
|
+
let fixedInstalled = false
|
|
210
|
+
let fixedFallbackOn = false
|
|
211
|
+
let fixedFallbackAcc = 0
|
|
212
|
+
|
|
213
|
+
// The FIXED phase: dt = Time.fixedDt (1/60) every call, once per physics substep, on every host. The
|
|
214
|
+
// engine drives it from inside its substep loop when it has the hook; otherwise (older wasm / Apple /
|
|
215
|
+
// Android builds, the setLoop path) the SDK steps its own accumulator right after the early phase -
|
|
216
|
+
// same step, same 4-substep cap, same drop-the-backlog rule as creator-gl's physics/world.cpp.
|
|
217
|
+
const fixedStep: PhaseFn = (dt) => { _phaseHooks.fixed?.(dt); fixedUpdaters.run("updateFixed", dt) }
|
|
218
|
+
const fixedFallback = (): void => {
|
|
219
|
+
const step = Time.fixedDt
|
|
220
|
+
fixedFallbackAcc += Time.dt // scaled: paused -> no steps, slow-motion -> fewer steps, never a shorter dt
|
|
221
|
+
let steps = 0
|
|
222
|
+
while (fixedFallbackAcc >= step && steps < 4) { fixedStep(step); fixedFallbackAcc -= step; steps++ }
|
|
223
|
+
if (steps === 4) fixedFallbackAcc = 0
|
|
224
|
+
}
|
|
198
225
|
|
|
199
226
|
const ensureDispatch = (): void => {
|
|
200
227
|
if (dispatchInstalled) return
|
|
201
228
|
dispatchInstalled = true
|
|
202
229
|
// Both phases scale their OWN raw dt (the engine hands the same value to both; a host or test that
|
|
203
230
|
// drives them apart still gets each phase's dt right). The frame counter / clocks advance once, early.
|
|
204
|
-
const early: PhaseFn = (dt) => {
|
|
205
|
-
|
|
231
|
+
const early: PhaseFn = (dt) => {
|
|
232
|
+
Time._beginFrame(dt)
|
|
233
|
+
_phaseHooks.early?.(dt)
|
|
234
|
+
earlyUpdaters.run("updateBefore", Time.dt)
|
|
235
|
+
if (fixedFallbackOn) fixedFallback()
|
|
236
|
+
}
|
|
237
|
+
const late: PhaseFn = (dt) => { lateUpdaters.run("update", Time._phaseDt(dt)); _phaseHooks.late?.(dt) }
|
|
206
238
|
// render-synced source if an engine layer installed one; else one host loop (early then late).
|
|
207
239
|
if (!(frameInstaller && frameInstaller(early, late))) {
|
|
208
240
|
setLoop((dt) => { early(dt); late(dt) })
|
|
209
241
|
}
|
|
210
242
|
}
|
|
243
|
+
// Registered lazily - an app with no `updateFixed` never makes the engine call into JS per substep.
|
|
244
|
+
const ensureFixedDispatch = (): void => {
|
|
245
|
+
if (fixedInstalled) return
|
|
246
|
+
fixedInstalled = true
|
|
247
|
+
ensureDispatch()
|
|
248
|
+
if (!(fixedInstaller && fixedInstaller(fixedStep))) fixedFallbackOn = true
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** @internal Force the frame dispatch on (the Net system pumps from the phase hooks even in an app
|
|
252
|
+
* with no aspects) — and the fixed phase with it, since Net.tick is a fixed-step count. */
|
|
253
|
+
export const _ensureNetDispatch = (): void => { ensureDispatch(); ensureFixedDispatch() }
|
|
211
254
|
|
|
212
255
|
const registerUpdater = (inst: Aspect<any, any, any>): void => {
|
|
213
256
|
const a = internals(inst)
|
|
214
257
|
const hasBefore = typeof a.updateBefore === "function"
|
|
215
258
|
const hasUpdate = typeof a.update === "function"
|
|
216
|
-
|
|
259
|
+
const hasFixed = typeof a.updateFixed === "function"
|
|
260
|
+
if (!hasBefore && !hasUpdate && !hasFixed) return
|
|
217
261
|
if (hasBefore) earlyUpdaters.add(a, "updateBefore")
|
|
218
262
|
if (hasUpdate) lateUpdaters.add(a, "update")
|
|
263
|
+
if (hasFixed) fixedUpdaters.add(a, "updateFixed")
|
|
219
264
|
ensureDispatch()
|
|
265
|
+
if (hasFixed) ensureFixedDispatch()
|
|
220
266
|
}
|
|
221
267
|
const unregisterUpdater = (inst: Aspect<any, any, any>): void => {
|
|
222
268
|
const a = internals(inst)
|
|
223
269
|
if (a._phases === 0) return
|
|
224
270
|
earlyUpdaters.remove(a)
|
|
225
271
|
lateUpdaters.remove(a)
|
|
272
|
+
fixedUpdaters.remove(a)
|
|
226
273
|
a._phases = 0
|
|
227
274
|
}
|
|
228
275
|
|
|
@@ -456,6 +503,15 @@ export abstract class Aspect<K extends string, P = Node, E extends EventMap = {}
|
|
|
456
503
|
* state. A class rarely needs both phases — that is two aspects on one node.
|
|
457
504
|
*/
|
|
458
505
|
protected updateBefore?(dt: number): void
|
|
506
|
+
/**
|
|
507
|
+
* FIXED phase - once per physics substep, BEFORE that substep's Jolt step, with `dt = Time.fixedDt`
|
|
508
|
+
* (1/60) exactly, on every machine: under `Time.scale` the NUMBER of steps per frame changes, never
|
|
509
|
+
* the dt (0-4 calls per frame). The home of simulation that must match another machine's - a
|
|
510
|
+
* multiplayer player sim, dt-dependent motion that may not drift with the frame rate. `controller.move()`
|
|
511
|
+
* / velocity writes here feed the same step; between steps the world is consistent, so `Physics.raycast`
|
|
512
|
+
* is fine. Anything that looks at the screen (cameras, HUD, FX) stays in `updateBefore` / `update`.
|
|
513
|
+
*/
|
|
514
|
+
protected updateFixed?(dt: number): void
|
|
459
515
|
|
|
460
516
|
/** Tick order within a phase and rank — ascending; default 0, ties keep attach order. A declared
|
|
461
517
|
* `static after` / `static before` constraint always beats this number. Read once, at attach —
|
|
@@ -496,7 +552,7 @@ export abstract class Aspect<K extends string, P = Node, E extends EventMap = {}
|
|
|
496
552
|
|
|
497
553
|
/**
|
|
498
554
|
* An aspect of a SCENE: game logic with no single node to live on — input mapping, the FX pools, the
|
|
499
|
-
* HUD, the game mode. Same lifecycle (`onAttach`/`onDetach`), same
|
|
555
|
+
* HUD, the game mode. Same lifecycle (`onAttach`/`onDetach`), same phases (`updateFixed` / `updateBefore` /
|
|
500
556
|
* `update`), same ordering (`updateOrder`, `static after`) and events as a node aspect; `this.scene` is the
|
|
501
557
|
* scene it was attached to. `S` = the scene kind (`Scene` by default, `Scene2D` for 2D games).
|
|
502
558
|
*
|