lecodes-cli 0.19.0 → 0.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/dist/index.js +730 -235
  2. package/package.json +14 -13
  3. package/runtime/scene-harness.json +1 -1
  4. package/runtime/sdk-types.json +1 -1
  5. package/src/api.ts +302 -0
  6. package/src/browserAuth.ts +87 -0
  7. package/src/cmgenTool.ts +104 -0
  8. package/src/commands/app.ts +891 -0
  9. package/src/commands/appAndroid.ts +603 -0
  10. package/src/commands/appDesktop.ts +326 -0
  11. package/src/commands/appDesktopMac.ts +470 -0
  12. package/src/commands/appIcon.ts +187 -0
  13. package/src/commands/appShared.ts +448 -0
  14. package/src/commands/appTemplates.ts +883 -0
  15. package/src/commands/appTemplatesAndroid.ts +599 -0
  16. package/src/commands/appTemplatesGradlew.ts +9 -0
  17. package/src/commands/assets.ts +28 -0
  18. package/src/commands/clone.ts +60 -0
  19. package/src/commands/compile.ts +253 -0
  20. package/src/commands/create.ts +59 -0
  21. package/src/commands/design.ts +863 -0
  22. package/src/commands/designTemplates.ts +11 -0
  23. package/src/commands/desktop.ts +122 -0
  24. package/src/commands/dev.ts +214 -0
  25. package/src/commands/diff.ts +59 -0
  26. package/src/commands/init.ts +191 -0
  27. package/src/commands/install.ts +198 -0
  28. package/src/commands/lightmap.ts +290 -0
  29. package/src/commands/link.ts +147 -0
  30. package/src/commands/login.ts +24 -0
  31. package/src/commands/navmesh.ts +225 -0
  32. package/src/commands/pn.ts +305 -0
  33. package/src/commands/projectTemplates.ts +129 -0
  34. package/src/commands/pull.ts +109 -0
  35. package/src/commands/push.ts +173 -0
  36. package/src/commands/render.ts +414 -0
  37. package/src/commands/scene.ts +148 -0
  38. package/src/commands/shaders.ts +157 -0
  39. package/src/commands/status.ts +29 -0
  40. package/src/commands/test.ts +370 -0
  41. package/src/commands/thumbs.ts +178 -0
  42. package/src/commands/types.ts +87 -0
  43. package/src/commands/update.ts +190 -0
  44. package/src/compile/assetIcons.ts +214 -0
  45. package/src/compile/collect.ts +46 -0
  46. package/src/compile/collectLocal.ts +37 -0
  47. package/src/compile/designCompile.ts +104 -0
  48. package/src/compile/fonts.ts +156 -0
  49. package/src/compile/headlessBundle.ts +129 -0
  50. package/src/compile/nativeStack.ts +21 -0
  51. package/src/compile/projectCompile.ts +109 -0
  52. package/src/compile/sceneCompile.ts +113 -0
  53. package/src/compile/screenEntry.ts +127 -0
  54. package/src/compile/shaders.ts +245 -0
  55. package/src/config.ts +42 -0
  56. package/src/designMeta.ts +35 -0
  57. package/src/desktopRenderer.ts +532 -0
  58. package/src/desktopScript.ts +276 -0
  59. package/src/dev/clientTemplates.ts +160 -0
  60. package/src/dev/devServer.ts +292 -0
  61. package/src/dev/wsServer.ts +144 -0
  62. package/src/distRoot.ts +20 -0
  63. package/src/ignore.ts +163 -0
  64. package/src/index.ts +491 -0
  65. package/src/lecodes-3d-editor.d.ts +41 -0
  66. package/src/lecodes-assets.d.ts +7 -0
  67. package/src/lecodes-design.d.ts +191 -0
  68. package/src/lecodes-renderer.d.ts +131 -0
  69. package/src/localFiles.ts +144 -0
  70. package/src/manifest.ts +46 -0
  71. package/src/matcTool.ts +137 -0
  72. package/src/peers.ts +40 -0
  73. package/src/project.ts +61 -0
  74. package/src/projectEnv.ts +94 -0
  75. package/src/qrcode-terminal.d.ts +9 -0
  76. package/src/releases.ts +125 -0
  77. package/src/serverDiff.ts +78 -0
  78. package/src/textDiff.ts +103 -0
  79. package/src/types.ts +0 -0
  80. package/src/util.ts +146 -0
  81. package/runtime/sdk/animate/animate.ts +0 -238
  82. package/runtime/sdk/animate/bezier.ts +0 -138
  83. package/runtime/sdk/animate/easings.ts +0 -126
  84. package/runtime/sdk/canvas/Canvas.ts +0 -305
  85. package/runtime/sdk/compile/aspectMacro.ts +0 -86
  86. package/runtime/sdk/compile/assetIconMacro.ts +0 -384
  87. package/runtime/sdk/compile/assetMacro.ts +0 -146
  88. package/runtime/sdk/compile/assetName.ts +0 -50
  89. package/runtime/sdk/compile/bundler.ts +0 -287
  90. package/runtime/sdk/compile/compileProject.ts +0 -129
  91. package/runtime/sdk/compile/detectEntry.ts +0 -128
  92. package/runtime/sdk/compile/fontMacro.ts +0 -459
  93. package/runtime/sdk/compile/fontRegistry.ts +0 -78
  94. package/runtime/sdk/compile/header.ts +0 -67
  95. package/runtime/sdk/compile/index.ts +0 -100
  96. package/runtime/sdk/compile/libraryImports.ts +0 -52
  97. package/runtime/sdk/compile/liteMaterial.ts +0 -247
  98. package/runtime/sdk/compile/sceneEditor.ts +0 -88
  99. package/runtime/sdk/compile/serverSplit.ts +0 -233
  100. package/runtime/sdk/compile/serverTypes.ts +0 -227
  101. package/runtime/sdk/compile/sfnt.ts +0 -98
  102. package/runtime/sdk/compile/shaderSchema.ts +0 -202
  103. package/runtime/sdk/compile/shaderTargets.ts +0 -81
  104. package/runtime/sdk/compile/sourcemap.ts +0 -25
  105. package/runtime/sdk/core/Aspect.ts +0 -568
  106. package/runtime/sdk/core/InspectorUI.ts +0 -212
  107. package/runtime/sdk/core/StateMachine.ts +0 -308
  108. package/runtime/sdk/core/color.ts +0 -66
  109. package/runtime/sdk/core/compWrite.ts +0 -42
  110. package/runtime/sdk/core/events.ts +0 -38
  111. package/runtime/sdk/core/fields.ts +0 -120
  112. package/runtime/sdk/core/registry.ts +0 -23
  113. package/runtime/sdk/core/signals.ts +0 -277
  114. package/runtime/sdk/core/time.ts +0 -84
  115. package/runtime/sdk/g2/Camera2D.ts +0 -40
  116. package/runtime/sdk/g2/CharacterController2D.ts +0 -276
  117. package/runtime/sdk/g2/Node2D.ts +0 -267
  118. package/runtime/sdk/g2/OneWay2D.ts +0 -66
  119. package/runtime/sdk/g2/Physics2D.ts +0 -346
  120. package/runtime/sdk/g2/Scene2D.ts +0 -209
  121. package/runtime/sdk/g2/Shape2D.ts +0 -259
  122. package/runtime/sdk/g2/Sprite.ts +0 -89
  123. package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
  124. package/runtime/sdk/g2/SpriteSheet.ts +0 -166
  125. package/runtime/sdk/g2/Texture2D.ts +0 -47
  126. package/runtime/sdk/g2/Tilemap.ts +0 -41
  127. package/runtime/sdk/g2/Tileset.ts +0 -71
  128. package/runtime/sdk/g2/Trigger2D.ts +0 -77
  129. package/runtime/sdk/g2/autotile.ts +0 -433
  130. package/runtime/sdk/g2/cells.ts +0 -91
  131. package/runtime/sdk/g2/defineScene2d.ts +0 -381
  132. package/runtime/sdk/g2/groups2d.ts +0 -106
  133. package/runtime/sdk/g2/loop.ts +0 -50
  134. package/runtime/sdk/g2/scenarios2d.ts +0 -69
  135. package/runtime/sdk/g2/touch.ts +0 -83
  136. package/runtime/sdk/gl/Camera.ts +0 -160
  137. package/runtime/sdk/gl/CameraPlace.ts +0 -52
  138. package/runtime/sdk/gl/CharacterController.ts +0 -238
  139. package/runtime/sdk/gl/Gearbox.ts +0 -212
  140. package/runtime/sdk/gl/Geometry.ts +0 -279
  141. package/runtime/sdk/gl/IK.ts +0 -193
  142. package/runtime/sdk/gl/InstancedMesh.ts +0 -132
  143. package/runtime/sdk/gl/Light.ts +0 -107
  144. package/runtime/sdk/gl/Lightmap.ts +0 -256
  145. package/runtime/sdk/gl/Material.ts +0 -260
  146. package/runtime/sdk/gl/Mesh.ts +0 -83
  147. package/runtime/sdk/gl/Model.ts +0 -124
  148. package/runtime/sdk/gl/NavAgent.ts +0 -337
  149. package/runtime/sdk/gl/NavMesh.ts +0 -397
  150. package/runtime/sdk/gl/Node.ts +0 -350
  151. package/runtime/sdk/gl/Noise.ts +0 -30
  152. package/runtime/sdk/gl/Particles.ts +0 -676
  153. package/runtime/sdk/gl/Physics.ts +0 -222
  154. package/runtime/sdk/gl/Plane.ts +0 -53
  155. package/runtime/sdk/gl/Ragdoll.ts +0 -270
  156. package/runtime/sdk/gl/Ray.ts +0 -16
  157. package/runtime/sdk/gl/Scene.ts +0 -486
  158. package/runtime/sdk/gl/Shape.ts +0 -409
  159. package/runtime/sdk/gl/Terrain.ts +0 -1092
  160. package/runtime/sdk/gl/Texture.ts +0 -63
  161. package/runtime/sdk/gl/Trigger.ts +0 -45
  162. package/runtime/sdk/gl/Vehicle.ts +0 -473
  163. package/runtime/sdk/gl/Wheel.ts +0 -240
  164. package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
  165. package/runtime/sdk/gl/animation/Animator.ts +0 -120
  166. package/runtime/sdk/gl/animation/Layer.ts +0 -31
  167. package/runtime/sdk/gl/animation/Loop.ts +0 -30
  168. package/runtime/sdk/gl/animation/Playback.ts +0 -44
  169. package/runtime/sdk/gl/animation/core.ts +0 -336
  170. package/runtime/sdk/gl/controls.ts +0 -95
  171. package/runtime/sdk/gl/physicsEvents.ts +0 -20
  172. package/runtime/sdk/gl/scenarios.ts +0 -291
  173. package/runtime/sdk/gl/state.ts +0 -6
  174. package/runtime/sdk/gl/terrainMesh.ts +0 -219
  175. package/runtime/sdk/gl/touch.ts +0 -68
  176. package/runtime/sdk/inject.ts +0 -197
  177. package/runtime/sdk/math/Mathf.ts +0 -118
  178. package/runtime/sdk/math/mat4.ts +0 -278
  179. package/runtime/sdk/math/quat.ts +0 -232
  180. package/runtime/sdk/math/vec.ts +0 -255
  181. package/runtime/sdk/net/codec.ts +0 -119
  182. package/runtime/sdk/net/core.ts +0 -384
  183. package/runtime/sdk/net/index.ts +0 -181
  184. package/runtime/sdk/net/replication.ts +0 -622
  185. package/runtime/sdk/plugins/camera.ts +0 -81
  186. package/runtime/sdk/plugins/geolocation.ts +0 -123
  187. package/runtime/sdk/plugins/oauth.ts +0 -61
  188. package/runtime/sdk/plugins/permission.ts +0 -7
  189. package/runtime/sdk/plugins/push.ts +0 -132
  190. package/runtime/sdk/plugins/qr.ts +0 -73
  191. package/runtime/sdk/plugins/service.ts +0 -47
  192. package/runtime/sdk/runtime/app.ts +0 -101
  193. package/runtime/sdk/runtime/appEvents.ts +0 -54
  194. package/runtime/sdk/runtime/channel.ts +0 -61
  195. package/runtime/sdk/runtime/clipboard.ts +0 -20
  196. package/runtime/sdk/runtime/datetime.ts +0 -329
  197. package/runtime/sdk/runtime/device.ts +0 -293
  198. package/runtime/sdk/runtime/fetch.ts +0 -77
  199. package/runtime/sdk/runtime/files.ts +0 -23
  200. package/runtime/sdk/runtime/input.ts +0 -175
  201. package/runtime/sdk/runtime/media.ts +0 -111
  202. package/runtime/sdk/runtime/misc.ts +0 -16
  203. package/runtime/sdk/runtime/net.ts +0 -36
  204. package/runtime/sdk/runtime/rpc.ts +0 -218
  205. package/runtime/sdk/runtime/service.ts +0 -83
  206. package/runtime/sdk/runtime/share.ts +0 -9
  207. package/runtime/sdk/runtime/storage.ts +0 -13
  208. package/runtime/sdk/runtime/touch.ts +0 -76
  209. package/runtime/sdk/scene/defineScene.ts +0 -1353
  210. package/runtime/sdk/scene/editorPlugins.ts +0 -110
  211. package/runtime/sdk/scene/gizmos.ts +0 -148
  212. package/runtime/sdk/scene/grammar.ts +0 -120
  213. package/runtime/sdk/scene/material.ts +0 -188
  214. package/runtime/sdk/server/auth/appConfig.ts +0 -12
  215. package/runtime/sdk/server/auth/global.ts +0 -80
  216. package/runtime/sdk/server/auth/host.ts +0 -318
  217. package/runtime/sdk/server/auth/models.ts +0 -83
  218. package/runtime/sdk/server/auth/types.ts +0 -50
  219. package/runtime/sdk/server/channel.ts +0 -56
  220. package/runtime/sdk/server/context.ts +0 -36
  221. package/runtime/sdk/server/db/defineDb.ts +0 -237
  222. package/runtime/sdk/server/db/fields.ts +0 -132
  223. package/runtime/sdk/server/db/httpTransport.ts +0 -93
  224. package/runtime/sdk/server/db/index.ts +0 -7
  225. package/runtime/sdk/server/db/marci/query.ts +0 -412
  226. package/runtime/sdk/server/db/types.ts +0 -202
  227. package/runtime/sdk/server/errors.ts +0 -12
  228. package/runtime/sdk/server/host.ts +0 -74
  229. package/runtime/sdk/server/inject.ts +0 -13
  230. package/runtime/sdk/server/runtime.ts +0 -133
  231. package/runtime/sdk/server/validate.ts +0 -87
  232. package/runtime/sdk/ui/NativeView.ts +0 -142
  233. package/runtime/sdk/ui/UI.ts +0 -39
  234. package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
  235. package/runtime/sdk/ui/UIButton.ts +0 -101
  236. package/runtime/sdk/ui/UIContainer.ts +0 -60
  237. package/runtime/sdk/ui/UIImage.ts +0 -83
  238. package/runtime/sdk/ui/UIInput.ts +0 -185
  239. package/runtime/sdk/ui/UIModal.ts +0 -139
  240. package/runtime/sdk/ui/UINode.ts +0 -830
  241. package/runtime/sdk/ui/UIPager.ts +0 -362
  242. package/runtime/sdk/ui/UIPopover.ts +0 -100
  243. package/runtime/sdk/ui/UIScreen.ts +0 -123
  244. package/runtime/sdk/ui/UIScrollable.ts +0 -87
  245. package/runtime/sdk/ui/UISpacer.ts +0 -14
  246. package/runtime/sdk/ui/UITabs.ts +0 -236
  247. package/runtime/sdk/ui/UIText.ts +0 -51
  248. package/runtime/sdk/ui/UIVideo.ts +0 -88
  249. package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
  250. package/runtime/sdk/ui/UIWidget.ts +0 -127
  251. package/runtime/sdk/ui/fonts.ts +0 -13
  252. package/runtime/sdk/ui/presentable.ts +0 -117
  253. package/runtime/sdk/ui/router.ts +0 -132
  254. package/runtime/sdk/ui/theme.ts +0 -84
