vehicle-path2 4.0.1 → 4.0.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/README.md +289 -289
- package/package.json +83 -83
package/README.md
CHANGED
|
@@ -1,289 +1,289 @@
|
|
|
1
|
-
# vehicle-path2
|
|
2
|
-
|
|
3
|
-
Library untuk simulasi pergerakan kendaraan multi-axle sepanjang jalur yang terdiri dari Lines dan Curves.
|
|
4
|
-
|
|
5
|
-
## Instalasi
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install vehicle-path2
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Konsep Dasar
|
|
12
|
-
|
|
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.
|
|
16
|
-
|
|
17
|
-
Titik acuan kendaraan adalah **axle paling belakang** (`axles[N-1]`). Semua axle lainnya dihitung ke depan berdasarkan `axleSpacings`.
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## Setup
|
|
22
|
-
|
|
23
|
-
```typescript
|
|
24
|
-
import { PathEngine } from 'vehicle-path2/core'
|
|
25
|
-
|
|
26
|
-
const engine = new PathEngine({
|
|
27
|
-
maxWheelbase: 100, // batas maksimum total panjang kendaraan
|
|
28
|
-
tangentMode: 'proportional-40' // mode kurva bezier
|
|
29
|
-
})
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## Manajemen Scene
|
|
35
|
-
|
|
36
|
-
### Lines
|
|
37
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
// Update posisi titik
|
|
44
|
-
engine.updateLine('L1', { end: { x: 500, y: 0 } })
|
|
45
|
-
engine.updateLineEndpoint('L1', 'start', { x: 10, y: 0 })
|
|
46
|
-
|
|
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: '...' }
|
|
50
|
-
|
|
51
|
-
// Hapus — cascade: semua curve yang terhubung ke line ini ikut dihapus
|
|
52
|
-
engine.removeLine('L1')
|
|
53
|
-
|
|
54
|
-
// Baca semua lines
|
|
55
|
-
engine.lines // → Line[]
|
|
56
|
-
```
|
|
57
|
-
|
|
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.
|
|
67
|
-
|
|
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[]
|
|
99
|
-
```
|
|
100
|
-
|
|
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
|
-
---
|
|
106
|
-
|
|
107
|
-
### Muat Scene Sekaligus
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
// Replace seluruh scene secara atomik
|
|
111
|
-
engine.setScene(lines, curves)
|
|
112
|
-
```
|
|
113
|
-
|
|
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
|
-
}
|
|
131
|
-
|
|
132
|
-
const truck: MyVehicle = { id: 'v1', name: 'Truck A', axleSpacings: [40, 40] } // 3 axle
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
> `axleSpacings[i]` = jarak arc-length antara `axles[i]` dan `axles[i+1]`. Array harus memiliki minimal 1 entry.
|
|
136
|
-
|
|
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]
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
### 2. `preparePath` — Tentukan tujuan
|
|
161
|
-
|
|
162
|
-
Jalankan Dijkstra untuk mencari rute dari posisi saat ini ke tujuan. Dipanggil **sekali** sebelum animasi dimulai.
|
|
163
|
-
|
|
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)
|
|
173
|
-
```
|
|
174
|
-
|
|
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
|
|
188
|
-
|
|
189
|
-
if (result.arrived) {
|
|
190
|
-
// Kendaraan sudah sampai tujuan
|
|
191
|
-
}
|
|
192
|
-
```
|
|
193
|
-
|
|
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' })
|
|
203
|
-
|
|
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
|
-
)
|
|
213
|
-
|
|
214
|
-
// Client mengelola data vehicle sendiri
|
|
215
|
-
const myVehicle = { id: 'v1', name: 'Truck', axleSpacings: [40] }
|
|
216
|
-
|
|
217
|
-
// 1. Tempatkan di L1, axle belakang di posisi 0
|
|
218
|
-
let state: VehiclePathState = engine.initializeVehicle('L1', 0, myVehicle)!
|
|
219
|
-
|
|
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) {
|
|
242
|
-
requestAnimationFrame(animate)
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
requestAnimationFrame(animate)
|
|
247
|
-
```
|
|
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
|
-
|
|
287
|
-
## License
|
|
288
|
-
|
|
289
|
-
MIT
|
|
1
|
+
# vehicle-path2
|
|
2
|
+
|
|
3
|
+
Library untuk simulasi pergerakan kendaraan multi-axle sepanjang jalur yang terdiri dari Lines dan Curves.
|
|
4
|
+
|
|
5
|
+
## Instalasi
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install vehicle-path2
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Konsep Dasar
|
|
12
|
+
|
|
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.
|
|
16
|
+
|
|
17
|
+
Titik acuan kendaraan adalah **axle paling belakang** (`axles[N-1]`). Semua axle lainnya dihitung ke depan berdasarkan `axleSpacings`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { PathEngine } from 'vehicle-path2/core'
|
|
25
|
+
|
|
26
|
+
const engine = new PathEngine({
|
|
27
|
+
maxWheelbase: 100, // batas maksimum total panjang kendaraan
|
|
28
|
+
tangentMode: 'proportional-40' // mode kurva bezier
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Manajemen Scene
|
|
35
|
+
|
|
36
|
+
### Lines
|
|
37
|
+
|
|
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
|
|
42
|
+
|
|
43
|
+
// Update posisi titik
|
|
44
|
+
engine.updateLine('L1', { end: { x: 500, y: 0 } })
|
|
45
|
+
engine.updateLineEndpoint('L1', 'start', { x: 10, y: 0 })
|
|
46
|
+
|
|
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: '...' }
|
|
50
|
+
|
|
51
|
+
// Hapus — cascade: semua curve yang terhubung ke line ini ikut dihapus
|
|
52
|
+
engine.removeLine('L1')
|
|
53
|
+
|
|
54
|
+
// Baca semua lines
|
|
55
|
+
engine.lines // → Line[]
|
|
56
|
+
```
|
|
57
|
+
|
|
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.
|
|
67
|
+
|
|
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[]
|
|
99
|
+
```
|
|
100
|
+
|
|
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
|
+
---
|
|
106
|
+
|
|
107
|
+
### Muat Scene Sekaligus
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// Replace seluruh scene secara atomik
|
|
111
|
+
engine.setScene(lines, curves)
|
|
112
|
+
```
|
|
113
|
+
|
|
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
|
+
}
|
|
131
|
+
|
|
132
|
+
const truck: MyVehicle = { id: 'v1', name: 'Truck A', axleSpacings: [40, 40] } // 3 axle
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
> `axleSpacings[i]` = jarak arc-length antara `axles[i]` dan `axles[i+1]`. Array harus memiliki minimal 1 entry.
|
|
136
|
+
|
|
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]
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 2. `preparePath` — Tentukan tujuan
|
|
161
|
+
|
|
162
|
+
Jalankan Dijkstra untuk mencari rute dari posisi saat ini ke tujuan. Dipanggil **sekali** sebelum animasi dimulai.
|
|
163
|
+
|
|
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)
|
|
173
|
+
```
|
|
174
|
+
|
|
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
|
|
188
|
+
|
|
189
|
+
if (result.arrived) {
|
|
190
|
+
// Kendaraan sudah sampai tujuan
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
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' })
|
|
203
|
+
|
|
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
|
+
)
|
|
213
|
+
|
|
214
|
+
// Client mengelola data vehicle sendiri
|
|
215
|
+
const myVehicle = { id: 'v1', name: 'Truck', axleSpacings: [40] }
|
|
216
|
+
|
|
217
|
+
// 1. Tempatkan di L1, axle belakang di posisi 0
|
|
218
|
+
let state: VehiclePathState = engine.initializeVehicle('L1', 0, myVehicle)!
|
|
219
|
+
|
|
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) {
|
|
242
|
+
requestAnimationFrame(animate)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
requestAnimationFrame(animate)
|
|
247
|
+
```
|
|
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
|
+
|
|
287
|
+
## License
|
|
288
|
+
|
|
289
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "vehicle-path2",
|
|
3
|
-
"version": "4.0.
|
|
4
|
-
"description": "Vehicle motion simulator library for dual-axle vehicle movement along paths composed of lines and Bezier curves",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/vehicle-path.cjs",
|
|
7
|
-
"module": "./dist/vehicle-path.js",
|
|
8
|
-
"types": "./dist/index.d.ts",
|
|
9
|
-
"exports": {
|
|
10
|
-
".": {
|
|
11
|
-
"import": {
|
|
12
|
-
"types": "./dist/index.d.ts",
|
|
13
|
-
"default": "./dist/vehicle-path.js"
|
|
14
|
-
},
|
|
15
|
-
"require": {
|
|
16
|
-
"types": "./dist/index.d.cts",
|
|
17
|
-
"default": "./dist/vehicle-path.cjs"
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"./core": {
|
|
21
|
-
"import": {
|
|
22
|
-
"types": "./dist/core/index.d.ts",
|
|
23
|
-
"default": "./dist/core.js"
|
|
24
|
-
},
|
|
25
|
-
"require": {
|
|
26
|
-
"types": "./dist/core/index.d.cts",
|
|
27
|
-
"default": "./dist/core.cjs"
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
"files": [
|
|
32
|
-
"dist"
|
|
33
|
-
],
|
|
34
|
-
"scripts": {
|
|
35
|
-
"dev": "vite build --watch",
|
|
36
|
-
"build": "vite build && tsc -p tsconfig.build.json",
|
|
37
|
-
"prepublishOnly": "npm run build",
|
|
38
|
-
"lint": "eslint .",
|
|
39
|
-
"test": "vitest",
|
|
40
|
-
"test:ui": "vitest --ui",
|
|
41
|
-
"test:coverage": "vitest --coverage"
|
|
42
|
-
},
|
|
43
|
-
"peerDependencies": {
|
|
44
|
-
"react": "^18.0.0 || ^19.0.0",
|
|
45
|
-
"react-dom": "^18.0.0 || ^19.0.0"
|
|
46
|
-
},
|
|
47
|
-
"devDependencies": {
|
|
48
|
-
"@eslint/js": "^9.39.1",
|
|
49
|
-
"@testing-library/react": "^16.3.1",
|
|
50
|
-
"@types/node": "^24.10.1",
|
|
51
|
-
"@types/react": "^19.2.5",
|
|
52
|
-
"@types/react-dom": "^19.2.3",
|
|
53
|
-
"@vitejs/plugin-react": "^5.1.1",
|
|
54
|
-
"@vitest/coverage-v8": "^4.0.16",
|
|
55
|
-
"@vitest/ui": "^4.0.16",
|
|
56
|
-
"eslint": "^9.39.1",
|
|
57
|
-
"eslint-plugin-react-hooks": "^7.0.1",
|
|
58
|
-
"eslint-plugin-react-refresh": "^0.4.24",
|
|
59
|
-
"globals": "^16.5.0",
|
|
60
|
-
"happy-dom": "^20.0.11",
|
|
61
|
-
"react": "^19.2.0",
|
|
62
|
-
"react-dom": "^19.2.0",
|
|
63
|
-
"typescript": "~5.9.3",
|
|
64
|
-
"typescript-eslint": "^8.46.4",
|
|
65
|
-
"vite": "^7.2.4",
|
|
66
|
-
"vitest": "^4.0.16"
|
|
67
|
-
},
|
|
68
|
-
"keywords": [
|
|
69
|
-
"vehicle",
|
|
70
|
-
"path",
|
|
71
|
-
"bezier",
|
|
72
|
-
"simulation",
|
|
73
|
-
"animation",
|
|
74
|
-
"dual-axle",
|
|
75
|
-
"react"
|
|
76
|
-
],
|
|
77
|
-
"author": "",
|
|
78
|
-
"license": "MIT",
|
|
79
|
-
"repository": {
|
|
80
|
-
"type": "git",
|
|
81
|
-
"url": ""
|
|
82
|
-
}
|
|
83
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "vehicle-path2",
|
|
3
|
+
"version": "4.0.2",
|
|
4
|
+
"description": "Vehicle motion simulator library for dual-axle vehicle movement along paths composed of lines and Bezier curves",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/vehicle-path.cjs",
|
|
7
|
+
"module": "./dist/vehicle-path.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/vehicle-path.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/vehicle-path.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./core": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/core/index.d.ts",
|
|
23
|
+
"default": "./dist/core.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/core/index.d.cts",
|
|
27
|
+
"default": "./dist/core.cjs"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"dev": "vite build --watch",
|
|
36
|
+
"build": "vite build && tsc -p tsconfig.build.json",
|
|
37
|
+
"prepublishOnly": "npm run build",
|
|
38
|
+
"lint": "eslint .",
|
|
39
|
+
"test": "vitest",
|
|
40
|
+
"test:ui": "vitest --ui",
|
|
41
|
+
"test:coverage": "vitest --coverage"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
45
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@eslint/js": "^9.39.1",
|
|
49
|
+
"@testing-library/react": "^16.3.1",
|
|
50
|
+
"@types/node": "^24.10.1",
|
|
51
|
+
"@types/react": "^19.2.5",
|
|
52
|
+
"@types/react-dom": "^19.2.3",
|
|
53
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
54
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
55
|
+
"@vitest/ui": "^4.0.16",
|
|
56
|
+
"eslint": "^9.39.1",
|
|
57
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
58
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
59
|
+
"globals": "^16.5.0",
|
|
60
|
+
"happy-dom": "^20.0.11",
|
|
61
|
+
"react": "^19.2.0",
|
|
62
|
+
"react-dom": "^19.2.0",
|
|
63
|
+
"typescript": "~5.9.3",
|
|
64
|
+
"typescript-eslint": "^8.46.4",
|
|
65
|
+
"vite": "^7.2.4",
|
|
66
|
+
"vitest": "^4.0.16"
|
|
67
|
+
},
|
|
68
|
+
"keywords": [
|
|
69
|
+
"vehicle",
|
|
70
|
+
"path",
|
|
71
|
+
"bezier",
|
|
72
|
+
"simulation",
|
|
73
|
+
"animation",
|
|
74
|
+
"dual-axle",
|
|
75
|
+
"react"
|
|
76
|
+
],
|
|
77
|
+
"author": "",
|
|
78
|
+
"license": "MIT",
|
|
79
|
+
"repository": {
|
|
80
|
+
"type": "git",
|
|
81
|
+
"url": ""
|
|
82
|
+
}
|
|
83
|
+
}
|