lecodes-sdk 2.4.1 → 2.4.3

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 (58) hide show
  1. package/dist/types/core/Aspect.d.ts +34 -5
  2. package/dist/types/g2/OneWay2D.d.ts +0 -1
  3. package/dist/types/g2/Physics2D.d.ts +8 -6
  4. package/dist/types/g2/Scene2D.d.ts +1 -1
  5. package/dist/types/g2/Shape2D.d.ts +43 -15
  6. package/dist/types/gl/Model.d.ts +3 -2
  7. package/dist/types/gl/Scene.d.ts +1 -1
  8. package/dist/types/gl/animation/DynamicBone.d.ts +99 -35
  9. package/dist/types/gl/animation/IK.d.ts +0 -1
  10. package/dist/types/gl/nav/NavAgent.d.ts +3 -2
  11. package/dist/types/gl/physics/Physics.d.ts +4 -5
  12. package/dist/types/gl/physics/Shape.d.ts +40 -15
  13. package/dist/types/gl/vehicle/Vehicle.d.ts +54 -35
  14. package/dist/types/gl/vehicle/Wheel.d.ts +31 -10
  15. package/dist/types/plugins/gen/ar/contract.d.ts +152 -0
  16. package/dist/types/plugins/gen/ar/sdk/ar.d.ts +47 -21
  17. package/dist/types/plugins/gen/ar/sdk/ar.gen.d.ts +17 -3
  18. package/dist/types/plugins/gen/audio-recorder/contract.d.ts +64 -0
  19. package/dist/types/plugins/gen/browser/contract.d.ts +108 -0
  20. package/dist/types/plugins/gen/browser/sdk/browser.d.ts +2 -1
  21. package/dist/types/plugins/gen/camera/contract.d.ts +27 -0
  22. package/dist/types/plugins/gen/geolocation/contract.d.ts +52 -0
  23. package/dist/types/plugins/gen/map/contract.d.ts +144 -0
  24. package/dist/types/plugins/gen/push/contract.d.ts +83 -0
  25. package/dist/types/plugins/gen/qr-scanner/contract.d.ts +16 -0
  26. package/dist/types/plugins/gen/speech/contract.d.ts +67 -0
  27. package/dist/types/plugins/gen/webrtc/contract.d.ts +342 -0
  28. package/dist/types/version.d.ts +1 -1
  29. package/dist/types.json +1 -1
  30. package/package.json +1 -1
  31. package/src/animate/tween/Timeline.ts +175 -175
  32. package/src/bridges/gl.d.ts +5 -3
  33. package/src/compile/compileProject.ts +45 -10
  34. package/src/compile/index.ts +7 -0
  35. package/src/compile/webHalf.ts +255 -0
  36. package/src/core/Aspect.ts +118 -35
  37. package/src/g2/OneWay2D.ts +0 -2
  38. package/src/g2/Physics2D.ts +8 -16
  39. package/src/g2/Scene2D.ts +1 -1
  40. package/src/g2/Shape2D.ts +51 -22
  41. package/src/gl/CameraPlace.ts +52 -52
  42. package/src/gl/Light.ts +257 -257
  43. package/src/gl/Mesh.ts +120 -120
  44. package/src/gl/Model.ts +3 -2
  45. package/src/gl/Scene.ts +1 -1
  46. package/src/gl/animation/DynamicBone.ts +549 -486
  47. package/src/gl/animation/IK.ts +0 -2
  48. package/src/gl/nav/NavAgent.ts +14 -12
  49. package/src/gl/physics/Physics.ts +4 -5
  50. package/src/gl/physics/Shape.ts +49 -23
  51. package/src/gl/scenarios.ts +291 -291
  52. package/src/gl/vehicle/Vehicle.ts +102 -58
  53. package/src/gl/vehicle/Wheel.ts +45 -11
  54. package/src/plugins/gen/ar/contract.d.ts +35 -1
  55. package/src/plugins/gen/ar/sdk/ar.gen.ts +25 -6
  56. package/src/plugins/gen/ar/sdk/ar.ts +102 -35
  57. package/src/plugins/gen/browser/sdk/browser.ts +11 -1
  58. package/src/version.ts +1 -1
@@ -28,8 +28,8 @@
28
28
  // `Wheel` aspect of the wheel's own node (which is where Jolt keeps it too); Vehicle collects every
29
29
  // Wheel under itself and groups them into axles by Z.
30
30
  //
31
- // The engine gets NUMBERS: the blob of settings (`_buildSettings`, live re-tunable) and one input
32
- // vector per frame. It never shapes them — no taper, no rate limit, no gearbox, no idle governor.
31
+ // The engine gets NUMBERS: the blob of settings (`_buildSettings`: its tuning half re-sent from
32
+ // `flush()` whenever a live value of the car or a wheel is written) and one input vector per frame. It never shapes them — no taper, no rate limit, no gearbox, no idle governor.
33
33
  //
