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.
- package/dist/types/core/Aspect.d.ts +34 -5
- package/dist/types/g2/OneWay2D.d.ts +0 -1
- package/dist/types/g2/Physics2D.d.ts +8 -6
- package/dist/types/g2/Scene2D.d.ts +1 -1
- package/dist/types/g2/Shape2D.d.ts +43 -15
- package/dist/types/gl/Model.d.ts +3 -2
- package/dist/types/gl/Scene.d.ts +1 -1
- package/dist/types/gl/animation/DynamicBone.d.ts +99 -35
- package/dist/types/gl/animation/IK.d.ts +0 -1
- package/dist/types/gl/nav/NavAgent.d.ts +3 -2
- package/dist/types/gl/physics/Physics.d.ts +4 -5
- package/dist/types/gl/physics/Shape.d.ts +40 -15
- package/dist/types/gl/vehicle/Vehicle.d.ts +54 -35
- package/dist/types/gl/vehicle/Wheel.d.ts +31 -10
- package/dist/types/plugins/gen/ar/contract.d.ts +152 -0
- package/dist/types/plugins/gen/ar/sdk/ar.d.ts +47 -21
- package/dist/types/plugins/gen/ar/sdk/ar.gen.d.ts +17 -3
- package/dist/types/plugins/gen/audio-recorder/contract.d.ts +64 -0
- package/dist/types/plugins/gen/browser/contract.d.ts +108 -0
- package/dist/types/plugins/gen/browser/sdk/browser.d.ts +2 -1
- package/dist/types/plugins/gen/camera/contract.d.ts +27 -0
- package/dist/types/plugins/gen/geolocation/contract.d.ts +52 -0
- package/dist/types/plugins/gen/map/contract.d.ts +144 -0
- package/dist/types/plugins/gen/push/contract.d.ts +83 -0
- package/dist/types/plugins/gen/qr-scanner/contract.d.ts +16 -0
- package/dist/types/plugins/gen/speech/contract.d.ts +67 -0
- package/dist/types/plugins/gen/webrtc/contract.d.ts +342 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/src/animate/tween/Timeline.ts +175 -175
- package/src/bridges/gl.d.ts +5 -3
- package/src/compile/compileProject.ts +45 -10
- package/src/compile/index.ts +7 -0
- package/src/compile/webHalf.ts +255 -0
- package/src/core/Aspect.ts +118 -35
- package/src/g2/OneWay2D.ts +0 -2
- package/src/g2/Physics2D.ts +8 -16
- package/src/g2/Scene2D.ts +1 -1
- package/src/g2/Shape2D.ts +51 -22
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Light.ts +257 -257
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Model.ts +3 -2
- package/src/gl/Scene.ts +1 -1
- package/src/gl/animation/DynamicBone.ts +549 -486
- package/src/gl/animation/IK.ts +0 -2
- package/src/gl/nav/NavAgent.ts +14 -12
- package/src/gl/physics/Physics.ts +4 -5
- package/src/gl/physics/Shape.ts +49 -23
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/vehicle/Vehicle.ts +102 -58
- package/src/gl/vehicle/Wheel.ts +45 -11
- package/src/plugins/gen/ar/contract.d.ts +35 -1
- package/src/plugins/gen/ar/sdk/ar.gen.ts +25 -6
- package/src/plugins/gen/ar/sdk/ar.ts +102 -35
- package/src/plugins/gen/browser/sdk/browser.ts +11 -1
- 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
|
|
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
|
-
|
|
203
|
-
|
|
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:
|
|
212
|
-
differential: DifferentialMode | number
|
|
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
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
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
|
|
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
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
|
399
|
-
*
|
|
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.
|
|
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
|
package/src/gl/vehicle/Wheel.ts
CHANGED
|
@@ -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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
161
|
-
/** The
|
|
162
|
-
*
|
|
163
|
-
|
|
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
|
-
/**
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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).
|