@@ -1,63 +0,0 @@
1
- // A 3D GPU texture, decoded natively by creator-gl. Load from a URL or an already-fetched response.
2
-
3
- import { fetch, type FetchResponse, type File } from "../runtime/fetch"
4
-
5
- export class Texture {
6
- /** @internal native texture handle. */
7
- readonly _id: number
8
- readonly width: number
9
- readonly height: number
10
-
11
- /** Horizontal wrap mode (U). */
12
- wrapS = 1
13
- /** Vertical wrap mode (V). */
14
- wrapT = 1
15
-
16
- constructor(width: number, height: number, id: number) {
17
- this.width = width
18
- this.height = height
19
- this._id = id
20
- }
21
-
22
- // Wrap a Canvas's baked surface as a 3D texture (RGBA8, already rasterized → synchronous, no decode).
23
- // The id is cached on the canvas (opaque _tex3dId), so backing several materials shares one texture and
24
- // canvas.update() re-uploads it. Structural type avoids a Canvas import (keeps 3D free of 2D-canvas code).
25
- static fromCanvas(canvas: { _bakeSurface(): number, width: number, height: number, pixelRatio: number, _tex3dId: number }): Texture {
26
- if (!canvas._tex3dId) {
27
- const surfaceId = canvas._bakeSurface()
28
- canvas._tex3dId = _creator.createTextureFromCanvas(surfaceId)
29
- }
30
- return new Texture(Math.round(canvas.width * canvas.pixelRatio), Math.round(canvas.height * canvas.pixelRatio), canvas._tex3dId)
31
- }
32
-
33
- /** A texture from raw pixels: `channels` (1–4, default 4) UBYTE per pixel, row-major, `width ×
34
- * height × channels` bytes; `srgb` (default true for colour) picks the sRGB internal format —
35
- * pass false for data (a heightmap, a mask). No mips. Throws on a host without the call. */
36
- static fromPixels(width: number, height: number, data: Uint8Array, opts: { channels?: 1 | 2 | 3 | 4, srgb?: boolean } = {}): Texture {
37
- if (!_creator.createTexturePixels) throw new Error("Texture.fromPixels needs a newer host")
38
- const channels = opts.channels ?? 4
39
- if (data.length < width * height * channels) throw new Error(`Texture.fromPixels: ${data.length} bytes for ${width}×${height}×${channels}`)
40
- const id = _creator.createTexturePixels(width, height, channels, data, opts.srgb ?? true)
41
- if (id === 0xFFFFFFFF) throw new Error("Texture.fromPixels: the host refused the texture")
42
- return new Texture(width, height, id)
43
- }
44
-
45
- /** Re-upload a rectangle of a `fromPixels` texture (same channel count). */
46
- update(x: number, y: number, width: number, height: number, data: Uint8Array): void {
47
- _creator.updateTexturePixels?.(this._id, x, y, width, height, data)
48
- }
49
-
50
- static load(source: string | FetchResponse | File): Promise<Texture> {
51
- if (typeof source === "string") {
52
- return fetch(source, { useOnce: true }).then((resp) => {
53
- if (resp.status >= 400) {
54
- return Promise.reject(new Error(`Failed to fetch texture from ${source}. HTTP ${resp.status}`))
55
- }
56
- return Texture.load(resp)
57
- })
58
- }
59
- return new Promise<Texture>((resolve, reject) => {
60
- _creator.createTexture((source as any)._id, (id, w, h) => resolve(new Texture(w, h, id)), reject)
61
- })
62
- }
63
- }
@@ -1,45 +0,0 @@
1
- // A trigger zone, as an aspect on a 3D Node. Requires a Shape (its geometry); creates a static SENSOR
2
- // body from it, so a physics body overlapping the zone fires the node's 'enter' / 'exit' events
3
- // instead of colliding. A trigger does not block movement, and is still pointer-pickable.
4
- //
5
- // const goal = new Mesh(box(), material)
6
- // .aspect(Shape, { box: [1, 2, 1] })
7
- // .aspect(Trigger)
8
- // goal.addEventListener('enter', other => win(other))
9
- // goal.addEventListener('exit', other => …)
10
-
11
- import { Aspect } from "../core/Aspect"
12
- import { Shape } from "./Shape"
13
- import { ensurePhysicsEvents } from "./physicsEvents"
14
- import type { Node } from "./Node"
15
-
16
- export class Trigger extends Aspect<"trigger", Node> {
17
- static readonly aspect = "trigger"
18
-
19
- private _bodyId = 0
20
- /** Native body id (0 if no physics support). */
21
- get id(): number { return this._bodyId }
22
-
23
- onAttach(): void {
24
- if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
25
- const shape = this.node.get(Shape)
26
- if (!shape) {
27
- throw new Error("Trigger requires a Shape aspect — add it first: node.aspect(Shape, {…}).aspect(Trigger)")
28
- }
29
- const shapeId = shape._claim()
30
- this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, 0 /* static */, 0, true /* sensor */, false)
31
- shape._ownBody(this._bodyId)
32
- ensurePhysicsEvents() // routes overlap events → the node's 'enter' / 'exit'
33
- }
34
-
35
- onDetach(): void {
36
- if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
37
- const shape = this.node.get(Shape)
38
- if (shape) {
39
- shape._ownBody(0) // the Shape held OUR body id — forget it, or its own detach removes it twice (native crash)
40
- shape._recreatePickBody()
41
- }
42
- }
43
-
44
- // No moveTo(): `node.position = p` moves the node AND the sensor body it owns (see Node._xf).
45
- }
@@ -1,473 +0,0 @@
1
- // A car, as an aspect on a 3D Node, backed by Jolt's wheeled-vehicle constraint in creator-gl. The
2
- // node is the CHASSIS: it needs a Shape (its collision body) and must NOT also carry a Physics aspect
3
- // — Vehicle owns its rigid body, the way CharacterController owns its controller.
4
- //
5
- // const car = new Node('car')
6
- // .add(body, wheelFL.aspect(Wheel), wheelFR.aspect(Wheel),
7
- // wheelRL.aspect(Wheel), wheelRR.aspect(Wheel))
8
- // .aspect(Shape, { box: [0.9, 0.4, 2.1] })
9
- // .aspect(Vehicle, { mass: 1200 })
10
- //
11
- // setLoop(() => {
12
- // const gas = (Input.key('KeyW') ? 1 : 0) - (Input.key('KeyS') ? 1 : 0)
13
- // car.vehicle.drive(gas, (Input.key('KeyD') ? 1 : 0) - (Input.key('KeyA') ? 1 : 0), 0, Input.key('Space') ? 1 : 0)
14
- // hud.text = `${Math.round(car.vehicle.speed * 3.6)} km/h gear ${car.gearbox.gear}`
15
- // })
16
- //
17
- // This aspect is the car as a WHOLE: mass, engine, steering, aero. Anything per-wheel lives on the
18
- // `Wheel` aspect of the wheel's own node (which is where Jolt keeps it too), and the gearbox is the
19
- // `Gearbox` aspect — policy, not physics. Vehicle collects every Wheel under itself, groups them into
20
- // axles by Z, and installs a default Gearbox if the node has none.
21
- //
22
- // Chassis space is forward −Z / up +Y — the SDK's own convention, the one `node.forward` reports: if
23
- // the model faces the other way, rotate the MODEL (a child), never the chassis node. Physics owns the
24
- // chassis transform: drive it with `drive()`, never per-frame `node.position` writes.
25
- //
26
- // Layering, and why it is where it is: docs/vehicle-rework-plan.md.
27
-
28
- import { Aspect } from "../core/Aspect"
29
- import type { FieldMeta } from "../core/fields"
30
- import { Mat4 } from "../math/mat4"
31
- import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
32
- import { cw, type QuatLike } from "../math/quat"
33
- import { Gizmos } from "../scene/gizmos"
34
- import { glState } from "./state"
35
- import { Gearbox } from "./Gearbox"
36
- import { Node } from "./Node"
37
- import { DEFAULT_FRICTION, Physics } from "./Physics"
38
- import { Shape } from "./Shape"
39
- import { STATE_HEADER, WHEEL_STATE, Wheel } from "./Wheel"
40
-
41
- /** Which axle the engine drives. A wheel's own `drive` overrides it. */
42
- export type DriveLayout = "rear" | "front" | "all"
43
-
44
- /**
45
- * How an axle's two wheels share torque.
46
- * - `lsd` — limited slip (Jolt's 1.4 ratio), the sane default.
47
- * - `open` — fully open: a lifted or spinning wheel takes ALL the torque.
48
- * - `locked` — both wheels forced to the same speed. Breaks traction predictably: the drift diff.
49
- * A number is the raw max/min wheel-speed ratio (> 1; smaller is more locked).
50
- */
51
- export type DifferentialMode = "lsd" | "open" | "locked"
52
-
53
- export interface EngineConfig {
54
- /** Peak torque in N·m (default 500). The single biggest "how fast is it" knob. */
55
- torque?: number
56
- /** Redline in RPM (default 6000). */
57
- maxRpm?: number
58
- /** Idle RPM (default 1000) — the engine never falls below it. */
59
- idleRpm?: number
60
- /**
61
- * The engine's CHARACTER: a flat list `[rpm, torque, rpm, torque, …]` in NORMALISED units — x is
62
- * a fraction of the redline, y a fraction of `torque`. Left out, Jolt's own broad curve is used.
63
- *
64
- * `[0, 0.8, 0.66, 1, 1, 0.8]` is an ordinary petrol engine (peak two thirds up, falling to the
65
- * redline); `[0, 1, 1, 1]` is flat, which is what an electric motor feels like.
66
- */
67
- curve?: number[]
68
- /** Moment of inertia in kg·m² (default 0.5) — how eagerly it revs when the clutch is out. */
69
- inertia?: number
70
- /** Engine braking: angular damping of the engine itself (default 0.2). */
71
- braking?: number
72
- }
73
-
74
- export interface SteeringConfig {
75
- /** Steering lock in degrees at a standstill (default 35). */
76
- lock?: number
77
- /**
78
- * The lock still available, in degrees, once the car reaches `speed` (default 10) — what stops a
79
- * car over-driving its own front tires.
80
- *
81
- * A tire makes its peak grip at a few degrees of slip angle, and the steering angle a corner needs
82
- * falls with the SQUARE of speed. Holding full lock at 100 km/h therefore asks the front for
83
- * several times what it can give: the tires saturate, the car ploughs on, and the wheel stops
84
- * answering. In between the lock eases on `(1 − v/speed)²`, which tracks the real curve closely.
85
- * `0` turns the taper off — full lock at any speed.
86
- */
87
- atSpeed?: number
88
- /** The speed in m/s at which the lock has fallen to `atSpeed` (default 30 ≈ 108 km/h). */
89
- speed?: number
90
- /** How fast the front wheels may turn, in degrees per second (default 220). A keyboard asks for
91
- * full lock in one frame, which no steering wheel can do, and that step alone snaps a tire past
92
- * its peak. `0` = instantly. */
93
- rate?: number
94
- }
95
-
96
- /**
97
- * Aerodynamics. Both numbers are quoted the same way — **a fraction of the car's own weight at
98
- * 30 m/s (108 km/h)** — and both grow with the SQUARE of speed from there, so `0.5` at 30 m/s is
99
- * `2.0` at 60 m/s. One mental model for both, and no drag coefficients or frontal areas to invent.
100
- */
101
- export interface AeroConfig {
102
- /**
103
- * Downforce (default 0 — a car with no wing). The knob that decides whether a car corners better
104
- * or worse the faster it goes: tire grip is a constant, but the force a corner demands grows with
105
- * v², so with no downforce every car runs out of road at speed. `0.6` roughly doubles a car's
106
- * cornering at 150 km/h; a road car is nearer `0.1`, a race car well past `1`.
107
- *
108
- * It presses along the CHASSIS' own down axis (that is what keeps a car planted over a crest) and
109
- * only while a wheel is on the ground — a car on its roof is never launched by its own wing.
110
- */
111
- downforce?: number
112
- /** Air resistance (default 0.03 — an ordinary car). What gives a car a top speed at all. */
113
- drag?: number
114
- }
115
-
116
- /** Blob v5 — see bridges.d.ts for the field-by-field layout. */
117
- const BLOB_VERSION = 5
118
- const BLOB_HEADER = 21
119
- const BLOB_WHEEL = 15
120
- const DIFFERENTIALS: Record<DifferentialMode, number> = { lsd: 1.4, open: 0, locked: 1 }
121
- const GRAVITY = 9.81 // only used to place the wheels at their resting height
122
- const AXLE_EPS = 0.35 // wheels within this many metres in Z are the same axle
123
- const clamp = (v: number, lo: number, hi: number): number => (v < lo ? lo : v > hi ? hi : v)
124
-
125
- export class Vehicle extends Aspect<"vehicle", Node> {
126
- static readonly aspect = "vehicle"
127
-
128
- /** Chassis mass in kg (default 1200). Wheels are massless in this model. */
129
- mass = 1200
130
- /** Centre of mass height in chassis space (metres, negative = lower). Left out, it drops to halfway
131
- * down the chassis shape — a car with its mass at the shape's centre tips over. */
132
- centerOfMass?: number
133
- /** Which axle the engine drives (default "rear"). A wheel's own `drive` overrides it. */
134
- driveAxle: DriveLayout = "rear"
135
- /** Differential lock across each driven axle. `locked` is the drift setup — both wheels turn
136
- * together, so the pair lets go as a pair. Live: re-configure the aspect to switch it mid-drive. */
137
- differential: DifferentialMode | number = "lsd"
138
- /**
139
- * Anti-roll bars between each axle's wheels — what stops a car rolling onto its inside wheels in a
140
- * fast corner. `true` (default) is a bar 0.01 as stiff as the wheel's own spring; a number is that
141
- * ratio directly, and `0` / `false` removes the bars.
142
- *
143
- * **The numbers are far smaller than they look and the gain is steep** — Jolt's bar feeds
144
- * `lengthDifference × stiffness × dt` as a bias into the suspension's own soft constraint, so 4%
145
- * of the spring already flattens the body. Measured on a 1250 kg car: `0` → 1.3° of roll at 0.86 g,
146
- * `0.01` → 0.6°, `0.15` → 0.2° (flat), `0.5` → the car HOPS.
147
- *
148
- * This is the knob for how much a car visibly LEANS, not for wheel lift: bars change how far the
149
- * body tips, not how much load transfers. For lift, lower `centerOfMass`.
150
- */
151
- antiRoll: boolean | number = true
152
- /** Pitch/roll limit in degrees (default 60) before the constraint rights the car. 180 turns it off. */
153
- maxTilt = 60
154
- /** Friction of the CHASSIS body itself — how it slides when it lands on its roof or side. Nothing
155
- * to do with the tires; those are the `Wheel`'s `tire.grip` × the ground's friction. */
156
- friction = DEFAULT_FRICTION
157
- /** Strength of the clutch when fully engaged (default 10). Small values slip; large ones weld the
158
- * engine to the wheels. The `Gearbox` scales it 0..1 while shifting. */
159
- clutchStrength = 10
160
-
161
- engine: EngineConfig = { torque: 500, maxRpm: 6000, idleRpm: 1000, inertia: 0.5, braking: 0.2 }
162
- steering: SteeringConfig = { lock: 35, atSpeed: 10, speed: 30, rate: 220 }
163
- aero: AeroConfig = { downforce: 0, drag: 0.03 }
164
-
165
- /** Scene-editor inspector: the flat, tunable half. The grouped objects are edited in code. */
166
- static fields: FieldMeta<Vehicle> = {
167
- mass: { min: 1, step: 10 },
168
- driveAxle: { label: "Drive", options: [ "rear", "front", "all" ] },
169
- differential: { options: [ "lsd", "open", "locked" ] },
170
- antiRoll: { label: "Anti-roll" },
171
- maxTilt: { label: "Max tilt", min: 0, max: 180, step: 5 },
172
- friction: { label: "Chassis friction", min: 0, max: 2, step: 0.05 },
173
- clutchStrength: { label: "Clutch", min: 0.5, max: 100, step: 0.5 },
174
- engine: { hidden: true },
175
- steering: { hidden: true },
176
- aero: { hidden: true },
177
- }
178
- static editor = { rebuild: true }
179
-
180
- private _id = 0
181
- private _bodyId = 0
182
- private _builtMass = 0
183
- private _wheels: Wheel[] = []
184
- private _state = new Float32Array(STATE_HEADER)
185
- private _stateFrame = -1
186
- private _throttle = 0
187
- private _steer = 0
188
- private _brake = 0
189
- private _handbrake = 0
190
-
191
- onAttach(): void {
192
- if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
193
- if (!_creator.vehicleCreate) {
194
- console.warn("Vehicle needs a newer host (no vehicleCreate) — the car will not move")
195
- return
196
- }
197
- const shape = this.node.get(Shape)
198
- if (!shape) {
199
- throw new Error("Vehicle requires a Shape aspect (the chassis collider) — add it first: node.aspect(Shape, { box: […] }).aspect(Vehicle, {…})")
200
- }
201
- if (shape._isTriangleMesh) {
202
- throw new Error("Vehicle: the chassis cannot use Shape { mesh: true } (a triangle mesh can't be a dynamic body) — use { mesh: 'convex' } or a box")
203
- }
204
- if (this.node.has(Physics)) {
205
- throw new Error("Vehicle owns its own rigid body — remove the Physics aspect from the chassis node (put `mass` on the Vehicle instead)")
206
- }
207
-
208
- this._wheels = this._collectWheels()
209
- if (this._wheels.length === 0) {
210
- throw new Error("Vehicle found no wheels — put a Wheel aspect on each wheel node under the chassis: wheelFL.aspect(Wheel)")
211
- }
212
-
213
- const shapeId = shape._claim() // the vehicle owns the shape; drop the pick-only body
214
- this._id = _creator.vehicleCreate(this.node.id, shapeId, this._buildSettings())
215
- if (!this._id) {
216
- console.warn(`Vehicle: the engine refused the settings for "${this.node.name}" — check the wheel list`)
217
- shape._recreatePickBody()
218
- return
219
- }
220
- this._bodyId = _creator.vehicleBodyId ? _creator.vehicleBodyId(this._id) : 0
221
- // The chassis body owns the node's transform, so route position writes to it (see Node._xf) —
222
- // `car.position = p` places the car instead of being silently overwritten by the next sync. It is
223
- // the WEAK form of the operation: it keeps the car's velocity and its roll, so a respawn still
224
- // wants `reset(p)`, which also uprights it and clears engine, gearbox and wheel spin.
225
- this.node._xf = this._bodyId
226
- this.node._xfKind = this._bodyId ? 2 : 0
227
- if (this._bodyId && this.friction !== DEFAULT_FRICTION) {
228
- _creator.physicsSetFriction?.(this._bodyId, this.friction)
229
- }
230
- this._builtMass = this.mass
231
- this._state = new Float32Array(STATE_HEADER + this._wheels.length * WHEEL_STATE)
232
- // A car with no gearbox still has to drive: install the default automatic.
233
- if (!this.node.has(Gearbox)) this.node.aspect(Gearbox)
234
- }
235
-
236
- onDetach(): void {
237
- if (this._id) { _creator.vehicleDestroy(this._id); this._id = 0; this._bodyId = 0 }
238
- for (const w of this._wheels) w._vehicle = null
239
- this._wheels = []
240
- this.node._xf = 0
241
- this.node._xfKind = 0
242
- this.node.get(Shape)?._recreatePickBody() // hands the routing back to the pick body
243
- }
244
-
245
- /**
246
- * Re-configuring an attached vehicle retunes it in place — the way a drift button switches the
247
- * differential without the car so much as blinking:
248
- *
249
- * car.aspect(Vehicle, { differential: drift ? 'locked' : 'lsd' })
250
- *
251
- * Live: `differential`, `engine`, `steering`, `aero`, `maxTilt`, `friction`, `clutchStrength`, and
252
- * every `Wheel`'s `tire.grip` and brake torques. Everything else describes the car's STRUCTURE —
253
- * `mass`, `centerOfMass`, which wheels exist and where, their suspension, `driveAxle`, `antiRoll` — and
254
- * is baked into the constraint at attach; to change one of those, re-attach the aspect.
255
- */
256
- onReconfigure(): void {
257
- if (!this._id) return
258
- if (this.mass !== this._builtMass) {
259
- console.warn(`Vehicle: mass is fixed once the body exists (still ${this._builtMass} kg) — re-add the aspect to change it`)
260
- this.mass = this._builtMass
261
- }
262
- _creator.vehicleSetTuning?.(this._id, this._buildSettings())
263
- if (this._bodyId) _creator.physicsSetFriction?.(this._bodyId, this.friction)
264
- }
265
-
266
- /** Native vehicle id (0 until attached / no physics support). */
267
- get id(): number { return this._id }
268
- /** The chassis rigid-body id — the same handle the plain body calls take. */
269
- get bodyId(): number { return this._bodyId }
270
- /** The wheels, in blob order (front axle first). */
271
- get wheels(): readonly Wheel[] { return this._wheels }
272
-
273
- // --- driving --------------------------------------------------------------------------------
274
-
275
- /**
276
- * The driver's controls, all in [-1, 1] (brakes in [0, 1]). Sticky: the last values keep applying
277
- * until the next call, so a held key does not need re-sending. `forward < 0` asks for reverse; the
278
- * `Gearbox` engages it once the car has stopped.
279
- */
280
- drive(forward: number, steer = 0, brake = 0, handbrake = 0): this {
281
- if (this._id) {
282
- this._throttle = clamp(forward, -1, 1)
283
- this._steer = clamp(steer, -1, 1)
284
- this._brake = clamp(brake, 0, 1)
285
- this._handbrake = clamp(handbrake, 0, 1)
286
- _creator.vehicleSetInput(this._id, this._throttle, this._steer, this._brake, this._handbrake)
287
- }
288
- return this
289
- }
290
-
291
- /** Teleport upright and clear all motion (velocity, engine, gearbox, wheel spin). Defaults to the
292
- * chassis node's current pose — `reset()` alone un-flips a car where it lies. */
293
- reset(position?: Vec3Like, rotation?: QuatLike): this {
294
- if (!this._id) return this
295
- const p = position ?? this.node.worldPosition
296
- // No rotation given: keep the heading, drop the roll/pitch — the point of reset is to land upright.
297
- const wq = this.node.worldQuaternion
298
- const q: QuatLike = rotation ?? [ 0, wq.y, 0, wq.w ]
299
- _creator.vehicleReset(this._id, cx(p), cy(p), cz(p), cx(q), cy(q), cz(q), cw(q))
300
- this._stateFrame = -1
301
- const box = this.node.get(Gearbox)
302
- if (box) box.gear = 0
303
- return this
304
- }
305
-
306
- /** Push the chassis (a boost pad, an explosion) — an instantaneous impulse in kg·m/s. */
307
- applyImpulse(v: Vec3Like): this {
308
- if (this._bodyId) _creator.physicsApplyImpulse(this._bodyId, cx(v), cy(v), cz(v))
309
- return this
310
- }
311
-
312
- /** @internal The Gearbox's latched pair: the one ratio the engine runs, and the clutch envelope. */
313
- _setTransmission(ratio: number, clutch: number): void {
314
- if (this._id) _creator.vehicleSetTransmission(this._id, ratio, clamp(clutch, 0, 1))
315
- }
316
-
317
- // --- readouts (one native read per frame, shared by the car and every wheel) -------------------
318
-
319
- private _read(): Float32Array {
320
- if (this._id && this._stateFrame !== glState.frame) {
321
- _creator.vehicleGetState(this._id, this._state)
322
- this._stateFrame = glState.frame
323
- }
324
- return this._state
325
- }
326
-
327
- /** @internal One value out of wheel `index`'s slice — see WHEEL_STATE in Wheel.ts. */
328
- _wheelState(index: number, k: number): number {
329
- const s = this._read()
330
- const at = STATE_HEADER + index * WHEEL_STATE + k
331
- return at < s.length ? s[at]! : 0
332
- }
333
-
334
- /** Speed along the car's forward axis in m/s — negative when reversing (× 3.6 for km/h). */
335
- get speed(): number { return this._read()[0]! }
336
- /** Engine revolutions per minute. */
337
- get rpm(): number { return this._read()[1]! }
338
- /** How many wheels are touching the ground. */
339
- get wheelsOnGround(): number { return this._read()[2]! }
340
- /** Any wheel on the ground — false while airborne. */
341
- get grounded(): boolean { return this._read()[2]! > 0 }
342
- /** Chassis velocity in world units/s (fresh Vec3). */
343
- get velocity(): Vec3 { const s = this._read(); return new Vec3(s[3]!, s[4]!, s[5]!) }
344
- /** The last throttle handed to `drive()` — what the gearbox reads to know which way you want to go. */
345
- get throttle(): number { return this._throttle }
346
- /** The last steer input handed to `drive()`, [-1, 1]. */
347
- get steerInput(): number { return this._steer }
348
- /** The last foot-brake input handed to `drive()`, [0, 1] — the pedal, not the resulting force. */
349
- get brake(): number { return this._brake }
350
- /** The last handbrake input handed to `drive()`, [0, 1]. */
351
- get handbrake(): number { return this._handbrake }
352
- /** Current gear from the attached `Gearbox`: −1 reverse, 0 neutral, 1… forward. */
353
- get gear(): number { return this.node.get(Gearbox)?.gear ?? 0 }
354
- /** The steering lock, in degrees, the car is allowed at its current speed — the full lock at rest,
355
- * falling to `steering.atSpeed`. The same curve the engine applies; useful on a HUD. */
356
- get steerLock(): number {
357
- const lock = this.steering.lock ?? 35
358
- const atSpeed = Math.max(0, this.steering.atSpeed ?? 10)
359
- if (atSpeed <= 0 || atSpeed >= lock) return lock
360
- const full = this.steering.speed ?? 30
361
- const t = full > 0 ? Math.min(Math.abs(this.speed) / full, 1) : 1
362
- return atSpeed + (lock - atSpeed) * (1 - t) * (1 - t)
363
- }
364
-
365
- // --- setup ----------------------------------------------------------------------------------
366
-
367
- /** Every Wheel under the chassis, ordered front axle first, with its axle group resolved. */
368
- private _collectWheels(): Wheel[] {
369
- const found: Wheel[] = []
370
- this.node.traverse((n) => { const w = n.get(Wheel); if (w) found.push(w) })
371
- if (found.length === 0) return found
372
-
373
- const chassisInv = this.node.worldMatrix.invert()
374
- const centres = found.map((w) => chassisInv.mul(w.node.worldMatrix).position)
375
-
376
- // Axles: sort by Z (forward is −Z, so the front axle is the most NEGATIVE) and start a new group
377
- // whenever the gap is more than AXLE_EPS.
378
- const order = centres.map((_, i) => i).sort((a, b) => centres[a]!.z - centres[b]!.z)
379
- const axleOf = new Array<number>(found.length).fill(0)
380
- let axle = 0
381
- let ref = centres[order[0]!]!.z
382
- for (const i of order) {
383
- if (Math.abs(centres[i]!.z - ref) > AXLE_EPS) { axle++; ref = centres[i]!.z }
384
- axleOf[i] = found[i]!.axle ?? axle
385
- }
386
- const maxAxle = Math.max(...axleOf)
387
-
388
- // Bind each wheel in front-to-back order, resolving what it did not say.
389
- const wheels: Wheel[] = []
390
- for (const i of order) {
391
- const w = found[i]!
392
- const a = axleOf[i]!
393
- const measured = w._measure()
394
- w.radius ??= measured.radius
395
- w.width ??= measured.width
396
- w.steer ??= a === 0
397
- w.drive ??= this.driveAxle === "all" || (this.driveAxle === "front" ? a === 0 : a === maxAxle)
398
- w.handbrake ??= !w.steer
399
- w.axle = a
400
- const rest = centres[i]!
401
- // Where the suspension hangs from, so the wheel CENTRE ends up where the author put it: the
402
- // spring sags by g/(2πf)² under its own load (mass-independent), and that sag is what separates
403
- // the resting length from full droop.
404
- const travel = w.suspension.travel ?? 0.4
405
- const stiffness = w.suspension.stiffness ?? 1.4
406
- const sag = Math.min(GRAVITY / ((2 * Math.PI * stiffness) ** 2), travel * 0.6)
407
- const attach = new Vec3(rest.x, rest.y + (travel - sag), rest.z)
408
- w._bind(this, wheels.length, rest, attach)
409
- wheels.push(w)
410
- }
411
- return wheels
412
- }
413
-
414
- /** Pack the whole configuration into the one blob the host ABI takes (see bridges.d.ts). */
415
- private _buildSettings(): Float32Array {
416
- const curve = this.engine.curve ?? []
417
- const curveCount = Math.floor(curve.length / 2)
418
- const diff = typeof this.differential === "number"
419
- ? this.differential
420
- : DIFFERENTIALS[this.differential] ?? DIFFERENTIALS.lsd
421
-
422
- const out = new Float32Array(BLOB_HEADER + curveCount * 2 + this._wheels.length * BLOB_WHEEL)
423
- out[0] = BLOB_VERSION
424
- out[1] = this.mass
425
- out[2] = this.centerOfMass === undefined ? 1 : 0
426
- out[3] = this.centerOfMass ?? 0
427
- out[4] = this.engine.torque ?? 500
428
- out[5] = this.engine.maxRpm ?? 6000
429
- out[6] = this.engine.idleRpm ?? 1000
430
- out[7] = this.engine.inertia ?? 0.5
431
- out[8] = this.engine.braking ?? 0.2
432
- out[9] = this.clutchStrength
433
- out[10] = diff
434
- out[11] = this.antiRoll === true ? 0.01 : this.antiRoll === false ? 0 : Math.max(0, this.antiRoll)
435
- out[12] = this.maxTilt
436
- out[13] = this.steering.lock ?? 35
437
- out[14] = Math.max(0, this.steering.atSpeed ?? 10)
438
- out[15] = this.steering.speed ?? 30
439
- out[16] = Math.max(0, this.steering.rate ?? 220)
440
- out[17] = Math.max(0, this.aero.downforce ?? 0)
441
- out[18] = Math.max(0, this.aero.drag ?? 0.03)
442
- out[19] = this._wheels.length
443
- out[20] = curveCount
444
- let at = BLOB_HEADER
445
- for (let i = 0; i < curveCount * 2; i++) out[at++] = curve[i]!
446
- for (const w of this._wheels) {
447
- out[at++] = w._attach.x
448
- out[at++] = w._attach.y
449
- out[at++] = w._attach.z
450
- out[at++] = w.radius ?? 0.35
451
- out[at++] = w.width ?? 0.25
452
- out[at++] = w.steer ? (this.steering.lock ?? 35) : 0
453
- out[at++] = w.drive ? 1 : 0
454
- out[at++] = w.brake ? w.brakeTorque : 0
455
- out[at++] = w.handbrake ? w.brakeTorque * 3 : 0
456
- out[at++] = w.axle ?? 0
457
- out[at++] = w.suspension.travel ?? 0.4
458
- out[at++] = w.suspension.stiffness ?? 1.4
459
- out[at++] = w.suspension.damping ?? 0.5
460
- out[at++] = w.tire.grip ?? 1
461
- out[at++] = w.tire.curve === "arcade" ? 1 : 0
462
- }
463
- return out
464
- }
465
-
466
- /** Edit mode: which way the chassis faces. Each wheel draws its own circle and travel range — see
467
- * `Wheel.rebuild()`. Play mode never calls this. */
468
- rebuild(): void {
469
- const w = new Mat4(this.node.worldMatrix)
470
- const P = (x: number, y: number, z: number): Vec3 => w.transformPoint([ x, y, z ])
471
- Gizmos.polyline([ P(0, 0, 0), P(0, 0, -1.2), P(-0.15, 0, -1.0), P(0, 0, -1.2), P(0.15, 0, -1.0) ], { color: "#ffffff" })
472
- }
473
- }