lecodes-cli 0.19.0 → 0.20.0
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/README.md +1 -0
- package/dist/index.js +1069 -396
- package/package.json +14 -13
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/assets/Roboto-Variable-DHm9jpd-.woff2 +0 -0
- package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/runtime/web/assets/basis-C64VHDVD.js +1 -0
- package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/runtime/web/assets/createViewerLite-BvHobVhp.js +852 -0
- package/runtime/web/assets/draco-BiISTFcR.js +118 -0
- package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/runtime/web/assets/index-CfNCCBjN.js +2 -0
- package/runtime/web/assets/mapViewImpl-B2JcES8l.js +810 -0
- package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +8 -0
- package/runtime/web/assets/neutral_ibl128-D-lzdkLN.ktx +0 -0
- package/runtime/web/assets/worker-Caf-yYEI.js +2 -0
- package/runtime/web/embed.html +32 -0
- package/runtime/web/embed.js +171 -0
- package/src/api.ts +302 -0
- package/src/browserAuth.ts +87 -0
- package/src/cmgenTool.ts +104 -0
- package/src/commands/app.ts +896 -0
- package/src/commands/appAndroid.ts +603 -0
- package/src/commands/appDesktop.ts +339 -0
- package/src/commands/appDesktopMac.ts +470 -0
- package/src/commands/appIcon.ts +187 -0
- package/src/commands/appShared.ts +542 -0
- package/src/commands/appTemplates.ts +907 -0
- package/src/commands/appTemplatesAndroid.ts +599 -0
- package/src/commands/appTemplatesGradlew.ts +9 -0
- package/src/commands/assets.ts +28 -0
- package/src/commands/clone.ts +60 -0
- package/src/commands/compile.ts +253 -0
- package/src/commands/create.ts +59 -0
- package/src/commands/design.ts +863 -0
- package/src/commands/designTemplates.ts +11 -0
- package/src/commands/desktop.ts +127 -0
- package/src/commands/dev.ts +236 -0
- package/src/commands/diff.ts +59 -0
- package/src/commands/init.ts +191 -0
- package/src/commands/install.ts +198 -0
- package/src/commands/lightmap.ts +290 -0
- package/src/commands/link.ts +147 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/navmesh.ts +225 -0
- package/src/commands/pn.ts +305 -0
- package/src/commands/projectTemplates.ts +130 -0
- package/src/commands/pull.ts +109 -0
- package/src/commands/push.ts +173 -0
- package/src/commands/render.ts +416 -0
- package/src/commands/scene.ts +148 -0
- package/src/commands/shaders.ts +157 -0
- package/src/commands/status.ts +29 -0
- package/src/commands/test.ts +371 -0
- package/src/commands/thumbs.ts +178 -0
- package/src/commands/types.ts +87 -0
- package/src/commands/update.ts +190 -0
- package/src/compile/assetIcons.ts +214 -0
- package/src/compile/collect.ts +46 -0
- package/src/compile/collectLocal.ts +37 -0
- package/src/compile/designCompile.ts +104 -0
- package/src/compile/fonts.ts +156 -0
- package/src/compile/headlessBundle.ts +129 -0
- package/src/compile/nativeStack.ts +21 -0
- package/src/compile/projectCompile.ts +109 -0
- package/src/compile/sceneCompile.ts +113 -0
- package/src/compile/screenEntry.ts +127 -0
- package/src/compile/shaders.ts +245 -0
- package/src/config.ts +42 -0
- package/src/designMeta.ts +35 -0
- package/src/desktopRenderer.ts +545 -0
- package/src/desktopScript.ts +276 -0
- package/src/dev/clientTemplates.ts +160 -0
- package/src/dev/devServer.ts +333 -0
- package/src/dev/webRunner.ts +22 -0
- package/src/dev/wsServer.ts +144 -0
- package/src/distRoot.ts +20 -0
- package/src/ignore.ts +163 -0
- package/src/index.ts +494 -0
- package/src/lecodes-3d-editor.d.ts +41 -0
- package/src/lecodes-assets.d.ts +7 -0
- package/src/lecodes-design.d.ts +191 -0
- package/src/lecodes-renderer.d.ts +131 -0
- package/src/localFiles.ts +144 -0
- package/src/manifest.ts +46 -0
- package/src/matcTool.ts +137 -0
- package/src/peers.ts +40 -0
- package/src/project.ts +61 -0
- package/src/projectEnv.ts +94 -0
- package/src/qrcode-terminal.d.ts +9 -0
- package/src/releases.ts +125 -0
- package/src/serverDiff.ts +78 -0
- package/src/textDiff.ts +103 -0
- package/src/types.ts +0 -0
- package/src/util.ts +146 -0
- package/runtime/sdk/animate/animate.ts +0 -238
- package/runtime/sdk/animate/bezier.ts +0 -138
- package/runtime/sdk/animate/easings.ts +0 -126
- package/runtime/sdk/canvas/Canvas.ts +0 -305
- package/runtime/sdk/compile/aspectMacro.ts +0 -86
- package/runtime/sdk/compile/assetIconMacro.ts +0 -384
- package/runtime/sdk/compile/assetMacro.ts +0 -146
- package/runtime/sdk/compile/assetName.ts +0 -50
- package/runtime/sdk/compile/bundler.ts +0 -287
- package/runtime/sdk/compile/compileProject.ts +0 -129
- package/runtime/sdk/compile/detectEntry.ts +0 -128
- package/runtime/sdk/compile/fontMacro.ts +0 -459
- package/runtime/sdk/compile/fontRegistry.ts +0 -78
- package/runtime/sdk/compile/header.ts +0 -67
- package/runtime/sdk/compile/index.ts +0 -100
- package/runtime/sdk/compile/libraryImports.ts +0 -52
- package/runtime/sdk/compile/liteMaterial.ts +0 -247
- package/runtime/sdk/compile/sceneEditor.ts +0 -88
- package/runtime/sdk/compile/serverSplit.ts +0 -233
- package/runtime/sdk/compile/serverTypes.ts +0 -227
- package/runtime/sdk/compile/sfnt.ts +0 -98
- package/runtime/sdk/compile/shaderSchema.ts +0 -202
- package/runtime/sdk/compile/shaderTargets.ts +0 -81
- package/runtime/sdk/compile/sourcemap.ts +0 -25
- package/runtime/sdk/core/Aspect.ts +0 -568
- package/runtime/sdk/core/InspectorUI.ts +0 -212
- package/runtime/sdk/core/StateMachine.ts +0 -308
- package/runtime/sdk/core/color.ts +0 -66
- package/runtime/sdk/core/compWrite.ts +0 -42
- package/runtime/sdk/core/events.ts +0 -38
- package/runtime/sdk/core/fields.ts +0 -120
- package/runtime/sdk/core/registry.ts +0 -23
- package/runtime/sdk/core/signals.ts +0 -277
- package/runtime/sdk/core/time.ts +0 -84
- package/runtime/sdk/g2/Camera2D.ts +0 -40
- package/runtime/sdk/g2/CharacterController2D.ts +0 -276
- package/runtime/sdk/g2/Node2D.ts +0 -267
- package/runtime/sdk/g2/OneWay2D.ts +0 -66
- package/runtime/sdk/g2/Physics2D.ts +0 -346
- package/runtime/sdk/g2/Scene2D.ts +0 -209
- package/runtime/sdk/g2/Shape2D.ts +0 -259
- package/runtime/sdk/g2/Sprite.ts +0 -89
- package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
- package/runtime/sdk/g2/SpriteSheet.ts +0 -166
- package/runtime/sdk/g2/Texture2D.ts +0 -47
- package/runtime/sdk/g2/Tilemap.ts +0 -41
- package/runtime/sdk/g2/Tileset.ts +0 -71
- package/runtime/sdk/g2/Trigger2D.ts +0 -77
- package/runtime/sdk/g2/autotile.ts +0 -433
- package/runtime/sdk/g2/cells.ts +0 -91
- package/runtime/sdk/g2/defineScene2d.ts +0 -381
- package/runtime/sdk/g2/groups2d.ts +0 -106
- package/runtime/sdk/g2/loop.ts +0 -50
- package/runtime/sdk/g2/scenarios2d.ts +0 -69
- package/runtime/sdk/g2/touch.ts +0 -83
- package/runtime/sdk/gl/Camera.ts +0 -160
- package/runtime/sdk/gl/CameraPlace.ts +0 -52
- package/runtime/sdk/gl/CharacterController.ts +0 -238
- package/runtime/sdk/gl/Gearbox.ts +0 -212
- package/runtime/sdk/gl/Geometry.ts +0 -279
- package/runtime/sdk/gl/IK.ts +0 -193
- package/runtime/sdk/gl/InstancedMesh.ts +0 -132
- package/runtime/sdk/gl/Light.ts +0 -107
- package/runtime/sdk/gl/Lightmap.ts +0 -256
- package/runtime/sdk/gl/Material.ts +0 -260
- package/runtime/sdk/gl/Mesh.ts +0 -83
- package/runtime/sdk/gl/Model.ts +0 -124
- package/runtime/sdk/gl/NavAgent.ts +0 -337
- package/runtime/sdk/gl/NavMesh.ts +0 -397
- package/runtime/sdk/gl/Node.ts +0 -350
- package/runtime/sdk/gl/Noise.ts +0 -30
- package/runtime/sdk/gl/Particles.ts +0 -676
- package/runtime/sdk/gl/Physics.ts +0 -222
- package/runtime/sdk/gl/Plane.ts +0 -53
- package/runtime/sdk/gl/Ragdoll.ts +0 -270
- package/runtime/sdk/gl/Ray.ts +0 -16
- package/runtime/sdk/gl/Scene.ts +0 -486
- package/runtime/sdk/gl/Shape.ts +0 -409
- package/runtime/sdk/gl/Terrain.ts +0 -1092
- package/runtime/sdk/gl/Texture.ts +0 -63
- package/runtime/sdk/gl/Trigger.ts +0 -45
- package/runtime/sdk/gl/Vehicle.ts +0 -473
- package/runtime/sdk/gl/Wheel.ts +0 -240
- package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
- package/runtime/sdk/gl/animation/Animator.ts +0 -120
- package/runtime/sdk/gl/animation/Layer.ts +0 -31
- package/runtime/sdk/gl/animation/Loop.ts +0 -30
- package/runtime/sdk/gl/animation/Playback.ts +0 -44
- package/runtime/sdk/gl/animation/core.ts +0 -336
- package/runtime/sdk/gl/controls.ts +0 -95
- package/runtime/sdk/gl/physicsEvents.ts +0 -20
- package/runtime/sdk/gl/scenarios.ts +0 -291
- package/runtime/sdk/gl/state.ts +0 -6
- package/runtime/sdk/gl/terrainMesh.ts +0 -219
- package/runtime/sdk/gl/touch.ts +0 -68
- package/runtime/sdk/inject.ts +0 -197
- package/runtime/sdk/math/Mathf.ts +0 -118
- package/runtime/sdk/math/mat4.ts +0 -278
- package/runtime/sdk/math/quat.ts +0 -232
- package/runtime/sdk/math/vec.ts +0 -255
- package/runtime/sdk/net/codec.ts +0 -119
- package/runtime/sdk/net/core.ts +0 -384
- package/runtime/sdk/net/index.ts +0 -181
- package/runtime/sdk/net/replication.ts +0 -622
- package/runtime/sdk/plugins/camera.ts +0 -81
- package/runtime/sdk/plugins/geolocation.ts +0 -123
- package/runtime/sdk/plugins/oauth.ts +0 -61
- package/runtime/sdk/plugins/permission.ts +0 -7
- package/runtime/sdk/plugins/push.ts +0 -132
- package/runtime/sdk/plugins/qr.ts +0 -73
- package/runtime/sdk/plugins/service.ts +0 -47
- package/runtime/sdk/runtime/app.ts +0 -101
- package/runtime/sdk/runtime/appEvents.ts +0 -54
- package/runtime/sdk/runtime/channel.ts +0 -61
- package/runtime/sdk/runtime/clipboard.ts +0 -20
- package/runtime/sdk/runtime/datetime.ts +0 -329
- package/runtime/sdk/runtime/device.ts +0 -293
- package/runtime/sdk/runtime/fetch.ts +0 -77
- package/runtime/sdk/runtime/files.ts +0 -23
- package/runtime/sdk/runtime/input.ts +0 -175
- package/runtime/sdk/runtime/media.ts +0 -111
- package/runtime/sdk/runtime/misc.ts +0 -16
- package/runtime/sdk/runtime/net.ts +0 -36
- package/runtime/sdk/runtime/rpc.ts +0 -218
- package/runtime/sdk/runtime/service.ts +0 -83
- package/runtime/sdk/runtime/share.ts +0 -9
- package/runtime/sdk/runtime/storage.ts +0 -13
- package/runtime/sdk/runtime/touch.ts +0 -76
- package/runtime/sdk/scene/defineScene.ts +0 -1353
- package/runtime/sdk/scene/editorPlugins.ts +0 -110
- package/runtime/sdk/scene/gizmos.ts +0 -148
- package/runtime/sdk/scene/grammar.ts +0 -120
- package/runtime/sdk/scene/material.ts +0 -188
- package/runtime/sdk/server/auth/appConfig.ts +0 -12
- package/runtime/sdk/server/auth/global.ts +0 -80
- package/runtime/sdk/server/auth/host.ts +0 -318
- package/runtime/sdk/server/auth/models.ts +0 -83
- package/runtime/sdk/server/auth/types.ts +0 -50
- package/runtime/sdk/server/channel.ts +0 -56
- package/runtime/sdk/server/context.ts +0 -36
- package/runtime/sdk/server/db/defineDb.ts +0 -237
- package/runtime/sdk/server/db/fields.ts +0 -132
- package/runtime/sdk/server/db/httpTransport.ts +0 -93
- package/runtime/sdk/server/db/index.ts +0 -7
- package/runtime/sdk/server/db/marci/query.ts +0 -412
- package/runtime/sdk/server/db/types.ts +0 -202
- package/runtime/sdk/server/errors.ts +0 -12
- package/runtime/sdk/server/host.ts +0 -74
- package/runtime/sdk/server/inject.ts +0 -13
- package/runtime/sdk/server/runtime.ts +0 -133
- package/runtime/sdk/server/validate.ts +0 -87
- package/runtime/sdk/ui/NativeView.ts +0 -142
- package/runtime/sdk/ui/UI.ts +0 -39
- package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
- package/runtime/sdk/ui/UIButton.ts +0 -101
- package/runtime/sdk/ui/UIContainer.ts +0 -60
- package/runtime/sdk/ui/UIImage.ts +0 -83
- package/runtime/sdk/ui/UIInput.ts +0 -185
- package/runtime/sdk/ui/UIModal.ts +0 -139
- package/runtime/sdk/ui/UINode.ts +0 -830
- package/runtime/sdk/ui/UIPager.ts +0 -362
- package/runtime/sdk/ui/UIPopover.ts +0 -100
- package/runtime/sdk/ui/UIScreen.ts +0 -123
- package/runtime/sdk/ui/UIScrollable.ts +0 -87
- package/runtime/sdk/ui/UISpacer.ts +0 -14
- package/runtime/sdk/ui/UITabs.ts +0 -236
- package/runtime/sdk/ui/UIText.ts +0 -51
- package/runtime/sdk/ui/UIVideo.ts +0 -88
- package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
- package/runtime/sdk/ui/UIWidget.ts +0 -127
- package/runtime/sdk/ui/fonts.ts +0 -13
- package/runtime/sdk/ui/presentable.ts +0 -117
- package/runtime/sdk/ui/router.ts +0 -132
- package/runtime/sdk/ui/theme.ts +0 -84
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
// The gearbox of a `Vehicle`, as an aspect on the same node. It is deliberately NOT in the engine:
|
|
2
|
-
// picking a gear is policy, not physics — two RPM thresholds and a latency timer — and every game
|
|
3
|
-
// wants its own. The engine keeps only the physical half (the clutch's stiffness and the coupled
|
|
4
|
-
// engine/wheel solve), and is told one ratio and one clutch scalar per frame.
|
|
5
|
-
//
|
|
6
|
-
// car.aspect(Gearbox, { ratios: [2.66, 1.78, 1.3, 1.0, 0.74], reverse: -2.9 })
|
|
7
|
-
//
|
|
8
|
-
// A `Vehicle` with no `Gearbox` attaches this one with its defaults, so a car that never mentions
|
|
9
|
-
// gears still drives.
|
|
10
|
-
//
|
|
11
|
-
// Custom behaviour is a subclass overriding ONE method:
|
|
12
|
-
//
|
|
13
|
-
// class SportBox extends Gearbox {
|
|
14
|
-
// select(v: Vehicle): number { return v.rpm > 6200 ? this.gear + 1 : this.gear }
|
|
15
|
-
// }
|
|
16
|
-
//
|
|
17
|
-
// The clutch is two things multiplied. The SHIFT envelope (`shiftTime`/`clutchTime`) stays with the
|
|
18
|
-
// base class on purpose: a forgotten ramp breaks a car silently — no drive at all, or a slammed
|
|
19
|
-
// clutch. The TAKE-UP (`takeUp`) is the left-foot half, and is overridable for a car that launches
|
|
20
|
-
// unusually — a torque converter, a rally launch control.
|
|
21
|
-
|
|
22
|
-
import { Aspect } from "../core/Aspect"
|
|
23
|
-
import type { FieldMeta } from "../core/fields"
|
|
24
|
-
import type { Node } from "./Node"
|
|
25
|
-
import type { Vehicle } from "./Vehicle"
|
|
26
|
-
|
|
27
|
-
// How far above idle counts as "revs to spare". A simulated engine cannot fall below its idle — the
|
|
28
|
-
// physics clamps it — so being AT idle is the observable form of "the wheels are dragging me under".
|
|
29
|
-
// This is the tolerance for reading that clamp, not a tuning knob.
|
|
30
|
-
const IDLE_MARGIN = 1.05
|
|
31
|
-
|
|
32
|
-
// The car this gearbox belongs to. Read through the aspect accessor rather than node.get(Vehicle),
|
|
33
|
-
// so this file needs Vehicle only as a TYPE and the two do not import each other at runtime.
|
|
34
|
-
const carOf = (n: Node): Vehicle | null => (n as unknown as { vehicle?: Vehicle }).vehicle ?? null
|
|
35
|
-
|
|
36
|
-
export class Gearbox extends Aspect<"gearbox", Node> {
|
|
37
|
-
static readonly aspect = "gearbox"
|
|
38
|
-
|
|
39
|
-
/** Forward gear ratios, first gear first. Engine RPM = wheel speed × ratio × the differential. */
|
|
40
|
-
ratios: number[] = [ 2.66, 1.78, 1.3, 1.0, 0.74 ]
|
|
41
|
-
/** Reverse ratio — negative, because a ratio's SIGN is what reverse means to the engine. */
|
|
42
|
-
reverse = -2.9
|
|
43
|
-
/** Shift up when the engine passes this fraction of its redline. */
|
|
44
|
-
shiftUp = 0.67
|
|
45
|
-
/** Shift down when it falls below this fraction. */
|
|
46
|
-
shiftDown = 0.33
|
|
47
|
-
/** How long a shift takes, in seconds — the clutch is out for this long. */
|
|
48
|
-
shiftTime = 0.2
|
|
49
|
-
/** How long the clutch takes to re-engage afterwards. */
|
|
50
|
-
clutchTime = 0.3
|
|
51
|
-
/** How long to wait after a shift before another is allowed — without it a car sitting on a
|
|
52
|
-
* threshold hunts between two gears. */
|
|
53
|
-
latency = 0.5
|
|
54
|
-
|
|
55
|
-
/** How far the clutch bites the instant drive is asked for from rest, 0..1. It only has to COUPLE
|
|
56
|
-
* the shafts — how much torque that passes is the engine's business, so a feathered throttle
|
|
57
|
-
* still pulls away gently at the same bite point. */
|
|
58
|
-
bite = 0.4
|
|
59
|
-
|
|
60
|
-
static fields: FieldMeta<Gearbox> = {
|
|
61
|
-
reverse: { max: -0.1, step: 0.1 },
|
|
62
|
-
shiftUp: { min: 0.1, max: 1, step: 0.01 },
|
|
63
|
-
shiftDown: { min: 0, max: 0.9, step: 0.01 },
|
|
64
|
-
shiftTime: { min: 0, max: 3, step: 0.05 },
|
|
65
|
-
clutchTime: { min: 0, max: 3, step: 0.05 },
|
|
66
|
-
latency: { min: 0, max: 3, step: 0.05 },
|
|
67
|
-
bite: { min: 0, max: 1, step: 0.05 },
|
|
68
|
-
ratios: { hidden: true },
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Current gear: −1 reverse, 0 neutral, 1… forward. Read it for a HUD. */
|
|
72
|
-
gear = 0
|
|
73
|
-
/** True while a shift is in progress — the clutch is out and the engine is not driving the wheels. */
|
|
74
|
-
get shifting(): boolean { return this._shiftLeft > 0 }
|
|
75
|
-
|
|
76
|
-
private _shiftLeft = 0 // seconds of clutch-out left
|
|
77
|
-
private _engageLeft = 0 // seconds of clutch re-engagement left
|
|
78
|
-
private _waitLeft = 0 // anti-hunting
|
|
79
|
-
private _take = 0 // how far the CAR's own state wants the clutch in, latched per frame
|
|
80
|
-
|
|
81
|
-
/** EARLY phase: the pair has to be latched before this frame's step consumes it. */
|
|
82
|
-
updateBefore(dt: number): void {
|
|
83
|
-
const v = carOf(this.node)
|
|
84
|
-
if (!v || !v.id) return
|
|
85
|
-
|
|
86
|
-
this._waitLeft = Math.max(0, this._waitLeft - dt)
|
|
87
|
-
|
|
88
|
-
if (this._shiftLeft > 0) {
|
|
89
|
-
// Mid-shift: clutch fully out, and the moment it ends the re-engagement ramp starts.
|
|
90
|
-
this._shiftLeft = Math.max(0, this._shiftLeft - dt)
|
|
91
|
-
if (this._shiftLeft === 0) this._engageLeft = this.clutchTime
|
|
92
|
-
} else {
|
|
93
|
-
const want = this.select(v)
|
|
94
|
-
if (want !== this.gear) this.shift(want)
|
|
95
|
-
else this._engageLeft = Math.max(0, this._engageLeft - dt)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// The pedal only moves while no shift is in flight. During one the revs say nothing about
|
|
99
|
-
// whether the wheels can sustain them: an engine still spinning down from the last gear reads as
|
|
100
|
-
// "revs to spare" and would drop the clutch straight back in — enough to shove a stopped car
|
|
101
|
-
// several km/h with no throttle at all. Freezing it is what a driver does anyway; the foot is
|
|
102
|
-
// already down, and the envelope owns the clutch until the gear is in.
|
|
103
|
-
if (this._shiftLeft === 0 && this._engageLeft === 0) {
|
|
104
|
-
this._take = Math.max(0, Math.min(1, this.takeUp(v, dt)))
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
v._setTransmission(this.ratio, this.clutch)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* The driver's left foot: where the clutch pedal is, ignoring shifts. Returns the new engagement.
|
|
112
|
-
*
|
|
113
|
-
* A driver does not compute anything. They watch the tacho: **revs sinking to idle means the
|
|
114
|
-
* wheels are about to drag the engine under, and that is when you push the clutch in.** That
|
|
115
|
-
* single test replaces every ratio and threshold — the crossover it finds is the exact speed where
|
|
116
|
-
* engine braking stops and the engine would start PUSHING instead, and it finds it per gear
|
|
117
|
-
* without being told the gear.
|
|
118
|
-
*
|
|
119
|
-
* The pedal comes back UP only for the throttle. Healthy revs are not on their own a reason to
|
|
120
|
-
* engage: a car standing still shows healthy revs too — the wheels simply are not turning to say
|
|
121
|
-
* otherwise — and a clutch dropped on that evidence lurches the car off with no throttle at all.
|
|
122
|
-
*
|
|
123
|
-
* Three things fall out of it for free:
|
|
124
|
-
*
|
|
125
|
-
* - **A braked car stops.** The engine cannot stall, so a clutch left in at 0 km/h feeds idle
|
|
126
|
-
* torque to the wheels forever and the car creeps against its own brakes. Here the revs pin at
|
|
127
|
-
* idle, so the pedal goes down and the brakes have nothing to fight.
|
|
128
|
-
* - **Reverse is not a special case.** No ratio takes part, so it needs no separate number — which
|
|
129
|
-
* is exactly why a fixed road-speed threshold got reverse wrong.
|
|
130
|
-
* - **Hill starts judder instead of stalling.** Let the pedal out, the load pulls the revs down,
|
|
131
|
-
* the rule pushes it straight back in, and the clutch sits slipping at the bite point until the
|
|
132
|
-
* car moves. So the test deliberately ignores the throttle — gating it on "no throttle" would
|
|
133
|
-
* stall the car under load. Throttle only holds the floor at `bite`.
|
|
134
|
-
*
|
|
135
|
-
* The cost is honest and small: once the pedal is down the revs sit at idle and cannot rise on
|
|
136
|
-
* their own, so **coasting downhill gives no engine braking until you touch the throttle** — which
|
|
137
|
-
* is precisely what a car with the clutch in does. Slipping it permanently to keep listening would
|
|
138
|
-
* bring the creep straight back.
|
|
139
|
-
*/
|
|
140
|
-
protected takeUp(v: Vehicle, dt: number): number {
|
|
141
|
-
if (this.gear === 0) return 0
|
|
142
|
-
const idle = v.engine.idleRpm ?? 1000
|
|
143
|
-
const step = dt / Math.max(this.clutchTime, dt)
|
|
144
|
-
const asked = v.throttle !== 0
|
|
145
|
-
let take = this._take
|
|
146
|
-
if (v.rpm <= idle * IDLE_MARGIN) take -= step // being dragged under: pedal goes down
|
|
147
|
-
else if (asked) take += step // revs to spare AND somewhere to go: let it up
|
|
148
|
-
// Asking for drive also holds the pedal at the bite point however far down the rule wants it;
|
|
149
|
-
// that is what makes a launch — and a hill start — slip instead of collapsing to nothing.
|
|
150
|
-
return asked ? Math.max(take, this.bite) : take
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Which gear the car should be in. The default is an ordinary automatic: pull away from neutral in
|
|
155
|
-
* the direction of the throttle, shift up past `shiftUp` of the redline and down below `shiftDown`,
|
|
156
|
-
* and refuse to shift again for `latency` seconds. Override this and nothing else.
|
|
157
|
-
*/
|
|
158
|
-
select(v: Vehicle): number {
|
|
159
|
-
const throttle = v.throttle
|
|
160
|
-
// Neutral, or asked to go the other way: engage once the car has (nearly) stopped.
|
|
161
|
-
if (this.gear === 0 || throttle * this.gear < 0) {
|
|
162
|
-
if (Math.abs(v.speed) > 1 && this.gear !== 0) return this.gear
|
|
163
|
-
return throttle > 0 ? 1 : throttle < 0 ? -1 : this.gear
|
|
164
|
-
}
|
|
165
|
-
if (this.gear < 0 || this._waitLeft > 0 || this.shifting) return this.gear
|
|
166
|
-
const rpm = v.rpm
|
|
167
|
-
const max = v.engine.maxRpm ?? 6000
|
|
168
|
-
// Upshifting needs COUPLED revs. With the clutch out the engine is free, so a blip at a
|
|
169
|
-
// standstill reads as 6000 rpm and the box would climb to top gear without the car moving —
|
|
170
|
-
// then launch in it. Downshifting stays open either way: it can only take you to a lower gear,
|
|
171
|
-
// which is where a car with dying revs belongs.
|
|
172
|
-
if (rpm > max * this.shiftUp && this.clutch >= 1 && this.gear < this.ratios.length) return this.gear + 1
|
|
173
|
-
if (rpm < max * this.shiftDown && this.gear > 1) return this.gear - 1
|
|
174
|
-
return this.gear
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Change gear now, with the clutch envelope. Safe to call from `select()` or from game code
|
|
178
|
-
* (a sequential manual is `if (Input.key('KeyE')) car.gearbox.shift(car.gearbox.gear + 1)`). */
|
|
179
|
-
shift(gear: number): void {
|
|
180
|
-
const clamped = Math.max(-1, Math.min(this.ratios.length, Math.round(gear)))
|
|
181
|
-
if (clamped === this.gear) return
|
|
182
|
-
// The envelope is for swapping one ENGAGED gear for another. If nothing is coupled there is
|
|
183
|
-
// nothing to disengage, so it is skipped: neutral at either end, and equally a car sitting still
|
|
184
|
-
// with the pedal already down — picking reverse there should pull away the moment it is asked
|
|
185
|
-
// to, not sit out a shift it never had to make.
|
|
186
|
-
const swapping = this.gear !== 0 && clamped !== 0 && this._take > 0
|
|
187
|
-
this.gear = clamped
|
|
188
|
-
this._shiftLeft = swapping ? this.shiftTime : 0
|
|
189
|
-
this._engageLeft = swapping ? this.clutchTime : 0
|
|
190
|
-
this._waitLeft = this.latency + this._shiftLeft
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** The ratio the engine should be running: 0 in neutral, negative in reverse. */
|
|
194
|
-
get ratio(): number {
|
|
195
|
-
if (this.gear === 0) return 0
|
|
196
|
-
if (this.gear < 0) return this.reverse
|
|
197
|
-
return this.ratios[this.gear - 1] ?? 1
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* The clutch scalar 0..1 the engine is given: the SHIFT envelope (out during a shift, ramping back
|
|
202
|
-
* over `clutchTime`) times the driver's pedal (`takeUp`). Two independent reasons for the clutch
|
|
203
|
-
* to be out, so they multiply: mid-shift is out however healthy the revs are, and an engine being
|
|
204
|
-
* dragged to idle is out whatever gear it just picked.
|
|
205
|
-
*/
|
|
206
|
-
get clutch(): number {
|
|
207
|
-
if (this._shiftLeft > 0) return 0
|
|
208
|
-
const envelope =
|
|
209
|
-
this._engageLeft > 0 && this.clutchTime > 0 ? 1 - this._engageLeft / this.clutchTime : 1
|
|
210
|
-
return envelope * this._take
|
|
211
|
-
}
|
|
212
|
-
}
|
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
// Mesh geometry: vertex / normal / index / uv buffers, plus the primitive builders. `Geometry.box()`
|
|
2
|
-
// etc. return a Geometry you can hand to Mesh.from(); Mesh.box() is sugar over them. Ported from
|
|
3
|
-
// worker/src/primitives/*.
|
|
4
|
-
|
|
5
|
-
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
6
|
-
|
|
7
|
-
export type MeshKind = "triangles" | "vertices" | "edges"
|
|
8
|
-
|
|
9
|
-
// local tuple helpers — geometry building works on raw Float32Array buffers, so it stays out of the
|
|
10
|
-
// Vec3 wrapper (no per-vertex allocation).
|
|
11
|
-
type Tup3 = [number, number, number]
|
|
12
|
-
const cross = (a: Tup3, b: Tup3): Tup3 => [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]
|
|
13
|
-
const normalize = (a: Tup3): Tup3 => {
|
|
14
|
-
const l = Math.hypot(a[0], a[1], a[2])
|
|
15
|
-
return l === 0 ? [ 0, 0, 0 ] : [ a[0] / l, a[1] / l, a[2] / l ]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class Geometry {
|
|
19
|
-
vertices: Float32Array
|
|
20
|
-
normals: Float32Array
|
|
21
|
-
indices: Uint16Array
|
|
22
|
-
uv: Float32Array
|
|
23
|
-
/** Lightmap UV set (2 floats per vertex, non-overlapping, inside the unit square) — the
|
|
24
|
-
* `Lightmap` atlas samples a Mesh through it. The box and cylinder builders fill it (their `uv`
|
|
25
|
-
* tiles per face, which would fold every face onto the same texels); absent = the host reuses
|
|
26
|
-
* `uv`, which is right for a plane. */
|
|
27
|
-
uv1?: Float32Array
|
|
28
|
-
/** @internal native mesh-type enum (0 triangles / 1 edges / 2 vertices). */
|
|
29
|
-
_meshKind = 0
|
|
30
|
-
|
|
31
|
-
constructor(vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array) {
|
|
32
|
-
this.vertices = vertices
|
|
33
|
-
this.normals = normals
|
|
34
|
-
this.indices = indices
|
|
35
|
-
this.uv = uv
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
translate(x: number, y: number, z: number): this {
|
|
39
|
-
for (let i = 0; i < this.vertices.length; i += 3) {
|
|
40
|
-
this.vertices[i] += x; this.vertices[i + 1] += y; this.vertices[i + 2] += z
|
|
41
|
-
}
|
|
42
|
-
return this
|
|
43
|
-
}
|
|
44
|
-
scale(x: number, y: number, z: number): this {
|
|
45
|
-
for (let i = 0; i < this.vertices.length; i += 3) {
|
|
46
|
-
this.vertices[i] *= x; this.vertices[i + 1] *= y; this.vertices[i + 2] *= z
|
|
47
|
-
}
|
|
48
|
-
return this
|
|
49
|
-
}
|
|
50
|
-
scaleUV(x: number, y: number): this {
|
|
51
|
-
for (let i = 0; i < this.uv.length; i += 2) { this.uv[i] *= x; this.uv[i + 1] *= y }
|
|
52
|
-
return this
|
|
53
|
-
}
|
|
54
|
-
set kind(kind: MeshKind) {
|
|
55
|
-
this._meshKind = kind === "edges" ? 1 : kind === "vertices" ? 2 : 0
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** A box; `size` (default 1) scales the vertices AND lays the lightmap chart out in proportion to
|
|
59
|
-
* the faces' areas (a floor's top face gets the texels, not its 1 m side strips). */
|
|
60
|
-
static box(size: Vec3Like | number = 1): Geometry { return createCube(size) }
|
|
61
|
-
static sphere(options?: SphereOptions): Geometry { return createSphere(options) }
|
|
62
|
-
static cylinder(options?: CylinderOptions): Geometry { return createCylinder(options) }
|
|
63
|
-
static plane(options?: PlaneOptions): Geometry { return createPlane(options) }
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Gutter between lightmap charts (fraction of the chart square): the bake dilates each rect by a
|
|
67
|
-
// few texels and the shader samples bilinearly, so neighbouring faces must not touch in uv1.
|
|
68
|
-
const LM_GUTTER = 0.03
|
|
69
|
-
|
|
70
|
-
type Rect = { x: number, y: number, w: number, h: number }
|
|
71
|
-
|
|
72
|
-
/** Shelf-pack rectangles (metres) into the unit square at the largest uniform scale — every face
|
|
73
|
-
* gets texels in proportion to its area, so a 64×1×64 floor's top face isn't squeezed into the
|
|
74
|
-
* same cell as its 1 m side strips. Returns rects in input order. */
|
|
75
|
-
const packCharts = (dims: [number, number][]): Rect[] => {
|
|
76
|
-
const order = dims.map((_, i) => i).sort((a, b) => dims[b][1] - dims[a][1])
|
|
77
|
-
const attempt = (scale: number): Rect[] | null => {
|
|
78
|
-
const out: Rect[] = new Array(dims.length)
|
|
79
|
-
let x = 0, y = 0, rowH = 0
|
|
80
|
-
for (const i of order) {
|
|
81
|
-
const w = dims[i][0] * scale, h = dims[i][1] * scale
|
|
82
|
-
if (x > 0 && x + w > 1) { y += rowH + LM_GUTTER; x = 0; rowH = 0 }
|
|
83
|
-
if (w > 1 || y + h > 1) return null
|
|
84
|
-
out[i] = { x, y, w, h }
|
|
85
|
-
x += w + LM_GUTTER
|
|
86
|
-
rowH = Math.max(rowH, h)
|
|
87
|
-
}
|
|
88
|
-
return out
|
|
89
|
-
}
|
|
90
|
-
let lo = 0, hi = 1 / Math.max(...dims.map((d) => Math.max(d[0], d[1])))
|
|
91
|
-
let best = attempt(hi)
|
|
92
|
-
if (!best) {
|
|
93
|
-
for (let it = 0; it < 40; it++) {
|
|
94
|
-
const mid = (lo + hi) / 2
|
|
95
|
-
const r = attempt(mid)
|
|
96
|
-
if (r) { best = r; lo = mid } else hi = mid
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return best ?? dims.map(() => ({ x: 0, y: 0, w: 1, h: 1 }))
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// --- cube ---
|
|
103
|
-
const createCube = (size: Vec3Like | number = 1): Geometry => {
|
|
104
|
-
const sx = typeof size === "number" ? size : cx(size)
|
|
105
|
-
const sy = typeof size === "number" ? size : cy(size)
|
|
106
|
-
const sz = typeof size === "number" ? size : cz(size)
|
|
107
|
-
const vertices = new Float32Array(6 * 4 * 3)
|
|
108
|
-
const normals = new Float32Array(6 * 4 * 3)
|
|
109
|
-
const uv = new Float32Array(6 * 4 * 2)
|
|
110
|
-
const uv1 = new Float32Array(6 * 4 * 2)
|
|
111
|
-
const indices = new Uint16Array(6 * 6)
|
|
112
|
-
let cnt = 0, indicesCnt = 0
|
|
113
|
-
const sides = [ -0.5, 0.5 ]
|
|
114
|
-
// face extents along the (a, b) parameters below: ±x faces span (z, y), ±y faces (x, z), ±z faces (x, y)
|
|
115
|
-
const charts = packCharts([ [ sz, sy ], [ sz, sy ], [ sx, sz ], [ sx, sz ], [ sx, sy ], [ sx, sy ] ])
|
|
116
|
-
for (let i = 0; i < 6; i++) {
|
|
117
|
-
const s = cnt
|
|
118
|
-
const side = i % 2 === 0 ? -0.5 : 0.5
|
|
119
|
-
const c = charts[i]
|
|
120
|
-
for (const a of sides) {
|
|
121
|
-
for (const b of sides) {
|
|
122
|
-
uv.set([ a + 0.5, b + 0.5 ], cnt * 2)
|
|
123
|
-
uv1.set([ c.x + (a + 0.5) * c.w, c.y + (b + 0.5) * c.h ], cnt * 2)
|
|
124
|
-
if (i === 0 || i === 1) { vertices.set([ side * sx, b * sy, -a * sz ], cnt * 3); normals.set([ side * 2, 0, 0 ], cnt * 3) }
|
|
125
|
-
if (i === 2 || i === 3) { vertices.set([ a * sx, side * sy, -b * sz ], cnt * 3); normals.set([ 0, side * 2, 0 ], cnt * 3) }
|
|
126
|
-
if (i === 4 || i === 5) { vertices.set([ a * sx, b * sy, side * sz ], cnt * 3); normals.set([ 0, 0, side * 2 ], cnt * 3) }
|
|
127
|
-
cnt++
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const s1 = s + 1, s2 = s + 2, s3 = s + 3
|
|
131
|
-
if (i % 2 === 0) indices.set([ s, s1, s2, s3, s2, s1 ], indicesCnt)
|
|
132
|
-
else indices.set([ s2, s1, s, s1, s2, s3 ], indicesCnt)
|
|
133
|
-
indicesCnt += 6
|
|
134
|
-
}
|
|
135
|
-
const g = new Geometry(vertices, normals, indices, uv)
|
|
136
|
-
g.uv1 = uv1
|
|
137
|
-
return g
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// --- sphere ---
|
|
141
|
-
export type SphereOptions = { radius?: number, widthSegments?: number, heightSegments?: number }
|
|
142
|
-
const createSphere = (options: SphereOptions = {}): Geometry => {
|
|
143
|
-
const { radius = 0.5, widthSegments = 32, heightSegments = 32 } = options
|
|
144
|
-
const verticesCount = (widthSegments + 1) * (heightSegments - 1) + widthSegments * 2
|
|
145
|
-
const vertices = new Float32Array(verticesCount * 3)
|
|
146
|
-
const normals = new Float32Array(verticesCount * 3)
|
|
147
|
-
const uvs = new Float32Array(verticesCount * 2)
|
|
148
|
-
const indices = new Uint16Array(widthSegments * (heightSegments - 2) * 6 + widthSegments * 3 * 2)
|
|
149
|
-
const phiLength = Math.PI * 2, thetaLength = Math.PI
|
|
150
|
-
let vertexCnt = 0, indicesCnt = 0
|
|
151
|
-
for (let iy = 1; iy < heightSegments; iy++) {
|
|
152
|
-
const v = iy / heightSegments
|
|
153
|
-
for (let ix = 0; ix <= widthSegments; ix++) {
|
|
154
|
-
const u = ix / widthSegments
|
|
155
|
-
const point: [number, number, number] = [
|
|
156
|
-
-Math.cos(u * phiLength) * Math.sin(v * thetaLength),
|
|
157
|
-
Math.cos(v * thetaLength),
|
|
158
|
-
Math.sin(u * phiLength) * Math.sin(v * thetaLength),
|
|
159
|
-
]
|
|
160
|
-
vertices.set([ point[0] * radius, point[1] * radius, point[2] * radius ], vertexCnt * 3)
|
|
161
|
-
normals.set(point, vertexCnt * 3)
|
|
162
|
-
uvs.set([ u, 1 - v ], vertexCnt * 2)
|
|
163
|
-
if (iy > 1 && ix > 0) {
|
|
164
|
-
const s = vertexCnt - 1
|
|
165
|
-
const s1 = vertexCnt - (widthSegments + 1) - 1
|
|
166
|
-
indices.set([ s + 1, s1, s, s1, s + 1, s1 + 1 ], indicesCnt)
|
|
167
|
-
indicesCnt += 6
|
|
168
|
-
}
|
|
169
|
-
vertexCnt++
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const sides = [ -radius, radius ]
|
|
173
|
-
const circleStart2 = vertexCnt - (widthSegments + 1)
|
|
174
|
-
for (const side of sides) {
|
|
175
|
-
const circleStart = side > 0 ? 0 : circleStart2
|
|
176
|
-
for (let ix = 0; ix < widthSegments; ix++) {
|
|
177
|
-
const u = (ix + 0.5) / widthSegments
|
|
178
|
-
vertices.set([ 0, side, 0 ], vertexCnt * 3)
|
|
179
|
-
normals.set([ 0, side < 0 ? -1 : 1, 0 ], vertexCnt * 3)
|
|
180
|
-
uvs.set([ u, side < 0 ? 0 : 1 ], vertexCnt * 2)
|
|
181
|
-
const s = circleStart + ix
|
|
182
|
-
if (side > 0) indices.set([ s, s + 1, vertexCnt ], indicesCnt)
|
|
183
|
-
else indices.set([ vertexCnt, s + 1, s ], indicesCnt)
|
|
184
|
-
indicesCnt += 3
|
|
185
|
-
vertexCnt++
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return new Geometry(vertices, normals, indices, uvs)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// --- cylinder ---
|
|
192
|
-
export type CylinderOptions = { radius?: number, radiusTop?: number, radiusBottom?: number, edges?: number, smooth?: boolean }
|
|
193
|
-
const createCylinder = (options: CylinderOptions = {}): Geometry => {
|
|
194
|
-
const { edges = 32, smooth = true, radius = 0.5, radiusTop = radius, radiusBottom = radius } = options
|
|
195
|
-
const verticesCount = edges * 4 + (edges + 1) * 2
|
|
196
|
-
const vertices = new Float32Array(verticesCount * 3)
|
|
197
|
-
const normals = new Float32Array(verticesCount * 3)
|
|
198
|
-
const uv = new Float32Array(verticesCount * 2)
|
|
199
|
-
// lightmap chart: the side strip across the upper half, the two caps as discs in the lower half
|
|
200
|
-
const uv1 = new Float32Array(verticesCount * 2)
|
|
201
|
-
const indices = new Uint16Array(edges * 6 + edges * 6)
|
|
202
|
-
const sides = [ -0.5, 0.5 ]
|
|
203
|
-
let cnt = 0, indicesCnt = 0
|
|
204
|
-
for (let i = 0; i < edges; i++) {
|
|
205
|
-
const s = cnt
|
|
206
|
-
const normalX = Math.cos((i + 0.5) / edges * Math.PI * 2)
|
|
207
|
-
const normalY = Math.sin((i + 0.5) / edges * Math.PI * 2)
|
|
208
|
-
for (let j = 0; j < 2; j++) {
|
|
209
|
-
const x = Math.cos((i + j) / edges * Math.PI * 2)
|
|
210
|
-
const y = Math.sin((i + j) / edges * Math.PI * 2)
|
|
211
|
-
for (const side of sides) {
|
|
212
|
-
if (side > 0) vertices.set([ x * radiusTop, side, y * radiusTop ], cnt * 3)
|
|
213
|
-
else vertices.set([ x * radiusBottom, side, y * radiusBottom ], cnt * 3)
|
|
214
|
-
if (smooth) normals.set([ x, 0, y ], cnt * 3)
|
|
215
|
-
else normals.set([ normalX, 0, normalY ], cnt * 3)
|
|
216
|
-
uv.set([ (i + j) / edges, side + 0.5 ], cnt * 2)
|
|
217
|
-
uv1.set([ (i + j) / edges, 0.55 + (side + 0.5) * 0.43 ], cnt * 2)
|
|
218
|
-
cnt++
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
const s1 = s + 1, s2 = (s + 2) % (edges * 4), s3 = (s + 3) % (edges * 4)
|
|
222
|
-
indices.set([ s, s1, s2, s3, s2, s1 ], indicesCnt)
|
|
223
|
-
indicesCnt += 6
|
|
224
|
-
}
|
|
225
|
-
for (const side of sides) {
|
|
226
|
-
const center = cnt
|
|
227
|
-
const capU = side > 0 ? 0.75 : 0.25 // lightmap disc centre per cap
|
|
228
|
-
vertices.set([ 0, side, 0 ], cnt * 3); normals.set([ 0, side * 2, 0 ], cnt * 3)
|
|
229
|
-
uv1.set([ capU, 0.25 ], cnt * 2); cnt++
|
|
230
|
-
for (let i = 0; i < edges; i++) {
|
|
231
|
-
const x = Math.cos(i / edges * Math.PI * 2), y = Math.sin(i / edges * Math.PI * 2)
|
|
232
|
-
if (side > 0) vertices.set([ x * radiusTop, side, y * radiusTop ], cnt * 3)
|
|
233
|
-
else vertices.set([ x * radiusBottom, side, y * radiusBottom ], cnt * 3)
|
|
234
|
-
normals.set([ 0, side * 2, 0 ], cnt * 3)
|
|
235
|
-
uv1.set([ capU + x * 0.22, 0.25 + y * 0.22 ], cnt * 2)
|
|
236
|
-
if (side < 0) indices.set([ center, center + i + 1, center + 1 + ((i + 1) % edges) ], indicesCnt)
|
|
237
|
-
else indices.set([ center + 1 + ((i + 1) % edges), center + i + 1, center ], indicesCnt)
|
|
238
|
-
indicesCnt += 3
|
|
239
|
-
cnt++
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
const g = new Geometry(vertices, normals, indices, uv)
|
|
243
|
-
g.uv1 = uv1
|
|
244
|
-
return g
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// --- plane ---
|
|
248
|
-
export type PlaneOptions = { normal?: Vec3Like }
|
|
249
|
-
const createPlane = (options: PlaneOptions = {}): Geometry => {
|
|
250
|
-
const normal: Tup3 = options.normal ? [ cx(options.normal), cy(options.normal), cz(options.normal) ] : [ 0, 0, 1 ]
|
|
251
|
-
const vertices = new Float32Array(4 * 3)
|
|
252
|
-
const normals = new Float32Array(4 * 3)
|
|
253
|
-
const uvs = new Float32Array(4 * 2)
|
|
254
|
-
const sides = [ -0.5, 0.5 ]
|
|
255
|
-
let cnt = 0
|
|
256
|
-
let upVector: Tup3, rightVec: Tup3
|
|
257
|
-
if (normal[1] === 0 && normal[2] === 0) { upVector = [ 0, 1, 0 ]; rightVec = [ 0, 0, -normal[0] ] }
|
|
258
|
-
else if (normal[0] === 0 && normal[2] === 0) { upVector = [ 0, 0, 1 ]; rightVec = [ -normal[1], 0, 0 ] }
|
|
259
|
-
else if (normal[0] === 0 && normal[1] === 0) { upVector = [ 0, 1, 0 ]; rightVec = [ normal[2], 0, 0 ] } // тут было верно
|
|
260
|
-
else {
|
|
261
|
-
const temp: Tup3 = Math.abs(normal[0]) < 0.5 ? [ 1, 0, 0 ] : [ 0, 0, 1 ]
|
|
262
|
-
upVector = normalize(cross(temp, normal))
|
|
263
|
-
rightVec = normalize(cross(upVector, normal))
|
|
264
|
-
}
|
|
265
|
-
for (const a of sides) {
|
|
266
|
-
for (const b of sides) {
|
|
267
|
-
normals.set(normal, cnt * 3)
|
|
268
|
-
uvs.set([ a + 0.5, b + 0.5 ], cnt * 2)
|
|
269
|
-
vertices.set([
|
|
270
|
-
rightVec[0] * a + upVector[0] * b,
|
|
271
|
-
rightVec[1] * a + upVector[1] * b,
|
|
272
|
-
rightVec[2] * a + upVector[2] * b,
|
|
273
|
-
], cnt * 3)
|
|
274
|
-
cnt++
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
const indices = new Uint16Array([ 2, 1, 0, 1, 2, 3 ])
|
|
278
|
-
return new Geometry(vertices, normals, indices, uvs)
|
|
279
|
-
}
|
package/runtime/sdk/gl/IK.ts
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
// IK — precise character actions as late-phase aspects on BONES (docs/animation-plan.md §2.5).
|
|
2
|
-
// They run after the animation wrote the pose (base anim or Animator) and before the skin is
|
|
3
|
-
// flushed, so their bone writes land in the skin. Attach to the END bone of the chain; the chain is
|
|
4
|
-
// walked up through parents — one instance per end effector, any Node hierarchy works.
|
|
5
|
-
//
|
|
6
|
-
// const foot = hero.bone('LeftFoot')!
|
|
7
|
-
// foot.aspect(IK.TwoBone, { target: footTarget, pole: kneeHint }) // upLeg → leg → foot
|
|
8
|
-
// foot.ik.weight = grounded ? 1 : 0 // blend in/out
|
|
9
|
-
// hero.bone('Head')!.aspect(IK.LookAt, { target: camera, limit: 70 }) // head tracks the camera
|
|
10
|
-
//
|
|
11
|
-
// Pure SDK/JS: a handful of world-matrix reads + two quaternion writes per solve. A native solver can
|
|
12
|
-
// slot in later behind the same API.
|
|
13
|
-
|
|
14
|
-
import { Aspect } from "../core/Aspect"
|
|
15
|
-
import { Vec3, type Vec3Like } from "../math/vec"
|
|
16
|
-
import { Quat } from "../math/quat"
|
|
17
|
-
import type { Node } from "./Node"
|
|
18
|
-
|
|
19
|
-
type Target = Node | Vec3Like
|
|
20
|
-
|
|
21
|
-
const targetPos = (t: Target | undefined): Vec3 | null => {
|
|
22
|
-
if (t === undefined || t === null) return null
|
|
23
|
-
const n = t as Node
|
|
24
|
-
if (typeof (n as { worldPosition?: unknown }).worldPosition === "object" && typeof n.id === "number") return n.worldPosition
|
|
25
|
-
return new Vec3(t as Vec3Like)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** World rotation of a node (parent chain composed natively). */
|
|
29
|
-
const worldRot = (n: Node): Quat => n.worldMatrix.rotation
|
|
30
|
-
|
|
31
|
-
/** Write a WORLD rotation to a node by converting through the parent's world rotation. */
|
|
32
|
-
const setWorldRot = (n: Node, q: Quat): void => {
|
|
33
|
-
const p = n.parent
|
|
34
|
-
n.quaternion = p ? worldRot(p).invert().mul(q).normalize() : q.normalize()
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const clampAngle = (q: Quat, maxRad: number): Quat => {
|
|
38
|
-
// angle of a unit quaternion = 2·acos(|w|)
|
|
39
|
-
const w = Math.min(1, Math.abs(q.w))
|
|
40
|
-
const ang = 2 * Math.acos(w)
|
|
41
|
-
if (ang <= maxRad || ang < 1e-6) return q
|
|
42
|
-
const s = Math.sqrt(1 - w * w)
|
|
43
|
-
const axis = new Vec3(q.x / s, q.y / s, q.z / s)
|
|
44
|
-
return Quat.fromAxisAngle(axis, q.w < 0 ? -maxRad : maxRad)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Two-bone analytic IK (limbs). Attach to the END bone: `foot.aspect(IK.TwoBone, { target })`
|
|
48
|
-
* solves upper (grandparent) + mid (parent) so the end reaches `target`; `pole` steers the bend
|
|
49
|
-
* (knee/elbow) — a Node or world point. */
|
|
50
|
-
class TwoBone extends Aspect<"ik", Node> {
|
|
51
|
-
static readonly aspect = "ik"
|
|
52
|
-
|
|
53
|
-
/** Where the end bone should be (Node or world position). */
|
|
54
|
-
target?: Target
|
|
55
|
-
/** Bend hint — the mid joint is pulled toward it (Node or world position). */
|
|
56
|
-
pole?: Target
|
|
57
|
-
/** World rotation the END bone takes after the solve — a Node (its world rotation) or a Quat.
|
|
58
|
-
* Unset = the end bone keeps its animated local rotation (a hand on a grip, a foot on a slope
|
|
59
|
-
* want it set). Blended by `weight · rotationWeight`. */
|
|
60
|
-
rotation?: Node | Quat
|
|
61
|
-
/** 0–1 contribution of `rotation` (on top of `weight`). */
|
|
62
|
-
rotationWeight = 1
|
|
63
|
-
/** 0–1 contribution (blend in/out, e.g. foot planting only while grounded). */
|
|
64
|
-
weight = 1
|
|
65
|
-
/** Solve every frame (default). Set false to drive `solve()` yourself. */
|
|
66
|
-
enabled = true
|
|
67
|
-
|
|
68
|
-
update(): void {
|
|
69
|
-
if (this.enabled) this.solve()
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** One solve at the current pose. Safe to call manually (e.g. from a later-ordered aspect). */
|
|
73
|
-
solve(): void {
|
|
74
|
-
const t = targetPos(this.target)
|
|
75
|
-
if (!t || this.weight <= 0) return
|
|
76
|
-
const end = this.node
|
|
77
|
-
const mid = end.parent
|
|
78
|
-
const root = mid?.parent
|
|
79
|
-
if (!mid || !root) return
|
|
80
|
-
|
|
81
|
-
const w = Math.min(1, this.weight)
|
|
82
|
-
const midLocal0 = mid.quaternion
|
|
83
|
-
const rootLocal0 = root.quaternion
|
|
84
|
-
|
|
85
|
-
const a = root.worldPosition, b = mid.worldPosition, c = end.worldPosition
|
|
86
|
-
const l1 = a.distanceTo(b), l2 = b.distanceTo(c)
|
|
87
|
-
if (l1 < 1e-6 || l2 < 1e-6) return
|
|
88
|
-
const eps = 1e-4
|
|
89
|
-
const toT = t.sub(a)
|
|
90
|
-
const d = Math.min(Math.max(toT.length(), eps), l1 + l2 - eps)
|
|
91
|
-
|
|
92
|
-
// 1. bend the mid joint to the angle the target distance demands (law of cosines)
|
|
93
|
-
const ba = a.sub(b), bc = c.sub(b)
|
|
94
|
-
const cur = ba.angle(bc)
|
|
95
|
-
const want = Math.acos(Math.min(1, Math.max(-1, (l1 * l1 + l2 * l2 - d * d) / (2 * l1 * l2))))
|
|
96
|
-
// a positive rotation about ba × bc moves `bc` AWAY from `ba`, i.e. opens the joint — so rotating the
|
|
97
|
-
// mid bone by (want − cur) about it opens/closes exactly as needed. (bc × ba has the opposite sense:
|
|
98
|
-
// with it every already-bent limb — which is every animated limb — bends the wrong way; the straight
|
|
99
|
-
// case below picks its axis as ba × ref, the same sense as ba × bc for a limb barely bent toward ref.)
|
|
100
|
-
let axis = ba.cross(bc)
|
|
101
|
-
if (axis.lengthSq() < 1e-10) {
|
|
102
|
-
// straight limb: bend toward the pole (or any perpendicular)
|
|
103
|
-
const p = targetPos(this.pole)
|
|
104
|
-
const ref = p ? p.sub(b) : new Vec3(0, 0, 1)
|
|
105
|
-
axis = ba.cross(ref)
|
|
106
|
-
if (axis.lengthSq() < 1e-10) axis = ba.cross(new Vec3(0, 1, 0))
|
|
107
|
-
if (axis.lengthSq() < 1e-10) axis = ba.cross(new Vec3(1, 0, 0))
|
|
108
|
-
}
|
|
109
|
-
axis = axis.normalize()
|
|
110
|
-
// rotating mid by (want - cur) about `axis` opens/closes the angle between ba and bc
|
|
111
|
-
const midWorld = worldRot(mid)
|
|
112
|
-
setWorldRot(mid, Quat.fromAxisAngle(axis, want - cur).mul(midWorld))
|
|
113
|
-
|
|
114
|
-
// 2. swing the root so the end lands on the target direction
|
|
115
|
-
const c2 = end.worldPosition
|
|
116
|
-
const rootWorld = worldRot(root)
|
|
117
|
-
setWorldRot(root, Quat.fromTo(c2.sub(a), toT).mul(rootWorld))
|
|
118
|
-
|
|
119
|
-
// 3. pole: spin the root about the root→target axis so the mid joint lies toward the pole
|
|
120
|
-
const p = targetPos(this.pole)
|
|
121
|
-
if (p) {
|
|
122
|
-
const dir = toT.normalize()
|
|
123
|
-
const b2 = mid.worldPosition
|
|
124
|
-
const midProj = b2.sub(a).sub(dir.scale(b2.sub(a).dot(dir)))
|
|
125
|
-
const poleProj = p.sub(a).sub(dir.scale(p.sub(a).dot(dir)))
|
|
126
|
-
if (midProj.lengthSq() > 1e-10 && poleProj.lengthSq() > 1e-10) {
|
|
127
|
-
setWorldRot(root, Quat.fromTo(midProj, poleProj).mul(worldRot(root)))
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 4. weight: blend from the animated pose to the solved one
|
|
132
|
-
if (w < 1) {
|
|
133
|
-
root.quaternion = rootLocal0.slerp(root.quaternion, w)
|
|
134
|
-
mid.quaternion = midLocal0.slerp(mid.quaternion, w)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// 5. end-bone rotation: take the target's world rotation (a hand stays on its grip, a foot
|
|
138
|
-
// flat on the slope) — the solve above only placed the end's ORIGIN
|
|
139
|
-
const r = this.rotation
|
|
140
|
-
if (r !== undefined && r !== null) {
|
|
141
|
-
const rw = w * Math.min(1, Math.max(0, this.rotationWeight))
|
|
142
|
-
if (rw > 0) {
|
|
143
|
-
const want = (r as Node).id !== undefined && typeof (r as Node).worldMatrix === "object" ? worldRot(r as Node) : (r as Quat)
|
|
144
|
-
const endLocal0 = end.quaternion
|
|
145
|
-
setWorldRot(end, want)
|
|
146
|
-
if (rw < 1) end.quaternion = endLocal0.slerp(end.quaternion, rw)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Aim a bone at a target (head / eyes / turret). Attach to the bone itself:
|
|
153
|
-
* `head.aspect(IK.LookAt, { target: camera, limit: 70 })`. `axis` is the bone's LOCAL forward
|
|
154
|
-
* (the direction that should point at the target) — rigs differ; default −Z, Mixamo heads look
|
|
155
|
-
* along +Z of the head bone in most exports, so pass `axis: [0, 0, 1]` there if it faces backwards. */
|
|
156
|
-
class LookAt extends Aspect<"lookAt", Node> {
|
|
157
|
-
static readonly aspect = "lookAt"
|
|
158
|
-
|
|
159
|
-
target?: Target
|
|
160
|
-
/** The bone's local axis that should point at the target. */
|
|
161
|
-
axis: Vec3Like = [ 0, 0, -1 ]
|
|
162
|
-
/** Max deflection from the animated direction, in degrees (default 80). */
|
|
163
|
-
limit = 80
|
|
164
|
-
/** 0–1 contribution. */
|
|
165
|
-
weight = 1
|
|
166
|
-
enabled = true
|
|
167
|
-
|
|
168
|
-
update(): void {
|
|
169
|
-
if (this.enabled) this.solve()
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
solve(): void {
|
|
173
|
-
const t = targetPos(this.target)
|
|
174
|
-
if (!t || this.weight <= 0) return
|
|
175
|
-
const bone = this.node
|
|
176
|
-
const w = Math.min(1, this.weight)
|
|
177
|
-
const local0 = bone.quaternion
|
|
178
|
-
const from = bone.worldPosition
|
|
179
|
-
const dir = t.sub(from)
|
|
180
|
-
if (dir.lengthSq() < 1e-10) return
|
|
181
|
-
const rot = worldRot(bone)
|
|
182
|
-
const current = new Vec3(this.axis).rotate(rot)
|
|
183
|
-
let delta = Quat.fromTo(current, dir)
|
|
184
|
-
delta = clampAngle(delta, (this.limit * Math.PI) / 180)
|
|
185
|
-
setWorldRot(bone, delta.mul(rot))
|
|
186
|
-
if (w < 1) bone.quaternion = local0.slerp(bone.quaternion, w)
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Inverse kinematics aspects — attach to bones (see file header). */
|
|
191
|
-
export const IK = { TwoBone, LookAt }
|
|
192
|
-
export type IKTwoBone = TwoBone
|
|
193
|
-
export type IKLookAt = LookAt
|