34
34
  // Chassis space is forward −Z / up +Y — the SDK's own convention, the one `node.forward` reports: if
35
35
  // the model faces the other way, rotate the MODEL (a child), never the chassis node. Physics owns the
@@ -175,6 +175,12 @@ export interface AeroConfig {
175
175
  drag?: number
176
176
  }
177
177
 
178
+ // The three groups' defaults — every key the live views carry (a key left out of an assigned object
179
+ // takes the value here). `curve` / `feedback` / `driver` default to "not given", as the blob reads them.
180
+ const ENGINE_DEFAULTS: EngineConfig = { torque: 500, maxRpm: 6000, idleRpm: 1000, curve: undefined, inertia: 0.5, braking: 0.2 }
181
+ const STEERING_DEFAULTS: SteeringConfig = { maxAngle: 35, caster: 0.03, trail: 0.025, trailAtPeak: 0.5, feedback: undefined, driver: undefined }
182
+ const AERO_DEFAULTS: AeroConfig = { downforce: 0, drag: 0.03 }
183
+
178
184
  /** Blob v15 — see bridges.d.ts for the field-by-field layout. */
179
185
  const BLOB_VERSION = 15
180
186
  const BLOB_HEADER = 34
@@ -199,21 +205,42 @@ export class Vehicle extends Aspect<"vehicle", Node> {
199
205
  * (`id` stays 0, with a warning). */
200
206
  static get supported(): boolean { return Physics.supported && typeof _creator.vehicleCreate === "function" }
201
207
 
202
- /** Chassis mass in kg (default 1200). Wheels are massless in this model. */
203
- mass = 1200
208
+ // ---- the car as hardware. LIVE (a write reaches the engine before the next step — a drift button
209
+ // is `car.vehicle.differential = 'locked'`): differential, antiRoll, maxTilt, friction,
210
+ // clutchStrength, clutchCapacity, and the three groups engine / steering / aero (write a field, or
211
+ // assign a whole object: what it leaves out is the default). STRUCTURAL (baked when the car is
212
+ // built — build it again with `car.aspect(Vehicle, {…})`, which resets its motion): mass,
213
+ // centerOfMass, driveAxle, and which wheels exist, where, with what width and travel.
214
+
215
+ private _mass = 1200
216
+ /** Chassis mass in kg (default 1200). Wheels are massless in this model. Structural: a write on a
217
+ * live car warns and keeps the built mass. */
218
+ get mass(): number { return this._mass }
219
+ set mass(v: number) {
220
+ if (this._id && v !== this._builtMass) {
221
+ console.warn(`Vehicle: mass is fixed once the body exists (still ${this._builtMass} kg) — build the aspect again to change it`)
222
+ return
223
+ }
224
+ this._mass = v
225
+ }
204
226
  /** Centre of mass height in chassis space (metres, negative = lower). Left out, it drops to halfway
205
- * down the chassis shape — a car with its mass at the shape's centre tips over. */
227
+ * down the chassis shape — a car with its mass at the shape's centre tips over. Structural. */
206
228
  centerOfMass?: number
207
229
  /** Which axle the engine drives (default "rear") — the default for every wheel's own `driven`
208
- * share: 1 on that axle, 0 elsewhere. `none` = no drive at all (a trailer, a pushed cart). */
230
+ * share: 1 on that axle, 0 elsewhere. `none` = no drive at all (a trailer, a pushed cart). Structural. */
209
231
  driveAxle: DriveLayout = "rear"
232
+
233
+ private _differential: DifferentialMode | number = "lsd"
210
234
  /** Differential lock across each driven axle. `locked` is the drift setup — both wheels turn
211
- * together, so the pair lets go as a pair. Live: re-configure the aspect to switch it mid-drive. */
212
- differential: DifferentialMode | number = "lsd"
235
+ * together, so the pair lets go as a pair. Live: switch it mid-drive. */
236
+ get differential(): DifferentialMode | number { return this._differential }
237
+ set differential(v: DifferentialMode | number) { this._differential = v; this.dirty() }
238
+ private _antiRoll: boolean | number = true
213
239
  /**
214
240
  * Anti-roll bars between each axle's wheels — what stops a car rolling onto its inside wheels in a
215
241
  * fast corner. `true` (default) is a bar 0.01 as stiff as the wheel's own spring; a number is that
216
- * ratio directly, and `0` / `false` removes the bars.
242
+ * ratio directly, and `0` / `false` removes the bars. Live: the stiffness, on axles built with a
243
+ * bar (`false` at build = no bar to stiffen later).
217
244
  *
218
245
  * **The numbers are far smaller than they look and the gain is steep** — Jolt's bar feeds
219
246
  * `lengthDifference × stiffness × dt` as a bias into the suspension's own soft constraint, so 4%
@@ -223,25 +250,44 @@ export class Vehicle extends Aspect<"vehicle", Node> {
223
250
  * This is the knob for how much a car visibly LEANS, not for wheel lift: bars change how far the
224
251
  * body tips, not how much load transfers. For lift, lower `centerOfMass`.
225
252
  */
226
- antiRoll: boolean | number = true
227
- /** Pitch/roll limit in degrees (default 60) before the constraint rights the car. 180 turns it off. */
228
- maxTilt = 60
253
+ get antiRoll(): boolean | number { return this._antiRoll }
254
+ set antiRoll(v: boolean | number) { this._antiRoll = v; this.dirty() }
255
+ private _maxTilt = 60
256
+ /** Pitch/roll limit in degrees (default 60) before the constraint rights the car. 180 turns it off. Live. */
257
+ get maxTilt(): number { return this._maxTilt }
258
+ set maxTilt(v: number) { this._maxTilt = v; this.dirty() }
259
+ private _friction = DEFAULT_FRICTION
229
260
  /** Friction of the CHASSIS body itself — how it slides when it lands on its roof or side. Nothing
230
- * to do with the tires; those are the `Wheel`'s `tire` × the ground's friction. */
231
- friction = DEFAULT_FRICTION
261
+ * to do with the tires; those are the `Wheel`'s `tire` × the ground's friction. Live, at once. */
262
+ get friction(): number { return this._friction }
263
+ set friction(v: number) { this._friction = v; if (this._bodyId) _creator.physicsSetFriction?.(this._bodyId, v) }
264
+ private _clutchStrength = 10
232
265
  /** Strength of the clutch when fully engaged (default 10). Small values slip; large ones weld the
233
- * engine to the wheels. `clutch` (the input) scales it 0..1. */
234
- clutchStrength = 10
266
+ * engine to the wheels. `clutch` (the input) scales it 0..1. Live. */
267
+ get clutchStrength(): number { return this._clutchStrength }
268
+ set clutchStrength(v: number) { this._clutchStrength = v; this.dirty() }
269
+ private _clutchCapacity = 0
235
270
  /** The clutch's torque CAPACITY, N·m (default 0 = none: Jolt's viscous clutch alone, torque =
236
271
  * `clutchStrength` × slip speed, no ceiling — rigid at any real slip). Set it and the clutch
237
272
  * SLIPS at this much torque × `clutch` — a dry clutch, emulated a physics step at a time: a
238
273
  * launch can be feathered, and a dumped clutch in too high a gear drags the engine down instead
239
- * of lurching. A road car's is ~1.5× the engine's peak torque. */
240
- clutchCapacity = 0
241
-
242
- engine: EngineConfig = { torque: 500, maxRpm: 6000, idleRpm: 1000, inertia: 0.5, braking: 0.2 }
243
- steering: SteeringConfig = { maxAngle: 35 }
244
- aero: AeroConfig = { downforce: 0, drag: 0.03 }
274
+ * of lurching. A road car's is ~1.5× the engine's peak torque. Live. */
275
+ get clutchCapacity(): number { return this._clutchCapacity }
276
+ set clutchCapacity(v: number) { this._clutchCapacity = v; this.dirty() }
277
+
278
+ private _engine = this.live(ENGINE_DEFAULTS)
279
+ /** The engine (`EngineConfig`). Live: `car.vehicle.engine.torque = 800`, or assign a whole config. */
280
+ get engine(): EngineConfig { return this._engine }
281
+ set engine(v: EngineConfig) { this._engine = this.live(ENGINE_DEFAULTS, v); this.dirty() }
282
+ private _steering = this.live(STEERING_DEFAULTS)
283
+ /** The steering (`SteeringConfig`). Live, one level deep: `feedback` and `driver` are each
284
+ * replaced as a whole (`steering.driver = { maxTorque: 90 }`), not edited inside. */
285
+ get steering(): SteeringConfig { return this._steering }
286
+ set steering(v: SteeringConfig) { this._steering = this.live(STEERING_DEFAULTS, v); this.dirty() }
287
+ private _aero = this.live(AERO_DEFAULTS)
288
+ /** Aerodynamics (`AeroConfig`). Live: a wing that deploys is `car.vehicle.aero.downforce = 0.6`. */
289
+ get aero(): AeroConfig { return this._aero }
290
+ set aero(v: AeroConfig) { this._aero = this.live(AERO_DEFAULTS, v); this.dirty() }
245
291
 
246
292
  /** Scene-editor inspector: the flat, tunable half. The grouped objects are edited in code. */
247
293
  static fields: FieldMeta<Vehicle> = {
@@ -276,9 +322,33 @@ export class Vehicle extends Aspect<"vehicle", Node> {
276
322
  private _input = new Float32Array(INPUT_FIXED)
277
323
  private _sent = new Float32Array(INPUT_FIXED)
278
324
  private _dirty = true // an input write since the last send
279
- private _retunePending = false
325
+ private _rebuildPending = false
326
+
327
+ onAttach(): void { this._setup(false) }
328
+ onDetach(): void { this._teardown() }
329
+
330
+ /** A live value changed on the car or a wheel: the tuning half of the blob is re-sent in
331
+ * `flush()`, one send however many values changed this frame. */
332
+ protected flush(): void {
333
+ if (this._rebuildPending) {
334
+ // A wheel came or went: the constraint is built again around the wheels that are there now,
335
+ // where the car stands, its motion reset (the inputs are kept and re-sent).
336
+ this._rebuildPending = false
337
+ this._teardown()
338
+ this._setup(true)
339
+ return
340
+ }
341
+ if (this._id) _creator.vehicleSetTuning?.(this._id, this._buildSettings())
342
+ }
343
+
344
+ /** @internal A wheel's live value changed: retune with the next flush. */
345
+ _retune(): void { this.dirty() }
346
+ /** @internal A wheel was built or removed under a live car: rebuild with the next flush. */
347
+ _rebuildSoon(): void { if (this._id) { this._rebuildPending = true; this.dirty() } }
280
348
 
281
- onAttach(): void {
349
+ // `rebuild` = from flush(), after a wheel change: an author error that attach throws on is a
350
+ // warning here, and the car stays torn down until the next change.
351
+ private _setup(rebuild: boolean): void {
282
352
  if (!Physics.supported) return
283
353
  if (!_creator.vehicleCreate) {
284
354
  console.warn("Vehicle: this host has no vehicle model (Vehicle.supported is false) — the car will not move")
@@ -297,7 +367,9 @@ export class Vehicle extends Aspect<"vehicle", Node> {
297
367
 
298
368
  this._wheels = this._collectWheels()
299
369
  if (this._wheels.length === 0) {
300
- throw new Error("Vehicle found no wheels — put a Wheel aspect on each wheel node under the chassis: hubFL.aspect(Wheel)")
370
+ const msg = "Vehicle found no wheels — put a Wheel aspect on each wheel node under the chassis: hubFL.aspect(Wheel)"
371
+ if (rebuild) { console.warn(msg); return }
372
+ throw new Error(msg)
301
373
  }
302
374
 
303
375
  const shapeId = shape._claim() // the vehicle owns the shape; drop the pick-only body
@@ -324,7 +396,7 @@ export class Vehicle extends Aspect<"vehicle", Node> {
324
396
  this._dirty = true
325
397
  }
326
398
 
327
- onDetach(): void {
399
+ private _teardown(): void {
328
400
  if (this._id) { _creator.vehicleDestroy(this._id); this._id = 0; this._bodyId = 0 }
329
401
  for (const w of this._wheels) w._vehicle = null
330
402
  this._wheels = []
@@ -333,31 +405,6 @@ export class Vehicle extends Aspect<"vehicle", Node> {
333
405
  this.node.get(Shape)?._recreatePickBody() // hands the routing back to the pick body
334
406
  }
335
407
 
336
- /**
337
- * Re-configuring an attached vehicle retunes it in place — the way a drift button switches the
338
- * differential without the car so much as blinking:
339
- *
340
- * car.aspect(Vehicle, { differential: drift ? 'locked' : 'lsd' })
341
- *
342
- * Live: `differential`, `engine`, `steering`, `aero`, `maxTilt`, `friction`, `clutchStrength`,
343
- * `clutchCapacity`, `antiRoll` (its stiffness, on axles that have a bar), and every `Wheel`'s
344
- * `tire`. Everything else describes the car's STRUCTURE — `mass`, `centerOfMass`, which wheels
345
- * exist and where, their suspension, the driven shares — and is baked into the constraint at
346
- * attach; to change one of those, re-attach the aspect.
347
- */
348
- onReconfigure(): void {
349
- if (!this._id) return
350
- if (this.mass !== this._builtMass) {
351
- console.warn(`Vehicle: mass is fixed once the body exists (still ${this._builtMass} kg) — re-add the aspect to change it`)
352
- this.mass = this._builtMass
353
- }
354
- this._retunePending = true
355
- if (this._bodyId) _creator.physicsSetFriction?.(this._bodyId, this.friction)
356
- }
357
-
358
- /** @internal A wheel's tire changed: re-send the tuning before the next step (one send however
359
- * many wheels changed this frame). */
360
- _retuneSoon(): void { this._retunePending = true }
361
408
 
362
409
  /** Native vehicle id (0 until attached / no physics support). */
363
410
  get id(): number { return this._id }
@@ -395,15 +442,12 @@ export class Vehicle extends Aspect<"vehicle", Node> {
395
442
  /**
396
443
  * EARLY phase: hand this frame's inputs to the engine, before the step. The vector is sent when
397
444
  * anything changed — and every frame while a throttle, steer or brake is non-zero, because that is
398
- * what keeps a driven car awake (an idle car with zero input sleeps). The pending re-tune (a
399
- * config or tire change) goes first, so the step runs on the new numbers.
445
+ * what keeps a driven car awake (an idle car with zero input sleeps). A pending re-tune (a live
446
+ * value of the car or a wheel changed) went out in `flush()`, before this phase, so the step runs
447
+ * on the new numbers.
400
448
  */
401
449
  protected updateBefore(): void {
402
450
  if (!this._id) return
403
- if (this._retunePending) {
404
- this._retunePending = false
405
- _creator.vehicleSetTuning?.(this._id, this._buildSettings())
406
- }
407
451
  const input = this._input
408
452
  input[0] = this._throttle
409
453
  input[1] = this._steer
@@ -567,7 +611,7 @@ export class Vehicle extends Aspect<"vehicle", Node> {
567
611
  const w = found[i]!
568
612
  const a = axleOf[i]!
569
613
  const measured = w._measure()
570
- w.radius ??= measured.radius
614
+ w._radius ??= measured.radius
571
615
  w.width ??= measured.width
572
616
  w.steer ??= a === 0
573
617
  w.driven ??= this.driveAxle === "none" ? 0 : this.driveAxle === "all" ? 1 : (this.driveAxle === "front" ? a === 0 : a === maxAxle) ? 1 : 0
@@ -138,13 +138,28 @@ export function resolveTire(tire: Tire): ResolvedTire {
138
138
  }
139
139
  }
140
140
 
141
+ /** `suspension`'s defaults — every key the live view carries. */
142
+ const SUSPENSION_DEFAULTS: Required<WheelSuspension> = { travel: 0.4, stiffness: 1.4, damping: 0.5 }
143
+
144
+ /**
145
+ * One wheel. LIVE: `radius`, `suspension.stiffness` / `damping` and `tire` — write them and the car
146
+ * retunes before the next step (a tire losing air: `wheel.radius *= 0.9`, `wheel.suspension.stiffness
147
+ * *= 0.7`, `wheel.tire = Tire.scale(wheel.tire, 0.5)`). STRUCTURAL: `width`, `suspension.travel`, `axle`,
148
+ * `steer`, `driven` — baked when the car is built; to change one, build the wheel again
149
+ * (`hub.aspect(Wheel, {…})` — the car rebuilds itself around it, its motion reset).
150
+ */
141
151
  export class Wheel extends Aspect<"wheel", Node> {
142
152
  static readonly aspect = "wheel"
143
153
 
154
+ /** @internal the radius as given, or as measured at bind (`radius` reads it). */
155
+ _radius?: number
144
156
  /** Wheel radius in metres. Default: measured from the node's rendered bounds, else 0.35. Too small
145
- * and the car scrapes, too large and it floats. */
146
- radius?: number
147
- /** Wheel width in metres. Default: measured, else 0.25. */
157
+ * and the car scrapes, too large and it floats. Live: the ROLLING radius — the suspension hangs
158
+ * where it was built, so a smaller one lowers that corner of the car, the way a tire losing air
159
+ * does; the wheel's inertia keeps the radius it was built with. */
160
+ get radius(): number | undefined { return this._radius }
161
+ set radius(v: number | undefined) { this._radius = v; this.dirty() }
162
+ /** Wheel width in metres. Default: measured, else 0.25. Structural. */
148
163
  width?: number
149
164
  /** Axle group, front to back. Default: derived from Z. Set it for a car whose wheels are not in neat
150
165
  * left/right pairs — an axle's wheels are paired for the differential and the anti-roll bar. */
@@ -157,10 +172,20 @@ export class Wheel extends Aspect<"wheel", Node> {
157
172
  * `0.2` on each rear. Structural: baked at attach. */
158
173
  driven?: number
159
174
 
160
- suspension: WheelSuspension = { travel: 0.4, stiffness: 1.4, damping: 0.5 }
161
- /** The tire (default `Tire.road()`). Live: re-assign it or re-configure the aspect and the car
162
- * retunes in place — a grip slider, a tire that heats up. */
163
- tire: Tire = Tire.road()
175
+ private _suspension = this.live(SUSPENSION_DEFAULTS)
176
+ /** The suspension. Live: `stiffness` and `damping` (a softer corner sags and rolls more) — write a
177
+ * field (`wheel.suspension.stiffness = 1`) or assign a whole object (what it leaves out is the
178
+ * default). `travel` is structural. */
179
+ get suspension(): WheelSuspension { return this._suspension }
180
+ set suspension(v: WheelSuspension) { this._suspension = this.live(SUSPENSION_DEFAULTS, v); this.dirty() }
181
+
182
+ private _tire = this.live(Tire.road())
183
+ /** The tire (default `Tire.road()`). Live: assign one (`wheel.tire = Tire.drift()`, or
184
+ * `Tire.scale(wheel.tire, 0.5)` for a grip slider, a tire that heats up) or write a field
185
+ * (`wheel.tire.traction = 1`); a curve is replaced, never edited in place. A tire assigned without
186
+ * `traction` / `circle` gets `road`'s. */
187
+ get tire(): Tire { return this._tire }
188
+ set tire(v: Tire) { this._tire = this.live(Tire.road(), v); this.dirty() }
164
189
 
165
190
  /** INPUT: brake torque on this wheel THIS frame, N·m (default 0). Sticky, like `Vehicle.throttle`:
166
191
  * write it every frame from a driver system's `updateBefore` — the foot brake's share, the
@@ -189,15 +214,24 @@ export class Wheel extends Aspect<"wheel", Node> {
189
214
  /** @internal Resting centre in chassis space (what the author placed). */
190
215
  _rest = new Vec3()
191
216
 
217
+ /** A wheel built under a car that already runs (`hub.aspect(Wheel, {…})` on a live car): the car
218
+ * rebuilds itself around the new set of wheels before the next step. */
219
+ onAttach(): void {
220
+ for (let n: Node | null = this.node.parent; n; n = n.parent) {
221
+ const v = (n as unknown as { vehicle?: Vehicle }).vehicle
222
+ if (v) { v._rebuildSoon(); return }
223
+ }
224
+ }
225
+
192
226
  onDetach(): void {
227
+ this._vehicle?._rebuildSoon() // one wheel fewer: the car is built again around what is left
193
228
  this._vehicle = null
194
229
  this._index = -1
195
230
  }
196
231
 
197
- /** Re-configuring a wheel on a live car retunes the car: the tire is live; size, suspension, the
198
- * driven share and the axle are structural (re-attach the Vehicle). */
199
- onReconfigure(): void {
200
- this._vehicle?._retuneSoon()
232
+ /** A live value changed (the tire, the radius, the spring): the car retunes in the same flush. */
233
+ protected flush(): void {
234
+ this._vehicle?._retune()
201
235
  }
202
236
 
203
237
  /** @internal Called by Vehicle once the car exists, with the geometry it resolved. */
@@ -5,6 +5,10 @@
5
5
  // camera and background the session drives every frame through the host's gl facade.
6
6
  // A session is ONE AR scene: ARScene opens it with the scene and its camera entity as params and
7
7
  // closes it with the scene. `@permission camera` on start: the host asks, the halves never do.
8
+ // What a session finds in the camera's picture and drives an entity to: an image target (a printed
9
+ // picture, by its bytes) and, since contract version 2, a QR code (a printed symbol, by its text) —
10
+ // both through the `image` event. Codes are qr-anchor's on every platform: its wasm in the WebXR arm,
11
+ // its C ABI on ARKit / ARCore (the plain-camera web arm tracks none and says so).
8
12
  import type { Service, File, Entity, Scene3D, f64 } from "../../../plugin"
9
13
 
10
14
  export type ARMode = "world" | "images"
@@ -31,6 +35,20 @@ export interface ImageTarget {
31
35
  entity: Entity
32
36
  }
33
37
 
38
+ /** A printed QR code the session tracks: found by its text in the camera's picture; while
39
+ * tracked its entity is driven like an image target's — a rigid pose in metres, no scale — the
40
+ * symbol's centre, Y out of it, X along its top edge, Z towards its bottom. WebXR, ARKit and
41
+ * ARCore; a half without a detector (the plain-camera web arm) tracks none. */
42
+ export interface QRTarget {
43
+ /** The code's text: a symbol with another text is not this target. "" = ANY code (the first
44
+ * one decoded): a test, or a plaque whose text is not known to the app. */
45
+ text: string
46
+ /** The printed side of the symbol (its modules, no quiet zone), metres. */
47
+ size: f64
48
+ /** The entity that follows the code (its Anchor node). */
49
+ entity: Entity
50
+ }
51
+
34
52
  export interface StartOptions {
35
53
  /** Default "world". "images": image tracking only (no planes, no hit test; iOS runs
36
54
  * ARImageTrackingConfiguration, which is lighter). */
@@ -38,6 +56,9 @@ export interface StartOptions {
38
56
  /** Default "horizontal". Ignored in "images". */
39
57
  planes?: PlaneDetection
40
58
  images?: ImageTarget[]
59
+ /** The QR codes to track (in "world" and in "images"); a half without them says so and drives
60
+ * none. @since 2 */
61
+ codes?: QRTarget[]
41
62
  /** Default "gravity". "gravityAndHeading": -Z points north (the compass). */
42
63
  worldAlignment?: WorldAlignment
43
64
  /** Deliver `light` events. Default false. */
@@ -76,7 +97,8 @@ export interface AREvents {
76
97
  /** An image target found, lost, or its size refined. While tracked its entity is driven: a rigid
77
98
  * pose in metres, no scale — the image's centre, Y out of it, the image in XZ (X along its width,
78
99
  * Z towards its bottom). `extent` = [width, height] metres, the framework's estimate (sent again
79
- * when it changes by a millimetre, at most 10 per second); the last one when lost. */
100
+ * when it changes by a millimetre, at most 10 per second); the last one when lost. A QR target's
101
+ * comes the same way (its frame: X along the symbol's top edge), `extent` = [size, size]. */
80
102
  image: { entity: Entity, tracked: boolean, extent: f64[] }
81
103
  /** An anchor made by `anchor()` lost or regained after a relocalization. */
82
104
  anchor: { entity: Entity, tracked: boolean }
@@ -86,6 +108,11 @@ export interface AREvents {
86
108
  /** The session failed and is over: "unavailable" (no ARKit / ARCore support, ARCore not
87
109
  * installed and declined), "camera" (the camera could not be used), "failed". */
88
110
  error: { code: ARErrorCode, message: string }
111
+ /** The session is over, and not by `stop()`: the user left it ("user" — the browser's back / exit
112
+ * of an immersive WebXR session) or the system took it ("system"). Not an error: ARScene closes
113
+ * the scene. ARKit and ARCore never end a session by themselves (an interruption is `tracking`
114
+ * limited): the native halves do not send it. @since 2 */
115
+ ended: { reason: "user" | "system" }
89
116
  }
90
117
 
91
118
  export interface AR extends Service<"ar", ARParams, AREvents> {
@@ -102,6 +129,13 @@ export interface AR extends Service<"ar", ARParams, AREvents> {
102
129
  * `StartOptions.images`.
103
130
  * @rejects notRunning failed */
104
131
  setImages(images: ImageTarget[]): Promise<void>
132
+ /** The twin of `setImages` for QR codes: replace the set of QR targets of a RUNNING session
133
+ * without restarting it; a code no longer in the set stops driving its entity (`tracked: false`
134
+ * once if it was tracked). Before `start`, the codes are `StartOptions.codes`. A half that tracks
135
+ * no codes (the plain-camera web arm) rejects "failed".
136
+ * @since 2
137
+ * @rejects notRunning failed */
138
+ setCodes(codes: QRTarget[]): Promise<void>
105
139
  /** What is under a point of the scene's view — in its touches' space (a scene touch's `clientX` /
106
140
  * `clientY`: the units of the gl facade's viewport) — nearest first. Empty when nothing is, or on
107
141
  * a host without surfaces (the web).
@@ -1,18 +1,19 @@
1
1
  // GENERATED by scripts/vendor-plugins.ts from lecodes-plugins/plugins/ar/sdk/ar.gen.ts — do not edit here.
2
2
  // Change it in the plugin repo and run `bun run vendor:plugins`; `bun run check:plugins` fails when this copy is stale.
3
3
  // GENERATED by `lecodes plugin gen` from contract.d.ts — do not edit.
4
- // ar: the service "ar", contract version 1.
4
+ // ar: the service "ar", contract version 2.
5
5
 
6
6
  import { ServiceChannel, _fromEntity, _fromFile, _fromScene3D, _toEntity, _toFile, type Entity, type EventReaders, type File } from "../../../../plugin"
7
- import type { AREvents, ARParams, HitResult, HitTarget, ImageTarget, Pose, StartOptions } from "../contract"
7
+ import type { AREvents, ARParams, HitResult, HitTarget, ImageTarget, Pose, QRTarget, StartOptions } from "../contract"
8
8
 
9
9
  /** What the halves register under. */
10
10
  export const NAME = "ar"
11
11
  /** The contract's version: the highest `@since` of its members. */
12
- export const VERSION = 1
12
+ export const VERSION = 2
13
13
 
14
- const _fromStartOptions = (v: StartOptions): any => ({ ...v, images: v.images === undefined ? undefined : v.images.map((x: any) => _fromImageTarget(x)) })
14
+ const _fromStartOptions = (v: StartOptions): any => ({ ...v, images: v.images === undefined ? undefined : v.images.map((x: any) => _fromImageTarget(x)), codes: v.codes === undefined ? undefined : v.codes.map((x: any) => _fromQRTarget(x)) })
15
15
  const _fromImageTarget = (v: ImageTarget): any => ({ ...v, image: _fromFile(v.image), entity: _fromEntity(v.entity) })
16
+ const _fromQRTarget = (v: QRTarget): any => ({ ...v, entity: _fromEntity(v.entity) })
16
17
  const _fromARParams = (v: ARParams): any => ({ ...v, scene: v.scene === undefined ? undefined : _fromScene3D(v.scene), camera: v.camera === undefined ? undefined : _fromEntity(v.camera) })
17
18
 
18
19
  /** The events whose payload holds a File: read before a listener sees it. */
@@ -42,6 +43,15 @@ export const stop = (service: ServiceChannel<AREvents>): Promise<void> =>
42
43
  export const setImages = (service: ServiceChannel<AREvents>, images: ImageTarget[]): Promise<void> =>
43
44
  service._call("setImages", [images.map((x: any) => _fromImageTarget(x))])
44
45
 
46
+ /** The twin of `setImages` for QR codes: replace the set of QR targets of a RUNNING session
47
+ * without restarting it; a code no longer in the set stops driving its entity (`tracked: false`
48
+ * once if it was tracked). Before `start`, the codes are `StartOptions.codes`. A half that tracks
49
+ * no codes (the plain-camera web arm) rejects "failed".
50
+ * Contract version 2: an older half rejects the call — ask `supports("setCodes")`.
51
+ * Rejects with "notRunning" / "failed". */
52
+ export const setCodes = (service: ServiceChannel<AREvents>, codes: QRTarget[]): Promise<void> =>
53
+ service._call("setCodes", [codes.map((x: any) => _fromQRTarget(x))], 2)
54
+
45
55
  /** What is under a point of the scene's view — in its touches' space (a scene touch's `clientX` /
46
56
  * `clientY`: the units of the gl facade's viewport) — nearest first. Empty when nothing is, or on
47
57
  * a host without surfaces (the web).
@@ -68,12 +78,14 @@ export const captureFrame = (service: ServiceChannel<AREvents>): Promise<File> =
68
78
  // ---- what the host's half has --------------------------------------------------------------------
69
79
 
70
80
  /** A method of the contract. */
71
- export type Method = "start" | "stop" | "setImages" | "hitTest" | "anchor" | "release" | "captureFrame"
81
+ export type Method = "start" | "stop" | "setImages" | "setCodes" | "hitTest" | "anchor" | "release" | "captureFrame"
82
+
83
+ const SINCE: { [method: string]: number } = { setCodes: 2 }
72
84
 
73
85
  /** Whether this host has the service "ar" — and, given a method, a half new enough for it
74
86
  * (a method added after the first version of the contract is tagged `@since`). */
75
87
  export const supports = (method?: Method): boolean =>
76
- ServiceChannel.supports(NAME) // every method is of version 1
88
+ ServiceChannel.supports(NAME, method === undefined ? 1 : SINCE[method] ?? 1)
77
89
 
78
90
  // ---- the client -----------------------------------------------------------------------------------
79
91
 
@@ -94,6 +106,13 @@ export class ARBase extends ServiceChannel<AREvents> {
94
106
  * `StartOptions.images`.
95
107
  * Rejects with "notRunning" / "failed". */
96
108
  setImages(images: ImageTarget[]): Promise<void> { return setImages(this, images) }
109
+ /** The twin of `setImages` for QR codes: replace the set of QR targets of a RUNNING session
110
+ * without restarting it; a code no longer in the set stops driving its entity (`tracked: false`
111
+ * once if it was tracked). Before `start`, the codes are `StartOptions.codes`. A half that tracks
112
+ * no codes (the plain-camera web arm) rejects "failed".
113
+ * Contract version 2: an older half rejects the call — ask `supports("setCodes")`.
114
+ * Rejects with "notRunning" / "failed". */
115
+ setCodes(codes: QRTarget[]): Promise<void> { return setCodes(this, codes) }
97
116
  /** What is under a point of the scene's view — in its touches' space (a scene touch's `clientX` /
98
117
  * `clientY`: the units of the gl facade's viewport) — nearest first. Empty when nothing is, or on
99
118
  * a host without surfaces (the web).