lecodes-sdk 0.20.0 → 1.0.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 +48 -5
- package/dist/inject.js +361 -260
- package/dist/types/audio/Bus.d.ts +45 -0
- package/dist/types/audio/Sound.d.ts +28 -0
- package/dist/types/audio/Voice.d.ts +27 -0
- package/dist/types/audio/audio.d.ts +83 -0
- package/dist/types/audio/support.d.ts +1 -0
- package/dist/types/canvas/Canvas.d.ts +2 -0
- package/dist/types/gl/DecalSet.d.ts +148 -0
- package/dist/types/gl/Geometry.d.ts +17 -0
- package/dist/types/gl/Light.d.ts +7 -0
- package/dist/types/gl/Lightmap.d.ts +9 -0
- package/dist/types/gl/Material.d.ts +90 -2
- package/dist/types/gl/Mesh.d.ts +18 -1
- package/dist/types/gl/Model.d.ts +34 -0
- package/dist/types/gl/Particles.d.ts +13 -0
- package/dist/types/gl/Scene.d.ts +23 -0
- package/dist/types/gl/Texture.d.ts +29 -1
- package/dist/types/gl/animation/AnimationClip.d.ts +25 -12
- package/dist/types/gl/animation/DynamicBone.d.ts +173 -0
- package/dist/types/gl/{IK.d.ts → animation/IK.d.ts} +4 -4
- package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
- package/dist/types/gl/animation/core.d.ts +15 -10
- package/dist/types/gl/audio/AudioSource.d.ts +60 -0
- package/dist/types/gl/audio/AudioZone.d.ts +32 -0
- package/dist/types/gl/audio/SceneAudio.d.ts +11 -0
- package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
- package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
- package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
- package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
- package/dist/types/gl/{Ragdoll.d.ts → physics/Ragdoll.d.ts} +4 -4
- package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
- package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
- package/dist/types/gl/state.d.ts +0 -1
- package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +6 -6
- package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
- package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
- package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
- package/dist/types/inject.d.ts +33 -21
- package/dist/types/runtime/files.d.ts +24 -1
- package/dist/types/runtime/input.d.ts +11 -0
- package/dist/types/scene/defineScene.d.ts +10 -3
- package/dist/types/ui/UIImage.d.ts +15 -5
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/prompts/dist/2d-game.md +408 -197
- package/prompts/dist/3d-app.md +491 -166
- package/prompts/dist/ar-app.md +373 -163
- package/prompts/dist/design.md +83 -87
- package/prompts/dist/ui-app.md +325 -136
- package/src/audio/Bus.ts +102 -0
- package/src/audio/Sound.ts +96 -0
- package/src/audio/Voice.ts +102 -0
- package/src/audio/audio.ts +161 -0
- package/src/audio/support.ts +6 -0
- package/src/bridges.d.ts +279 -32
- package/src/canvas/Canvas.ts +21 -0
- package/src/compile/__tests__/compile.test.ts +11 -0
- package/src/compile/compileProject.ts +30 -15
- package/src/compile/header.ts +6 -3
- package/src/compile/index.ts +4 -0
- package/src/compile/sceneEditor.ts +42 -1
- package/src/core/Aspect.ts +33 -8
- package/src/g2/Scene2D.ts +7 -0
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/DecalSet.ts +360 -0
- package/src/gl/Geometry.ts +348 -279
- package/src/gl/Light.ts +16 -0
- package/src/gl/Lightmap.ts +35 -7
- package/src/gl/Material.ts +173 -4
- package/src/gl/Mesh.ts +120 -83
- package/src/gl/Model.ts +33 -1
- package/src/gl/Node.ts +1 -1
- package/src/gl/Particles.ts +21 -3
- package/src/gl/Scene.ts +41 -7
- package/src/gl/Texture.ts +43 -3
- package/src/gl/animation/AnimationClip.ts +43 -20
- package/src/gl/animation/Animator.ts +4 -3
- package/src/gl/animation/DynamicBone.ts +459 -0
- package/src/gl/{IK.ts → animation/IK.ts} +4 -4
- package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
- package/src/gl/animation/core.ts +20 -15
- package/src/gl/audio/AudioSource.ts +113 -0
- package/src/gl/audio/AudioZone.ts +75 -0
- package/src/gl/audio/SceneAudio.ts +26 -0
- package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
- package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
- package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
- package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
- package/src/gl/{Ragdoll.ts → physics/Ragdoll.ts} +272 -270
- package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
- package/src/gl/{Trigger.ts → physics/Trigger.ts} +2 -2
- package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/state.ts +1 -1
- package/src/gl/{Terrain.ts → terrain/Terrain.ts} +10 -10
- package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
- package/src/gl/vehicle/Vehicle.ts +666 -0
- package/src/gl/vehicle/Wheel.ts +290 -0
- package/src/inject.ts +226 -212
- package/src/runtime/files.ts +32 -2
- package/src/runtime/input.ts +6 -1
- package/src/scene/defineScene.ts +26 -10
- package/src/scene/gizmos.ts +148 -148
- package/src/scene/level.ts +2 -2
- package/src/ui/UIImage.ts +21 -7
- package/dist/types/gl/Gearbox.d.ts +0 -86
- package/dist/types/gl/Vehicle.d.ts +0 -191
- package/dist/types/gl/Wheel.d.ts +0 -95
- package/src/gl/Gearbox.ts +0 -212
- package/src/gl/Vehicle.ts +0 -473
- package/src/gl/Wheel.ts +0 -240
- /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
package/src/scene/defineScene.ts
CHANGED
|
@@ -46,13 +46,14 @@ import { CameraPlace } from "../gl/CameraPlace"
|
|
|
46
46
|
import { Node } from "../gl/Node"
|
|
47
47
|
import { Mesh } from "../gl/Mesh"
|
|
48
48
|
import { Model } from "../gl/Model"
|
|
49
|
-
import { Physics } from "../gl/Physics"
|
|
49
|
+
import { Physics } from "../gl/physics/Physics"
|
|
50
|
+
import { CharacterController } from "../gl/physics/CharacterController"
|
|
50
51
|
import { Lightmap } from "../gl/Lightmap"
|
|
51
|
-
import { NavMesh } from "../gl/NavMesh"
|
|
52
|
-
import { Terrain, type TerrainLayer, type TerrainRegion, type TerrainOptions } from "../gl/Terrain"
|
|
52
|
+
import { NavMesh } from "../gl/nav/NavMesh"
|
|
53
|
+
import { Terrain, type TerrainLayer, type TerrainRegion, type TerrainOptions } from "../gl/terrain/Terrain"
|
|
53
54
|
import { Light, type SunOptions } from "../gl/Light"
|
|
54
55
|
import { assignMaterialDef, MaterialHandle, resolveMaterialDef, type MaterialDef } from "./material"
|
|
55
|
-
import type { CylinderOptions, PlaneOptions, SphereOptions } from "../gl/Geometry"
|
|
56
|
+
import type { CapsuleOptions, CylinderOptions, PlaneOptions, SphereOptions } from "../gl/Geometry"
|
|
56
57
|
import { GizmoBuffer, Gizmos, withGizmoScope, type GizmoBatch } from "./gizmos"
|
|
57
58
|
|
|
58
59
|
// ---- the literal grammar (what the visual editor reads and writes) -----------
|
|
@@ -61,6 +62,7 @@ export type MeshDef =
|
|
|
61
62
|
| { kind: "box", size?: Vec3Like | number }
|
|
62
63
|
| ({ kind: "sphere" } & SphereOptions)
|
|
63
64
|
| ({ kind: "cylinder" } & CylinderOptions)
|
|
65
|
+
| ({ kind: "capsule" } & CapsuleOptions)
|
|
64
66
|
| ({ kind: "plane" } & PlaneOptions)
|
|
65
67
|
|
|
66
68
|
export type LightDef = { kind: "sun" } & SunOptions
|
|
@@ -230,6 +232,10 @@ export type SceneLightmapDef = {
|
|
|
230
232
|
* ambient cube (2026-08-31) movers shade like the statics and the default is right; it stays as a
|
|
231
233
|
* trim. See `LightmapLoadOptions.volumeBoost`. */
|
|
232
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"
|
|
233
239
|
}
|
|
234
240
|
|
|
235
241
|
/** `env.navmesh` — the level's navigation mesh (see navmesh.md): the file `lecodes navmesh bake`
|
|
@@ -402,6 +408,7 @@ const createMesh = async (def: MeshDef, material?: MaterialDef): Promise<Mesh> =
|
|
|
402
408
|
case "box": mesh = Mesh.box({ size: def.size, material: mat }); break
|
|
403
409
|
case "sphere": { const { kind: _k, ...opts } = def; mesh = Mesh.sphere({ ...opts, material: mat }); break }
|
|
404
410
|
case "cylinder": { const { kind: _k, ...opts } = def; mesh = Mesh.cylinder({ ...opts, material: mat }); break }
|
|
411
|
+
case "capsule": { const { kind: _k, ...opts } = def; mesh = Mesh.capsule({ ...opts, material: mat }); break }
|
|
405
412
|
case "plane": { const { kind: _k, ...opts } = def; mesh = Mesh.plane({ ...opts, material: mat }); break }
|
|
406
413
|
}
|
|
407
414
|
if (material instanceof MaterialHandle) material._users.add({ node: mesh, slot: 0 })
|
|
@@ -417,11 +424,15 @@ const createMesh = async (def: MeshDef, material?: MaterialDef): Promise<Mesh> =
|
|
|
417
424
|
|
|
418
425
|
type LightmapCtx = { prefix: string, volume: boolean, dynamicOnly?: boolean }
|
|
419
426
|
|
|
427
|
+
// A CharacterController moves its node as surely as a dynamic body does (2026-09-11: the hero's
|
|
428
|
+
// capsule was read as static, so its sword baked a shadow into the terrain at the spawn point).
|
|
420
429
|
const movesByPhysics = (def: SceneNodeDef): boolean =>
|
|
421
430
|
(def.aspects ?? []).some((e) =>
|
|
422
|
-
(e.ctor as unknown) === Physics && ((e.props as { motion?: string } | undefined)?.motion ?? "dynamic") !== "static")
|
|
431
|
+
((e.ctor as unknown) === Physics && ((e.props as { motion?: string } | undefined)?.motion ?? "dynamic") !== "static")
|
|
432
|
+
|| (e.ctor as unknown) === CharacterController)
|
|
423
433
|
|
|
424
|
-
/** The static verdict for one node def (the editor's "Baked lighting" switch shows the same rule).
|
|
434
|
+
/** The static verdict for one node def (the editor's "Baked lighting" switch shows the same rule).
|
|
435
|
+
* Children of a mover are movers too — buildNodes carries that down, this is the node's own verdict. */
|
|
425
436
|
export const isLightmapStatic = (def: SceneNodeDef): boolean => def.lightmap ?? !movesByPhysics(def)
|
|
426
437
|
|
|
427
438
|
// `lightmap` undefined = Model.load's own default ('dynamic' while a level with a light volume runs —
|
|
@@ -860,7 +871,9 @@ const buildNodes = async (
|
|
|
860
871
|
): Promise<void> => {
|
|
861
872
|
const pending: { path: string, node: Node, def: SceneNodeDef }[] = []
|
|
862
873
|
|
|
863
|
-
|
|
874
|
+
// `parentMoves`: a child of a mover moves with it (the body under the hero's capsule) — it never
|
|
875
|
+
// bakes, whatever its own def says short of an explicit `lightmap: true`
|
|
876
|
+
const build = async (name: string, nd: SceneNodeDef, parentNode: Node | null, parentPath: string, parentMoves = false): Promise<void> => {
|
|
864
877
|
// names are path segments — '/' would fork the path, ':' would ambiguate editor card keys
|
|
865
878
|
if (name === "" || name.includes("/") || name.includes(":")) {
|
|
866
879
|
throw new Error(`Scene node name "${name}" is invalid — names are non-empty and contain no '/' or ':'`)
|
|
@@ -868,7 +881,7 @@ const buildNodes = async (
|
|
|
868
881
|
// the path is derived from def keys BEFORE any await, so it is deterministic even though
|
|
869
882
|
// Promise.all makes build completion (and record insertion) order nondeterministic
|
|
870
883
|
const path = parentPath === "" ? name : `${parentPath}/${name}`
|
|
871
|
-
const lmStatic = lm !== null && !lm.dynamicOnly && isLightmapStatic(nd)
|
|
884
|
+
const lmStatic = lm !== null && !lm.dynamicOnly && (nd.lightmap ?? (!parentMoves && isLightmapStatic(nd)))
|
|
872
885
|
// a mover in a level with a light volume reads the volume (models: the engine binds it; meshes: registered below)
|
|
873
886
|
const lmDynamic = lm !== null && !lmStatic && lm.volume
|
|
874
887
|
const node = await createSource(path, nd, lmStatic ? true : lmDynamic ? "dynamic" : undefined)
|
|
@@ -886,7 +899,7 @@ const buildNodes = async (
|
|
|
886
899
|
if (nd.prefab !== undefined) await attachPrefab(node, path, nd, scene, stack, lmStatic ? { prefix: `${lm!.prefix}${path}/`, volume: lm!.volume } : lm && lm.volume ? { prefix: `${lm.prefix}${path}/`, volume: true, dynamicOnly: true } : null)
|
|
887
900
|
pending.push({ path, node, def: nd })
|
|
888
901
|
nodes[path] = node
|
|
889
|
-
await Promise.all(Object.entries(nd.children ?? {}).map(([ childName, child ]) => build(childName, child, node, path)))
|
|
902
|
+
await Promise.all(Object.entries(nd.children ?? {}).map(([ childName, child ]) => build(childName, child, node, path, parentMoves || (lm !== null && !lmStatic))))
|
|
890
903
|
}
|
|
891
904
|
|
|
892
905
|
await Promise.all(Object.entries(defs).map(([ name, nd ]) => build(name, nd, parent, "")))
|
|
@@ -983,7 +996,7 @@ const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ sc
|
|
|
983
996
|
if (lm && lightmap) {
|
|
984
997
|
await Lightmap.load(scene, { data: lightmap.data, texture: lightmap.texture, light: lightmap.light, volume: lightmap.volume },
|
|
985
998
|
{ sunStrength: lightmap.sunStrength, ambientScale: lightmap.ambientScale, aoStrength: lightmap.aoStrength,
|
|
986
|
-
lightBoost: lightmap.lightBoost, volumeBoost: lightmap.volumeBoost })
|
|
999
|
+
lightBoost: lightmap.lightBoost, volumeBoost: lightmap.volumeBoost, occluderShadows: lightmap.occluderShadows })
|
|
987
1000
|
}
|
|
988
1001
|
// navigation: load the baked mesh — or, under `lecodes navmesh bake`, dump the level's collision
|
|
989
1002
|
// geometry for it (the static bodies exist by now; edit mode never loads it — v1)
|
|
@@ -1117,6 +1130,9 @@ export class SceneHandle<D extends SceneDef = SceneDef> {
|
|
|
1117
1130
|
scene.add(node)
|
|
1118
1131
|
applyNode(node, p.slice(p.lastIndexOf("/") + 1), d)
|
|
1119
1132
|
if (isEditMode()) addEditorMarker(node, d)
|
|
1133
|
+
// a prefab wrapper instantiates its file's nodes (same path as buildNodes; no lightmap
|
|
1134
|
+
// context — a patched node is an edit-mode rebuild, baked lighting is the level's business)
|
|
1135
|
+
if (d.prefab !== undefined) await attachPrefab(node, p, d, scene, new Set([ this.def ]), null)
|
|
1120
1136
|
attachAspects(node, p, d, nodes, scene, this._editorRuns)
|
|
1121
1137
|
nodes[p] = node
|
|
1122
1138
|
await Promise.all(Object.entries(d.children ?? {}).map(([ cn, cd ]) => build(`${p}/${cn}`, cd, node)))
|
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/scene/level.ts
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import { Node } from "../gl/Node"
|
|
16
16
|
import { Model } from "../gl/Model"
|
|
17
|
-
import { Shape } from "../gl/Shape"
|
|
18
|
-
import { Physics } from "../gl/Physics"
|
|
17
|
+
import { Shape } from "../gl/physics/Shape"
|
|
18
|
+
import { Physics } from "../gl/physics/Physics"
|
|
19
19
|
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
20
20
|
|
|
21
21
|
/** One placement handed to a `build` function (all coordinates local to the group). */
|
package/src/ui/UIImage.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { FetchResponse } from "../runtime/fetch"
|
|
|
2
2
|
import type { SvgSourceValue as SvgSource } from "../runtime/misc"
|
|
3
3
|
import type { File } from "../runtime/fetch"
|
|
4
4
|
import { Canvas, isCanvas } from "../canvas/Canvas"
|
|
5
|
+
import type { Scene2D } from "../g2/Scene2D"
|
|
5
6
|
import { Element, type Color, type ElementStyle, type UIElementBase } from "./UINode"
|
|
6
7
|
|
|
7
8
|
export type UIImageStyle = ElementStyle & { borderRadius?: number, objectFit?: "cover" | "contain" | "fill", tintColor?: Color }
|
|
@@ -10,19 +11,32 @@ export type UIImageStyle = ElementStyle & { borderRadius?: number, objectFit?: "
|
|
|
10
11
|
* docs/canvas-contract.md ("Pending"). */
|
|
11
12
|
export type CanvasImageSource = { canvasSurface: number }
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
14
|
+
/** A live 2D scene as an image source: the host draws the scene into the node's box every frame
|
|
15
|
+
* (the scene's camera, projected into the box). No intrinsic size — give the node a width and
|
|
16
|
+
* height. */
|
|
17
|
+
export type Scene2DImageSource = { scene2d: number }
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
/** Accepted image sources. A Canvas is baked to a surface marker on assign; a Scene2D becomes a
|
|
20
|
+
* live-scene marker. */
|
|
21
|
+
export type ImageSource = string | FetchResponse | File | SvgSource | Canvas | Scene2D
|
|
22
|
+
|
|
23
|
+
// An engine object that knows how to present itself as an image source (Scene2D._imageSrc). Duck-typed
|
|
24
|
+
// so the UI layer never imports an engine class — a UI-only bundle carries no engine bridge.
|
|
25
|
+
const hasImageSrc = (v: unknown): v is { _imageSrc(): Scene2DImageSource } =>
|
|
26
|
+
typeof v === "object" && v !== null && typeof (v as { _imageSrc?: unknown })._imageSrc === "function"
|
|
27
|
+
|
|
28
|
+
const toSrc = (src: ImageSource): ResolvedImageSource =>
|
|
17
29
|
// _bakeSurfaceForUI (not _bakeSurface) also flags the canvas UI-bound, so canvas.update() later
|
|
18
30
|
// refreshes this image node in place — that's what makes an animated UIImage(canvas) update.
|
|
19
|
-
isCanvas(src) ? { canvasSurface: src._bakeSurfaceForUI() } : src
|
|
31
|
+
isCanvas(src) ? { canvasSurface: src._bakeSurfaceForUI() } : hasImageSrc(src) ? src._imageSrc() : src
|
|
20
32
|
|
|
21
|
-
/** The stored/resolved image source (a Canvas is resolved to a surface marker
|
|
22
|
-
|
|
33
|
+
/** The stored/resolved image source (a Canvas is resolved to a surface marker, a Scene2D to a
|
|
34
|
+
* live-scene marker). */
|
|
35
|
+
export type ResolvedImageSource = string | FetchResponse | File | SvgSource | CanvasImageSource | Scene2DImageSource
|
|
23
36
|
|
|
24
37
|
/** An image element. Sources: URL / `asset(...)`, fetch response / File, an SVG source
|
|
25
|
-
* (`assetIcon`),
|
|
38
|
+
* (`assetIcon`), a live Canvas (`canvas.update()` refreshes the node in place), or a live Scene2D
|
|
39
|
+
* (drawn into the box every frame — a minimap). */
|
|
26
40
|
export interface UIImage extends UIElementBase<UIImageStyle> {
|
|
27
41
|
readonly type: "image",
|
|
28
42
|
/** The image source — writable to swap the picture in place. */
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { Aspect } from "../core/Aspect";
|
|
2
|
-
import type { FieldMeta } from "../core/fields";
|
|
3
|
-
import type { Node } from "./Node";
|
|
4
|
-
import type { Vehicle } from "./Vehicle";
|
|
5
|
-
export declare class Gearbox extends Aspect<"gearbox", Node> {
|
|
6
|
-
static readonly aspect = "gearbox";
|
|
7
|
-
/** Forward gear ratios, first gear first. Engine RPM = wheel speed × ratio × the differential. */
|
|
8
|
-
ratios: number[];
|
|
9
|
-
/** Reverse ratio — negative, because a ratio's SIGN is what reverse means to the engine. */
|
|
10
|
-
reverse: number;
|
|
11
|
-
/** Shift up when the engine passes this fraction of its redline. */
|
|
12
|
-
shiftUp: number;
|
|
13
|
-
/** Shift down when it falls below this fraction. */
|
|
14
|
-
shiftDown: number;
|
|
15
|
-
/** How long a shift takes, in seconds — the clutch is out for this long. */
|
|
16
|
-
shiftTime: number;
|
|
17
|
-
/** How long the clutch takes to re-engage afterwards. */
|
|
18
|
-
clutchTime: number;
|
|
19
|
-
/** How long to wait after a shift before another is allowed — without it a car sitting on a
|
|
20
|
-
* threshold hunts between two gears. */
|
|
21
|
-
latency: number;
|
|
22
|
-
/** How far the clutch bites the instant drive is asked for from rest, 0..1. It only has to COUPLE
|
|
23
|
-
* the shafts — how much torque that passes is the engine's business, so a feathered throttle
|
|
24
|
-
* still pulls away gently at the same bite point. */
|
|
25
|
-
bite: number;
|
|
26
|
-
static fields: FieldMeta<Gearbox>;
|
|
27
|
-
/** Current gear: −1 reverse, 0 neutral, 1… forward. Read it for a HUD. */
|
|
28
|
-
gear: number;
|
|
29
|
-
/** True while a shift is in progress — the clutch is out and the engine is not driving the wheels. */
|
|
30
|
-
get shifting(): boolean;
|
|
31
|
-
private _shiftLeft;
|
|
32
|
-
private _engageLeft;
|
|
33
|
-
private _waitLeft;
|
|
34
|
-
private _take;
|
|
35
|
-
/** EARLY phase: the pair has to be latched before this frame's step consumes it. */
|
|
36
|
-
updateBefore(dt: number): void;
|
|
37
|
-
/**
|
|
38
|
-
* The driver's left foot: where the clutch pedal is, ignoring shifts. Returns the new engagement.
|
|
39
|
-
*
|
|
40
|
-
* A driver does not compute anything. They watch the tacho: **revs sinking to idle means the
|
|
41
|
-
* wheels are about to drag the engine under, and that is when you push the clutch in.** That
|
|
42
|
-
* single test replaces every ratio and threshold — the crossover it finds is the exact speed where
|
|
43
|
-
* engine braking stops and the engine would start PUSHING instead, and it finds it per gear
|
|
44
|
-
* without being told the gear.
|
|
45
|
-
*
|
|
46
|
-
* The pedal comes back UP only for the throttle. Healthy revs are not on their own a reason to
|
|
47
|
-
* engage: a car standing still shows healthy revs too — the wheels simply are not turning to say
|
|
48
|
-
* otherwise — and a clutch dropped on that evidence lurches the car off with no throttle at all.
|
|
49
|
-
*
|
|
50
|
-
* Three things fall out of it for free:
|
|
51
|
-
*
|
|
52
|
-
* - **A braked car stops.** The engine cannot stall, so a clutch left in at 0 km/h feeds idle
|
|
53
|
-
* torque to the wheels forever and the car creeps against its own brakes. Here the revs pin at
|
|
54
|
-
* idle, so the pedal goes down and the brakes have nothing to fight.
|
|
55
|
-
* - **Reverse is not a special case.** No ratio takes part, so it needs no separate number — which
|
|
56
|
-
* is exactly why a fixed road-speed threshold got reverse wrong.
|
|
57
|
-
* - **Hill starts judder instead of stalling.** Let the pedal out, the load pulls the revs down,
|
|
58
|
-
* the rule pushes it straight back in, and the clutch sits slipping at the bite point until the
|
|
59
|
-
* car moves. So the test deliberately ignores the throttle — gating it on "no throttle" would
|
|
60
|
-
* stall the car under load. Throttle only holds the floor at `bite`.
|
|
61
|
-
*
|
|
62
|
-
* The cost is honest and small: once the pedal is down the revs sit at idle and cannot rise on
|
|
63
|
-
* their own, so **coasting downhill gives no engine braking until you touch the throttle** — which
|
|
64
|
-
* is precisely what a car with the clutch in does. Slipping it permanently to keep listening would
|
|
65
|
-
* bring the creep straight back.
|
|
66
|
-
*/
|
|
67
|
-
protected takeUp(v: Vehicle, dt: number): number;
|
|
68
|
-
/**
|
|
69
|
-
* Which gear the car should be in. The default is an ordinary automatic: pull away from neutral in
|
|
70
|
-
* the direction of the throttle, shift up past `shiftUp` of the redline and down below `shiftDown`,
|
|
71
|
-
* and refuse to shift again for `latency` seconds. Override this and nothing else.
|
|
72
|
-
*/
|
|
73
|
-
select(v: Vehicle): number;
|
|
74
|
-
/** Change gear now, with the clutch envelope. Safe to call from `select()` or from game code
|
|
75
|
-
* (a sequential manual is `if (Input.key('KeyE')) car.gearbox.shift(car.gearbox.gear + 1)`). */
|
|
76
|
-
shift(gear: number): void;
|
|
77
|
-
/** The ratio the engine should be running: 0 in neutral, negative in reverse. */
|
|
78
|
-
get ratio(): number;
|
|
79
|
-
/**
|
|
80
|
-
* The clutch scalar 0..1 the engine is given: the SHIFT envelope (out during a shift, ramping back
|
|
81
|
-
* over `clutchTime`) times the driver's pedal (`takeUp`). Two independent reasons for the clutch
|
|
82
|
-
* to be out, so they multiply: mid-shift is out however healthy the revs are, and an engine being
|
|
83
|
-
* dragged to idle is out whatever gear it just picked.
|
|
84
|
-
*/
|
|
85
|
-
get clutch(): number;
|
|
86
|
-
}
|