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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # vehicle-path2
2
2
 
3
- Library untuk simulasi pergerakan kendaraan dual-axle sepanjang jalur.
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
- ## Quick Start
11
+ ## Konsep Dasar
12
12
 
13
- ```tsx
14
- import { useVehicleSimulation } from 'vehicle-path2/react'
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
- function App() {
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
- // Buat jalur
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
- // Tambah kendaraan
25
- sim.addVehicles({ id: 'v1', lineId: 'line1', position: 0 })
21
+ ## Setup
26
22
 
27
- // Gerakkan ke tujuan
28
- sim.goto({ id: 'v1', lineId: 'line2', position: 1.0 })
23
+ ```typescript
24
+ import { PathEngine } from 'vehicle-path2/core'
29
25
 
30
- // Jalankan animasi
31
- sim.prepare()
32
- sim.tick(5) // panggil di animation loop
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
- ## API
32
+ ---
37
33
 
38
- ### Format Posisi
34
+ ## Manajemen Scene
39
35
 
40
- Semua nilai posisi menggunakan format **0-1** untuk persentase:
41
- - `0` = 0% (awal line)
42
- - `0.5` = 50% (tengah line)
43
- - `1` = 100% (ujung line)
36
+ ### Lines
44
37
 
45
- Untuk posisi absolut (dalam satuan koordinat), gunakan `isPercentage: false`.
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
- ### Setup
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
- ```ts
50
- const sim = useVehicleSimulation({
51
- wheelbase: 30, // jarak antar roda
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
- ### Scene
51
+ // Hapus — cascade: semua curve yang terhubung ke line ini ikut dihapus
52
+ engine.removeLine('L1')
57
53
 
58
- ```ts
59
- sim.addLine({ id: 'line1', start: [0, 0], end: [400, 0] })
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
- ### Koneksi
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
- ```ts
68
- sim.connect('line1', 'line2')
69
- sim.connect('line1', 'line2', { fromOffset: 0.8, toOffset: 0.2 })
70
- sim.connect('line1', 'line2', { fromOffset: 150, fromIsPercentage: false, toOffset: 50, toIsPercentage: false })
71
- sim.updateConnection('line1', 'line2', { fromOffset: 0.5 }) // update offset
72
- sim.updateConnection('line1', 'line2', { toOffset: 100, toIsPercentage: false }) // absolute
73
- sim.disconnect('line1', 'line2')
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
- ### Kendaraan
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
- ```ts
79
- sim.addVehicles({ id: 'v1', lineId: 'line1', position: 0 })
80
- sim.addVehicles({ id: 'v2', lineId: 'line1', position: 150, isPercentage: false }) // absolute
81
- sim.updateVehicle('v1', { position: 0.5 }) // pindah ke 50%
82
- sim.updateVehicle('v1', { lineId: 'line2' }) // pindah ke line lain
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
- ### Pergerakan
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
- ```ts
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
- ### Animasi
135
+ > `axleSpacings[i]` = jarak arc-length antara `axles[i]` dan `axles[i+1]`. Array harus memiliki minimal 1 entry.
99
136
 
100
- ```ts
101
- sim.prepare() // siapkan sebelum animasi
102
- sim.tick(5) // gerakkan 5 pixel per tick
103
- sim.reset() // kembali ke posisi awal
104
- sim.isMoving() // cek ada yang bergerak
105
- sim.continueVehicle('v1') // lanjutkan vehicle yang wait
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
- ### Load dari DSL
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
- ```ts
111
- sim.loadFromDSL(`
112
- line1 : (0, 0) -> (400, 0)
113
- line2 : (400, 0) -> (400, 300)
114
- line1 -> line2
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
- ### State
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
- ```ts
121
- sim.lines // semua line
122
- sim.curves // semua koneksi
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
- ## Contoh Lengkap
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
- ```tsx
130
- import { useVehicleSimulation } from 'vehicle-path2/react'
131
- import { useEffect } from 'react'
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
- function AnimatedVehicle() {
134
- const sim = useVehicleSimulation({ wheelbase: 30 })
214
+ // Client mengelola data vehicle sendiri
215
+ const myVehicle = { id: 'v1', name: 'Truck', axleSpacings: [40] }
135
216
 
136
- useEffect(() => {
137
- sim.addLine({ id: 'line1', start: [100, 100], end: [500, 100] })
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
- const animate = () => {
143
- if (sim.tick(3)) {
144
- requestAnimationFrame(animate)
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
+ };
@@ -9,11 +9,11 @@
9
9
  * ```typescript
10
10
  * import { PathEngine } from 'vehicle-path/core'
11
11
  *
12
- * const engine = new PathEngine({ wheelbase: 30, tangentMode: 'proportional-40' })
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 axleSpacings - Jarak antar axle berurutan (N-1 nilai untuk N axle)
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, axleSpacings: number[]): VehiclePathState | null;
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
  *
@@ -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';
@@ -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 suitable for clipboard or storage.
25
- * Strips derived fields (bezier curves, axle positions) only source-of-truth
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 e=require("./index-BUYdltrL.cjs");function f(a,t,i){const r={lines:a,curves:t.map(n=>({id:n.id,fromLineId:n.fromLineId,toLineId:n.toLineId,fromOffset:n.fromOffset,fromIsPercentage:n.fromIsPercentage??!1,toOffset:n.toOffset,toIsPercentage:n.toIsPercentage??!1})),vehicles:i.map(n=>({id:n.id,lineId:n.axles[0].lineId,axles:n.axles.map(o=>({offset:o.offset})),axleSpacings:n.axleSpacings,isPercentage:n.isPercentage??!1}))};return JSON.stringify(r,null,2)}function g(a){let t;try{t=JSON.parse(a)}catch{throw new Error("deserializeScene: invalid JSON")}if(!t||typeof t!="object"||Array.isArray(t))throw new Error("deserializeScene: expected a JSON object");const i=t;if(!Array.isArray(i.lines))throw new Error('deserializeScene: missing "lines"');if(!Array.isArray(i.curves))throw new Error('deserializeScene: missing "curves"');if(!Array.isArray(i.vehicles))throw new Error('deserializeScene: missing "vehicles"');return{lines:i.lines,curves:i.curves,vehicles:i.vehicles}}function h(a,t){const i=t.end.x-t.start.x,r=t.end.y-t.start.y,n=i*i+r*r;if(n===0)return{offset:0,distance:Math.sqrt((a.x-t.start.x)**2+(a.y-t.start.y)**2)};const o=Math.max(0,Math.min(1,((a.x-t.start.x)*i+(a.y-t.start.y)*r)/n)),s=t.start.x+o*i,c=t.start.y+o*r,l=Math.sqrt((a.x-s)**2+(a.y-c)**2);return{offset:o*Math.sqrt(n),distance:l}}function d(a,t){const i=e.getLineLength(a),r=t.reduce((o,s)=>o+s,0);return[0,Math.max(0,i-r)]}function u(a,t){let i=0;for(const r of t)r.fromLineId===a&&!r.fromIsPercentage&&r.fromOffset!==void 0&&(i=Math.max(i,r.fromOffset)),r.toLineId===a&&!r.toIsPercentage&&r.toOffset!==void 0&&(i=Math.max(i,r.toOffset));return i}exports.PathEngine=e.PathEngine;exports.arcLengthToSegmentPosition=e.arcLengthToSegmentPosition;exports.buildArcLengthTable=e.buildArcLengthTable;exports.buildGraph=e.buildGraph;exports.calculateBezierArcLength=e.calculateBezierArcLength;exports.calculateFrontAxlePosition=e.calculateFrontAxlePosition;exports.calculateInitialAxlePositions=e.calculateInitialAxlePositions;exports.calculatePositionOnCurve=e.calculatePositionOnCurve;exports.calculatePositionOnLine=e.calculatePositionOnLine;exports.calculateTangentLength=e.calculateTangentLength;exports.createBezierCurve=e.createBezierCurve;exports.createInitialMovementState=e.createInitialMovementState;exports.distance=e.distance;exports.distanceToT=e.distanceToT;exports.findPath=e.findPath;exports.getArcLength=e.getArcLength;exports.getCumulativeArcLength=e.getCumulativeArcLength;exports.getLineLength=e.getLineLength;exports.getPointOnBezier=e.getPointOnBezier;exports.getPointOnLine=e.getPointOnLine;exports.getPointOnLineByOffset=e.getPointOnLineByOffset;exports.getPositionFromOffset=e.getPositionFromOffset;exports.handleArrival=e.handleArrival;exports.initializeAllVehicles=e.initializeAllVehicles;exports.initializeMovingVehicle=e.initializeMovingVehicle;exports.isPointNearPoint=e.isPointNearPoint;exports.moveVehicle=e.moveVehicle;exports.normalize=e.normalize;exports.prepareCommandPath=e.prepareCommandPath;exports.resolveFromLineOffset=e.resolveFromLineOffset;exports.resolveToLineOffset=e.resolveToLineOffset;exports.updateAxlePosition=e.updateAxlePosition;exports.computeMinLineLength=u;exports.deserializeScene=g;exports.getValidRearOffsetRange=d;exports.projectPointOnLine=h;exports.serializeScene=f;
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;