lecodes-cli 0.18.0 → 0.18.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 (62) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +2013 -564
  3. package/package.json +4 -4
  4. package/runtime/scene-harness.json +1 -1
  5. package/runtime/sdk/core/Aspect.ts +512 -255
  6. package/runtime/sdk/core/compWrite.ts +42 -0
  7. package/runtime/sdk/core/fields.ts +1 -1
  8. package/runtime/sdk/core/time.ts +81 -0
  9. package/runtime/sdk/g2/Camera2D.ts +8 -1
  10. package/runtime/sdk/g2/CharacterController2D.ts +253 -53
  11. package/runtime/sdk/g2/Node2D.ts +80 -10
  12. package/runtime/sdk/g2/OneWay2D.ts +66 -0
  13. package/runtime/sdk/g2/Physics2D.ts +240 -30
  14. package/runtime/sdk/g2/Scene2D.ts +33 -1
  15. package/runtime/sdk/g2/Shape2D.ts +218 -22
  16. package/runtime/sdk/g2/Trigger2D.ts +42 -12
  17. package/runtime/sdk/g2/groups2d.ts +106 -0
  18. package/runtime/sdk/g2/loop.ts +15 -4
  19. package/runtime/sdk/gl/Camera.ts +40 -1
  20. package/runtime/sdk/gl/CameraPlace.ts +52 -51
  21. package/runtime/sdk/gl/CharacterController.ts +184 -56
  22. package/runtime/sdk/gl/Gearbox.ts +212 -0
  23. package/runtime/sdk/gl/Geometry.ts +70 -9
  24. package/runtime/sdk/gl/Light.ts +64 -2
  25. package/runtime/sdk/gl/Lightmap.ts +179 -0
  26. package/runtime/sdk/gl/Material.ts +25 -0
  27. package/runtime/sdk/gl/Mesh.ts +6 -23
  28. package/runtime/sdk/gl/Model.ts +16 -2
  29. package/runtime/sdk/gl/Node.ts +119 -39
  30. package/runtime/sdk/gl/Physics.ts +75 -24
  31. package/runtime/sdk/gl/Scene.ts +161 -5
  32. package/runtime/sdk/gl/Shape.ts +42 -3
  33. package/runtime/sdk/gl/Trigger.ts +45 -50
  34. package/runtime/sdk/gl/Vehicle.ts +276 -322
  35. package/runtime/sdk/gl/Wheel.ts +240 -0
  36. package/runtime/sdk/gl/scenarios.ts +4 -30
  37. package/runtime/sdk/gl/state.ts +6 -6
  38. package/runtime/sdk/inject.ts +186 -171
  39. package/runtime/sdk/runtime/device.ts +102 -1
  40. package/runtime/sdk/scene/defineScene.ts +108 -53
  41. package/runtime/sdk/scene/gizmos.ts +31 -11
  42. package/runtime/sdk/scene/material.ts +188 -0
  43. package/runtime/sdk-types.json +1 -1
  44. package/runtime/sdk/compile/aspectMacro.ts +0 -42
  45. package/runtime/sdk/compile/assetIconMacro.ts +0 -384
  46. package/runtime/sdk/compile/assetMacro.ts +0 -45
  47. package/runtime/sdk/compile/assetName.ts +0 -50
  48. package/runtime/sdk/compile/bundler.ts +0 -252
  49. package/runtime/sdk/compile/compileProject.ts +0 -129
  50. package/runtime/sdk/compile/detectEntry.ts +0 -128
  51. package/runtime/sdk/compile/fontMacro.ts +0 -459
  52. package/runtime/sdk/compile/fontRegistry.ts +0 -78
  53. package/runtime/sdk/compile/header.ts +0 -67
  54. package/runtime/sdk/compile/index.ts +0 -85
  55. package/runtime/sdk/compile/libraryImports.ts +0 -52
  56. package/runtime/sdk/compile/liteMaterial.ts +0 -247
  57. package/runtime/sdk/compile/sceneEditor.ts +0 -78
  58. package/runtime/sdk/compile/serverSplit.ts +0 -233
  59. package/runtime/sdk/compile/serverTypes.ts +0 -227
  60. package/runtime/sdk/compile/sfnt.ts +0 -98
  61. package/runtime/sdk/compile/shaderTargets.ts +0 -42
  62. package/runtime/sdk/compile/sourcemap.ts +0 -25
