vehicle-path2 2.4.0 → 3.0.1
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 +234 -113
- package/dist/core/algorithms/acceleration.d.ts +75 -0
- package/dist/core/engine.d.ts +7 -7
- package/dist/core/index.d.ts +2 -1
- package/dist/core/snapshot.d.ts +4 -26
- package/dist/core/types/vehicle.d.ts +12 -2
- package/dist/core.cjs +1 -1
- package/dist/core.js +144 -80
- package/dist/index-C4FckUel.cjs +1 -0
- package/dist/{index-wjEuFiRl.js → index-C7pH7jZo.js} +281 -277
- package/dist/vehicle-path.cjs +1 -1
- package/dist/vehicle-path.js +5 -5
- package/package.json +1 -1
- package/dist/index-BUYdltrL.cjs +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# vehicle-path2
|
|
2
2
|
|
|
3
|
-
Library untuk simulasi pergerakan kendaraan
|
|
3
|
+
Library untuk simulasi pergerakan kendaraan multi-axle sepanjang jalur yang terdiri dari Lines dan Curves.
|
|
4
4
|
|
|
5
5
|
## Instalasi
|
|
6
6
|
|
|
@@ -8,161 +8,282 @@ Library untuk simulasi pergerakan kendaraan dual-axle sepanjang jalur.
|
|
|
8
8
|
npm install vehicle-path2
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Konsep Dasar
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
- **Line** — segmen garis lurus antara dua titik. Kendaraan bergerak di atas line.
|
|
14
|
+
- **Curve** — koneksi antara dua line. Berbentuk kurva bezier, menghubungkan ujung satu line ke awal line berikutnya.
|
|
15
|
+
- **Vehicle** — kendaraan dengan N axle yang ditempatkan di atas sebuah line. Library tidak menyimpan daftar vehicle — itu tanggung jawab client.
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
const sim = useVehicleSimulation({ wheelbase: 30 })
|
|
17
|
+
Titik acuan kendaraan adalah **axle paling belakang** (`axles[N-1]`). Semua axle lainnya dihitung ke depan berdasarkan `axleSpacings`.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
sim.addLine({ id: 'line1', start: [0, 0], end: [400, 0] })
|
|
21
|
-
sim.addLine({ id: 'line2', start: [400, 0], end: [400, 300] })
|
|
22
|
-
sim.connect('line1', 'line2')
|
|
19
|
+
---
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
sim.addVehicles({ id: 'v1', lineId: 'line1', position: 0 })
|
|
21
|
+
## Setup
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
```typescript
|
|
24
|
+
import { PathEngine } from 'vehicle-path2/core'
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
26
|
+
const engine = new PathEngine({
|
|
27
|
+
maxWheelbase: 100, // batas maksimum total panjang kendaraan
|
|
28
|
+
tangentMode: 'proportional-40' // mode kurva bezier
|
|
29
|
+
})
|
|
34
30
|
```
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
---
|
|
37
33
|
|
|
38
|
-
|
|
34
|
+
## Manajemen Scene
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
- `0` = 0% (awal line)
|
|
42
|
-
- `0.5` = 50% (tengah line)
|
|
43
|
-
- `1` = 100% (ujung line)
|
|
36
|
+
### Lines
|
|
44
37
|
|
|
45
|
-
|
|
38
|
+
```typescript
|
|
39
|
+
// Tambah line
|
|
40
|
+
engine.addLine({ id: 'L1', start: { x: 0, y: 0 }, end: { x: 400, y: 0 } })
|
|
41
|
+
// → false jika ID sudah ada
|
|
46
42
|
|
|
47
|
-
|
|
43
|
+
// Update posisi titik
|
|
44
|
+
engine.updateLine('L1', { end: { x: 500, y: 0 } })
|
|
45
|
+
engine.updateLineEndpoint('L1', 'start', { x: 10, y: 0 })
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
tangentMode: 'proportional-40' // mode kurva (opsional)
|
|
53
|
-
})
|
|
54
|
-
```
|
|
47
|
+
// Rename — cascade: semua curve yang referensi 'L1' otomatis diupdate ke 'L1-new'
|
|
48
|
+
engine.renameLine('L1', 'L1-new')
|
|
49
|
+
// → { success: true } atau { success: false, error: '...' }
|
|
55
50
|
|
|
56
|
-
|
|
51
|
+
// Hapus — cascade: semua curve yang terhubung ke line ini ikut dihapus
|
|
52
|
+
engine.removeLine('L1')
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
sim.updateLine('line1', { end: [500, 100] })
|
|
61
|
-
sim.removeLine('line1')
|
|
62
|
-
sim.clearScene()
|
|
54
|
+
// Baca semua lines
|
|
55
|
+
engine.lines // → Line[]
|
|
63
56
|
```
|
|
64
57
|
|
|
65
|
-
|
|
58
|
+
> **Konsekuensi `removeLine`:** Semua curve yang `fromLineId` atau `toLineId`-nya adalah line tersebut akan otomatis ikut dihapus.
|
|
59
|
+
|
|
60
|
+
> **Konsekuensi `renameLine`:** Semua curve yang `fromLineId` atau `toLineId`-nya adalah ID lama otomatis diupdate ke ID baru. Graph di-rebuild secara otomatis.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
### Curves
|
|
65
|
+
|
|
66
|
+
Curve menghubungkan **ujung satu line** ke **awal line lain**. Tidak bisa berdiri sendiri — harus selalu mereferensi dua line yang valid.
|
|
66
67
|
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
```typescript
|
|
69
|
+
import type { Curve } from 'vehicle-path2/core'
|
|
70
|
+
|
|
71
|
+
// Tambah curve
|
|
72
|
+
engine.addCurve({
|
|
73
|
+
fromLineId: 'L1',
|
|
74
|
+
toLineId: 'L2',
|
|
75
|
+
fromOffset: 380, // posisi di L1 (px dari start)
|
|
76
|
+
fromIsPercentage: false,
|
|
77
|
+
toOffset: 20, // posisi di L2 (px dari start)
|
|
78
|
+
toIsPercentage: false,
|
|
79
|
+
} as Curve)
|
|
80
|
+
|
|
81
|
+
// Atau dengan offset persentase (0–1)
|
|
82
|
+
engine.addCurve({
|
|
83
|
+
fromLineId: 'L1',
|
|
84
|
+
toLineId: 'L2',
|
|
85
|
+
fromOffset: 0.95,
|
|
86
|
+
fromIsPercentage: true,
|
|
87
|
+
toOffset: 0.05,
|
|
88
|
+
toIsPercentage: true,
|
|
89
|
+
} as Curve)
|
|
90
|
+
|
|
91
|
+
// Update curve berdasarkan index
|
|
92
|
+
engine.updateCurve(0, { fromOffset: 0.9 })
|
|
93
|
+
|
|
94
|
+
// Hapus curve berdasarkan index
|
|
95
|
+
engine.removeCurve(0)
|
|
96
|
+
|
|
97
|
+
// Baca semua curves
|
|
98
|
+
engine.getCurves() // → Curve[]
|
|
74
99
|
```
|
|
75
100
|
|
|
76
|
-
|
|
101
|
+
> **Catatan:** Curve diidentifikasi via **array index** (bukan named ID). Gunakan `getCurves()` lalu cari index yang sesuai sebelum update/delete.
|
|
102
|
+
|
|
103
|
+
> **Konsekuensi `removeLine`:** Curve yang terhubung ke line yang dihapus otomatis ikut terhapus, sehingga index curve-curve lainnya bisa bergeser.
|
|
104
|
+
|
|
105
|
+
---
|
|
77
106
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
sim.updateVehicle('v1', { lineId: 'line2', position: 0.8 }) // pindah ke 80% di line2
|
|
84
|
-
sim.removeVehicle('v1')
|
|
85
|
-
sim.clearVehicles()
|
|
107
|
+
### Muat Scene Sekaligus
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// Replace seluruh scene secara atomik
|
|
111
|
+
engine.setScene(lines, curves)
|
|
86
112
|
```
|
|
87
113
|
|
|
88
|
-
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Vehicle
|
|
117
|
+
|
|
118
|
+
Library menyediakan tipe dasar `VehicleDefinition`. Client bebas extend dengan field tambahan (id, name, color, dsb).
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import type { VehicleDefinition } from 'vehicle-path2/core'
|
|
122
|
+
|
|
123
|
+
// Definisi minimal
|
|
124
|
+
const def: VehicleDefinition = { axleSpacings: [40] } // 2 axle, jarak 40px
|
|
125
|
+
|
|
126
|
+
// Client extend sesuai kebutuhan
|
|
127
|
+
interface MyVehicle extends VehicleDefinition {
|
|
128
|
+
id: string
|
|
129
|
+
name: string
|
|
130
|
+
}
|
|
89
131
|
|
|
90
|
-
|
|
91
|
-
sim.goto({ id: 'v1', lineId: 'line2' }) // default position = 1.0 (ujung)
|
|
92
|
-
sim.goto({ id: 'v1', lineId: 'line2', position: 0.5 }) // 0.5 = tengah line
|
|
93
|
-
sim.goto({ id: 'v1', lineId: 'line2', position: 150, isPercentage: false }) // absolute
|
|
94
|
-
sim.goto({ id: 'v1', lineId: 'line2', payload: { orderId: '123' } }) // dengan payload
|
|
95
|
-
sim.clearQueue('v1')
|
|
132
|
+
const truck: MyVehicle = { id: 'v1', name: 'Truck A', axleSpacings: [40, 40] } // 3 axle
|
|
96
133
|
```
|
|
97
134
|
|
|
98
|
-
|
|
135
|
+
> `axleSpacings[i]` = jarak arc-length antara `axles[i]` dan `axles[i+1]`. Array harus memiliki minimal 1 entry.
|
|
99
136
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Pergerakan Kendaraan
|
|
140
|
+
|
|
141
|
+
Tiga method berikut digunakan secara berurutan:
|
|
142
|
+
|
|
143
|
+
### 1. `initializeVehicle` — Tempatkan kendaraan
|
|
144
|
+
|
|
145
|
+
Hitung posisi awal semua axle di atas sebuah line.
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const state = engine.initializeVehicle(
|
|
149
|
+
'L1', // lineId: line tempat kendaraan ditempatkan
|
|
150
|
+
0, // rearOffset: jarak dari start line ke axle paling belakang (px)
|
|
151
|
+
truck // VehicleDefinition (atau object yang extends-nya)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
// state → VehiclePathState | null (null jika lineId tidak ditemukan)
|
|
155
|
+
// state.axles[0] = axle terdepan
|
|
156
|
+
// state.axles[N-1] = axle paling belakang (titik acuan)
|
|
157
|
+
// state.axleSpacings = [40, 40]
|
|
106
158
|
```
|
|
107
159
|
|
|
108
|
-
###
|
|
160
|
+
### 2. `preparePath` — Tentukan tujuan
|
|
161
|
+
|
|
162
|
+
Jalankan Dijkstra untuk mencari rute dari posisi saat ini ke tujuan. Dipanggil **sekali** sebelum animasi dimulai.
|
|
109
163
|
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
164
|
+
```typescript
|
|
165
|
+
const execution = engine.preparePath(
|
|
166
|
+
state, // posisi vehicle sekarang (dari initializeVehicle atau tick sebelumnya)
|
|
167
|
+
'L3', // targetLineId: line tujuan
|
|
168
|
+
0.5, // targetOffset: posisi di line tujuan
|
|
169
|
+
true // isPercentage: true = 50% dari panjang line, false = nilai absolut (px)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
// execution → PathExecution | null (null jika tidak ada rute)
|
|
116
173
|
```
|
|
117
174
|
|
|
118
|
-
###
|
|
175
|
+
### 3. `moveVehicle` — Jalankan per tick
|
|
176
|
+
|
|
177
|
+
Dipanggil setiap frame di animation loop. Mengembalikan posisi baru semua axle.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const result = engine.moveVehicle(
|
|
181
|
+
state, // posisi vehicle sekarang
|
|
182
|
+
execution, // rencana rute (dari preparePath)
|
|
183
|
+
speed * deltaTime // jarak yang ditempuh di frame ini (px)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
state = result.state // posisi baru → simpan untuk tick berikutnya
|
|
187
|
+
execution = result.execution // progress terbaru → simpan untuk tick berikutnya
|
|
119
188
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
sim.vehicles // kendaraan (posisi awal)
|
|
124
|
-
sim.movingVehicles // kendaraan (posisi saat animasi)
|
|
189
|
+
if (result.arrived) {
|
|
190
|
+
// Kendaraan sudah sampai tujuan
|
|
191
|
+
}
|
|
125
192
|
```
|
|
126
193
|
|
|
127
|
-
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
### Contoh Lengkap: Animation Loop
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import { PathEngine } from 'vehicle-path2/core'
|
|
200
|
+
import type { VehiclePathState, PathExecution } from 'vehicle-path2/core'
|
|
201
|
+
|
|
202
|
+
const engine = new PathEngine({ maxWheelbase: 100, tangentMode: 'proportional-40' })
|
|
128
203
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
204
|
+
engine.setScene(
|
|
205
|
+
[
|
|
206
|
+
{ id: 'L1', start: { x: 0, y: 0 }, end: { x: 400, y: 0 } },
|
|
207
|
+
{ id: 'L2', start: { x: 400, y: 0 }, end: { x: 400, y: 300 } },
|
|
208
|
+
],
|
|
209
|
+
[
|
|
210
|
+
{ fromLineId: 'L1', toLineId: 'L2', fromOffset: 1.0, fromIsPercentage: true, toOffset: 0.0, toIsPercentage: true }
|
|
211
|
+
]
|
|
212
|
+
)
|
|
132
213
|
|
|
133
|
-
|
|
134
|
-
|
|
214
|
+
// Client mengelola data vehicle sendiri
|
|
215
|
+
const myVehicle = { id: 'v1', name: 'Truck', axleSpacings: [40] }
|
|
135
216
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
sim.addVehicles({ id: 'v1', lineId: 'line1', position: 0 })
|
|
139
|
-
sim.goto({ id: 'v1', lineId: 'line1', position: 1.0 })
|
|
140
|
-
sim.prepare()
|
|
217
|
+
// 1. Tempatkan di L1, axle belakang di posisi 0
|
|
218
|
+
let state: VehiclePathState = engine.initializeVehicle('L1', 0, myVehicle)!
|
|
141
219
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
220
|
+
// 2. Tentukan tujuan: ujung L2
|
|
221
|
+
let execution: PathExecution = engine.preparePath(state, 'L2', 1.0, true)!
|
|
222
|
+
|
|
223
|
+
const speed = 80 // px/detik
|
|
224
|
+
let lastTime = performance.now()
|
|
225
|
+
|
|
226
|
+
function animate() {
|
|
227
|
+
const now = performance.now()
|
|
228
|
+
const deltaTime = (now - lastTime) / 1000 // dalam detik
|
|
229
|
+
lastTime = now
|
|
230
|
+
|
|
231
|
+
// 3. Gerakkan vehicle setiap frame
|
|
232
|
+
const result = engine.moveVehicle(state, execution, speed * deltaTime)
|
|
233
|
+
state = result.state
|
|
234
|
+
execution = result.execution
|
|
235
|
+
|
|
236
|
+
// Render semua axle
|
|
237
|
+
for (const axle of state.axles) {
|
|
238
|
+
drawCircle(axle.position.x, axle.position.y)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!result.arrived) {
|
|
147
242
|
requestAnimationFrame(animate)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return (
|
|
151
|
-
<svg width={600} height={200}>
|
|
152
|
-
{sim.movingVehicles.map(v => (
|
|
153
|
-
<circle
|
|
154
|
-
key={v.id}
|
|
155
|
-
cx={v.rear.position.x}
|
|
156
|
-
cy={v.rear.position.y}
|
|
157
|
-
r={10}
|
|
158
|
-
fill="blue"
|
|
159
|
-
/>
|
|
160
|
-
))}
|
|
161
|
-
</svg>
|
|
162
|
-
)
|
|
243
|
+
}
|
|
163
244
|
}
|
|
245
|
+
|
|
246
|
+
requestAnimationFrame(animate)
|
|
164
247
|
```
|
|
165
248
|
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Serialisasi Scene
|
|
252
|
+
|
|
253
|
+
Hanya lines dan curves yang di-serialize oleh library. **Vehicle tidak termasuk** — persistensi vehicle adalah tanggung jawab client.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import { serializeScene, deserializeScene } from 'vehicle-path2/core'
|
|
257
|
+
|
|
258
|
+
// Simpan scene ke JSON string
|
|
259
|
+
const json = serializeScene(engine.lines, engine.getCurves())
|
|
260
|
+
|
|
261
|
+
// Muat kembali
|
|
262
|
+
const snapshot = deserializeScene(json)
|
|
263
|
+
engine.setScene(snapshot.lines, snapshot.curves)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Geometry Utilities
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { projectPointOnLine, getValidRearOffsetRange, computeMinLineLength } from 'vehicle-path2/core'
|
|
272
|
+
|
|
273
|
+
// Proyeksikan mouse/pointer ke garis — berguna untuk hit detection
|
|
274
|
+
const { offset, distance } = projectPointOnLine({ x: 150, y: 10 }, line)
|
|
275
|
+
// offset = jarak dari start line ke titik proyeksi (px)
|
|
276
|
+
// distance = jarak tegak lurus dari point ke garis (px)
|
|
277
|
+
|
|
278
|
+
// Hitung range offset yang valid untuk axle belakang agar semua axle muat di line
|
|
279
|
+
const [min, max] = getValidRearOffsetRange(line, myVehicle.axleSpacings)
|
|
280
|
+
|
|
281
|
+
// Hitung panjang minimum sebuah line agar semua curve offset-nya tidak keluar batas
|
|
282
|
+
const minLen = computeMinLineLength('L1', engine.getCurves())
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
166
287
|
## License
|
|
167
288
|
|
|
168
289
|
MIT
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { VehiclePathState, PathExecution } from '../engine';
|
|
2
|
+
import type { Line } from '../types/geometry';
|
|
3
|
+
/**
|
|
4
|
+
* Konfigurasi acceleration/deceleration untuk vehicle.
|
|
5
|
+
* Nilai-nilai ini bersifat global (sama untuk semua segmen path).
|
|
6
|
+
*/
|
|
7
|
+
export interface AccelerationConfig {
|
|
8
|
+
/** px/s² — laju percepatan dari berhenti menuju maxSpeed */
|
|
9
|
+
acceleration: number;
|
|
10
|
+
/** px/s² — laju perlambatan (digunakan untuk curve dan arrival) */
|
|
11
|
+
deceleration: number;
|
|
12
|
+
/** px/s — kecepatan maksimum di garis lurus */
|
|
13
|
+
maxSpeed: number;
|
|
14
|
+
/** px/s — kecepatan minimum saat memasuki curve (tidak berhenti total) */
|
|
15
|
+
minCurveSpeed: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* State kecepatan kendaraan saat ini.
|
|
19
|
+
* Caller menyimpan ini di luar dan meneruskan ke setiap tick.
|
|
20
|
+
*/
|
|
21
|
+
export interface AccelerationState {
|
|
22
|
+
/** Kecepatan kendaraan saat ini dalam px/s */
|
|
23
|
+
currentSpeed: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Hitung jarak tersisa dari posisi rear axle ke akhir path (tujuan).
|
|
27
|
+
*/
|
|
28
|
+
export declare function computeRemainingToArrival(execution: PathExecution): number;
|
|
29
|
+
/**
|
|
30
|
+
* Hitung jarak dari posisi front axle ke awal segment curve berikutnya di path.
|
|
31
|
+
* Return 0 jika front axle sudah berada di dalam curve.
|
|
32
|
+
* Return null jika tidak ada curve lagi di depan.
|
|
33
|
+
*
|
|
34
|
+
* Menggunakan front axle (axleExecutions[0]) sebagai referensi — karena deceleration
|
|
35
|
+
* harus selesai (mencapai minCurveSpeed) tepat saat front axle secara visual memasuki
|
|
36
|
+
* curve. Jika menggunakan rear axle, kendaraan baru melambat setelah front axle
|
|
37
|
+
* sudah masuk curve.
|
|
38
|
+
*/
|
|
39
|
+
export declare function computeDistToNextCurve(execution: PathExecution): number | null;
|
|
40
|
+
/**
|
|
41
|
+
* Hitung target speed berdasarkan lookahead jarak ke arrival dan curve.
|
|
42
|
+
*
|
|
43
|
+
* Menggunakan formula fisika: v = sqrt(2 * a * d)
|
|
44
|
+
* - Arrival: target = sqrt(2 * decel * distToArrival) → berhenti di tujuan
|
|
45
|
+
* - Curve: target = sqrt(minCurveSpeed² + 2 * decel * distToNextCurve) → capai minCurveSpeed di curve
|
|
46
|
+
* - Batas atas: maxSpeed
|
|
47
|
+
*/
|
|
48
|
+
export declare function computeTargetSpeed(distToArrival: number, distToNextCurve: number | null, config: AccelerationConfig): number;
|
|
49
|
+
/**
|
|
50
|
+
* Sesuaikan kecepatan saat ini menuju target dengan laju acceleration/deceleration.
|
|
51
|
+
*/
|
|
52
|
+
export declare function approachSpeed(current: number, target: number, acceleration: number, deceleration: number, deltaTime: number): number;
|
|
53
|
+
/**
|
|
54
|
+
* Gerakkan vehicle per tick dengan efek acceleration dan deceleration.
|
|
55
|
+
*
|
|
56
|
+
* Versi eksperimental dari PathEngine.moveVehicle yang menambahkan:
|
|
57
|
+
* - Startup acceleration: kendaraan mulai dari berhenti dan mempercepat
|
|
58
|
+
* - Pre-curve deceleration: melambat sebelum memasuki curve
|
|
59
|
+
* - Arrival deceleration: melambat hingga berhenti total di tujuan
|
|
60
|
+
*
|
|
61
|
+
* Tidak memodifikasi PathEngine atau moveVehicle yang sudah ada.
|
|
62
|
+
*
|
|
63
|
+
* @param state - Posisi vehicle saat ini
|
|
64
|
+
* @param execution - Rencana rute (dari preparePath atau tick sebelumnya)
|
|
65
|
+
* @param accelState - State kecepatan saat ini (simpan antar tick)
|
|
66
|
+
* @param config - Parameter acceleration/deceleration
|
|
67
|
+
* @param deltaTime - Durasi frame dalam detik (misal: 1/60 untuk 60fps)
|
|
68
|
+
* @param linesMap - Map dari line ID ke Line object (buat dari engine.lines)
|
|
69
|
+
*/
|
|
70
|
+
export declare function moveVehicleWithAcceleration(state: VehiclePathState, execution: PathExecution, accelState: AccelerationState, config: AccelerationConfig, deltaTime: number, linesMap: Map<string, Line>): {
|
|
71
|
+
state: VehiclePathState;
|
|
72
|
+
execution: PathExecution;
|
|
73
|
+
accelState: AccelerationState;
|
|
74
|
+
arrived: boolean;
|
|
75
|
+
};
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
* ```typescript
|
|
10
10
|
* import { PathEngine } from 'vehicle-path/core'
|
|
11
11
|
*
|
|
12
|
-
* const engine = new PathEngine({
|
|
12
|
+
* const engine = new PathEngine({ maxWheelbase: 100, tangentMode: 'proportional-40' })
|
|
13
13
|
*
|
|
14
14
|
* engine.setScene(lines, curves)
|
|
15
15
|
*
|
|
16
|
-
* const state = engine.initializeVehicle('line-1', 0)
|
|
16
|
+
* const state = engine.initializeVehicle('line-1', 0, { axleSpacings: [40] })
|
|
17
17
|
* const execution = engine.preparePath(state, 'line-3', 1.0, true)
|
|
18
18
|
*
|
|
19
19
|
* // In your animation/game loop:
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
28
|
import type { Line, Curve, Point } from './types/geometry';
|
|
29
|
+
import type { VehicleDefinition } from './types/vehicle';
|
|
29
30
|
import type { MovementConfig, CurveData } from './types/movement';
|
|
30
31
|
import type { PathResult } from './algorithms/pathFinding';
|
|
31
32
|
import type { TangentMode } from './types/config';
|
|
@@ -37,14 +38,12 @@ export interface PathEngineConfig {
|
|
|
37
38
|
* Multi-axle position state for use with PathEngine.
|
|
38
39
|
* axles[0] = terdepan, axles[N-1] = paling belakang.
|
|
39
40
|
*/
|
|
40
|
-
export interface VehiclePathState {
|
|
41
|
+
export interface VehiclePathState extends VehicleDefinition {
|
|
41
42
|
axles: Array<{
|
|
42
43
|
lineId: string;
|
|
43
44
|
offset: number;
|
|
44
45
|
position: Point;
|
|
45
46
|
}>;
|
|
46
|
-
/** N-1 jarak arc-length antar axle berurutan */
|
|
47
|
-
axleSpacings: number[];
|
|
48
47
|
}
|
|
49
48
|
/**
|
|
50
49
|
* Active path execution state for a vehicle in motion.
|
|
@@ -127,10 +126,11 @@ export declare class PathEngine {
|
|
|
127
126
|
*
|
|
128
127
|
* @param lineId - The line to place the vehicle on
|
|
129
128
|
* @param rearOffset - Absolute distance offset untuk axle paling belakang
|
|
130
|
-
* @param
|
|
129
|
+
* @param vehicle - VehicleDefinition (or any object extending it with axleSpacings)
|
|
131
130
|
* @returns Initial VehiclePathState, or null if lineId does not exist
|
|
131
|
+
* @throws if axleSpacings is empty
|
|
132
132
|
*/
|
|
133
|
-
initializeVehicle(lineId: string, rearOffset: number,
|
|
133
|
+
initializeVehicle(lineId: string, rearOffset: number, vehicle: VehicleDefinition): VehiclePathState | null;
|
|
134
134
|
/**
|
|
135
135
|
* Prepare a path from the vehicle's current position to a target.
|
|
136
136
|
*
|
package/dist/core/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* ```
|
|
12
12
|
*/
|
|
13
13
|
export type { Point, Line, BezierCurve, Curve } from './types/geometry';
|
|
14
|
-
export type { VehicleState, VehicleStart, Vehicle, AxleState, GotoCommand, GotoCompletionInfo, GotoCompletionCallback } from './types/vehicle';
|
|
14
|
+
export type { VehicleDefinition, VehicleState, VehicleStart, Vehicle, AxleState, GotoCommand, GotoCompletionInfo, GotoCompletionCallback } from './types/vehicle';
|
|
15
15
|
export type { CurveData, AxleExecutionState, PathExecutionState, VehicleMovementState, MovementConfig, SceneContext } from './types/movement';
|
|
16
16
|
export type { TangentMode } from './types/config';
|
|
17
17
|
export type { CoordinateInput, SceneLineInput, SceneConnectionInput, SceneConfig, VehicleInput, VehicleUpdateInput, ConnectionUpdateInput, GotoCommandInput } from './types/api';
|
|
@@ -20,4 +20,5 @@ export { initializeMovingVehicle, createInitialMovementState, initializeAllVehic
|
|
|
20
20
|
export { PathEngine, type PathEngineConfig, type VehiclePathState, type PathExecution } from './engine';
|
|
21
21
|
export { distance, normalize, getPointOnLine, getPointOnLineByOffset, getPointOnBezier, createBezierCurve, buildArcLengthTable, distanceToT, getArcLength, calculateTangentLength, isPointNearPoint, type ArcLengthEntry, type CurveOffsetOptions } from './algorithms/math';
|
|
22
22
|
export { serializeScene, deserializeScene, type SceneSnapshot } from './snapshot';
|
|
23
|
+
export { moveVehicleWithAcceleration, computeRemainingToArrival, computeDistToNextCurve, computeTargetSpeed, approachSpeed, type AccelerationConfig, type AccelerationState } from './algorithms/acceleration';
|
|
23
24
|
export { projectPointOnLine, getValidRearOffsetRange, computeMinLineLength } from './algorithms/geometry';
|
package/dist/core/snapshot.d.ts
CHANGED
|
@@ -10,24 +10,10 @@ export interface SceneSnapshot {
|
|
|
10
10
|
toOffset: number;
|
|
11
11
|
toIsPercentage: boolean;
|
|
12
12
|
}>;
|
|
13
|
-
vehicles: Array<{
|
|
14
|
-
id: string;
|
|
15
|
-
lineId: string;
|
|
16
|
-
axles: Array<{
|
|
17
|
-
offset: number;
|
|
18
|
-
}>;
|
|
19
|
-
axleSpacings: number[];
|
|
20
|
-
isPercentage: boolean;
|
|
21
|
-
}>;
|
|
22
13
|
}
|
|
23
14
|
/**
|
|
24
|
-
* Serialize scene state to a JSON string
|
|
25
|
-
*
|
|
26
|
-
* data is included.
|
|
27
|
-
*
|
|
28
|
-
* Note: lineId is per-vehicle (not per-axle) because this snapshot captures
|
|
29
|
-
* static placement where all axles share the same line. For mid-movement state,
|
|
30
|
-
* use AxleState directly.
|
|
15
|
+
* Serialize scene state (lines + curves) to a JSON string.
|
|
16
|
+
* Vehicles are NOT included — vehicle persistence is the client's responsibility.
|
|
31
17
|
*/
|
|
32
18
|
export declare function serializeScene(lines: Line[], curves: Array<{
|
|
33
19
|
id: string;
|
|
@@ -37,18 +23,10 @@ export declare function serializeScene(lines: Line[], curves: Array<{
|
|
|
37
23
|
fromIsPercentage?: boolean;
|
|
38
24
|
toOffset: number;
|
|
39
25
|
toIsPercentage?: boolean;
|
|
40
|
-
}>, vehicles: Array<{
|
|
41
|
-
id: string;
|
|
42
|
-
axles: Array<{
|
|
43
|
-
lineId: string;
|
|
44
|
-
offset: number;
|
|
45
|
-
[key: string]: unknown;
|
|
46
|
-
}>;
|
|
47
|
-
axleSpacings: number[];
|
|
48
|
-
isPercentage?: boolean;
|
|
49
26
|
}>): string;
|
|
50
27
|
/**
|
|
51
28
|
* Deserialize a JSON string back into a SceneSnapshot.
|
|
52
|
-
* Throws if the string is not valid JSON or missing required fields.
|
|
29
|
+
* Throws if the string is not valid JSON or missing required fields (lines, curves).
|
|
30
|
+
* Extra fields in the JSON (e.g. legacy "vehicles") are silently ignored.
|
|
53
31
|
*/
|
|
54
32
|
export declare function deserializeScene(json: string): SceneSnapshot;
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
* Vehicle-related types
|
|
3
3
|
*/
|
|
4
4
|
import type { Point } from './geometry';
|
|
5
|
+
/**
|
|
6
|
+
* Base definition of a vehicle's physical structure.
|
|
7
|
+
* Client code is free to extend this with additional fields (id, name, color, etc).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* interface MyVehicle extends VehicleDefinition { id: string; name: string }
|
|
11
|
+
*/
|
|
12
|
+
export interface VehicleDefinition {
|
|
13
|
+
/** N-1 arc-length spacings between consecutive axles. axleSpacings[i] = distance from axles[i] to axles[i+1]. */
|
|
14
|
+
axleSpacings: number[];
|
|
15
|
+
}
|
|
5
16
|
/**
|
|
6
17
|
* Animation state for a vehicle
|
|
7
18
|
*/
|
|
@@ -28,14 +39,13 @@ export interface AxleState {
|
|
|
28
39
|
/**
|
|
29
40
|
* Vehicle with runtime state (used during animation)
|
|
30
41
|
*/
|
|
31
|
-
export interface Vehicle {
|
|
42
|
+
export interface Vehicle extends VehicleDefinition {
|
|
32
43
|
id: string;
|
|
33
44
|
lineId: string;
|
|
34
45
|
offset: number;
|
|
35
46
|
isPercentage: boolean;
|
|
36
47
|
state: VehicleState;
|
|
37
48
|
axles: AxleState[];
|
|
38
|
-
axleSpacings: number[];
|
|
39
49
|
}
|
|
40
50
|
/**
|
|
41
51
|
* Command to move a vehicle to a target position
|
package/dist/core.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const i=require("./index-C4FckUel.cjs");function P(n,e){const a={lines:n,curves:e.map(t=>({id:t.id,fromLineId:t.fromLineId,toLineId:t.toLineId,fromOffset:t.fromOffset,fromIsPercentage:t.fromIsPercentage??!1,toOffset:t.toOffset,toIsPercentage:t.toIsPercentage??!1}))};return JSON.stringify(a,null,2)}function O(n){let e;try{e=JSON.parse(n)}catch{throw new Error("deserializeScene: invalid JSON")}if(!e||typeof e!="object"||Array.isArray(e))throw new Error("deserializeScene: expected a JSON object");const a=e;if(!Array.isArray(a.lines))throw new Error('deserializeScene: missing "lines"');if(!Array.isArray(a.curves))throw new Error('deserializeScene: missing "curves"');return{lines:a.lines,curves:a.curves}}function h(n){const e=n.axleExecutions[n.axleExecutions.length-1],a=i.getCumulativeArcLength(n.path,e.segmentIndex,e.segmentDistance);return Math.max(0,n.path.totalDistance-a)}function d(n){const e=n.axleExecutions[0],a=i.getCumulativeArcLength(n.path,e.segmentIndex,e.segmentDistance);let t=0;for(let r=0;r<n.path.segments.length;r++){const o=n.path.segments[r];if(r>=e.segmentIndex&&o.type==="curve")return Math.max(0,t-a);t+=o.length}return null}function p(n,e,a){let t=a.maxSpeed;const r=Math.sqrt(2*a.deceleration*Math.max(0,n));if(t=Math.min(t,r),e!==null){const o=Math.sqrt(a.minCurveSpeed**2+2*a.deceleration*e);t=Math.min(t,o)}return Math.max(0,t)}function x(n,e,a,t,r){return n<e?Math.min(e,n+a*r):n>e?Math.max(e,n-t*r):n}function S(n,e,a,t,r,o){const c=h(e),l=d(e),f=p(c,l,t),g=x(a.currentSpeed,f,t.acceleration,t.deceleration,r),u=g*r,v=n.axles.map(s=>({lineId:s.lineId,position:s.position,absoluteOffset:s.offset})),L=e.axleExecutions.map(s=>({currentSegmentIndex:s.segmentIndex,segmentDistance:s.segmentDistance})),m=i.moveVehicle(v,L,e.path,u,o,e.curveDataMap);return{state:{axles:m.axles.map(s=>({lineId:s.lineId,offset:s.absoluteOffset,position:s.position})),axleSpacings:n.axleSpacings},execution:{...e,axleExecutions:m.axleExecutions.map(s=>({segmentIndex:s.currentSegmentIndex,segmentDistance:s.segmentDistance}))},accelState:{currentSpeed:g},arrived:m.arrived}}function A(n,e){const a=e.end.x-e.start.x,t=e.end.y-e.start.y,r=a*a+t*t;if(r===0)return{offset:0,distance:Math.sqrt((n.x-e.start.x)**2+(n.y-e.start.y)**2)};const o=Math.max(0,Math.min(1,((n.x-e.start.x)*a+(n.y-e.start.y)*t)/r)),c=e.start.x+o*a,l=e.start.y+o*t,f=Math.sqrt((n.x-c)**2+(n.y-l)**2);return{offset:o*Math.sqrt(r),distance:f}}function I(n,e){const a=i.getLineLength(n),t=e.reduce((o,c)=>o+c,0);return[0,Math.max(0,a-t)]}function M(n,e){let a=0;for(const t of e)t.fromLineId===n&&!t.fromIsPercentage&&t.fromOffset!==void 0&&(a=Math.max(a,t.fromOffset)),t.toLineId===n&&!t.toIsPercentage&&t.toOffset!==void 0&&(a=Math.max(a,t.toOffset));return a}exports.PathEngine=i.PathEngine;exports.arcLengthToSegmentPosition=i.arcLengthToSegmentPosition;exports.buildArcLengthTable=i.buildArcLengthTable;exports.buildGraph=i.buildGraph;exports.calculateBezierArcLength=i.calculateBezierArcLength;exports.calculateFrontAxlePosition=i.calculateFrontAxlePosition;exports.calculateInitialAxlePositions=i.calculateInitialAxlePositions;exports.calculatePositionOnCurve=i.calculatePositionOnCurve;exports.calculatePositionOnLine=i.calculatePositionOnLine;exports.calculateTangentLength=i.calculateTangentLength;exports.createBezierCurve=i.createBezierCurve;exports.createInitialMovementState=i.createInitialMovementState;exports.distance=i.distance;exports.distanceToT=i.distanceToT;exports.findPath=i.findPath;exports.getArcLength=i.getArcLength;exports.getCumulativeArcLength=i.getCumulativeArcLength;exports.getLineLength=i.getLineLength;exports.getPointOnBezier=i.getPointOnBezier;exports.getPointOnLine=i.getPointOnLine;exports.getPointOnLineByOffset=i.getPointOnLineByOffset;exports.getPositionFromOffset=i.getPositionFromOffset;exports.handleArrival=i.handleArrival;exports.initializeAllVehicles=i.initializeAllVehicles;exports.initializeMovingVehicle=i.initializeMovingVehicle;exports.isPointNearPoint=i.isPointNearPoint;exports.moveVehicle=i.moveVehicle;exports.normalize=i.normalize;exports.prepareCommandPath=i.prepareCommandPath;exports.resolveFromLineOffset=i.resolveFromLineOffset;exports.resolveToLineOffset=i.resolveToLineOffset;exports.updateAxlePosition=i.updateAxlePosition;exports.approachSpeed=x;exports.computeDistToNextCurve=d;exports.computeMinLineLength=M;exports.computeRemainingToArrival=h;exports.computeTargetSpeed=p;exports.deserializeScene=O;exports.getValidRearOffsetRange=I;exports.moveVehicleWithAcceleration=S;exports.projectPointOnLine=A;exports.serializeScene=P;
|