vehicle-path 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ import type { GotoCompletionInfo, VehicleState } from '../types/vehicle';
2
+ import type { Point } from '../types/core';
3
+ /**
4
+ * Position update data for a vehicle
5
+ */
6
+ export interface VehiclePositionUpdate {
7
+ vehicleId: string;
8
+ /** Rear axle position */
9
+ rear: Point;
10
+ /** Front axle position */
11
+ front: Point;
12
+ /** Center point between rear and front axles */
13
+ center: Point;
14
+ /** Angle in radians from rear to front (heading direction) */
15
+ angle: number;
16
+ }
17
+ /**
18
+ * Event map for vehicle-related events
19
+ */
20
+ export interface VehicleEventMap {
21
+ /** Fired when a goto command completes (vehicle arrives at destination) */
22
+ commandComplete: GotoCompletionInfo;
23
+ /** Fired when vehicle state changes */
24
+ stateChange: {
25
+ vehicleId: string;
26
+ from: VehicleState;
27
+ to: VehicleState;
28
+ };
29
+ /** Fired on each frame when vehicle position updates */
30
+ positionUpdate: VehiclePositionUpdate;
31
+ }
32
+ export type VehicleEventType = keyof VehicleEventMap;
33
+ export type VehicleEventCallback<K extends VehicleEventType> = (data: VehicleEventMap[K]) => void;
34
+ export type Unsubscribe = () => void;
35
+ /**
36
+ * Event emitter for vehicle movement events.
37
+ * Allows multiple subscribers to listen for events like command completion,
38
+ * state changes, etc.
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const emitter = new VehicleEventEmitter()
43
+ *
44
+ * // Subscribe to events
45
+ * const unsubscribe = emitter.on('commandComplete', (info) => {
46
+ * console.log(`Vehicle ${info.vehicleId} arrived with payload:`, info.payload)
47
+ * })
48
+ *
49
+ * // Later, unsubscribe
50
+ * unsubscribe()
51
+ * ```
52
+ */
53
+ export declare class VehicleEventEmitter {
54
+ private listeners;
55
+ /**
56
+ * Subscribe to an event
57
+ * @param event - The event type to listen for
58
+ * @param callback - Function to call when event is emitted
59
+ * @returns Unsubscribe function
60
+ */
61
+ on<K extends VehicleEventType>(event: K, callback: VehicleEventCallback<K>): Unsubscribe;
62
+ /**
63
+ * Emit an event to all subscribers
64
+ * @param event - The event type to emit
65
+ * @param data - The event data
66
+ */
67
+ emit<K extends VehicleEventType>(event: K, data: VehicleEventMap[K]): void;
68
+ /**
69
+ * Remove all listeners for a specific event, or all events if no event specified
70
+ * @param event - Optional event type to clear listeners for
71
+ */
72
+ off(event?: VehicleEventType): void;
73
+ /**
74
+ * Get the number of listeners for a specific event
75
+ * @param event - The event type
76
+ * @returns Number of listeners
77
+ */
78
+ listenerCount(event: VehicleEventType): number;
79
+ }
@@ -0,0 +1,9 @@
1
+ import type { Point, Line } from '../types/core';
2
+ import type { TangentMode } from '../types/ui';
3
+ export declare function setupCanvas(canvas: HTMLCanvasElement, container: HTMLDivElement): void;
4
+ export declare function getMousePos(e: React.MouseEvent<HTMLCanvasElement>, canvas: HTMLCanvasElement | null, container: HTMLDivElement | null): Point;
5
+ export declare function findEndpointAtPoint(pos: Point, lines: Line[], threshold?: number): {
6
+ lineId: string;
7
+ endpoint: 'start' | 'end';
8
+ } | null;
9
+ export declare function parseTangentMode(smoothness: string): TangentMode;
@@ -0,0 +1,13 @@
1
+ import type { Point, Line, BezierCurve } from '../types/core';
2
+ import type { TangentMode } from '../types/ui';
3
+ import type { Vehicle } from '../types/vehicle';
4
+ import { type CurveOffsetOptions } from './math';
5
+ export declare function drawLine(ctx: CanvasRenderingContext2D, line: Line, hoveredEndpoint: {
6
+ lineId: string;
7
+ endpoint: 'start' | 'end';
8
+ } | null): void;
9
+ export declare function drawTempLine(ctx: CanvasRenderingContext2D, start: Point, end: Point): void;
10
+ export declare function drawCurve(ctx: CanvasRenderingContext2D, fromLine: Line, toLine: Line, wheelbase: number, tangentMode: TangentMode, offsetOptions?: CurveOffsetOptions): void;
11
+ export declare function drawBezierCurve(ctx: CanvasRenderingContext2D, bezier: BezierCurve): void;
12
+ export declare function drawTempCurveLine(ctx: CanvasRenderingContext2D, start: Point, end: Point): void;
13
+ export declare function drawVehicle(ctx: CanvasRenderingContext2D, vehicle: Vehicle): void;
@@ -0,0 +1,35 @@
1
+ import type { Point, Line, BezierCurve } from '../types/core';
2
+ import type { TangentMode } from '../types/ui';
3
+ import type { MovementConfig } from '../types/movement';
4
+ export declare function distance(p1: Point, p2: Point): number;
5
+ export declare function normalize(p1: Point, p2: Point): Point;
6
+ export declare function calculateTangentLength(mode: TangentMode, distance: number): number;
7
+ export interface CurveOffsetOptions {
8
+ fromOffset?: number;
9
+ fromIsPercentage?: boolean;
10
+ toOffset?: number;
11
+ toIsPercentage?: boolean;
12
+ }
13
+ export declare function createBezierCurve(line: Line, nextLine: Line, config: MovementConfig, willFlip?: boolean, offsetOptions?: CurveOffsetOptions): BezierCurve;
14
+ export declare function getPointOnLine(line: Line, t: number): Point;
15
+ export declare function getPointOnLineByOffset(line: Line, offset: number, isPercentage: boolean): Point;
16
+ export declare function getPointOnBezier(bezier: BezierCurve, t: number): Point;
17
+ export declare function isPointNearPoint(p1: Point, p2: Point, threshold?: number): boolean;
18
+ export interface ArcLengthEntry {
19
+ t: number;
20
+ distance: number;
21
+ }
22
+ /**
23
+ * Build a lookup table for arc-length parameterization.
24
+ * Maps t values to cumulative distances along the curve.
25
+ */
26
+ export declare function buildArcLengthTable(bezier: BezierCurve, samples?: number): ArcLengthEntry[];
27
+ /**
28
+ * Convert a distance along the curve to the corresponding t parameter.
29
+ * Uses linear interpolation between table entries for smooth results.
30
+ */
31
+ export declare function distanceToT(table: ArcLengthEntry[], targetDistance: number): number;
32
+ /**
33
+ * Get the total arc length from a pre-built table
34
+ */
35
+ export declare function getArcLength(table: ArcLengthEntry[]): number;
@@ -0,0 +1,75 @@
1
+ import type { Line, Curve, BezierCurve } from '../types/core';
2
+ import type { MovementConfig } from '../types/movement';
3
+ export interface GraphEdge {
4
+ curveIndex: number;
5
+ fromLineId: string;
6
+ toLineId: string;
7
+ fromOffset: number;
8
+ toOffset: number;
9
+ curveLength: number;
10
+ }
11
+ export interface Graph {
12
+ adjacency: Map<string, GraphEdge[]>;
13
+ lines: Map<string, Line>;
14
+ lineLengths: Map<string, number>;
15
+ }
16
+ export interface PathSegment {
17
+ type: 'line' | 'curve';
18
+ lineId?: string;
19
+ curveIndex?: number;
20
+ startOffset: number;
21
+ endOffset: number;
22
+ length: number;
23
+ }
24
+ export interface PathResult {
25
+ segments: PathSegment[];
26
+ totalDistance: number;
27
+ }
28
+ export interface VehiclePosition {
29
+ lineId: string;
30
+ offset: number;
31
+ }
32
+ /**
33
+ * Menghitung arc length dari bezier curve menggunakan numerical integration
34
+ */
35
+ export declare function calculateBezierArcLength(bezier: BezierCurve, segments?: number): number;
36
+ /**
37
+ * Menghitung absolute offset dari offset yang mungkin percentage
38
+ * (Legacy function - use resolveFromLineOffset/resolveToLineOffset for curve offsets)
39
+ */
40
+ export declare function resolveOffset(line: Line, offset: number | undefined, isPercentage: boolean | undefined, defaultPercentage: number): number;
41
+ /**
42
+ * Resolve offset untuk FROM line (garis asal kurva)
43
+ * - 0% → wheelbase (bukan 0, untuk memberi ruang vehicle)
44
+ * - 100% → lineLength (ujung garis)
45
+ *
46
+ * Effective range: [wheelbase, lineLength]
47
+ */
48
+ export declare function resolveFromLineOffset(line: Line, offset: number | undefined, isPercentage: boolean | undefined, defaultPercentage: number, wheelbase: number): number;
49
+ /**
50
+ * Resolve offset untuk TO line (garis tujuan kurva)
51
+ * - 0% → 0 (awal garis)
52
+ * - 100% → lineLength - wheelbase (untuk memberi ruang vehicle)
53
+ *
54
+ * Effective range: [0, lineLength - wheelbase]
55
+ */
56
+ export declare function resolveToLineOffset(line: Line, offset: number | undefined, isPercentage: boolean | undefined, defaultPercentage: number, wheelbase: number): number;
57
+ /**
58
+ * Membangun graph dari lines dan curves
59
+ */
60
+ export declare function buildGraph(lines: Line[], curves: Curve[], config: MovementConfig): Graph;
61
+ /**
62
+ * Mencari path terpendek dari posisi vehicle ke target
63
+ *
64
+ * @returns PathResult jika path ditemukan, null jika tidak ada path valid
65
+ */
66
+ export declare function findPath(graph: Graph, vehiclePos: VehiclePosition, targetLineId: string, targetOffset: number, // Absolute offset
67
+ targetIsPercentage?: boolean): PathResult | null;
68
+ /**
69
+ * Mengecek apakah path valid dari posisi vehicle ke target
70
+ */
71
+ export declare function canReachTarget(graph: Graph, vehiclePos: VehiclePosition, targetLineId: string, targetOffset: number, targetIsPercentage?: boolean): boolean;
72
+ /**
73
+ * Mendapatkan semua kurva yang bisa diambil dari posisi tertentu pada line
74
+ */
75
+ export declare function getReachableCurves(graph: Graph, lineId: string, currentOffset: number): GraphEdge[];
@@ -0,0 +1,22 @@
1
+ import type { VehicleStart, GotoCommand } from '../types/vehicle';
2
+ import type { SceneDefinition } from '../types/movement';
3
+ export declare function parseSceneDefinition(text: string): SceneDefinition;
4
+ export declare function generateSceneDefinition(scene: SceneDefinition): string;
5
+ /**
6
+ * Parse vehicle start positions from initial movement text
7
+ * Format: v1 start line001 10 or v2 start line002 20%
8
+ */
9
+ export declare function parseVehicleStarts(text: string): VehicleStart[];
10
+ /**
11
+ * Generate initial movement text from vehicle starts
12
+ */
13
+ export declare function generateVehicleStarts(vehicles: VehicleStart[]): string;
14
+ /**
15
+ * Parse goto commands from movement sequence text
16
+ * Format: v1 goto line001 100% [--wait] [--payload {...}]
17
+ */
18
+ export declare function parseGotoCommands(text: string): GotoCommand[];
19
+ /**
20
+ * Generate movement sequence text from goto commands
21
+ */
22
+ export declare function generateGotoCommands(commands: GotoCommand[]): string;
@@ -0,0 +1,14 @@
1
+ import type { Line } from '../types/core';
2
+ import type { Vehicle, VehicleStart, GotoCommand } from '../types/vehicle';
3
+ export declare function validateAndCreateVehicles(vehicleStarts: VehicleStart[], lines: Line[], wheelbase?: number): {
4
+ vehicles: Vehicle[];
5
+ errors: string[];
6
+ };
7
+ export declare function getNextStartVehicleId(existingVehicles: VehicleStart[]): string;
8
+ export interface GotoValidationResult {
9
+ commands: GotoCommand[];
10
+ errors: string[];
11
+ vehicleQueues: Map<string, GotoCommand[]>;
12
+ }
13
+ export declare function validateGotoCommands(commands: GotoCommand[], vehicles: Vehicle[], lines: Line[]): GotoValidationResult;
14
+ export declare function getNextGotoVehicleId(existingCommands: GotoCommand[], vehicles: Vehicle[]): string | null;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Arc-length tracking utilities for dual-axle vehicle movement
3
+ *
4
+ * Enables calculating front axle position from rear axle position
5
+ * by maintaining wheelbase distance in arc-length terms.
6
+ */
7
+ import type { PathResult } from '../pathFinding';
8
+ /**
9
+ * Calculate cumulative arc-length from start of path
10
+ *
11
+ * @param path - The path being followed
12
+ * @param segmentIndex - Current segment index
13
+ * @param segmentDistance - Distance along current segment
14
+ * @returns Total arc-length from path start
15
+ */
16
+ export declare function getCumulativeArcLength(path: PathResult, segmentIndex: number, segmentDistance: number): number;
17
+ /**
18
+ * Convert arc-length to segment position
19
+ *
20
+ * @param path - The path being followed
21
+ * @param targetArcLength - Target cumulative arc-length
22
+ * @returns Segment position or null if exceeds path length
23
+ */
24
+ export declare function arcLengthToSegmentPosition(path: PathResult, targetArcLength: number): {
25
+ segmentIndex: number;
26
+ segmentDistance: number;
27
+ } | null;
28
+ /**
29
+ * Calculate front axle position from rear position + wheelbase
30
+ *
31
+ * @param path - The path being followed
32
+ * @param rearSegmentIndex - Rear axle segment index
33
+ * @param rearSegmentDistance - Rear axle distance in segment
34
+ * @param wheelbase - Distance between front and rear axles
35
+ * @returns Front axle position or null if exceeds path
36
+ */
37
+ export declare function calculateFrontAxlePosition(path: PathResult, rearSegmentIndex: number, rearSegmentDistance: number, wheelbase: number): {
38
+ segmentIndex: number;
39
+ segmentDistance: number;
40
+ } | null;
@@ -0,0 +1,8 @@
1
+ export type { CurveData, PathExecutionState, VehicleMovementState } from '../../types/movement';
2
+ export { getPositionFromOffset, getLineLength } from './shared';
3
+ export { checkRearCompletion, handleArrival, type SegmentCompletionContext, type SegmentCompletionResult, type SegmentVehicleState } from './segmentTransition';
4
+ export { updateAxlePosition, calculatePositionOnLine, calculatePositionOnCurve } from './positionUpdate';
5
+ export { calculateFrontAxlePosition, getCumulativeArcLength, arcLengthToSegmentPosition } from './arcLengthTracking';
6
+ export { calculateInitialFrontPosition } from './initialize';
7
+ export { initializeMovingVehicle, createInitialMovementState, initializeAllVehicles, type InitializationResult } from './initialize';
8
+ export { prepareCommandPath, type PreparedPath } from './pathPreparation';
@@ -0,0 +1,37 @@
1
+ import type { Vehicle, AxleState } from '../../types/vehicle';
2
+ import type { Line } from '../../types/core';
3
+ import type { VehicleMovementState } from '../../types/movement';
4
+ /**
5
+ * Calculate initial front axle position from rear position
6
+ *
7
+ * @param rLineId - Rear axle line ID
8
+ * @param rAbsoluteOffset - Rear axle absolute offset on line
9
+ * @param wheelbase - Distance between front and rear axles
10
+ * @param line - The line the vehicle is on
11
+ * @returns Front axle state
12
+ */
13
+ export declare function calculateInitialFrontPosition(rLineId: string, rAbsoluteOffset: number, wheelbase: number, line: Line): AxleState;
14
+ /**
15
+ * Initialize Vehicle with runtime state for dual-axle
16
+ *
17
+ * @param vehicle - Vehicle to initialize (must have rear and front already populated)
18
+ * @param _line - The line (kept for compatibility)
19
+ * @returns Vehicle with state set to idle
20
+ */
21
+ export declare function initializeMovingVehicle(vehicle: Vehicle, _line: Line): Vehicle;
22
+ /**
23
+ * Create initial movement state for a vehicle
24
+ */
25
+ export declare function createInitialMovementState(vehicle: Vehicle): VehicleMovementState;
26
+ export interface InitializationResult {
27
+ movingVehicles: Vehicle[];
28
+ stateMap: Map<string, VehicleMovementState>;
29
+ }
30
+ /**
31
+ * Initialize all vehicles and their movement states
32
+ *
33
+ * @param vehicles - Array of vehicles to initialize
34
+ * @param linesMap - Map of line IDs to Line objects
35
+ * @returns InitializationResult with moving vehicles and state map
36
+ */
37
+ export declare function initializeAllVehicles(vehicles: Vehicle[], linesMap: Map<string, Line>): InitializationResult;
@@ -0,0 +1,18 @@
1
+ import type { Vehicle, GotoCommand } from '../../types/vehicle';
2
+ import type { SceneContext } from '../../types/movement';
3
+ import { type PathResult } from '../pathFinding';
4
+ import type { CurveData } from '../../types/movement';
5
+ export interface PreparedPath {
6
+ path: PathResult;
7
+ curveDataMap: Map<number, CurveData>;
8
+ }
9
+ /**
10
+ * Prepare path and curve data for a goto command
11
+ * Returns null if no path found
12
+ *
13
+ * @param vehicle - The vehicle to move
14
+ * @param command - The goto command with target
15
+ * @param ctx - Scene context with graph, linesMap, curves, and config
16
+ * @returns PreparedPath with path and curve data, or null if no path found
17
+ */
18
+ export declare function prepareCommandPath(vehicle: Vehicle, command: GotoCommand, ctx: SceneContext): PreparedPath | null;
@@ -0,0 +1,44 @@
1
+ import type { Line, Point } from '../../types/core';
2
+ import type { AxleState } from '../../types/vehicle';
3
+ import type { AxleExecutionState } from '../../types/movement';
4
+ import type { PathResult } from '../pathFinding';
5
+ import type { CurveData } from '../../types/movement';
6
+ /**
7
+ * Calculate position on line (pure function)
8
+ *
9
+ * @param line - The line to calculate position on
10
+ * @param absoluteOffset - The offset along the line
11
+ * @returns Position data
12
+ */
13
+ export declare function calculatePositionOnLine(line: Line, absoluteOffset: number): {
14
+ position: Point;
15
+ lineId: string;
16
+ absoluteOffset: number;
17
+ };
18
+ /**
19
+ * Calculate position on curve (pure function)
20
+ *
21
+ * @param curveData - The curve data with arc-length table
22
+ * @param segmentDistance - Distance along the curve
23
+ * @returns Position data
24
+ */
25
+ export declare function calculatePositionOnCurve(curveData: CurveData, segmentDistance: number): {
26
+ position: Point;
27
+ };
28
+ /**
29
+ * Update single axle position and handle segment transitions
30
+ *
31
+ * @param axleState - Current axle state
32
+ * @param axleExecution - Current execution state for this axle
33
+ * @param path - The path being followed
34
+ * @param velocity - Distance to move this frame
35
+ * @param linesMap - Map of line IDs to Line objects
36
+ * @param curveDataMap - Map of curve indices to curve data
37
+ * @param maxOffset - Optional max offset for extending beyond path (for front axle)
38
+ * @returns Updated axle state, execution state, and completion flag
39
+ */
40
+ export declare function updateAxlePosition(axleState: AxleState, axleExecution: AxleExecutionState, path: PathResult, velocity: number, linesMap: Map<string, Line>, curveDataMap: Map<number, CurveData>, maxOffset?: number): {
41
+ axleState: AxleState;
42
+ execution: AxleExecutionState;
43
+ completed: boolean;
44
+ };
@@ -0,0 +1,39 @@
1
+ import type { Line } from '../../types/core';
2
+ import type { GotoCommand, GotoCompletionCallback } from '../../types/vehicle';
3
+ import type { MovementConfig, SceneContext } from '../../types/movement';
4
+ import type { Vehicle } from '../../types/vehicle';
5
+ import type { PathResult } from '../pathFinding';
6
+ import type { CurveData, PathExecutionState, VehicleMovementState } from '../../types/movement';
7
+ interface SegmentCompletionContext {
8
+ linesMap: Map<string, Line>;
9
+ config: MovementConfig;
10
+ vehicleQueues: Map<string, GotoCommand[]>;
11
+ curves: import('../../types/core').Curve[];
12
+ graphRef: {
13
+ current: ReturnType<typeof import('../pathFinding').buildGraph> | null;
14
+ };
15
+ prepareCommandPath: (vehicle: Vehicle, command: GotoCommand, ctx: SceneContext) => {
16
+ path: PathResult;
17
+ curveDataMap: Map<number, CurveData>;
18
+ } | null;
19
+ onCommandComplete?: GotoCompletionCallback;
20
+ }
21
+ export interface SegmentCompletionResult {
22
+ handled: boolean;
23
+ vehicle: Vehicle;
24
+ /** New execution state (null if completed or unchanged) */
25
+ newExecution?: PathExecutionState | null;
26
+ /** True if vehicle is waiting for confirmation to continue */
27
+ isWaiting?: boolean;
28
+ }
29
+ /**
30
+ * Handle arrival at destination - all segments completed (pure function)
31
+ * Checks for next command in queue and starts it, or marks vehicle as arrived
32
+ */
33
+ export declare function handleArrival(state: VehicleMovementState, overflow: number, ctx: SegmentCompletionContext): SegmentCompletionResult;
34
+ /**
35
+ * Check if rear axle has completed all segments
36
+ * Note: This is called from the animation loop which uses updateAxlePosition for movement
37
+ */
38
+ export declare function checkRearCompletion(state: VehicleMovementState, ctx: SegmentCompletionContext): SegmentCompletionResult;
39
+ export type { SegmentCompletionContext, VehicleMovementState as SegmentVehicleState };
@@ -0,0 +1,10 @@
1
+ import type { Line, Point } from '../../types/core';
2
+ /**
3
+ * Get position on line from absolute offset
4
+ * This is a pure function that converts absolute offset to Point coordinates
5
+ */
6
+ export declare function getPositionFromOffset(line: Line, absoluteOffset: number): Point;
7
+ /**
8
+ * Calculate line length
9
+ */
10
+ export declare function getLineLength(line: Line): number;
@@ -0,0 +1,14 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const f=require("react");function R(e,t){const n=t.x-e.x,s=t.y-e.y;return Math.sqrt(n*n+s*s)}function K(e,t){const n=t.x-e.x,s=t.y-e.y,r=Math.sqrt(n*n+s*s);return r===0?{x:0,y:0}:{x:n/r,y:s/r}}function we(e,t){return t*(e==="proportional-40"?.4:.5522)}function ne(e,t,n,s=!1,r){const{wheelbase:c,tangentMode:o}=n;let i;r?.fromOffset!==void 0?i=H(e,r.fromOffset,r.fromIsPercentage??!1):i=e.end;let l;r?.toOffset!==void 0?l=H(t,r.toOffset,r.toIsPercentage??!1):l=t.start;const a=K(e.start,e.end),u=s?{x:i.x-a.x*c,y:i.y-a.y*c}:i,m=K(e.start,e.end),g=K(t.start,t.end),I=R(u,l),p=we(o,I),d=s?{x:u.x-m.x*p,y:u.y-m.y*p}:{x:u.x+m.x*p,y:u.y+m.y*p},v={x:l.x-g.x*p,y:l.y-g.y*p};return{p0:u,p1:d,p2:v,p3:l}}function le(e,t){return{x:e.start.x+(e.end.x-e.start.x)*t,y:e.start.y+(e.end.y-e.start.y)*t}}function H(e,t,n){const s=R(e.start,e.end);let r;return n?r=t/100:r=s>0?t/s:0,r=Math.max(0,Math.min(1,r)),le(e,r)}function Y(e,t){const{p0:n,p1:s,p2:r,p3:c}=e,o=1-t,i=o*o,l=i*o,a=t*t,u=a*t;return{x:l*n.x+3*i*t*s.x+3*o*a*r.x+u*c.x,y:l*n.y+3*i*t*s.y+3*o*a*r.y+u*c.y}}function de(e,t=100){const n=[{t:0,distance:0}];let s=e.p0,r=0;for(let c=1;c<=t;c++){const o=c/t,i=Y(e,o);r+=R(s,i),n.push({t:o,distance:r}),s=i}return n}function me(e,t){if(t<=0)return 0;const n=e[e.length-1].distance;if(t>=n)return 1;let s=0,r=e.length-1;for(;s<r-1;){const u=Math.floor((s+r)/2);e[u].distance<t?s=u:r=u}const c=e[s].distance,o=e[r].distance,i=e[s].t,l=e[r].t;if(o===c)return i;const a=(t-c)/(o-c);return i+a*(l-i)}function Te(e){return e[e.length-1].distance}function ge(e,t=100){let n=0,s=e.p0;for(let r=1;r<=t;r++){const c=r/t,o=Y(e,c);n+=R(s,o),s=o}return n}function Ee(e,t,n,s){const r=R(e.start,e.end);return t===void 0?s/100*r:n?t/100*r:t}function se(e,t,n,s,r){const c=R(e.start,e.end),o=c-r;if(o<=0)return c;let i;if(t===void 0)i=s;else if(n)i=t;else return Math.max(r,Math.min(t,c));return r+i/100*o}function re(e,t,n,s,r){const o=R(e.start,e.end)-r;if(o<=0)return 0;let i;if(t===void 0)i=s;else if(n)i=t;else return Math.max(0,Math.min(t,o));return i/100*o}function he(e,t,n){const s=new Map,r=new Map,c=new Map;for(const o of e)r.set(o.id,o),c.set(o.id,R(o.start,o.end)),s.set(o.id,[]);for(let o=0;o<t.length;o++){const i=t[o],l=r.get(i.fromLineId),a=r.get(i.toLineId);if(!l||!a)continue;const u=se(l,i.fromOffset,i.fromIsPercentage,100,n.wheelbase),m=re(a,i.toOffset,i.toIsPercentage,0,n.wheelbase),g=ne(l,a,n,!1,{fromOffset:u,fromIsPercentage:!1,toOffset:m,toIsPercentage:!1}),I=ge(g),p={curveIndex:o,fromLineId:i.fromLineId,toLineId:i.toLineId,fromOffset:u,toOffset:m,curveLength:I};s.get(i.fromLineId).push(p)}return{adjacency:s,lines:r,lineLengths:c}}function oe(e,t,n,s,r=!1){const{adjacency:c,lines:o,lineLengths:i}=e;if(!o.get(n))return null;const a=i.get(n),u=r?s/100*a:s,m=[],g=new Map,I=(d,v)=>`${d}:${Math.round(v)}`;if(t.lineId===n&&u>=t.offset){const d=u-t.offset;return{segments:[{type:"line",lineId:t.lineId,startOffset:t.offset,endOffset:u,length:d}],totalDistance:d}}const p=c.get(t.lineId)||[];for(const d of p){if(d.fromOffset<t.offset)continue;const v=d.fromOffset-t.offset,O=v+d.curveLength,L={type:"line",lineId:t.lineId,startOffset:t.offset,endOffset:d.fromOffset,length:v},h={type:"curve",curveIndex:d.curveIndex,startOffset:0,endOffset:d.curveLength,length:d.curveLength};m.push({lineId:d.toLineId,entryOffset:d.toOffset,totalDistance:O,path:[L,h]})}for(m.sort((d,v)=>d.totalDistance-v.totalDistance);m.length>0;){const d=m.shift(),v=I(d.lineId,d.entryOffset),O=g.get(v);if(O!==void 0&&O<=d.totalDistance)continue;if(g.set(v,d.totalDistance),d.lineId===n){const h=Math.abs(u-d.entryOffset);if(u>=d.entryOffset){const x={type:"line",lineId:n,startOffset:d.entryOffset,endOffset:u,length:h};return{segments:[...d.path,x],totalDistance:d.totalDistance+h}}}const L=c.get(d.lineId)||[];for(const h of L){if(h.fromOffset<d.entryOffset)continue;const x=h.fromOffset-d.entryOffset,M=d.totalDistance+x+h.curveLength,C=I(h.toLineId,h.toOffset),V=g.get(C);if(V!==void 0&&V<=M)continue;const D={type:"line",lineId:d.lineId,startOffset:d.entryOffset,endOffset:h.fromOffset,length:x},y={type:"curve",curveIndex:h.curveIndex,startOffset:0,endOffset:h.curveLength,length:h.curveLength};m.push({lineId:h.toLineId,entryOffset:h.toOffset,totalDistance:M,path:[...d.path,D,y]})}m.sort((h,x)=>h.totalDistance-x.totalDistance)}return null}function Ve(e,t,n,s,r=!1){return oe(e,t,n,s,r)!==null}function $e(e,t,n){return(e.adjacency.get(t)||[]).filter(r=>r.fromOffset>=n)}function ie(e,t){const n=Math.sqrt(Math.pow(e.end.x-e.start.x,2)+Math.pow(e.end.y-e.start.y,2)),s=n>0?t/n:0;return{x:e.start.x+(e.end.x-e.start.x)*Math.min(1,Math.max(0,s)),y:e.start.y+(e.end.y-e.start.y)*Math.min(1,Math.max(0,s))}}function pe(e){return Math.sqrt(Math.pow(e.end.x-e.start.x,2)+Math.pow(e.end.y-e.start.y,2))}function ve(e,t,n){let s=0;for(let r=0;r<t;r++)s+=e.segments[r].length;return s+=n,s}function Ie(e,t){let n=0;for(let s=0;s<e.segments.length;s++){const r=e.segments[s],c=n+r.length;if(t<c)return{segmentIndex:s,segmentDistance:t-n};if(t===c)return s+1<e.segments.length?{segmentIndex:s+1,segmentDistance:0}:{segmentIndex:s,segmentDistance:r.length};n+=r.length}return null}function X(e,t,n,s){const c=ve(e,t,n)+s;return Ie(e,c)}function ce(e,t,n){const s=e.execution,r=n.vehicleQueues.get(e.vehicle.id),c=r?.[s.currentCommandIndex];if(c&&n.onCommandComplete&&n.onCommandComplete({vehicleId:e.vehicle.id,command:c,finalPosition:{lineId:e.vehicle.rear.lineId,absoluteOffset:e.vehicle.rear.absoluteOffset,position:e.vehicle.rear.position},payload:c.payload}),c?.awaitConfirmation)return{handled:!0,vehicle:{...e.vehicle,state:"waiting"},newExecution:s,isWaiting:!0};const o=s.currentCommandIndex+1;if(r&&o<r.length){const l=r[o],a=n.graphRef.current;if(a){const u={graph:a,linesMap:n.linesMap,curves:n.curves,config:n.config},m=n.prepareCommandPath(e.vehicle,l,u);if(m){const g=X(m.path,0,t,n.config.wheelbase),I={path:m.path,curveDataMap:m.curveDataMap,currentCommandIndex:o,rear:{currentSegmentIndex:0,segmentDistance:t},front:g?{currentSegmentIndex:g.segmentIndex,segmentDistance:g.segmentDistance}:{currentSegmentIndex:0,segmentDistance:t}};return{handled:!0,vehicle:{...e.vehicle,state:"moving"},newExecution:I}}}}return{handled:!0,vehicle:{...e.vehicle,state:"idle"},newExecution:null}}function be(e,t){const n=e.execution;return n?n.rear.currentSegmentIndex>=n.path.segments.length?ce(e,n.rear.segmentDistance,t):{handled:!1,vehicle:e.vehicle}:{handled:!1,vehicle:e.vehicle}}function k(e,t){return{position:ie(e,t),lineId:e.id,absoluteOffset:t}}function U(e,t){const n=me(e.arcLengthTable,t);return{position:Y(e.bezier,n)}}function Z(e,t,n,s,r,c,o){const i=n.segments[t.currentSegmentIndex],l=t.segmentDistance+s;if(l>=i.length){const u=l-i.length,m=t.currentSegmentIndex+1;if(m>=n.segments.length){if(o!==void 0&&i.type==="line"){const d=r.get(i.lineId),v=i.startOffset+l;if(v<=o){const L=k(d,v);return{axleState:{...e,...L},execution:{...t,segmentDistance:l},completed:!1}}const O=k(d,o);return{axleState:{...e,...O},execution:{...t,segmentDistance:o-i.startOffset},completed:!0}}const p=i.type==="line"?k(r.get(i.lineId),i.endOffset):U(c.get(i.curveIndex),i.length);return{axleState:{...e,...p},execution:{...t,segmentDistance:i.length},completed:!0}}const g=n.segments[m],I=g.type==="line"?k(r.get(g.lineId),g.startOffset+u):U(c.get(g.curveIndex),u);return{axleState:{...e,...I},execution:{currentSegmentIndex:m,segmentDistance:u},completed:!1}}const a=i.type==="line"?k(r.get(i.lineId),i.startOffset+l):U(c.get(i.curveIndex),l);return{axleState:{...e,...a},execution:{...t,segmentDistance:l},completed:!1}}function xe(e,t,n,s){const r=Math.sqrt(Math.pow(s.end.x-s.start.x,2)+Math.pow(s.end.y-s.start.y,2));let c=t+n;c=Math.min(c,r);const o=ie(s,c);return{lineId:e,position:o,absoluteOffset:c}}function ye(e,t){return{...e,state:"idle"}}function Se(e){return{vehicle:e,execution:null}}function ee(e,t){const n=[],s=new Map;for(const r of e){if(!t.get(r.lineId))continue;const o=ye(r);n.push(o);const i=Se(o);s.set(r.id,i)}return{movingVehicles:n,stateMap:s}}function Re(e,t,n,s){const r=new Map;for(const c of e.segments)if(c.type==="curve"&&c.curveIndex!==void 0){const o=t[c.curveIndex];if(o){const i=n.get(o.fromLineId),l=n.get(o.toLineId);if(i&&l){const a=se(i,o.fromOffset,o.fromIsPercentage,100,s.wheelbase),u=re(l,o.toOffset,o.toIsPercentage,0,s.wheelbase),m=ne(i,l,s,!1,{fromOffset:a,fromIsPercentage:!1,toOffset:u,toIsPercentage:!1}),g=de(m);r.set(c.curveIndex,{bezier:m,arcLengthTable:g})}}}return r}function W(e,t,n){const{graph:s,linesMap:r,curves:c,config:o}=n,i=r.get(t.targetLineId);if(!i)return null;const a=pe(i)-o.wheelbase;if(a<=0)return null;const u=t.isPercentage?t.targetOffset/100*a:Math.min(t.targetOffset,a),m=oe(s,{lineId:e.lineId,offset:e.rear.absoluteOffset},t.targetLineId,u,!1);if(!m)return null;const g=Re(m,c,r,o);return{path:m,curveDataMap:g}}function Ae({vehicles:e,lines:t,vehicleQueues:n,velocity:s,wheelbase:r,tangentMode:c,curves:o,eventEmitter:i}){const[l,a]=f.useState("stopped"),[u,m]=f.useState([]),g=f.useMemo(()=>({wheelbase:r,tangentMode:c}),[r,c]),I=f.useMemo(()=>new Map(t.map(y=>[y.id,y])),[t]),p=f.useRef(null),d=f.useRef(new Map);f.useEffect(()=>{const{movingVehicles:y,stateMap:S}=ee(e,I);d.current=S;const P=setTimeout(()=>{m(y)},0);return()=>clearTimeout(P)},[e,I]);const v=f.useRef(null);f.useEffect(()=>{t.length>0&&(v.current=he(t,o,g))},[t,o,g]);const O=f.useRef(!1),L=f.useRef(()=>{}),h=f.useRef(0),x=f.useRef(new Set);f.useEffect(()=>{L.current=()=>{const y=s;let S=!1;for(const[,P]of d.current)P.vehicle.state==="moving"&&(S=!0);if(!S){O.current=!1,h.current=0;return}m(P=>P.map($=>{const w=d.current.get($.id);if(!w||$.state!=="moving"||!w.execution)return $;const T=w.execution;let A;if(T.front.currentSegmentIndex<T.path.segments.length){const F=T.path.segments[T.front.currentSegmentIndex];if(F.type==="line"){const E=I.get(F.lineId);E&&(A=Math.sqrt(Math.pow(E.end.x-E.start.x,2)+Math.pow(E.end.y-E.start.y,2)))}}const q=Z($.rear,T.rear,T.path,y,I,T.curveDataMap),b=Z($.front,T.front,T.path,y,I,T.curveDataMap,A);if(q.completed){const F={linesMap:I,config:g,vehicleQueues:n,curves:o,graphRef:v,prepareCommandPath:W,onCommandComplete:i?j=>i.emit("commandComplete",j):void 0},E={...$,rear:q.axleState,front:b.axleState};w.vehicle=E,w.execution.rear=q.execution,w.execution.front=b.execution;const ae=q.execution.segmentDistance,z=ce(w,ae,F);if(w.vehicle=z.vehicle,z.newExecution!==void 0&&(w.execution=z.newExecution),i){const j=z.vehicle.rear.position,G=z.vehicle.front.position;i.emit("positionUpdate",{vehicleId:z.vehicle.id,rear:j,front:G,center:{x:(j.x+G.x)/2,y:(j.y+G.y)/2},angle:Math.atan2(G.y-j.y,G.x-j.x)})}return z.vehicle}const N={...$,rear:q.axleState,front:b.axleState};if(w.vehicle=N,w.execution.rear=q.execution,w.execution.front=b.execution,i){const F=N.rear.position,E=N.front.position;i.emit("positionUpdate",{vehicleId:N.id,rear:F,front:E,center:{x:(F.x+E.x)/2,y:(F.y+E.y)/2},angle:Math.atan2(E.y-F.y,E.x-F.x)})}return N})),O.current&&(p.current=requestAnimationFrame(()=>L.current()))}},[s,I,o,g,n,i]),f.useEffect(()=>(l==="running"?(O.current=!0,p.current=requestAnimationFrame(()=>L.current())):O.current=!1,()=>{p.current&&cancelAnimationFrame(p.current)}),[l]);const M=f.useCallback(()=>{if(l==="running")return;const y=v.current;y&&(h.current=0,x.current.clear(),m(S=>S.map(P=>{const $=n.get(P.id);if(!$||$.length===0)return P;const w=$[0],A=W(P,w,{graph:y,linesMap:I,curves:o,config:g});if(!A)return console.warn(`No path found for vehicle ${P.id}`),P;const q=d.current.get(P.id);if(q){const b=X(A.path,0,0,r);q.execution={path:A.path,curveDataMap:A.curveDataMap,currentCommandIndex:0,rear:{currentSegmentIndex:0,segmentDistance:0},front:b?{currentSegmentIndex:b.segmentIndex,segmentDistance:b.segmentDistance}:{currentSegmentIndex:0,segmentDistance:0}},q.vehicle={...P,state:"moving"}}return{...P,state:"moving"}})),a("running"))},[l,I,o,n,g,r]),C=f.useCallback(()=>{p.current&&(cancelAnimationFrame(p.current),p.current=null),a("paused")},[]),V=f.useCallback(()=>{p.current&&(cancelAnimationFrame(p.current),p.current=null);const{movingVehicles:y,stateMap:S}=ee(e,I);d.current=S,m(y),a("stopped")},[e,I]),D=f.useCallback(y=>{const S=d.current.get(y);if(!S||S.vehicle.state!=="waiting")return!1;const P=n.get(y),$=S.execution;if(!$)return!1;const w=$.currentCommandIndex+1;if(P&&w<P.length){const T=v.current;if(T){const A=P[w],q={graph:T,linesMap:I,curves:o,config:g},b=W(S.vehicle,A,q);if(b){const N=X(b.path,0,0,r);return S.execution={path:b.path,curveDataMap:b.curveDataMap,currentCommandIndex:w,rear:{currentSegmentIndex:0,segmentDistance:0},front:N?{currentSegmentIndex:N.segmentIndex,segmentDistance:N.segmentDistance}:{currentSegmentIndex:0,segmentDistance:0}},S.vehicle={...S.vehicle,state:"moving"},m(F=>F.map(E=>E.id===y?S.vehicle:E)),l!=="running"&&a("running"),!0}}}return S.vehicle={...S.vehicle,state:"idle"},S.execution=null,m(T=>T.map(A=>A.id===y?S.vehicle:A)),!0},[n,I,o,g,r,l]);return{movingVehicles:u,playbackState:l,handleRun:M,handlePause:C,handleReset:V,continueVehicle:D}}class Le{listeners=new Map;on(t,n){return this.listeners.has(t)||this.listeners.set(t,new Set),this.listeners.get(t).add(n),()=>{this.listeners.get(t)?.delete(n)}}emit(t,n){this.listeners.get(t)?.forEach(s=>{try{s(n)}catch(r){console.error(`Error in event listener for "${t}":`,r)}})}off(t){t?this.listeners.delete(t):this.listeners.clear()}listenerCount(t){return this.listeners.get(t)?.size??0}}const Me=f.createContext(null);function Oe(){const e=f.useContext(Me);if(!e)throw new Error("useVehicleEventEmitter must be used within a VehicleEventProvider");return e}function qe(){return f.useMemo(()=>new Le,[])}function Fe(e,t,n=[]){const s=Oe();f.useEffect(()=>s.on(e,t),[s,e,...n])}function te(e){const t=[],n=[],s=[],r=e.trim().split(`
2
+ `);for(const c of r){const o=c.trim();if(!o||o.startsWith("#"))continue;const i=o.match(/^(\w+)\s*:\s*\((\d+),\s*(\d+)\)\s*->\s*\((\d+),\s*(\d+)\)/);if(i){t.push({id:i[1],start:{x:parseInt(i[2]),y:parseInt(i[3])},end:{x:parseInt(i[4]),y:parseInt(i[5])}});continue}const l=o.match(/^(\w+)(?:\s+(\d+(?:\.\d+)?)(%?))??\s*->\s*(\w+)(?:\s+(\d+(?:\.\d+)?)(%?))?/);if(l){const u={fromLineId:l[1],toLineId:l[4]};l[2]&&(u.fromOffset=parseFloat(l[2]),u.fromIsPercentage=l[3]==="%"),l[5]&&(u.toOffset=parseFloat(l[5]),u.toIsPercentage=l[6]==="%"),n.push(u);continue}const a=o.match(/^(\w+)\s+start\s+(\w+)\s+(\d+(?:\.\d+)?)(%?)/);if(a){const u=parseFloat(a[3]),m=a[4]==="%";s.push({vehicleId:a[1],lineId:a[2],offset:u,isPercentage:m});continue}}return{lines:t,curves:n,vehicles:s}}function Ce(e){const t=[];return e.lines.forEach(n=>{t.push(`${n.id} : (${Math.round(n.start.x)}, ${Math.round(n.start.y)}) -> (${Math.round(n.end.x)}, ${Math.round(n.end.y)})`)}),e.lines.length>0&&e.curves.length>0&&t.push(""),e.curves.forEach(n=>{let s=`${n.fromLineId}`;n.fromOffset!==void 0&&(s+=` ${n.fromOffset}${n.fromIsPercentage?"%":""}`),s+=" -> ",s+=`${n.toLineId}`,n.toOffset!==void 0&&(s+=` ${n.toOffset}${n.toIsPercentage?"%":""}`),t.push(s)}),(e.lines.length>0||e.curves.length>0)&&e.vehicles.length>0&&t.push(""),e.vehicles.forEach(n=>{const s=n.isPercentage?`${n.offset}%`:`${n.offset}`;t.push(`${n.vehicleId} start ${n.lineId} ${s}`)}),t.join(`
3
+ `)}function B(e){const t=[],n=e.trim().split(`
4
+ `);for(const s of n){const r=s.trim();if(!r||r.startsWith("#"))continue;const c=r.match(/^(\w+)\s+start\s+(\w+)\s+(\d+(?:\.\d+)?)(%?)/);c&&t.push({vehicleId:c[1],lineId:c[2],offset:parseFloat(c[3]),isPercentage:c[4]==="%"})}return t}function De(e){return e.map(t=>{const n=t.isPercentage?`${t.offset}%`:`${t.offset}`;return`${t.vehicleId} start ${t.lineId} ${n}`}).join(`
5
+ `)}function Q(e){const t=[],n=e.trim().split(`
6
+ `);for(const s of n){const r=s.trim();if(!r||r.startsWith("#"))continue;const c=r.match(/^(\w+)\s+goto\s+(\w+)\s+(\d+(?:\.\d+)?)(%?)/);if(c){const o=r.slice(c[0].length),i=o.includes("--wait");let l;const a=o.match(/--payload\s+(\{.*\})/);if(a)try{l=JSON.parse(a[1])}catch{console.warn(`Invalid JSON payload in goto command: ${a[1]}`)}t.push({vehicleId:c[1],targetLineId:c[2],targetOffset:parseFloat(c[3]),isPercentage:c[4]==="%",awaitConfirmation:i,payload:l})}}return t}function Pe(e){return e.map(t=>{const n=t.isPercentage?`${t.targetOffset}%`:`${t.targetOffset}`,s=t.awaitConfirmation?" --wait":"",r=t.payload!==void 0?` --payload ${JSON.stringify(t.payload)}`:"";return`${t.vehicleId} goto ${t.targetLineId} ${n}${s}${r}`}).join(`
7
+ `)}function Ne(){const[e,t]=f.useState([]),[n,s]=f.useState([]),[r,c]=f.useState(""),[o,i]=f.useState(null),[l,a]=f.useState(!1),[u,m]=f.useState(0),g=f.useRef(!1),I=f.useRef("");return f.useEffect(()=>{I.current=r},[r]),f.useEffect(()=>{if(g.current)return;const p=te(I.current),d=Ce({lines:e,curves:n,vehicles:p.vehicles});d!==I.current&&(g.current=!0,c(d),setTimeout(()=>{g.current=!1},50))},[e,n]),f.useEffect(()=>{if(g.current)return;const p=setTimeout(()=>{a(!0),m(v=>v+1)},0),d=setTimeout(()=>{try{const v=te(r);a(!1);const O=JSON.stringify(v.lines)!==JSON.stringify(e),L=JSON.stringify(v.curves)!==JSON.stringify(n);(O||L)&&(g.current=!0,O&&t(v.lines),L&&s(v.curves),setTimeout(()=>{g.current=!1},100)),i(null)}catch(v){a(!1),i(v instanceof Error?v.message:"Invalid scene definition")}},2e3);return()=>{clearTimeout(p),clearTimeout(d)}},[r,e,n]),{lines:e,curves:n,sceneDefinitionText:r,sceneError:o,isDebouncing:l,debounceKey:u,setLines:t,setCurves:s,setSceneDefinitionText:c}}function J(e,t,n=0){const s=[],r=[],c=new Set;for(const o of e){if(c.has(o.vehicleId)){r.push(`Duplicate vehicle ID: ${o.vehicleId}`);continue}c.add(o.vehicleId);const i=t.find(p=>p.id===o.lineId);if(!i){r.push(`Vehicle ${o.vehicleId}: Line "${o.lineId}" not found`);continue}const l=R(i.start,i.end),a=Math.max(0,l-n);let u;if(o.isPercentage){if(o.offset<0||o.offset>100){r.push(`Vehicle ${o.vehicleId}: Offset ${o.offset}% must be between 0% and 100%`);continue}u=o.offset/100*a}else{if(o.offset<0||o.offset>l){r.push(`Vehicle ${o.vehicleId}: Offset ${o.offset} exceeds line length ${l.toFixed(2)}`);continue}u=Math.min(o.offset,a)}const m=H(i,u,!1),g={lineId:o.lineId,position:m,absoluteOffset:u},I=xe(o.lineId,u,n,i);s.push({id:o.vehicleId,lineId:o.lineId,offset:o.offset,isPercentage:o.isPercentage,state:"idle",rear:g,front:I})}return{vehicles:s,errors:r}}function ze(e){const t=e.map(s=>{const r=s.vehicleId.match(/^v(\d+)$/);return r?parseInt(r[1]):0}).filter(s=>s>0);return`v${(t.length>0?Math.max(...t):0)+1}`}function _(e,t,n){const s=[],r=[],c=new Map,o=new Set(t.map(a=>a.id)),i=new Set(n.map(a=>a.id)),l=new Map(n.map(a=>{const u=a.end.x-a.start.x,m=a.end.y-a.start.y;return[a.id,Math.sqrt(u*u+m*m)]}));for(const a of e){if(!o.has(a.vehicleId)){s.push(`Vehicle "${a.vehicleId}" does not exist`);continue}if(!i.has(a.targetLineId)){s.push(`Line "${a.targetLineId}" does not exist`);continue}const u=l.get(a.targetLineId),m=a.isPercentage?a.targetOffset/100*u:a.targetOffset;if(m<0||m>u){s.push(`Offset ${a.targetOffset}${a.isPercentage?"%":""} is out of bounds for ${a.targetLineId}`);continue}r.push(a);const g=c.get(a.vehicleId)||[];g.push(a),c.set(a.vehicleId,g)}return{commands:r,errors:s,vehicleQueues:c}}function je(e,t){if(t.length===0)return null;if(e.length===0)return t[0].id;const n=new Map;for(const s of t)n.set(s.id,0);for(const s of e){const r=n.get(s.vehicleId)||0;n.set(s.vehicleId,r+1)}return t[0].id}function ke({lines:e,wheelbase:t}){const[n,s]=f.useState([]),[r,c]=f.useState(""),[o,i]=f.useState(null),[l,a]=f.useState(!1),[u,m]=f.useState(0),g=f.useRef(!1),I=f.useRef(e),p=f.useRef(e),d=f.useRef(t),v=f.useRef(t);f.useEffect(()=>{I.current=e},[e]),f.useEffect(()=>{d.current=t},[t]),f.useEffect(()=>{if(g.current)return;const L=setTimeout(()=>{a(!0),m(x=>x+1)},0),h=setTimeout(()=>{try{const x=B(r),{vehicles:M,errors:C}=J(x,I.current,d.current);a(!1),C.length>0?(i(C.join(`
8
+ `)),s(M)):(i(null),s(M))}catch(x){a(!1),i(x instanceof Error?x.message:"Invalid initial movement"),s([])}},2e3);return()=>{clearTimeout(L),clearTimeout(h)}},[r]),f.useEffect(()=>{if(!(JSON.stringify(p.current)!==JSON.stringify(e))||(p.current=e,g.current||!r.trim()))return;const h=setTimeout(()=>{try{const x=B(r),{vehicles:M,errors:C}=J(x,e,d.current);C.length>0?(i(C.join(`
9
+ `)),s(M)):(i(null),s(M))}catch(x){i(x instanceof Error?x.message:"Invalid initial movement"),s([])}},0);return()=>clearTimeout(h)},[e,r]),f.useEffect(()=>{if(v.current===t||(v.current=t,g.current||!r.trim()))return;const L=setTimeout(()=>{try{const h=B(r),{vehicles:x,errors:M}=J(h,I.current,t);M.length>0?(i(M.join(`
10
+ `)),s(x)):(i(null),s(x))}catch(h){i(h instanceof Error?h.message:"Invalid initial movement"),s([])}},0);return()=>clearTimeout(L)},[t,r]);const O=f.useCallback(()=>{const L=B(r),h=ze(L),x=e.length>0?e[0]:null;if(!x){i("No lines available. Please create at least one line first.");return}const M={vehicleId:h,lineId:x.id,offset:0,isPercentage:!1},C=[...L,M],V=De(C),{vehicles:D,errors:y}=J(C,e,t);g.current=!0,c(V),y.length>0?i(y.join(`
11
+ `)):i(null),s(D),setTimeout(()=>{g.current=!1},50)},[r,e,t]);return{vehicles:n,initialMovementText:r,movementError:o,isDebouncing:l,debounceKey:u,setInitialMovementText:c,handleAddStartCommand:O}}function Ge({lines:e,vehicles:t}){const[n,s]=f.useState(""),[r,c]=f.useState([]),[o,i]=f.useState(new Map),[l,a]=f.useState(null),[u,m]=f.useState(!1),[g,I]=f.useState(0),p=f.useRef(!1),d=f.useRef(t),v=f.useRef(e);f.useEffect(()=>{d.current=t,v.current=e},[t,e]),f.useEffect(()=>{if(p.current)return;const L=setTimeout(()=>{m(!0),I(x=>x+1)},0),h=setTimeout(()=>{try{const x=Q(n),{commands:M,errors:C,vehicleQueues:V}=_(x,d.current,v.current);m(!1),C.length>0?a(C.join(`
12
+ `)):a(null),c(M),i(V)}catch(x){m(!1),a(x instanceof Error?x.message:"Invalid movement sequence"),c([]),i(new Map)}},2e3);return()=>{clearTimeout(L),clearTimeout(h)}},[n]),f.useEffect(()=>{if(p.current||!n.trim())return;const L=setTimeout(()=>{try{const h=Q(n),{commands:x,errors:M,vehicleQueues:C}=_(h,t,e);M.length>0?a(M.join(`
13
+ `)):a(null),c(x),i(C)}catch(h){a(h instanceof Error?h.message:"Invalid movement sequence"),c([]),i(new Map)}},0);return()=>clearTimeout(L)},[t,e,n]);const O=f.useCallback(()=>{if(t.length===0){a("No vehicles available. Please create at least one vehicle first.");return}if(e.length===0){a("No lines available. Please create at least one line first.");return}const L=Q(n),h=je(L,t);if(!h){a("No vehicles available.");return}const x=e[0],M={vehicleId:h,targetLineId:x.id,targetOffset:100,isPercentage:!0},C=[...L,M],V=Pe(C),{commands:D,errors:y,vehicleQueues:S}=_(C,t,e);p.current=!0,s(V),y.length>0?a(y.join(`
14
+ `)):a(null),c(D),i(S),setTimeout(()=>{p.current=!1},50)},[n,t,e]);return{movementSequenceText:n,gotoCommands:r,vehicleQueues:o,sequenceError:l,isDebouncing:u,debounceKey:g,setMovementSequenceText:s,handleAddGotoCommand:O}}function ue(e,t,n){if(!t||!n)return{x:0,y:0};const s=t.getBoundingClientRect();return{x:e.clientX-s.left,y:e.clientY-s.top}}function fe(e,t,n=10){for(const s of t){if(R(e,s.start)<n)return{lineId:s.id,endpoint:"start"};if(R(e,s.end)<n)return{lineId:s.id,endpoint:"end"}}return null}function Be({canvasRef:e,containerRef:t,drawMode:n,lines:s,curves:r,setLines:c,setCurves:o,lineCounterRef:i}){const[l,a]=f.useState(!1),[u,m]=f.useState(null),[g,I]=f.useState(null),[p,d]=f.useState(null),[v,O]=f.useState(null),[L,h]=f.useState({x:0,y:0});return{handleMouseDown:V=>{const D=ue(V,e.current,t.current),y=fe(D,s);if(n==="line")y||(a(!0),m({start:D,current:D}));else if(n==="drag")y&&d(y);else if(n==="curve")if(y)if(!v)O(y);else{if(v.lineId!==y.lineId){const S={fromLineId:v.lineId,toLineId:y.lineId};o([...r,S])}O(null)}else v&&O(null)},handleMouseMove:V=>{const D=ue(V,e.current,t.current);if(h(D),l&&u)m({...u,current:D});else if(p){const y=s.map(S=>S.id===p.lineId?p.endpoint==="start"?{...S,start:D}:{...S,end:D}:S);c(y)}else{const y=fe(D,s);I(y)}},handleMouseUp:()=>{if(l&&u){if(R(u.start,u.current)>20){const D={id:`line${String(i.current).padStart(3,"0")}`,start:u.start,end:u.current};i.current++,c([...s,D])}m(null),a(!1)}else p&&d(null)},tempLine:u,hoveredEndpoint:g,curveStart:v,mousePos:L}}exports.VehicleEventContext=Me;exports.VehicleEventEmitter=Le;exports.arcLengthToSegmentPosition=Ie;exports.buildArcLengthTable=de;exports.buildGraph=he;exports.calculateBezierArcLength=ge;exports.calculateFrontAxlePosition=X;exports.calculateInitialFrontPosition=xe;exports.calculatePositionOnCurve=U;exports.calculatePositionOnLine=k;exports.canReachTarget=Ve;exports.checkRearCompletion=be;exports.createBezierCurve=ne;exports.createInitialMovementState=Se;exports.distance=R;exports.distanceToT=me;exports.findPath=oe;exports.generateGotoCommands=Pe;exports.generateSceneDefinition=Ce;exports.generateVehicleStarts=De;exports.getArcLength=Te;exports.getCumulativeArcLength=ve;exports.getLineLength=pe;exports.getPointOnBezier=Y;exports.getPointOnLine=le;exports.getPointOnLineByOffset=H;exports.getPositionFromOffset=ie;exports.getReachableCurves=$e;exports.handleArrival=ce;exports.initializeAllVehicles=ee;exports.initializeMovingVehicle=ye;exports.normalize=K;exports.parseGotoCommands=Q;exports.parseSceneDefinition=te;exports.parseVehicleStarts=B;exports.prepareCommandPath=W;exports.resolveFromLineOffset=se;exports.resolveOffset=Ee;exports.resolveToLineOffset=re;exports.updateAxlePosition=Z;exports.useCanvasInteraction=Be;exports.useCreateVehicleEventEmitter=qe;exports.useInitialMovement=ke;exports.useMovementSequence=Ge;exports.useSceneDefinition=Ne;exports.useVehicleEvent=Fe;exports.useVehicleEventEmitter=Oe;exports.useVehicleMovement=Ae;