lecodes-sdk 2.4.1 → 2.4.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.
- 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/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/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/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/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/version.ts +1 -1
package/src/gl/animation/IK.ts
CHANGED
|
@@ -79,8 +79,6 @@ abstract class Chain<Name extends string> extends Aspect<Name, Node> {
|
|
|
79
79
|
this._ik = 0
|
|
80
80
|
this._rig = undefined
|
|
81
81
|
}
|
|
82
|
-
onReconfigure(): void { this.push() }
|
|
83
|
-
|
|
84
82
|
protected params(): Float32Array {
|
|
85
83
|
const p = new Float32Array(P.COUNT)
|
|
86
84
|
p[P.WEIGHT] = this._weight
|
package/src/gl/nav/NavAgent.ts
CHANGED
|
@@ -74,26 +74,26 @@ export class NavAgent extends Aspect<"agent", Node, NavAgentEvents> {
|
|
|
74
74
|
|
|
75
75
|
/** Agent radius (metres). Defaults to the navmesh's. */
|
|
76
76
|
get radius(): number { return this._radius }
|
|
77
|
-
set radius(v: number) { this._radius = v; this.
|
|
77
|
+
set radius(v: number) { this._radius = v; this.dirty() }
|
|
78
78
|
get height(): number { return this._height }
|
|
79
|
-
set height(v: number) { this._height = v; this.
|
|
79
|
+
set height(v: number) { this._height = v; this.dirty() }
|
|
80
80
|
/** Top speed (m/s). */
|
|
81
81
|
get speed(): number { return this._speed }
|
|
82
|
-
set speed(v: number) { this._speed = v; this.
|
|
82
|
+
set speed(v: number) { this._speed = v; this.dirty() }
|
|
83
83
|
get acceleration(): number { return this._acceleration }
|
|
84
|
-
set acceleration(v: number) { this._acceleration = v; this.
|
|
84
|
+
set acceleration(v: number) { this._acceleration = v; this.dirty() }
|
|
85
85
|
/** Local (agent-vs-agent) avoidance quality. */
|
|
86
86
|
get avoidance(): NavAgentAvoidance { return this._avoidance }
|
|
87
|
-
set avoidance(v: NavAgentAvoidance) { this._avoidance = v; this.
|
|
87
|
+
set avoidance(v: NavAgentAvoidance) { this._avoidance = v; this.dirty() }
|
|
88
88
|
/** Separation force weight (0 = none). */
|
|
89
89
|
get separation(): number { return this._separation }
|
|
90
|
-
set separation(v: number) { this._separation = v; this.
|
|
90
|
+
set separation(v: number) { this._separation = v; this.dirty() }
|
|
91
91
|
/** Area names this agent may walk (default: all). */
|
|
92
92
|
get include(): string[] | undefined { return this._include }
|
|
93
|
-
set include(v: string[] | undefined) { this._include = v; this.
|
|
93
|
+
set include(v: string[] | undefined) { this._include = v; this.dirty() }
|
|
94
94
|
/** Area names this agent never enters. */
|
|
95
95
|
get exclude(): string[] | undefined { return this._exclude }
|
|
96
|
-
set exclude(v: string[] | undefined) { this._exclude = v; this.
|
|
96
|
+
set exclude(v: string[] | undefined) { this._exclude = v; this.dirty() }
|
|
97
97
|
|
|
98
98
|
state: NavAgentState = "idle"
|
|
99
99
|
/** The current velocity after avoidance (m/s). */
|
|
@@ -111,7 +111,6 @@ export class NavAgent extends Aspect<"agent", Node, NavAgentEvents> {
|
|
|
111
111
|
private _target: Vec3 | null = null
|
|
112
112
|
/** velocity-driven (`move`) rather than target-driven */
|
|
113
113
|
private _steering = false
|
|
114
|
-
private _dirty = false
|
|
115
114
|
private _faceX = 0
|
|
116
115
|
private _faceZ = -1
|
|
117
116
|
private _retryAt = 0
|
|
@@ -125,7 +124,12 @@ export class NavAgent extends Aspect<"agent", Node, NavAgentEvents> {
|
|
|
125
124
|
this._join()
|
|
126
125
|
}
|
|
127
126
|
|
|
128
|
-
|
|
127
|
+
/** A parameter changed: push the agent's params to the crowd (a no-op until it has a slot —
|
|
128
|
+
* `_join` sends them with the add). */
|
|
129
|
+
protected flush(): void {
|
|
130
|
+
const crowd = this._crowd
|
|
131
|
+
if (crowd && this._slot >= 0 && crowd._mesh === NavMesh.active) _creatorNav.crowdSetParams(crowd._id, this._slot, this.params())
|
|
132
|
+
}
|
|
129
133
|
|
|
130
134
|
onDetach(): void {
|
|
131
135
|
this._leave()
|
|
@@ -216,7 +220,6 @@ export class NavAgent extends Aspect<"agent", Node, NavAgentEvents> {
|
|
|
216
220
|
if (slot < 0) { this._retryAt = crowd._step + JOIN_RETRY_STEPS; return false }
|
|
217
221
|
this._crowd = crowd
|
|
218
222
|
this._slot = slot
|
|
219
|
-
this._dirty = false
|
|
220
223
|
this._lastSeen = -1
|
|
221
224
|
crowd._agents.add(this)
|
|
222
225
|
if (this._target && !_creatorNav.crowdSetTarget(crowd._id, slot, this._target.x, this._target.y, this._target.z)) this._target = null
|
|
@@ -253,7 +256,6 @@ export class NavAgent extends Aspect<"agent", Node, NavAgentEvents> {
|
|
|
253
256
|
}
|
|
254
257
|
const crowd = this._crowd!
|
|
255
258
|
if (crowd._mesh !== NavMesh.active) { this._leave(); return }
|
|
256
|
-
if (this._dirty) { this._dirty = false; _creatorNav.crowdSetParams(crowd._id, this._slot, this.params()) }
|
|
257
259
|
if (crowd._step === this._lastSeen) return // the crowd has not advanced since (a frame with no step)
|
|
258
260
|
this._lastSeen = crowd._step
|
|
259
261
|
const b = crowd._buf
|
|
@@ -72,11 +72,10 @@ export class Physics extends Aspect<"physics", Node> implements CompWriter {
|
|
|
72
72
|
* `1`, ice `0.02`. A Vehicle's tires read the GROUND body's value, so this is the single number
|
|
73
73
|
* that decides how well a car corners.
|
|
74
74
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* even when no code reads `.friction`.)
|
|
75
|
+
* Live: `floor.physics.friction = 0.02` (an ice patch) pushes the new surface to the body at
|
|
76
|
+
* once. (chisel keeps an instance setter whose name appears as an object-literal key anywhere in
|
|
77
|
+
* the bundle, so `aspect(Physics, { friction })` at build survives tree-shaking even when no code
|
|
78
|
+
* reads `.friction`.)
|
|
80
79
|
*/
|
|
81
80
|
get friction(): number { return this._friction }
|
|
82
81
|
set friction(v: number) {
|
package/src/gl/physics/Shape.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// (which follows the sim), rather than relying on the pick body.
|
|
19
19
|
|
|
20
20
|
import { Aspect } from "../../core/Aspect"
|
|
21
|
+
import type { FieldMeta } from "../../core/fields"
|
|
21
22
|
import { cx, cy, cz, type Vec3Like } from "../../math/vec"
|
|
22
23
|
import type { Node } from "../Node"
|
|
23
24
|
|
|
@@ -51,23 +52,42 @@ const dim = (v: number): number => Math.max(MIN_HALF, r4(v))
|
|
|
51
52
|
export class Shape extends Aspect<"shape", Node> {
|
|
52
53
|
static readonly aspect = "shape"
|
|
53
54
|
|
|
55
|
+
// Every dimension is LIVE and applies AT ONCE: a write rebuilds the geometry in place, the body
|
|
56
|
+
// keeping its id, velocity and place (a crouch, a live editor edit) — synchronous, because the
|
|
57
|
+
// caller reads the result back right away (`controller.resizing`, the node's height). A write that
|
|
58
|
+
// leaves the geometry's signature unchanged does nothing. `fit()` writes every field and rebuilds once.
|
|
59
|
+
private _box?: Vec3Like
|
|
60
|
+
private _sphere?: number
|
|
61
|
+
private _cylinder?: { halfHeight: number, radius: number }
|
|
62
|
+
private _capsule?: { halfHeight: number, radius: number }
|
|
63
|
+
private _mesh?: boolean | "convex"
|
|
64
|
+
private _heightfield?: boolean
|
|
65
|
+
private _origin?: Vec3Like
|
|
66
|
+
private _raycast = true
|
|
67
|
+
|
|
54
68
|
/** Box half-extents [hx, hy, hz] (world units). */
|
|
55
|
-
box
|
|
69
|
+
get box(): Vec3Like | undefined { return this._box }
|
|
70
|
+
set box(v: Vec3Like | undefined) { this._box = v; this._changed() }
|
|
56
71
|
/** Sphere radius. */
|
|
57
|
-
sphere
|
|
72
|
+
get sphere(): number | undefined { return this._sphere }
|
|
73
|
+
set sphere(v: number | undefined) { this._sphere = v; this._changed() }
|
|
58
74
|
/** Cylinder aligned on Y: half-height + radius. */
|
|
59
|
-
cylinder
|
|
75
|
+
get cylinder(): { halfHeight: number, radius: number } | undefined { return this._cylinder }
|
|
76
|
+
set cylinder(v: { halfHeight: number, radius: number } | undefined) { this._cylinder = v; this._changed() }
|
|
60
77
|
/** Capsule aligned on Y: half-height of the cylinder section + radius. */
|
|
61
|
-
capsule
|
|
78
|
+
get capsule(): { halfHeight: number, radius: number } | undefined { return this._capsule }
|
|
79
|
+
set capsule(v: { halfHeight: number, radius: number } | undefined) { this._capsule = v; this._changed() }
|
|
62
80
|
/** The node's own triangles (a Mesh's geometry or a Model's GLB, world scale applied).
|
|
63
81
|
* `true` = exact triangle mesh — static/kinematic bodies, triggers, characters' ground and picking
|
|
64
82
|
* only (a dynamic body can't be a triangle mesh; Physics throws). `'convex'` = convex hull of the
|
|
65
83
|
* vertices, works for dynamic bodies too. A Model uses its bind pose (skinned parts skipped). */
|
|
66
|
-
mesh
|
|
84
|
+
get mesh(): boolean | "convex" | undefined { return this._mesh }
|
|
85
|
+
set mesh(v: boolean | "convex" | undefined) { this._mesh = v; this._changed() }
|
|
67
86
|
/** The node's terrain (`Terrain.create(...).node`) as a Jolt height field — the exact drawn grid,
|
|
68
87
|
* holes included (docs/terrain-plan.md). Static / kinematic / picking / character ground only,
|
|
69
88
|
* like `mesh: true`. `terrain.commit()` pushes edits into the live shape. */
|
|
70
|
-
heightfield
|
|
89
|
+
get heightfield(): boolean | undefined { return this._heightfield }
|
|
90
|
+
set heightfield(v: boolean | undefined) { this._heightfield = v; this._changed() }
|
|
71
91
|
/**
|
|
72
92
|
* The shape's centre, relative to the node's origin — world units in the node's ROTATED frame
|
|
73
93
|
* (the node's scale is ignored, exactly like the explicit dimensions above). Default [0, 0, 0].
|
|
@@ -81,15 +101,22 @@ export class Shape extends Aspect<"shape", Node> {
|
|
|
81
101
|
* you would expect. With no `origin` an auto shape (`{}`) still centres itself on the node's own
|
|
82
102
|
* mesh bounds — only explicit dimensions sit on the pivot by default.
|
|
83
103
|
*/
|
|
84
|
-
origin
|
|
85
|
-
|
|
86
|
-
|
|
104
|
+
get origin(): Vec3Like | undefined { return this._origin }
|
|
105
|
+
set origin(v: Vec3Like | undefined) { this._origin = v; this._changed() }
|
|
106
|
+
/** Whether pointer rays can hit this shape (click / touchstart). Default true. Live, at once. */
|
|
107
|
+
get raycast(): boolean { return this._raycast }
|
|
108
|
+
set raycast(v: boolean) { this._raycast = v; if (this._bodyId) _creator.physicsSetPickable(this._bodyId, v) }
|
|
109
|
+
|
|
110
|
+
/** Scene-editor inspector: the geometry fields (accessors, so they are listed here by name). */
|
|
111
|
+
static fields: FieldMeta<Shape> = {
|
|
112
|
+
box: {}, sphere: {}, cylinder: {}, capsule: {}, mesh: {}, heightfield: {}, origin: {}, raycast: {},
|
|
113
|
+
}
|
|
87
114
|
|
|
88
115
|
private _shapeId = 0
|
|
89
116
|
private _bodyId = 0
|
|
90
117
|
/** Centre the "auto" box measured for itself — used when `origin` isn't set explicitly. */
|
|
91
118
|
private _autoOrigin: [number, number, number] | null = null
|
|
92
|
-
/** Geometry props as of the last build (see
|
|
119
|
+
/** Geometry props as of the last build (see _changed). */
|
|
93
120
|
private _sig = ""
|
|
94
121
|
|
|
95
122
|
/** @internal native shape id (0 if the build has no physics). */
|
|
@@ -110,11 +137,10 @@ export class Shape extends Aspect<"shape", Node> {
|
|
|
110
137
|
this.node._xfKind = 0
|
|
111
138
|
}
|
|
112
139
|
|
|
113
|
-
/**
|
|
114
|
-
*
|
|
115
|
-
|
|
116
|
-
if (this._signature() !== this._sig) this._rebuild()
|
|
117
|
-
if (this._bodyId) _creator.physicsSetPickable(this._bodyId, this.raycast)
|
|
140
|
+
/** A dimension, the kind or the origin was written: rebuild the geometry when it actually changed.
|
|
141
|
+
* The body keeps its id and velocity. */
|
|
142
|
+
private _changed(): void {
|
|
143
|
+
if (this._shapeId && this._signature() !== this._sig) this._rebuild()
|
|
118
144
|
}
|
|
119
145
|
|
|
120
146
|
/**
|
|
@@ -134,14 +160,14 @@ export class Shape extends Aspect<"shape", Node> {
|
|
|
134
160
|
console.warn(`Shape.fit: "${this.node.name}" has no geometry to measure yet (a model still loading, or a node with no mesh)`)
|
|
135
161
|
return this
|
|
136
162
|
}
|
|
137
|
-
this.
|
|
138
|
-
this.
|
|
139
|
-
this.
|
|
140
|
-
this.
|
|
141
|
-
if (kind === "mesh") this.
|
|
142
|
-
else if (kind === "convex") this.
|
|
143
|
-
else if (kind !== undefined) this.
|
|
144
|
-
this.
|
|
163
|
+
this._box = f.box
|
|
164
|
+
this._sphere = f.sphere
|
|
165
|
+
this._cylinder = f.cylinder
|
|
166
|
+
this._capsule = f.capsule
|
|
167
|
+
if (kind === "mesh") this._mesh = true
|
|
168
|
+
else if (kind === "convex") this._mesh = "convex"
|
|
169
|
+
else if (kind !== undefined) this._mesh = undefined
|
|
170
|
+
this._origin = f.origin
|
|
145
171
|
this._rebuild()
|
|
146
172
|
return this
|
|
147
173
|
}
|
|
@@ -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 the web's (WebXR) first: the native halves take them and
|
|
11
|
+
// say they track none yet.
|
|
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. Web (WebXR) first;
|
|
41
|
+
* the native halves later. */
|
|
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 yet (the native ones) 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).
|