@@ -0,0 +1,240 @@
1
+ // One wheel of a `Vehicle`, as an aspect on the wheel's own node. The node's position IS the wheel's
2
+ // position (where its centre rests on flat ground), and everything else about it lives here too:
3
+ //
4
+ // const car = new Node('car')
5
+ // .add(body, wheelFL.aspect(Wheel), wheelFR.aspect(Wheel),
6
+ // wheelRL.aspect(Wheel), wheelRR.aspect(Wheel))
7
+ // .aspect(Shape, { box: [0.9, 0.4, 2.1] })
8
+ // .aspect(Vehicle, { mass: 1200 })
9
+ //
10
+ // That is the whole minimum: `Vehicle` collects every `Wheel` under itself, groups them into axles by
11
+ // Z (front first), and the defaults fall out — the front axle steers, the rear one drives and carries
12
+ // the handbrake. Override any of it per wheel.
13
+ //
14
+ // This aspect is also what POSES the wheel: it runs in the LATE phase (after the physics step, before
15
+ // the frame draws) and writes the node's transform from the suspension length, steer angle and spin
16
+ // the engine reports. Nothing else writes that transform — the engine deliberately does not, so a
17
+ // wheel model's own conventions (which way its axle points, how big it looks) are settings here
18
+ // rather than constants in C++, and a part that should steer but NOT spin is simply a sibling node.
19
+ //
20
+ // Chassis space is forward −Z / up +Y, the SDK's own convention (`node.forward`).
21
+
22
+ import { Aspect } from "../core/Aspect"
23
+ import type { FieldMeta } from "../core/fields"
24
+ import { Mat4 } from "../math/mat4"
25
+ import { Quat } from "../math/quat"
26
+ import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
27
+ import { Gizmos } from "../scene/gizmos"
28
+ import { Node } from "./Node"
29
+ import { Shape } from "./Shape"
30
+ import type { Vehicle } from "./Vehicle"
31
+
32
+ /** Floats `vehicleGetState` writes before the per-wheel block: speed, rpm, contacts, vx, vy, vz. */
33
+ export const STATE_HEADER = 6
34
+ /** Floats per wheel: contact, slip, slipAngle, suspensionLength, steerDeg, spin. */
35
+ export const WHEEL_STATE = 6
36
+
37
+ /** How a tire behaves past its grip peak. `road` is Jolt's own curve — grip falls off after the peak,
38
+ * so the car can be made to let go. `arcade` holds the peak all the way out: much harder to spin. */
39
+ export type TireCurve = "road" | "arcade"
40
+
41
+ export interface WheelSuspension {
42
+ /** Total travel in metres, full droop to fully compressed (default 0.4). */
43
+ travel?: number
44
+ /** Spring frequency in Hz — bigger is stiffer (default 1.4; a race car ~2.5, a truck ~1). It also
45
+ * sets the ride-height sag, and the sag is how far the wheel can still extend DOWN. */
46
+ stiffness?: number
47
+ /** Damping ratio 0..1 (default 0.5). Low values pogo. */
48
+ damping?: number
49
+ }
50
+
51
+ export interface WheelTire {
52
+ /** Grip multiplier on the friction curves (default 1). The grip a wheel actually gets is combined
53
+ * with the ground it stands on as `sqrt(tire × groundFriction)`, and a body's friction defaults to
54
+ * 0.6 — so a road a car should really corner on wants `Physics { friction: 1 }`. */
55
+ grip?: number
56
+ /** Which friction curve (default `road`). */
57
+ curve?: TireCurve
58
+ }
59
+
60
+ export interface WheelVisual {
61
+ /** The MODEL's axle direction in its own space (default `[1, 0, 0]`). Set it instead of parenting
62
+ * the mesh under a corrective node when the art points another way. */
63
+ axis?: Vec3Like
64
+ /** Drawn radius, if the art is not exactly the physical `radius` — sinks a wheel into its arch or
65
+ * lets a fat tire overhang, with no effect on the simulation. Default: the physical radius. */
66
+ radius?: number
67
+ /** Let the model spin (default true). `false` keeps the steer angle and suspension travel but not
68
+ * the rotation — a brake caliper. */
69
+ spin?: boolean
70
+ }
71
+
72
+ const DEG2RAD = Math.PI / 180
73
+ const UP = new Vec3(0, 1, 0)
74
+
75
+ export class Wheel extends Aspect<"wheel", Node> {
76
+ static readonly aspect = "wheel"
77
+
78
+ /** Wheel radius in metres. Default: measured from the node's rendered bounds, else 0.35. Too small
79
+ * and the car scrapes, too large and it floats. */
80
+ radius?: number
81
+ /** Wheel width in metres. Default: measured, else 0.25. */
82
+ width?: number
83
+ /** Does this wheel steer? Default: the wheels on the front axle. */
84
+ steer?: boolean
85
+ /** Does the engine drive it? Default: from the vehicle's `drive` layout. */
86
+ drive?: boolean
87
+ /** Foot brake on this wheel. */
88
+ brake = true
89
+ /** Handbrake on this wheel. Default: every non-steering wheel (locking the front wheels of a moving
90
+ * car is not a handbrake turn, it is a crash). */
91
+ handbrake?: boolean
92
+ /** Axle group, front to back. Default: derived from Z. Set it for a car whose wheels are not in neat
93
+ * left/right pairs — an axle's wheels are paired for the differential and the anti-roll bar. */
94
+ axle?: number
95
+ /** Foot-brake torque in N·m (default 1500); the handbrake pulls 3× on the wheels that have it. */
96
+ brakeTorque = 1500
97
+
98
+ suspension: WheelSuspension = { travel: 0.4, stiffness: 1.4, damping: 0.5 }
99
+ tire: WheelTire = { grip: 1, curve: "road" }
100
+ visual: WheelVisual = {}
101
+
102
+ /** Scene-editor inspector. `steer` / `drive` / `handbrake` are deliberately absent: they are
103
+ * tri-state (undefined = derive it from the axle), which a switch cannot express. */
104
+ static fields: FieldMeta<Wheel> = {
105
+ radius: { min: 0.05, step: 0.01 },
106
+ width: { min: 0.02, step: 0.01 },
107
+ axle: { min: 0, step: 1 },
108
+ brake: { label: "Foot brake" },
109
+ brakeTorque: { label: "Brake torque", min: 0, step: 50 },
110
+ suspension: { hidden: true },
111
+ tire: { hidden: true },
112
+ visual: { hidden: true },
113
+ }
114
+ static editor = { rebuild: true }
115
+
116
+ /** @internal Set by `Vehicle` at attach — the car this wheel belongs to, and its index in the blob. */
117
+ _vehicle: Vehicle | null = null
118
+ /** @internal */
119
+ _index = -1
120
+ /** @internal Suspension attachment point in chassis space; the wheel hangs below it. */
121
+ _attach = new Vec3()
122
+ /** @internal Resting centre in chassis space (what the author placed). */
123
+ _rest = new Vec3()
124
+
125
+ private _angle = 0 // integrated spin, radians — the engine reports rate, not angle
126
+ private _scale = new Vec3(1, 1, 1) // the node's own scale, preserved through the pose write
127
+ private _local = true // is the node a direct child of the chassis?
128
+
129
+ onAttach(): void {
130
+ this._scale = this.node.scale
131
+ }
132
+
133
+ onDetach(): void {
134
+ this._vehicle = null
135
+ this._index = -1
136
+ }
137
+
138
+ /** @internal Called by Vehicle once the car exists, with the geometry it resolved. */
139
+ _bind(vehicle: Vehicle, index: number, rest: Vec3, attach: Vec3): void {
140
+ this._vehicle = vehicle
141
+ this._index = index
142
+ this._rest = rest
143
+ this._attach = attach
144
+ this._scale = this.node.scale
145
+ this._local = this.node.parent === vehicle.node
146
+ }
147
+
148
+ // ---- live state ------------------------------------------------------------------------------
149
+ // Every getter reads the car's ONE shared state buffer, which is refreshed once per frame.
150
+
151
+ private _state(k: number): number {
152
+ const v = this._vehicle
153
+ if (!v || this._index < 0) return 0
154
+ return v._wheelState(this._index, k)
155
+ }
156
+
157
+ /** Touching the ground this step. */
158
+ get contact(): boolean { return this._state(0) !== 0 }
159
+ /** Longitudinal slip RATIO: 0 = rolling in sync with the ground, ~1 = spinning up or locked.
160
+ * Unsigned — smoke, tire marks and wheelspin sound key off this. */
161
+ get slip(): number { return this._state(1) }
162
+ /** Slip ANGLE in degrees: how far the tire's travel direction is off from where it points.
163
+ * Unsigned, 0..90 — past the curve's peak (~3°) the tire is sliding sideways. Drift particles. */
164
+ get slipAngle(): number { return this._state(2) }
165
+ /** Steer angle in degrees, positive to the right. */
166
+ get steerAngle(): number { return this._state(4) }
167
+ /** Wheel spin in rad/s (× radius ≈ the speed the tire is laying down). */
168
+ get spin(): number { return this._state(5) }
169
+ /** Suspension compression, 0 = fully extended (drooping) … 1 = fully compressed. */
170
+ get compression(): number {
171
+ const travel = this.suspension.travel ?? 0.4
172
+ if (travel <= 0) return 0
173
+ const len = this._state(3)
174
+ const min = travel * 0.4
175
+ return Math.max(0, Math.min(1, (travel - len) / (travel - min)))
176
+ }
177
+
178
+ // ---- the visual ------------------------------------------------------------------------------
179
+
180
+ /** LATE phase: after the step and the transform sync, right before the frame draws. */
181
+ update(dt: number): void {
182
+ const v = this._vehicle
183
+ if (!v || this._index < 0 || !v.id) return
184
+
185
+ if (this.visual.spin !== false) this._angle += this._state(5) * dt
186
+
187
+ // Down the suspension from the attachment point by however far it currently hangs.
188
+ const length = this._state(3)
189
+ const centre = new Vec3(this._attach.x, this._attach.y - length, this._attach.z)
190
+
191
+ // Steer about the chassis' up axis, then spin about the MODEL's own axle in the steered frame.
192
+ // Both reports are negated, for the same reason: the engine describes them in the DRIVER's terms
193
+ // and the scene is right-handed with forward on −Z. Steer is reported right-positive while a yaw
194
+ // about +Y turns left. Spin is reported positive-means-forward, but a positive turn about the
195
+ // right-hand axle carries the top of the wheel to +Z — which rolls the car backwards.
196
+ const steer = Quat.fromAxisAngle(UP, -this._state(4) * DEG2RAD)
197
+ const axis = this.visual.axis ?? [1, 0, 0]
198
+ const rotation = this.visual.spin === false ? steer : steer.mul(Quat.fromAxisAngle(axis, -this._angle))
199
+
200
+ // A model that draws bigger or smaller than it collides scales on top of the node's own scale.
201
+ const vr = this.visual.radius
202
+ const k = vr !== undefined && this.radius ? vr / this.radius : 1
203
+ const scale = k === 1 ? this._scale : new Vec3(this._scale.x * k, this._scale.y * k, this._scale.z * k)
204
+
205
+ const m = Mat4.compose(centre, rotation, scale)
206
+ // The common case is a direct child of the chassis, where chassis space IS the local space.
207
+ if (this._local) this.node.matrix = m
208
+ else this.node.worldMatrix = new Mat4(v.node.worldMatrix).mul(m)
209
+ }
210
+
211
+ /** Edit mode: this wheel's own circle at its resting height, its travel range, and a mark when the
212
+ * engine drives it — enough to place a wheel without running the game. */
213
+ rebuild(): void {
214
+ const travel = this.suspension.travel ?? 0.4
215
+ const r = this.radius ?? this._measure().radius
216
+ const world = new Mat4(this.node.worldMatrix)
217
+ const P = (x: number, y: number, z: number): Vec3 => world.transformPoint([ x, y, z ])
218
+ const ring: Vec3[] = []
219
+ for (let i = 0; i < 24; i++) {
220
+ const t = (i / 24) * Math.PI * 2
221
+ ring.push(P(0, Math.sin(t) * r, Math.cos(t) * r))
222
+ }
223
+ Gizmos.polyline(ring, { color: this.steer === false ? "#9aa3b2" : "#5b8ef0", closed: true })
224
+ Gizmos.line(P(0, travel * 0.6, 0), P(0, -travel * 0.4, 0), { color: "#59606d" })
225
+ if (this.drive) Gizmos.cross(P(0, 0, 0), r * 0.35, { color: "#e0b25a" })
226
+ }
227
+
228
+ /** @internal Radius/width from the node's rendered bounds, so a placed model needs no numbers. */
229
+ _measure(): { radius: number, width: number } {
230
+ const b = Shape.boundsOf(this.node)
231
+ if (!b) return { radius: 0.35, width: 0.25 }
232
+ const size = new Vec3(cx(b.max) - cx(b.min), cy(b.max) - cy(b.min), cz(b.max) - cz(b.min))
233
+ // A wheel is a disc: its diameter is the larger of the two axes across the axle, and the width is
234
+ // the axle direction itself.
235
+ const axis = this.visual.axis ?? [1, 0, 0]
236
+ const along = Math.abs(cx(axis)) >= Math.abs(cz(axis)) ? size.x : size.z
237
+ const across = Math.max(Math.abs(cx(axis)) >= Math.abs(cz(axis)) ? size.z : size.x, size.y)
238
+ return { radius: across > 0 ? across * 0.5 : 0.35, width: along > 0 ? along : 0.25 }
239
+ }
240
+ }
@@ -16,11 +16,8 @@ import type { InspectorUI } from "../core/InspectorUI"
16
16
  import { Vec3 } from "../math/vec"
