lecodes-sdk 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/global.d.ts +18 -4
- package/dist/inject.js +260 -361
- package/dist/types/animate/tween/Animation.d.ts +69 -0
- package/dist/types/animate/tween/Timeline.d.ts +55 -0
- package/dist/types/animate/tween/animateValue.d.ts +27 -0
- package/dist/types/animate/tween/easing.d.ts +29 -0
- package/dist/types/animate/tween/spec.d.ts +178 -0
- package/dist/types/g2/Node2D.d.ts +16 -0
- package/dist/types/g2/Sprite.d.ts +11 -1
- package/dist/types/gl/Camera.d.ts +15 -1
- package/dist/types/gl/Foliage.d.ts +47 -0
- package/dist/types/gl/Geometry.d.ts +24 -0
- package/dist/types/gl/Light.d.ts +28 -7
- package/dist/types/gl/Lightmap.d.ts +90 -60
- package/dist/types/gl/Material.d.ts +28 -20
- package/dist/types/gl/Model.d.ts +7 -5
- package/dist/types/gl/Node.d.ts +18 -0
- package/dist/types/gl/Particles.d.ts +55 -1
- package/dist/types/gl/Scene.d.ts +20 -0
- package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
- package/dist/types/gl/animation/Animator.d.ts +27 -0
- package/dist/types/gl/animation/DynamicBone.d.ts +19 -8
- package/dist/types/gl/animation/IK.d.ts +86 -30
- package/dist/types/gl/animation/Locomotion.d.ts +52 -3
- package/dist/types/gl/animation/Warp.d.ts +2 -1
- package/dist/types/gl/animation/core.d.ts +35 -4
- package/dist/types/gl/physics/Ragdoll.d.ts +87 -12
- package/dist/types/gl/terrain/Terrain.d.ts +12 -2
- package/dist/types/inject.d.ts +8 -2
- package/dist/types/runtime/input.d.ts +7 -0
- package/dist/types/scene/defineScene.d.ts +44 -32
- package/dist/types/ui/UIButton.d.ts +3 -1
- package/dist/types/ui/UIInput.d.ts +5 -1
- package/dist/types/ui/UINode.d.ts +24 -24
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/prompts/core-design.md +27 -4
- package/prompts/core.md +35 -6
- package/prompts/dist/2d-game.md +197 -408
- package/prompts/dist/3d-app.md +166 -491
- package/prompts/dist/ar-app.md +163 -373
- package/prompts/dist/design.md +87 -83
- package/prompts/dist/ui-app.md +136 -325
- package/prompts/select.ts +19 -4
- package/src/animate/tween/Animation.ts +378 -0
- package/src/animate/tween/Timeline.ts +175 -0
- package/src/animate/tween/animateValue.ts +100 -0
- package/src/animate/tween/easing.ts +172 -0
- package/src/animate/tween/spec.ts +479 -0
- package/src/audio/audio.ts +161 -161
- package/src/bridges.d.ts +235 -65
- package/src/compile/__tests__/assetMacro.test.ts +26 -0
- package/src/compile/__tests__/detectEntry.test.ts +19 -0
- package/src/compile/__tests__/serverSplit.test.ts +27 -0
- package/src/compile/bundler.ts +34 -4
- package/src/compile/compileProject.ts +31 -1
- package/src/compile/detectEntry.ts +8 -3
- package/src/compile/index.ts +2 -0
- package/src/compile/serverSplit.ts +9 -3
- package/src/g2/Node2D.ts +38 -0
- package/src/g2/Sprite.ts +20 -1
- package/src/gl/Camera.ts +34 -1
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Foliage.ts +102 -0
- package/src/gl/Geometry.ts +393 -348
- package/src/gl/Light.ts +49 -16
- package/src/gl/Lightmap.ts +439 -275
- package/src/gl/Material.ts +59 -47
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Model.ts +23 -12
- package/src/gl/Node.ts +39 -0
- package/src/gl/Particles.ts +80 -2
- package/src/gl/Scene.ts +34 -1
- package/src/gl/animation/AnimationClip.ts +52 -0
- package/src/gl/animation/Animator.ts +42 -2
- package/src/gl/animation/DynamicBone.ts +35 -12
- package/src/gl/animation/IK.ts +173 -152
- package/src/gl/animation/Locomotion.ts +72 -8
- package/src/gl/animation/Playback.ts +5 -4
- package/src/gl/animation/Warp.ts +5 -2
- package/src/gl/animation/core.ts +65 -4
- package/src/gl/physics/Ragdoll.ts +451 -272
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +33 -2
- package/src/inject.ts +236 -226
- package/src/runtime/input.ts +11 -0
- package/src/scene/defineScene.ts +72 -62
- package/src/scene/gizmos.ts +148 -148
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIInput.ts +3 -3
- package/src/ui/UINode.ts +61 -36
- package/dist/types/animate/animate.d.ts +0 -20
- package/src/animate/animate.ts +0 -238
package/src/scene/defineScene.ts
CHANGED
|
@@ -48,10 +48,12 @@ import { Mesh } from "../gl/Mesh"
|
|
|
48
48
|
import { Model } from "../gl/Model"
|
|
49
49
|
import { Physics } from "../gl/physics/Physics"
|
|
50
50
|
import { CharacterController } from "../gl/physics/CharacterController"
|
|
51
|
-
import { Lightmap } from "../gl/Lightmap"
|
|
51
|
+
import { Lightmap, type LightmapTransmit } from "../gl/Lightmap"
|
|
52
|
+
import { Foliage, type FoliageOptions } from "../gl/Foliage"
|
|
52
53
|
import { NavMesh } from "../gl/nav/NavMesh"
|
|
53
54
|
import { Terrain, type TerrainLayer, type TerrainRegion, type TerrainOptions } from "../gl/terrain/Terrain"
|
|
54
55
|
import { Light, type SunOptions } from "../gl/Light"
|
|
56
|
+
import { Material } from "../gl/Material"
|
|
55
57
|
import { assignMaterialDef, MaterialHandle, resolveMaterialDef, type MaterialDef } from "./material"
|
|
56
58
|
import type { CapsuleOptions, CylinderOptions, PlaneOptions, SphereOptions } from "../gl/Geometry"
|
|
57
59
|
import { GizmoBuffer, Gizmos, withGizmoScope, type GizmoBatch } from "./gizmos"
|
|
@@ -184,6 +186,10 @@ export type SceneNodeDef = {
|
|
|
184
186
|
* Default: static unless a `Physics` aspect moves the node (`dynamic` — Physics' default — or
|
|
185
187
|
* `kinematic`). Set it only to override that rule; prefab subtrees inherit the verdict. */
|
|
186
188
|
lightmap?: boolean
|
|
189
|
+
/** `model` nodes: vegetation — load through the FOLIAGE tier (wind, touch bending, per-copy tint; see
|
|
190
|
+
* `Foliage`). `{ fade: [start, end] }` also thins the cards out over that distance range (m) and drops the
|
|
191
|
+
* copy past `end` — for ground cover. The fade is per ASSET: every node of the same GLB shares it. */
|
|
192
|
+
foliage?: boolean | { fade?: [number, number] }
|
|
187
193
|
/** Navigation (a scene with `env.navmesh`, see navmesh.md): every STATIC body is walkable by
|
|
188
194
|
* default. `false` leaves this one out of the bake; `'unwalkable'` cuts its footprint out
|
|
189
195
|
* (nobody stands on or crosses it). */
|
|
@@ -201,41 +207,44 @@ export type SceneCameraDef = CameraProjectionDef & {
|
|
|
201
207
|
target?: Vec3Like
|
|
202
208
|
}
|
|
203
209
|
|
|
204
|
-
/** `env.lightmap` — the level's baked lighting (
|
|
205
|
-
*
|
|
210
|
+
/** `env.lightmap` — the level's baked lighting (packages/creator-bake): the files `lecodes lightmap bake`
|
|
211
|
+
* writes. Absent = real-time only. */
|
|
206
212
|
export type SceneLightmapDef = {
|
|
207
|
-
/** `asset('./assets/lightmap/
|
|
213
|
+
/** `asset('./assets/lightmap/level.bake')` */
|
|
208
214
|
data: string
|
|
209
|
-
/** `asset('./assets/lightmap/
|
|
210
|
-
* (`lecodes lightmap bake --pages`), every page in order: `[asset('x.ktx2'), asset('
|
|
211
|
-
|
|
212
|
-
/** `asset('./assets/lightmap/
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
light
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
/** `asset('./assets/lightmap/level-light.ktx2')` — or, for a bake that took more than one atlas PAGE
|
|
216
|
+
* (`lecodes lightmap bake --pages`), every page in order: `[asset('x-light.ktx2'), asset('x-light_1.ktx2')]`. */
|
|
217
|
+
light: string | string[]
|
|
218
|
+
/** `asset('./assets/lightmap/level-aux.ktx2')` (pages like `light`) — sun / sky visibility + light direction. */
|
|
219
|
+
aux: string | string[]
|
|
220
|
+
/** DEBUG: the page sets a `lecodes lightmap bake --split` wrote (`<stem>-direct[_n].ktx2`, `<stem>-indirect[_n].ktx2`),
|
|
221
|
+
* pages like `light` — the `Lightmap.debug("direct" | "indirect")` views bind them in place of `light`. */
|
|
222
|
+
direct?: string | string[]
|
|
223
|
+
indirect?: string | string[]
|
|
224
|
+
/** `asset('./assets/lightmap/level-probes.ktx2')` — the bake's reflection probes (one prefiltered cubemap of the level's
|
|
225
|
+
* radiance per probe; every static reflects the nearest). Absent = statics reflect the sky's SH alone. */
|
|
226
|
+
probes?: string
|
|
227
|
+
/** FOR THE BAKE: the radiance in cd / m² of emission 1.0 on this level (emissive factor x strength x map) - every lit
|
|
228
|
+
* region of an emissive surface bakes as a rectangle lamp. An imported pack's emission is a LOOK (the school's panels
|
|
229
|
+
* say 10); this is where the level says what they give: a 1.3 m ceiling panel of ~8000 lm is ~1600 nits. Default: what
|
|
230
|
+
* the camera makes of 1.0 (`1.2 x 2^EV100`). 0 = emissive surfaces light nothing. `--emissive-nits` overrides. */
|
|
231
|
+
emissiveNits?: number
|
|
232
|
+
/** `false` = the emissive surfaces light nothing in the bake - the level is lit by its lamps (`Light.point` with a
|
|
233
|
+
* `bakeArea` next to every panel, the Unity packs' way) and the panels are decoration; `emissiveNits` then only says how
|
|
234
|
+
* bright their glow is DRAWN (scene-referred: nits through the camera's exposure). Default true. */
|
|
235
|
+
emissiveBake?: boolean
|
|
236
|
+
/** FOR THE BAKE: materials that let light THROUGH them, by glTF material name (`"name*"` = every name with that start) →
|
|
237
|
+
* the share of a shadow ray that passes, tinted by the material's base colour x map: `{ mat_awning: 0.3 }` puts the
|
|
238
|
+
* awning's warm, patterned light on the sand. The surface stays opaque - baked, drawn as before, a shadow caster.
|
|
239
|
+
* `{ through, diffuse }` splits it: `through` goes straight (the picture of the map), `diffuse` is scattered by the
|
|
240
|
+
* fibres (the underside glows, a soft fill with no picture) - dense canvas: `{ through: 0.08, diffuse: 0.25 }`. */
|
|
241
|
+
transmit?: Record<string, LightmapTransmit>
|
|
242
|
+
/** `asset('./assets/lightmap/level.lgrid')` — THE LIGHT GRID for movers: every node a Physics aspect or a
|
|
243
|
+
* CharacterController moves (and what code marks with `Lightmap.track`) takes its ambient light from the bake at the
|
|
244
|
+
* place it is at, whatever its shader. Absent = movers keep the scene's IBL. */
|
|
219
245
|
volume?: string
|
|
220
|
-
/**
|
|
221
|
-
|
|
222
|
-
/** Multiplier on the ambient share in the shadow math (1 = filament's own darkness). Default 1. */
|
|
223
|
-
ambientScale?: number
|
|
224
|
-
/** How much of the baked ambient occlusion applies: 1 = all of it, 0 = none. An interior lit by its
|
|
225
|
-
* ambient probe alone can want less than the geometric truth. Default 1. */
|
|
226
|
-
aoStrength?: number
|
|
227
|
-
/** Multiplier on the baked point lights' irradiance (default 1) - dial the lamps up or down
|
|
228
|
-
* without a re-bake, since their irradiance lives in the atlas. Scales the light volume with the
|
|
229
|
-
* atlas, so movers match the statics. See `LightmapLoadOptions.lightBoost`. */
|
|
230
|
-
lightBoost?: number
|
|
231
|
-
/** Multiplier on the light VOLUME's irradiance alone (default = `lightBoost`). Since the volume is an
|
|
232
|
-
* ambient cube (2026-08-31) movers shade like the statics and the default is right; it stays as a
|
|
233
|
-
* trim. See `LightmapLoadOptions.volumeBoost`. */
|
|
234
|
-
volumeBoost?: number
|
|
235
|
-
/** Occluder-only statics (props without lightmap UVs): `"baked"` (default) = their shadow is in the
|
|
236
|
-
* atlas, they stop casting in real time and the shadow pass draws only the movers; `"realtime"` =
|
|
237
|
-
* they keep casting (their shadow on other non-receiver props). See `LightmapLoadOptions.occluderShadows`. */
|
|
238
|
-
occluderShadows?: "baked" | "realtime"
|
|
246
|
+
/** A DEBUG multiplier on the atlas' lux (default 1). */
|
|
247
|
+
lightScale?: number
|
|
239
248
|
}
|
|
240
249
|
|
|
241
250
|
/** `env.navmesh` — the level's navigation mesh (see navmesh.md): the file `lecodes navmesh bake`
|
|
@@ -250,7 +259,7 @@ export type SceneNavmeshDef = {
|
|
|
250
259
|
}
|
|
251
260
|
|
|
252
261
|
export type SceneDef = {
|
|
253
|
-
env?: SceneOptions & { lightmap?: SceneLightmapDef, navmesh?: SceneNavmeshDef }
|
|
262
|
+
env?: SceneOptions & { lightmap?: SceneLightmapDef, navmesh?: SceneNavmeshDef, foliage?: FoliageOptions }
|
|
254
263
|
camera?: SceneCameraDef
|
|
255
264
|
nodes?: Record<string, SceneNodeDef>
|
|
256
265
|
}
|
|
@@ -422,7 +431,7 @@ const createMesh = async (def: MeshDef, material?: MaterialDef): Promise<Mesh> =
|
|
|
422
431
|
// rename honestly invalidates the rect. make() subtrees are code — they register themselves with
|
|
423
432
|
// Lightmap.add. Edit mode never bakes or applies (the editor shows real-time shadows).
|
|
424
433
|
|
|
425
|
-
type LightmapCtx = { prefix: string
|
|
434
|
+
type LightmapCtx = { prefix: string }
|
|
426
435
|
|
|
427
436
|
// A CharacterController moves its node as surely as a dynamic body does (2026-09-11: the hero's
|
|
428
437
|
// capsule was read as static, so its sword baked a shadow into the terrain at the spawn point).
|
|
@@ -435,12 +444,12 @@ const movesByPhysics = (def: SceneNodeDef): boolean =>
|
|
|
435
444
|
* Children of a mover are movers too — buildNodes carries that down, this is the node's own verdict. */
|
|
436
445
|
export const isLightmapStatic = (def: SceneNodeDef): boolean => def.lightmap ?? !movesByPhysics(def)
|
|
437
446
|
|
|
438
|
-
// `lightmap` undefined = Model.load's own default ('dynamic' while a level with a light volume runs —
|
|
439
|
-
// this is how a weapon or arms scene instantiated from code reads the volume; an explicit false would win)
|
|
440
447
|
/** A terrain node: from its file or a fresh flat grid; textures load before the node appears (like a
|
|
441
448
|
* mesh's material). Stamps and holes apply later in buildNodes, once every target node exists. */
|
|
442
|
-
const createTerrain = async (def: TerrainNodeDef): Promise<Node> => {
|
|
443
|
-
const { data, stamps: _s, holes: _h, ...
|
|
449
|
+
const createTerrain = async (def: TerrainNodeDef, lightmap: boolean): Promise<Node> => {
|
|
450
|
+
const { data, stamps: _s, holes: _h, ...rest } = def
|
|
451
|
+
// a lightmap static is built with the baked material from the start, like a model is loaded lightmapped
|
|
452
|
+
const opts = lightmap ? { ...rest, material: Material.terrainLightmap() } : rest
|
|
444
453
|
const t = data !== undefined ? await Terrain.load(data, opts) : Terrain.create(opts)
|
|
445
454
|
await t.ready
|
|
446
455
|
return t.node
|
|
@@ -457,14 +466,11 @@ const createTerrain = async (def: TerrainNodeDef): Promise<Node> => {
|
|
|
457
466
|
// cannot leak into the next node that leaves the flag alone.
|
|
458
467
|
const modelTemplates = new Map<string, Promise<Model>>()
|
|
459
468
|
|
|
460
|
-
const loadModel = (source: string, lightmap: boolean
|
|
461
|
-
|
|
462
|
-
// sets — so the KEY has to hold the resolved mode, not the argument.
|
|
463
|
-
const mode = lightmap ?? Model._lightmapDefault
|
|
464
|
-
const key = `${mode}\u0000${source}`
|
|
469
|
+
const loadModel = (source: string, lightmap: boolean, foliage: boolean): Promise<Model> => {
|
|
470
|
+
const key = `${lightmap}\u0000${foliage ? "foliage" : ""}\u0000${source}`
|
|
465
471
|
let template = modelTemplates.get(key)
|
|
466
472
|
if (template === undefined) {
|
|
467
|
-
template = Model.load(source, { lightmap
|
|
473
|
+
template = Model.load(source, { lightmap, foliage })
|
|
468
474
|
modelTemplates.set(key, template)
|
|
469
475
|
// a failed load must not poison the cache — the next node retries
|
|
470
476
|
void template.catch(() => modelTemplates.delete(key))
|
|
@@ -472,12 +478,12 @@ const loadModel = (source: string, lightmap: boolean | "dynamic" | undefined): P
|
|
|
472
478
|
return template.then((t) => t.clone())
|
|
473
479
|
}
|
|
474
480
|
|
|
475
|
-
const createSource = (path: string, def: SceneNodeDef, lightmap: boolean
|
|
481
|
+
const createSource = (path: string, def: SceneNodeDef, lightmap: boolean): Node | Promise<Node> => {
|
|
476
482
|
const sources = [ def.mesh, def.model, def.light, def.make, def.prefab, def.camera, def.terrain ].filter((s) => s !== undefined).length
|
|
477
483
|
if (sources > 1) throw new Error(`Scene node "${path}" declares more than one source (mesh/model/light/make/prefab/camera/terrain)`)
|
|
478
|
-
if (def.model !== undefined) return loadModel(def.model, lightmap)
|
|
484
|
+
if (def.model !== undefined) return loadModel(def.model, lightmap, !!def.foliage)
|
|
479
485
|
if (def.mesh !== undefined) return createMesh(def.mesh, def.material)
|
|
480
|
-
if (def.terrain !== undefined) return createTerrain(def.terrain)
|
|
486
|
+
if (def.terrain !== undefined) return createTerrain(def.terrain, lightmap)
|
|
481
487
|
if (def.light !== undefined) { const { kind: _k, ...opts } = def.light; return Light.sun(opts) }
|
|
482
488
|
// make/prefab/camera/group nodes are plain wrappers — make/prefab subtrees mount under them
|
|
483
489
|
// during the build (attachPrefab) or in phase 2 (attachMake); a camera node drives scene.camera
|
|
@@ -531,6 +537,8 @@ const applyNode = (node: Node, name: string, def: SceneNodeDef): void => {
|
|
|
531
537
|
if (t && def.receiveShadows !== undefined) t.receiveShadows = def.receiveShadows
|
|
532
538
|
}
|
|
533
539
|
if (def.overrides && def.model !== undefined) applyModelOverrides(node, def.overrides)
|
|
540
|
+
// the foliage fade is per asset — one node's range is every copy's (the last def wins)
|
|
541
|
+
if (typeof def.foliage === "object" && def.foliage.fade && node instanceof Model && node._foliage) Foliage.fade(node, def.foliage.fade[0], def.foliage.fade[1])
|
|
534
542
|
}
|
|
535
543
|
|
|
536
544
|
// ---- edit-mode node markers + the play-mode camera rig -------------------------------------------
|
|
@@ -870,6 +878,7 @@ const buildNodes = async (
|
|
|
870
878
|
lm: LightmapCtx | null = null,
|
|
871
879
|
): Promise<void> => {
|
|
872
880
|
const pending: { path: string, node: Node, def: SceneNodeDef }[] = []
|
|
881
|
+
const moverRoots: Node[] = []
|
|
873
882
|
|
|
874
883
|
// `parentMoves`: a child of a mover moves with it (the body under the hero's capsule) — it never
|
|
875
884
|
// bakes, whatever its own def says short of an explicit `lightmap: true`
|
|
@@ -881,10 +890,9 @@ const buildNodes = async (
|
|
|
881
890
|
// the path is derived from def keys BEFORE any await, so it is deterministic even though
|
|
882
891
|
// Promise.all makes build completion (and record insertion) order nondeterministic
|
|
883
892
|
const path = parentPath === "" ? name : `${parentPath}/${name}`
|
|
884
|
-
const lmStatic = lm !== null &&
|
|
893
|
+
const lmStatic = lm !== null && (nd.lightmap ?? (!parentMoves && isLightmapStatic(nd)))
|
|
885
894
|
// a mover in a level with a light volume reads the volume (models: the engine binds it; meshes: registered below)
|
|
886
|
-
const
|
|
887
|
-
const node = await createSource(path, nd, lmStatic ? true : lmDynamic ? "dynamic" : undefined)
|
|
895
|
+
const node = await createSource(path, nd, lmStatic)
|
|
888
896
|
// a `mount` def parents to an internal part of the parent asset — safe here: the parent's
|
|
889
897
|
// model was awaited and attachPrefab completed before its children build
|
|
890
898
|
if (parentNode) attachHost(parentNode, nd, path).add(node)
|
|
@@ -892,11 +900,12 @@ const buildNodes = async (
|
|
|
892
900
|
scene.add(node)
|
|
893
901
|
applyNode(node, name, nd) // engine-side name stays the bare sibling segment
|
|
894
902
|
if (lmStatic && (node instanceof Model || node instanceof Mesh)) Lightmap._register(node, lm!.prefix + path)
|
|
895
|
-
else if (lmStatic &&
|
|
896
|
-
|
|
903
|
+
else if (lmStatic && nd.terrain !== undefined) Lightmap._registerGround(node, lm!.prefix + path)
|
|
904
|
+
// the top of a moving subtree: everything under it reads the level's light grid (Lightmap.track walks it at load)
|
|
905
|
+
if (lm !== null && !lmStatic && !parentMoves) moverRoots.push(node)
|
|
897
906
|
if (isEditMode()) addEditorMarker(node, nd)
|
|
898
907
|
// a prefab under a static wrapper bakes; under a mover its internals stay movers (volume-lit)
|
|
899
|
-
if (nd.prefab !== undefined) await attachPrefab(node, path, nd, scene, stack, lmStatic ? { prefix: `${lm!.prefix}${path}
|
|
908
|
+
if (nd.prefab !== undefined) await attachPrefab(node, path, nd, scene, stack, lmStatic ? { prefix: `${lm!.prefix}${path}/` } : null)
|
|
900
909
|
pending.push({ path, node, def: nd })
|
|
901
910
|
nodes[path] = node
|
|
902
911
|
await Promise.all(Object.entries(nd.children ?? {}).map(([ childName, child ]) => build(childName, child, node, path, parentMoves || (lm !== null && !lmStatic))))
|
|
@@ -926,6 +935,8 @@ const buildNodes = async (
|
|
|
926
935
|
for (const h of td.holes ?? []) t.hole(h)
|
|
927
936
|
t.commit()
|
|
928
937
|
}
|
|
938
|
+
// the movers read the level's light grid: their subtrees are whole by now (prefab internals, children)
|
|
939
|
+
for (const n of moverRoots) Lightmap.track(n)
|
|
929
940
|
}
|
|
930
941
|
|
|
931
942
|
// ---- navigation (env.navmesh) ------------------------------------------------------------------
|
|
@@ -954,14 +965,14 @@ const registerNavOverrides = (defs: Record<string, SceneNodeDef> | undefined, no
|
|
|
954
965
|
}
|
|
955
966
|
|
|
956
967
|
const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ scene: Scene, nodes: Record<string, Node> }> => {
|
|
957
|
-
const { lightmap, navmesh, ...env } = def.env ?? {}
|
|
968
|
+
const { lightmap, navmesh, foliage, ...env } = def.env ?? {}
|
|
958
969
|
const scene = new Scene(env)
|
|
970
|
+
// the vegetation tier's level-wide knobs (wind, sway, variation) — before the models load
|
|
971
|
+
if (foliage) Foliage.configure(foliage)
|
|
959
972
|
const nodes: Record<string, Node> = {}
|
|
960
973
|
// baked lighting is a play-mode concern; the level owns the registry (a rebuild starts clean)
|
|
961
|
-
const lm: LightmapCtx | null = lightmap && !isEditMode() ? { prefix: ""
|
|
974
|
+
const lm: LightmapCtx | null = lightmap && !isEditMode() ? { prefix: "" } : null
|
|
962
975
|
if (lm) Lightmap.clear()
|
|
963
|
-
// with a light volume every model loaded from now on (weapons, spawns, make() subtrees) is a mover by default
|
|
964
|
-
if (lm) Model._lightmapDefault = lm.volume ? "dynamic" : false
|
|
965
976
|
await buildNodes(def.nodes ?? {}, null, scene, nodes, editorRuns, new Set([ def ]), lm)
|
|
966
977
|
if (navmesh && !isEditMode()) registerNavOverrides(def.nodes, nodes)
|
|
967
978
|
|
|
@@ -994,9 +1005,8 @@ const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ sc
|
|
|
994
1005
|
|
|
995
1006
|
// the level is complete: apply the bake — or, under `lecodes lightmap bake`, run it
|
|
996
1007
|
if (lm && lightmap) {
|
|
997
|
-
await Lightmap.load(scene, { data: lightmap.data,
|
|
998
|
-
|
|
999
|
-
lightBoost: lightmap.lightBoost, volumeBoost: lightmap.volumeBoost, occluderShadows: lightmap.occluderShadows })
|
|
1008
|
+
await Lightmap.load(scene, { data: lightmap.data, light: lightmap.light, aux: lightmap.aux, direct: lightmap.direct, indirect: lightmap.indirect, probes: lightmap.probes, volume: lightmap.volume },
|
|
1009
|
+
{ lightScale: lightmap.lightScale, emissiveNits: lightmap.emissiveNits, emissiveBake: lightmap.emissiveBake, transmit: lightmap.transmit })
|
|
1000
1010
|
}
|
|
1001
1011
|
// navigation: load the baked mesh — or, under `lecodes navmesh bake`, dump the level's collision
|
|
1002
1012
|
// geometry for it (the static bodies exist by now; edit mode never loads it — v1)
|
|
@@ -1125,7 +1135,7 @@ export class SceneHandle<D extends SceneDef = SceneDef> {
|
|
|
1125
1135
|
if (parent) attachHost(parent, d, p).add(existing)
|
|
1126
1136
|
return existing
|
|
1127
1137
|
}
|
|
1128
|
-
const node = await createSource(p, d,
|
|
1138
|
+
const node = await createSource(p, d, false)
|
|
1129
1139
|
if (parent) attachHost(parent, d, p).add(node)
|
|
1130
1140
|
scene.add(node)
|
|
1131
1141
|
applyNode(node, p.slice(p.lastIndexOf("/") + 1), d)
|
package/src/scene/gizmos.ts
CHANGED
|
@@ -1,148 +1,148 @@
|
|
|
1
|
-
// Editor gizmos: immediate-mode line drawing for code that runs while a scene is EDITED — a
|
|
2
|
-
// generator aspect's `rebuild()`, a `make()` factory, an editor tool's hooks. Nothing here touches
|
|
3
|
-
// the scene: calls accumulate WORLD-space line segments into the buffer of the run that is
|
|
4
|
-
// currently executing (an ambient scope the scene loader opens around each synchronous call), and
|
|
5
|
-
// the scene editor pushes those buffers to the lite engine's overlay layer, which draws them over
|
|
6
|
-
// the picture — never outlined, never picked, never rendered by Filament, absent in play mode.
|
|
7
|
-
//
|
|
8
|
-
// rebuild() {
|
|
9
|
-
// Gizmos.polyline(points, { color: "#5b8ef0", closed: true })
|
|
10
|
-
// for (const p of points) Gizmos.cross(p, 0.07)
|
|
11
|
-
// }
|
|
12
|
-
//
|
|
13
|
-
// Every run starts with an EMPTY buffer — what a call draws is the whole picture (like
|
|
14
|
-
// `generated.clear()`, only implicit). Outside any scope (play mode, hand-attached aspects, a call
|
|
15
|
-
// after an `await` inside rebuild) the calls are no-ops: rebuild is synchronous by contract.
|
|
16
|
-
//
|
|
17
|
-
// ANCHORED gizmos (`{ node }` in the style): points are in that node's LOCAL frame and the engine
|
|
18
|
-
// follows the node live — through a gizmo drag, not only after it — with the node's scale
|
|
19
|
-
// stripped (an empty scaled ×10 for its children keeps a normal-sized cross). An anchored picture
|
|
20
|
-
// is the node's own: clicking it in the viewport selects the node, and it draws in the selection
|
|
21
|
-
// accent while selected. World-space calls (path lines) are never pickable.
|
|
22
|
-
|
|
23
|
-
import { Mat4, type Mat4Like } from "../math/mat4"
|
|
24
|
-
import type { Vec3Like } from "../math/vec"
|
|
25
|
-
|
|
26
|
-
/** The anchor of a gizmo call: any object with an engine entity id (a `Node`). */
|
|
27
|
-
export type GizmoAnchor = { readonly id: number }
|
|
28
|
-
|
|
29
|
-
export type GizmoStyle = {
|
|
30
|
-
/** CSS hex color (`#rgb` / `#rrggbb`); default a neutral light gray. */
|
|
31
|
-
color?: string
|
|
32
|
-
/** 0..1, default 1. */
|
|
33
|
-
alpha?: number
|
|
34
|
-
/** Anchor: points are in this node's LOCAL frame (scale ignored); the lines follow the node
|
|
35
|
-
* live, are pickable (a click selects the node) and turn the selection accent when it is
|
|
36
|
-
* selected. Omit for world-space lines (not pickable). */
|
|
37
|
-
node?: GizmoAnchor | null
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** One color batch of LINES (flat `[x,y,z, x,y,z]` per segment) — what a host hands the engine.
|
|
41
|
-
* With `entityId` the segments are in that entity's local frame (see GizmoStyle.node). */
|
|
42
|
-
export type GizmoBatch = { color: string, alpha: number, segments: number[], entityId?: number }
|
|
43
|
-
|
|
44
|
-
const DEFAULT_COLOR = "#cfd4dd"
|
|
45
|
-
|
|
46
|
-
/** The per-run collector (owned by the scene loader's EditorRun). Batches are keyed by style so a
|
|
47
|
-
* hundred same-colored segments cost one draw. */
|
|
48
|
-
export class GizmoBuffer {
|
|
49
|
-
batches: GizmoBatch[] = []
|
|
50
|
-
private _byStyle = new Map<string, GizmoBatch>()
|
|
51
|
-
|
|
52
|
-
clear(): void {
|
|
53
|
-
this.batches = []
|
|
54
|
-
this._byStyle.clear()
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
segments(style: GizmoStyle | undefined): number[] {
|
|
58
|
-
const color = style?.color ?? DEFAULT_COLOR
|
|
59
|
-
const alpha = Math.max(0, Math.min(1, style?.alpha ?? 1))
|
|
60
|
-
const entityId = style?.node?.id
|
|
61
|
-
const key = `${color}@${alpha}@${entityId ?? ""}`
|
|
62
|
-
let b = this._byStyle.get(key)
|
|
63
|
-
if (!b) {
|
|
64
|
-
b = entityId ? { color, alpha, segments: [], entityId } : { color, alpha, segments: [] }
|
|
65
|
-
this._byStyle.set(key, b)
|
|
66
|
-
this.batches.push(b)
|
|
67
|
-
}
|
|
68
|
-
return b.segments
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
let current: GizmoBuffer | null = null
|
|
73
|
-
let warnedOutOfScope = false
|
|
74
|
-
|
|
75
|
-
const xyz = (p: Vec3Like): [number, number, number] =>
|
|
76
|
-
Array.isArray(p) ? [ p[0] ?? 0, p[1] ?? 0, p[2] ?? 0 ] : [ (p as { x: number }).x, (p as { y: number }).y, (p as { z: number }).z ]
|
|
77
|
-
|
|
78
|
-
const target = (style: GizmoStyle | undefined): number[] | null => {
|
|
79
|
-
if (current) return current.segments(style)
|
|
80
|
-
if (!warnedOutOfScope && (globalThis as { __lecodesSceneEdit?: boolean }).__lecodesSceneEdit) {
|
|
81
|
-
warnedOutOfScope = true
|
|
82
|
-
console.warn("[Gizmos] draw call outside an editor run — gizmos must be drawn synchronously inside rebuild() / a make() factory / a tool hook")
|
|
83
|
-
}
|
|
84
|
-
return null
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** @internal Run `fn` with `buffer` as the ambient gizmo target (cleared first). Nested scopes
|
|
88
|
-
* restore the outer one. */
|
|
89
|
-
export const withGizmoScope = <T>(buffer: GizmoBuffer, fn: () => T): T => {
|
|
90
|
-
const prev = current
|
|
91
|
-
buffer.clear()
|
|
92
|
-
current = buffer
|
|
93
|
-
try { return fn() } finally { current = prev }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Editor-only line drawing, available inside generator `rebuild()`, `make()` factories and editor
|
|
98
|
-
* tool hooks. Points are WORLD space. Drawn by the scene editor's viewport overlay (depth-test
|
|
99
|
-
* off — a path through a wall is still a path); invisible everywhere else.
|
|
100
|
-
*/
|
|
101
|
-
export const Gizmos = {
|
|
102
|
-
/** One segment from `a` to `b`. */
|
|
103
|
-
line(a: Vec3Like, b: Vec3Like, style?: GizmoStyle): void {
|
|
104
|
-
const out = target(style)
|
|
105
|
-
if (!out) return
|
|
106
|
-
out.push(...xyz(a), ...xyz(b))
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
/** Consecutive segments through `points`; `closed` joins the last point back to the first. */
|
|
110
|
-
polyline(points: readonly Vec3Like[], style?: GizmoStyle & { closed?: boolean }): void {
|
|
111
|
-
if (points.length < 2) return
|
|
112
|
-
const out = target(style)
|
|
113
|
-
if (!out) return
|
|
114
|
-
const n = points.length
|
|
115
|
-
const last = style?.closed ? n : n - 1
|
|
116
|
-
for (let i = 0; i < last; i++) out.push(...xyz(points[i]!), ...xyz(points[(i + 1) % n]!))
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
/** A wireframe camera frustum looking down −Z: the four edges from the origin to a rect
|
|
120
|
-
* `length` metres ahead sized by `fov` (vertical, degrees) × `aspect` (default 16:9), plus an
|
|
121
|
-
* "up" fin above the rect. Anchor it (`{ node }`) for a camera node's marker — the frustum
|
|
122
|
-
* then follows the node's pose; `matrix` places an unanchored one in world space. */
|
|
123
|
-
frustum(fov: number, style?: GizmoStyle & { aspect?: number, length?: number, matrix?: Mat4Like }): void {
|
|
124
|
-
const out = target(style)
|
|
125
|
-
if (!out) return
|
|
126
|
-
const aspect = style?.aspect ?? 16 / 9, length = style?.length ?? 0.6
|
|
127
|
-
const m = style?.matrix ? new Mat4(style.matrix) : null
|
|
128
|
-
const h = Math.tan((fov * Math.PI) / 360) * length, w = h * aspect, z = -length
|
|
129
|
-
const P = (x: number, y: number, zz: number): [number, number, number] => {
|
|
130
|
-
if (!m) return [ x, y, zz ]
|
|
131
|
-
const v = m.transformPoint([ x, y, zz ])
|
|
132
|
-
return [ v.x, v.y, v.z ]
|
|
133
|
-
}
|
|
134
|
-
const o = P(0, 0, 0)
|
|
135
|
-
const c = [ P(-w, -h, z), P(w, -h, z), P(w, h, z), P(-w, h, z) ]
|
|
136
|
-
for (const q of c) out.push(...o, ...q)
|
|
137
|
-
for (let i = 0; i < 4; i++) out.push(...c[i]!, ...c[(i + 1) % 4]!)
|
|
138
|
-
out.push(...P(-w * 0.4, h, z), ...P(0, h * 1.6, z), ...P(0, h * 1.6, z), ...P(w * 0.4, h, z))
|
|
139
|
-
},
|
|
140
|
-
|
|
141
|
-
/** A three-axis cross centred on `p` (a point marker); `size` = half extent, default 0.1. */
|
|
142
|
-
cross(p: Vec3Like, size = 0.1, style?: GizmoStyle): void {
|
|
143
|
-
const out = target(style)
|
|
144
|
-
if (!out) return
|
|
145
|
-
const [ x, y, z ] = xyz(p)
|
|
146
|
-
out.push(x - size, y, z, x + size, y, z, x, y - size, z, x, y + size, z, x, y, z - size, x, y, z + size)
|
|
147
|
-
},
|
|
148
|
-
}
|
|
1
|
+
// Editor gizmos: immediate-mode line drawing for code that runs while a scene is EDITED — a
|
|
2
|
+
// generator aspect's `rebuild()`, a `make()` factory, an editor tool's hooks. Nothing here touches
|
|
3
|
+
// the scene: calls accumulate WORLD-space line segments into the buffer of the run that is
|
|
4
|
+
// currently executing (an ambient scope the scene loader opens around each synchronous call), and
|
|
5
|
+
// the scene editor pushes those buffers to the lite engine's overlay layer, which draws them over
|
|
6
|
+
// the picture — never outlined, never picked, never rendered by Filament, absent in play mode.
|
|
7
|
+
//
|
|
8
|
+
// rebuild() {
|
|
9
|
+
// Gizmos.polyline(points, { color: "#5b8ef0", closed: true })
|
|
10
|
+
// for (const p of points) Gizmos.cross(p, 0.07)
|
|
11
|
+
// }
|
|
12
|
+
//
|
|
13
|
+
// Every run starts with an EMPTY buffer — what a call draws is the whole picture (like
|
|
14
|
+
// `generated.clear()`, only implicit). Outside any scope (play mode, hand-attached aspects, a call
|
|
15
|
+
// after an `await` inside rebuild) the calls are no-ops: rebuild is synchronous by contract.
|
|
16
|
+
//
|
|
17
|
+
// ANCHORED gizmos (`{ node }` in the style): points are in that node's LOCAL frame and the engine
|
|
18
|
+
// follows the node live — through a gizmo drag, not only after it — with the node's scale
|
|
19
|
+
// stripped (an empty scaled ×10 for its children keeps a normal-sized cross). An anchored picture
|
|
20
|
+
// is the node's own: clicking it in the viewport selects the node, and it draws in the selection
|
|
21
|
+
// accent while selected. World-space calls (path lines) are never pickable.
|
|
22
|
+
|
|
23
|
+
import { Mat4, type Mat4Like } from "../math/mat4"
|
|
24
|
+
import type { Vec3Like } from "../math/vec"
|
|
25
|
+
|
|
26
|
+
/** The anchor of a gizmo call: any object with an engine entity id (a `Node`). */
|
|
27
|
+
export type GizmoAnchor = { readonly id: number }
|
|
28
|
+
|
|
29
|
+
export type GizmoStyle = {
|
|
30
|
+
/** CSS hex color (`#rgb` / `#rrggbb`); default a neutral light gray. */
|
|
31
|
+
color?: string
|
|
32
|
+
/** 0..1, default 1. */
|
|
33
|
+
alpha?: number
|
|
34
|
+
/** Anchor: points are in this node's LOCAL frame (scale ignored); the lines follow the node
|
|
35
|
+
* live, are pickable (a click selects the node) and turn the selection accent when it is
|
|
36
|
+
* selected. Omit for world-space lines (not pickable). */
|
|
37
|
+
node?: GizmoAnchor | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One color batch of LINES (flat `[x,y,z, x,y,z]` per segment) — what a host hands the engine.
|
|
41
|
+
* With `entityId` the segments are in that entity's local frame (see GizmoStyle.node). */
|
|
42
|
+
export type GizmoBatch = { color: string, alpha: number, segments: number[], entityId?: number }
|
|
43
|
+
|
|
44
|
+
const DEFAULT_COLOR = "#cfd4dd"
|
|
45
|
+
|
|
46
|
+
/** The per-run collector (owned by the scene loader's EditorRun). Batches are keyed by style so a
|
|
47
|
+
* hundred same-colored segments cost one draw. */
|
|
48
|
+
export class GizmoBuffer {
|
|
49
|
+
batches: GizmoBatch[] = []
|
|
50
|
+
private _byStyle = new Map<string, GizmoBatch>()
|
|
51
|
+
|
|
52
|
+
clear(): void {
|
|
53
|
+
this.batches = []
|
|
54
|
+
this._byStyle.clear()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
segments(style: GizmoStyle | undefined): number[] {
|
|
58
|
+
const color = style?.color ?? DEFAULT_COLOR
|
|
59
|
+
const alpha = Math.max(0, Math.min(1, style?.alpha ?? 1))
|
|
60
|
+
const entityId = style?.node?.id
|
|
61
|
+
const key = `${color}@${alpha}@${entityId ?? ""}`
|
|
62
|
+
let b = this._byStyle.get(key)
|
|
63
|
+
if (!b) {
|
|
64
|
+
b = entityId ? { color, alpha, segments: [], entityId } : { color, alpha, segments: [] }
|
|
65
|
+
this._byStyle.set(key, b)
|
|
66
|
+
this.batches.push(b)
|
|
67
|
+
}
|
|
68
|
+
return b.segments
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let current: GizmoBuffer | null = null
|
|
73
|
+
let warnedOutOfScope = false
|
|
74
|
+
|
|
75
|
+
const xyz = (p: Vec3Like): [number, number, number] =>
|
|
76
|
+
Array.isArray(p) ? [ p[0] ?? 0, p[1] ?? 0, p[2] ?? 0 ] : [ (p as { x: number }).x, (p as { y: number }).y, (p as { z: number }).z ]
|
|
77
|
+
|
|
78
|
+
const target = (style: GizmoStyle | undefined): number[] | null => {
|
|
79
|
+
if (current) return current.segments(style)
|
|
80
|
+
if (!warnedOutOfScope && (globalThis as { __lecodesSceneEdit?: boolean }).__lecodesSceneEdit) {
|
|
81
|
+
warnedOutOfScope = true
|
|
82
|
+
console.warn("[Gizmos] draw call outside an editor run — gizmos must be drawn synchronously inside rebuild() / a make() factory / a tool hook")
|
|
83
|
+
}
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @internal Run `fn` with `buffer` as the ambient gizmo target (cleared first). Nested scopes
|
|
88
|
+
* restore the outer one. */
|
|
89
|
+
export const withGizmoScope = <T>(buffer: GizmoBuffer, fn: () => T): T => {
|
|
90
|
+
const prev = current
|
|
91
|
+
buffer.clear()
|
|
92
|
+
current = buffer
|
|
93
|
+
try { return fn() } finally { current = prev }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Editor-only line drawing, available inside generator `rebuild()`, `make()` factories and editor
|
|
98
|
+
* tool hooks. Points are WORLD space. Drawn by the scene editor's viewport overlay (depth-test
|
|
99
|
+
* off — a path through a wall is still a path); invisible everywhere else.
|
|
100
|
+
*/
|
|
101
|
+
export const Gizmos = {
|
|
102
|
+
/** One segment from `a` to `b`. */
|
|
103
|
+
line(a: Vec3Like, b: Vec3Like, style?: GizmoStyle): void {
|
|
104
|
+
const out = target(style)
|
|
105
|
+
if (!out) return
|
|
106
|
+
out.push(...xyz(a), ...xyz(b))
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/** Consecutive segments through `points`; `closed` joins the last point back to the first. */
|
|
110
|
+
polyline(points: readonly Vec3Like[], style?: GizmoStyle & { closed?: boolean }): void {
|
|
111
|
+
if (points.length < 2) return
|
|
112
|
+
const out = target(style)
|
|
113
|
+
if (!out) return
|
|
114
|
+
const n = points.length
|
|
115
|
+
const last = style?.closed ? n : n - 1
|
|
116
|
+
for (let i = 0; i < last; i++) out.push(...xyz(points[i]!), ...xyz(points[(i + 1) % n]!))
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
/** A wireframe camera frustum looking down −Z: the four edges from the origin to a rect
|
|
120
|
+
* `length` metres ahead sized by `fov` (vertical, degrees) × `aspect` (default 16:9), plus an
|
|
121
|
+
* "up" fin above the rect. Anchor it (`{ node }`) for a camera node's marker — the frustum
|
|
122
|
+
* then follows the node's pose; `matrix` places an unanchored one in world space. */
|
|
123
|
+
frustum(fov: number, style?: GizmoStyle & { aspect?: number, length?: number, matrix?: Mat4Like }): void {
|
|
124
|
+
const out = target(style)
|
|
125
|
+
if (!out) return
|
|
126
|
+
const aspect = style?.aspect ?? 16 / 9, length = style?.length ?? 0.6
|
|
127
|
+
const m = style?.matrix ? new Mat4(style.matrix) : null
|
|
128
|
+
const h = Math.tan((fov * Math.PI) / 360) * length, w = h * aspect, z = -length
|
|
129
|
+
const P = (x: number, y: number, zz: number): [number, number, number] => {
|
|
130
|
+
if (!m) return [ x, y, zz ]
|
|
131
|
+
const v = m.transformPoint([ x, y, zz ])
|
|
132
|
+
return [ v.x, v.y, v.z ]
|
|
133
|
+
}
|
|
134
|
+
const o = P(0, 0, 0)
|
|
135
|
+
const c = [ P(-w, -h, z), P(w, -h, z), P(w, h, z), P(-w, h, z) ]
|
|
136
|
+
for (const q of c) out.push(...o, ...q)
|
|
137
|
+
for (let i = 0; i < 4; i++) out.push(...c[i]!, ...c[(i + 1) % 4]!)
|
|
138
|
+
out.push(...P(-w * 0.4, h, z), ...P(0, h * 1.6, z), ...P(0, h * 1.6, z), ...P(w * 0.4, h, z))
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
/** A three-axis cross centred on `p` (a point marker); `size` = half extent, default 0.1. */
|
|
142
|
+
cross(p: Vec3Like, size = 0.1, style?: GizmoStyle): void {
|
|
143
|
+
const out = target(style)
|
|
144
|
+
if (!out) return
|
|
145
|
+
const [ x, y, z ] = xyz(p)
|
|
146
|
+
out.push(x - size, y, z, x + size, y, z, x, y - size, z, x, y + size, z, x, y, z - size, x, y, z + size)
|
|
147
|
+
},
|
|
148
|
+
}
|
package/src/ui/UIButton.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { ClickEvent, TouchStartEvent, LongPressEvent } from "../runtime/touch"
|
|
2
2
|
import type { ClickEvent as ClickEventType, TouchStartEvent as TouchStartEventType, LongPressEvent as LongPressEventType } from "../runtime/touch"
|
|
3
|
-
import { type ElementStyle, type ContainerStyle, ContainerElement, buildUI, type DrawableStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, type BaseStyle, type Color } from "./UINode"
|
|
3
|
+
import { type ElementStyle, type ContainerStyle, ContainerElement, buildUI, type DrawableStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, type BaseStyle, type Color, type LayerEasing } from "./UINode"
|
|
4
4
|
|
|
5
5
|
export type UIButtonStyle = ElementStyle & DrawableStyle & ContainerStyle & { rippleColor?: Color | "default" }
|
|
6
6
|
|
|
7
7
|
/** A tappable container — row + centered children by default. Press feedback via the `onPressed`
|
|
8
8
|
* style block, the `$pressed` class, or `rippleColor`. */
|
|
9
|
-
export interface UIButton extends UIContainerBase<UIButtonStyle & { onPressed?: UIButtonStyle & { duration?: number } }, DrawableStyle & BaseStyle> {
|
|
9
|
+
export interface UIButton extends UIContainerBase<UIButtonStyle & { onPressed?: UIButtonStyle & { duration?: number, delay?: number, easing?: LayerEasing } }, DrawableStyle & BaseStyle> {
|
|
10
10
|
readonly type: "button",
|
|
11
11
|
|
|
12
12
|
/** Fired on a completed tap/click. */
|
package/src/ui/UIInput.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ElementStyle, type DrawableStyle, Element, type TextStyle, type Color, type BaseStyle, type UIElementBase } from "./UINode";
|
|
1
|
+
import { type ElementStyle, type DrawableStyle, Element, type TextStyle, type Color, type BaseStyle, type UIElementBase, type LayerEasing } from "./UINode";
|
|
2
2
|
|
|
3
3
|
type InputStyle = {
|
|
4
4
|
placeholder?: string,
|
|
@@ -51,7 +51,7 @@ type SubmitStyle = {
|
|
|
51
51
|
|
|
52
52
|
/** A single-line text input. Keyboard/picker kind via the `type` style, return-key behavior via
|
|
53
53
|
* `enterKey` + `onSubmit`; keyboard policy on `keyboardShrink`/`keyboardDismiss`. */
|
|
54
|
-
export interface UIInput extends UIElementBase<UIInputStyle & SubmitStyle & { onFocused?: UIInputStyle & { duration?: number } }, DrawableStyle & BaseStyle> {
|
|
54
|
+
export interface UIInput extends UIElementBase<UIInputStyle & SubmitStyle & { onFocused?: UIInputStyle & { duration?: number, delay?: number, easing?: LayerEasing } }, DrawableStyle & BaseStyle> {
|
|
55
55
|
readonly type: "input",
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -78,7 +78,7 @@ export interface UIInput extends UIElementBase<UIInputStyle & SubmitStyle & { on
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/** A multi-line text field — see the {@link UITextArea} factory for the auto-grow contract. */
|
|
81
|
-
export interface UITextArea extends UIElementBase<UIInputStyle & { onFocused?: UIInputStyle & { duration?: number } }, DrawableStyle & BaseStyle> {
|
|
81
|
+
export interface UITextArea extends UIElementBase<UIInputStyle & { onFocused?: UIInputStyle & { duration?: number, delay?: number, easing?: LayerEasing } }, DrawableStyle & BaseStyle> {
|
|
82
82
|
readonly type: "textarea",
|
|
83
83
|
|
|
84
84
|
/** The field's text — same contract as {@link UIInput.value}: settable before mount to pre-fill. */
|