17
17
  import { Quat } from "../math/quat"
18
18
  import { Node } from "./Node"
19
- import { Mesh } from "./Mesh"
20
19
  import { Model } from "./Model"
21
20
  import type { Playback } from "./animation/Playback"
22
- import { Geometry } from "./Geometry"
23
- import { Material } from "./Material"
24
21
  import { Gizmos } from "../scene/gizmos"
25
22
 
26
23
  const MOVE_LINE = "#5b8ef0"
@@ -49,25 +46,6 @@ const toParentLocal = (node: Node, world: Vec3): Vec3 => {
49
46
  return parent ? parent.worldMatrix.invert().transformPoint(world) : world
50
47
  }
51
48
 
52
- // ---- editor marker meshes (defineScene's empty/camera markers — real meshes, so they pick) -------
53
-
54
- /** @internal An "edges" line mesh from flat segment endpoints ([x,y,z, x,y,z] per segment) —
55
- * the scene editor's node markers (defineScene). Path lines are `Gizmos` calls instead. */
56
- export const edgesMesh = (segments: number[], color: string): Mesh => {
57
- const vertices = new Float32Array(segments)
58
- const count = vertices.length / 3
59
- const normals = new Float32Array(count * 3)
60
- for (let i = 0; i < count; i++) normals[i * 3 + 1] = 1
61
- const indices = new Uint16Array(count)
62
- for (let i = 0; i < count; i++) indices[i] = i
63
- const geo = new Geometry(vertices, normals, indices, new Float32Array(count * 2))
64
- geo.kind = "edges"
65
- const mesh = Mesh.from(geo, { material: Material.unlit({ color }), name: "__editor_line" })
66
- mesh.castShadows = false
67
- mesh.receiveShadows = false
68
- return mesh
69
- }
70
-
71
49
  /** A path node's waypoints: its children in scene-FILE order (`_sceneChildOrder`, stamped by
72
50
  * defineScene — the engine's live child order is insertion-based and may differ), editor helper
73
51
  * nodes (`__`-prefixed) excluded. Runtime-added children append in engine order. */
@@ -127,9 +105,8 @@ export class MoveTo extends Aspect<"moveTo"> {
127
105
  /** Editor-only path line (edit mode; play mode never calls this). */
128
106
  rebuild(): void {
129
107
  if (!this.target) return
130
- const b = this.target.worldPosition
131
- Gizmos.line(this.node.worldPosition, b, { color: MOVE_LINE })
132
- Gizmos.cross(b, 0.09, { color: MOVE_LINE })
108
+ // the target is an empty — its own anchored marker shows the endpoint
109
+ Gizmos.line(this.node.worldPosition, this.target.worldPosition, { color: MOVE_LINE })
133
110
  }
134
111
  }
135
112
 
@@ -203,8 +180,7 @@ export class FollowPath extends Aspect<"followPath"> {
203
180
  /** Editor-only waypoint polyline (edit mode; play mode never calls this). */
204
181
  rebuild(): void {
205
182
  const points = this._points() // loop mode already appends the first point again
206
- Gizmos.polyline(points, { color: MOVE_LINE })
207
- for (const p of points) Gizmos.cross(p, 0.07, { color: MOVE_LINE })
183
+ Gizmos.polyline(points, { color: MOVE_LINE }) // waypoints are empties: they carry their own markers
208
184
  }
209
185
  }
210
186
 
@@ -262,9 +238,7 @@ export class LookAt extends Aspect<"facing"> {
262
238
  /** Editor-only sight line (edit mode; play mode never calls this). */
263
239
  rebuild(): void {
264
240
  if (!this.target) return
265
- const b = this.target.worldPosition
266
- Gizmos.line(this.node.worldPosition, b, { color: LOOK_LINE })
267
- Gizmos.cross(b, 0.07, { color: LOOK_LINE })
241
+ Gizmos.line(this.node.worldPosition, this.target.worldPosition, { color: LOOK_LINE })
268
242
  }
269
243
  }
270
244
 
@@ -1,6 +1,6 @@
1
- // Tiny shared module to break the Node ↔ Scene ↔ touch dependency cycle. The active 3D scene is set
2
- // here by Scene.open()/close() and read by the touch dispatch system.
3
-
4
- import type { Scene } from "./Scene"
5
-
6
- export const glState: { activeScene: Scene | null, frame: number } = { activeScene: null, frame: 0 }
1
+ // Tiny shared module to break the Node ↔ Scene ↔ touch dependency cycle. The active 3D scene is set
2
+ // here by Scene.open()/close() and read by the touch dispatch system.
3
+
4
+ import type { Scene } from "./Scene"
5
+
6
+ export const glState: { activeScene: Scene | null, frame: number } = { activeScene: null, frame: 0 }