From c4089aeb206a00188a21ae2b774691d0c4055f6a Mon Sep 17 00:00:00 2001 From: LGram16 Date: Thu, 18 Dec 2025 23:52:02 +0900 Subject: [PATCH] Initial commit: Refactor AgvAutoRunControls --- .gitignore | 24 + App.tsx | 1270 ++++++++++++++++++++ NewMap.json | 1219 +++++++++++++++++++ README.md | 20 + components/AcsControls.tsx | 142 +++ components/AgvAutoRunControls.tsx | 104 ++ components/AgvControls.tsx | 167 +++ components/AgvManualControls.tsx | 119 ++ components/AgvStatusPanel.tsx | 104 ++ components/BmsPanel.tsx | 88 ++ components/ConnectionStatusBar.tsx | 99 ++ components/EditorToolbar.tsx | 51 + components/SerialConsole.tsx | 134 +++ components/SimulationCanvas.tsx | 1561 ++++++++++++++++++++++++ components/SystemLogPanel.tsx | 69 ++ constants.ts | 32 + index.html | 41 + index.tsx | 15 + metadata.json | 7 + package-lock.json | 1771 ++++++++++++++++++++++++++++ package.json | 22 + services/serialService.ts | 124 ++ tsconfig.json | 29 + types.ts | 282 +++++ vite.config.ts | 23 + 25 files changed, 7517 insertions(+) create mode 100644 .gitignore create mode 100644 App.tsx create mode 100644 NewMap.json create mode 100644 README.md create mode 100644 components/AcsControls.tsx create mode 100644 components/AgvAutoRunControls.tsx create mode 100644 components/AgvControls.tsx create mode 100644 components/AgvManualControls.tsx create mode 100644 components/AgvStatusPanel.tsx create mode 100644 components/BmsPanel.tsx create mode 100644 components/ConnectionStatusBar.tsx create mode 100644 components/EditorToolbar.tsx create mode 100644 components/SerialConsole.tsx create mode 100644 components/SimulationCanvas.tsx create mode 100644 components/SystemLogPanel.tsx create mode 100644 constants.ts create mode 100644 index.html create mode 100644 index.tsx create mode 100644 metadata.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 services/serialService.ts create mode 100644 tsconfig.json create mode 100644 types.ts create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..f365636 --- /dev/null +++ b/App.tsx @@ -0,0 +1,1270 @@ +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { ToolType, SimulationMap, AgvState, AgvMotionState, LogEntry, AgvError, AgvSignal, SystemFlag0, SystemFlag1, CSharpMapData, MapNode, MapEdge, FloorMark, MagnetLine, NodeType } from './types'; +import { INITIAL_MAP, MAX_SPEED } from './constants'; +import EditorToolbar from './components/EditorToolbar'; +import SimulationCanvas from './components/SimulationCanvas'; +import SerialConsole from './components/SerialConsole'; +import AgvControls from './components/AgvControls'; +import BmsPanel from './components/BmsPanel'; +import AcsControls from './components/AcsControls'; +import AgvStatusPanel from './components/AgvStatusPanel'; +import SystemLogPanel from './components/SystemLogPanel'; +import { SerialPortHandler } from './services/serialService'; + +// --- ACS CRC16 Table Generation (Matches C# Logic) --- +const ACS_CRC_TABLE = new Uint16Array(256); +(function initAcsCrcTable() { + const polynomial = 0x7979; + for (let i = 0; i < 256; i++) { + let value = 0; + let temp = i; + for (let j = 0; j < 8; j++) { + if (((value ^ temp) & 0x0001) !== 0) { + value = (value >> 1) ^ polynomial; + } else { + value >>= 1; + } + temp >>= 1; + } + ACS_CRC_TABLE[i] = value & 0xFFFF; + } +})(); + +const calculateAcsCrc16 = (data: number[] | Uint8Array): number => { + let crc = 0xFFFF; + for (let i = 0; i < data.length; i++) { + const index = (crc ^ data[i]) & 0xFF; + crc = (crc >> 8) ^ ACS_CRC_TABLE[index]; + } + return crc & 0xFFFF; +}; + +const App: React.FC = () => { + // --- State --- + const [activeTool, setActiveTool] = useState(ToolType.SELECT); + + // Map Data + const [mapData, setMapData] = useState(INITIAL_MAP); + + // AGV Logic State + const [agvState, setAgvState] = useState({ + x: 100, + y: 100, + rotation: 0, + targetRotation: null, + speed: MAX_SPEED, + liftHeight: 0, + motionState: AgvMotionState.IDLE, + runConfig: { + direction: 'FWD', + branch: 'STRAIGHT', + speedLevel: 'M' + }, + error: null, + sensorStatus: '1', // Default per C# DevAGV.cs + detectedRfid: null, + sensorLineFront: false, + sensorLineRear: false, + sensorMark: false, + + // Lift & Magnet + magnetOn: false, + liftStatus: 'IDLE', + lidarEnabled: true, // Default ON + + // Protocol Flags Initial State + system0: 0, + system1: (1 << SystemFlag1.agv_stop), // Default STOP state + errorFlags: 0, + signalFlags: 0, // Will be updated by useEffect + + batteryLevel: 95, + maxCapacity: 100, // 100Ah + batteryTemp: 25, + cellVoltages: Array(8).fill(3.2), + }); + + // Ref to hold latest state for Serial Callbacks (prevents stale closures) + const agvStateRef = useRef(agvState); + useEffect(() => { + agvStateRef.current = agvState; + }, [agvState]); + + // Logs + const [logs, setLogs] = useState([]); + + // Auto-clear logic: Keep last 500 lines + const addLog = useCallback((source: 'AGV' | 'BMS' | 'ACS' | 'SYSTEM', type: 'INFO' | 'RX' | 'TX' | 'ERROR', message: string) => { + setLogs(prev => [...prev.slice(-499), { + id: crypto.randomUUID(), + timestamp: new Date().toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }), + source, + type, + message + }]); + }, []); + + const clearLogs = useCallback((source: 'AGV' | 'BMS' | 'ACS' | 'SYSTEM') => { + setLogs(prev => prev.filter(l => l.source !== source)); + }, []); + + // Serial Handlers + const agvSerialRef = useRef(null); + const bmsSerialRef = useRef(null); + const acsSerialRef = useRef(null); + + const [agvConnected, setAgvConnected] = useState(false); + const [bmsConnected, setBmsConnected] = useState(false); + const [acsConnected, setAcsConnected] = useState(false); + + const [agvPortInfo, setAgvPortInfo] = useState(null); + const [bmsPortInfo, setBmsPortInfo] = useState(null); + const [acsPortInfo, setAcsPortInfo] = useState(null); + + // Serial Configuration + const [agvBaudRate, setAgvBaudRate] = useState(57600); + const [bmsBaudRate, setBmsBaudRate] = useState(9600); + const [acsBaudRate, setAcsBaudRate] = useState(9600); + + // --- Helpers to Set/Clear Bits --- + const setAgvBit = (field: 'system0' | 'system1' | 'errorFlags' | 'signalFlags', bit: number, value: boolean) => { + setAgvState(prev => { + let current = prev[field]; + if (value) current |= (1 << bit); + else current &= ~(1 << bit); + + if (current === prev[field]) return prev; + return { ...prev, [field]: current }; + }); + }; + + // --- AGV Protocol Parser --- + const agvBufferRef = useRef([]); + + // Format: [STX]ACK[CMD]**[ETX] + const sendAck = useCallback((commandToAck: string) => { + if (agvSerialRef.current) { + const encoder = new TextEncoder(); + const cmdBytes = encoder.encode("ACK"); + const valBytes = encoder.encode(commandToAck); + + const payload = new Uint8Array(1 + cmdBytes.length + valBytes.length + 2 + 1); + payload[0] = 0x02; // STX + payload.set(cmdBytes, 1); + payload.set(valBytes, 1 + cmdBytes.length); + payload[payload.length - 3] = 0x2A; // '*' + payload[payload.length - 2] = 0x2A; // '*' + payload[payload.length - 1] = 0x03; // ETX + + agvSerialRef.current.send(payload); + addLog('AGV', 'TX', `ACK ${commandToAck}`); + } + }, [addLog]); + + // Tag Transmission Logic + const sendTag = useCallback((tagId: string) => { + if (agvSerialRef.current) { + const paddedId = tagId.padStart(6, '0').slice(-6); + const encoder = new TextEncoder(); + const cmdBytes = encoder.encode("TAG"); + const valBytes = encoder.encode(paddedId); + + const payload = new Uint8Array(1 + cmdBytes.length + valBytes.length + 2 + 1); + let offset = 0; + payload[offset++] = 0x02; // STX + payload.set(cmdBytes, offset); offset += cmdBytes.length; + payload.set(valBytes, offset); offset += valBytes.length; + payload[offset++] = 0x2A; // '*' + payload[offset++] = 0x2A; // '*' + payload[offset++] = 0x03; // ETX + + agvSerialRef.current.send(payload); + addLog('AGV', 'TX', `TAG ${paddedId}`); + } + }, [addLog]); + + const handleAgvData = useCallback((data: Uint8Array) => { + for(let i=0; i 0) { + buf.splice(0, stxIdx); + } + + const etxIdx = buf.indexOf(0x03); + if (etxIdx === -1) { + return; + } + + const frameBytes = buf.splice(0, etxIdx + 1); + processAgvFrame(frameBytes); + } + }, []); + + const processAgvFrame = (frame: number[]) => { + if (frame.length < 7) return; + + const textDecoder = new TextDecoder(); + const cmd = textDecoder.decode(new Uint8Array(frame.slice(1, 4))); + const dataStr = textDecoder.decode(new Uint8Array(frame.slice(4, frame.length - 3))).trim(); + + addLog('AGV', 'RX', `${cmd} ${dataStr}`); + + switch (cmd) { + case 'CRN': // Run Command (Auto Run) + if (dataStr.length >= 4) { + const dirChar = dataStr[0].toUpperCase(); + const bunki = dataStr[1]; + const speed = dataStr[2]; + const sensor = dataStr[3]; + + const isBack = dirChar === 'B' || dirChar === 'R'; + const direction = isBack ? 'BWD' : 'FWD'; + + setAgvState(s => { + const newBranch = (bunki === 'L' ? 'LEFT' : bunki === 'R' ? 'RIGHT' : bunki === 'S' ? 'STRAIGHT' : s.runConfig.branch); + const newSpeed = (['L','M','H'].includes(speed) ? speed : s.runConfig.speedLevel) as any; + + // Treat '0' as placeholder for sensor if we want to preserve CBR logic, + // BUT allow manual toggle via UI to override later. + // Here, we assume CRN sending '0' shouldn't turn off sensor if user set it via CBR. + const newSensor = (sensor === '0') ? s.sensorStatus : sensor; + const newLidar = newSensor !== '0'; + + return { + ...s, + sensorStatus: newSensor, + lidarEnabled: newLidar, + motionState: AgvMotionState.RUNNING, + runConfig: { + direction: direction, + branch: newBranch, + speedLevel: newSpeed + } + }; + }); + addLog('AGV', 'INFO', `Run: Dir=${direction} Branch=${bunki} Speed=${speed} Sensor=${sensor}`); + } else { + setAgvState(s => ({ ...s, motionState: AgvMotionState.RUNNING })); + } + // Update Flags + setAgvBit('system1', SystemFlag1.agv_stop, false); + setAgvBit('system1', SystemFlag1.agv_run, true); + break; + + case 'CST': // Stop Command + if (dataStr.length > 0 && dataStr.charAt(0).toUpperCase() === 'M') { + // Mark Stop Command (Drive to next mark) + setAgvState(s => ({ + ...s, + motionState: AgvMotionState.MARK_STOPPING, + runConfig: { + ...s.runConfig, + speedLevel: 'L' // Force low speed for precision + } + })); + // In Mark Stopping mode, AGV is still technically running until it hits the mark + setAgvBit('system1', SystemFlag1.agv_stop, false); + setAgvBit('system1', SystemFlag1.agv_run, true); + addLog('AGV', 'INFO', 'CMD: Mark Stop Mode Started'); + } else { + // Normal Stop + setAgvBit('system1', SystemFlag1.agv_run, false); + setAgvBit('system1', SystemFlag1.agv_stop, true); + setAgvState(s => ({ ...s, motionState: AgvMotionState.IDLE })); + } + break; + + case 'CBR': // Branch + if (dataStr.length >= 3) { + const dirChar = dataStr[0].toUpperCase(); + const bunki = dataStr[1]; + const speed = dataStr[2]; + // Default to '1' (ON) if not provided, assuming CBR implies a valid path + const sensor = dataStr.length > 3 ? dataStr[3] : '1'; + + const isBack = dirChar === 'B' || dirChar === 'R'; + const direction = isBack ? 'BWD' : 'FWD'; + + setAgvState(s => { + // Logic: Update if specific char provided, else keep existing + const newBranch = (bunki === 'L' ? 'LEFT' : bunki === 'R' ? 'RIGHT' : bunki === 'S' ? 'STRAIGHT' : s.runConfig.branch); + const newSpeed = (['L','M','H'].includes(speed) ? speed : s.runConfig.speedLevel) as any; + + const newLidar = sensor !== '0'; + + return { + ...s, + sensorStatus: sensor, + lidarEnabled: newLidar, + runConfig: { + direction: direction, + branch: newBranch, + speedLevel: newSpeed + } + }; + }); + addLog('AGV', 'INFO', `Branch Set: Dir=${direction} Branch=${bunki} Speed=${speed} Sensor=${sensor}`); + } + break; + + case 'CRT': // Manual Control (Remote / Jog) + const op = dataStr.charAt(0).toUpperCase(); + let manualMotion = AgvMotionState.IDLE; + let targetDir: 'FWD' | 'BWD' = 'FWD'; + + if (op === 'F') { + manualMotion = AgvMotionState.FORWARD; + targetDir = 'FWD'; + } + else if (op === 'B') { + manualMotion = AgvMotionState.BACKWARD; + targetDir = 'BWD'; + } + else if (op === 'L') manualMotion = AgvMotionState.TURN_LEFT; + else if (op === 'R') manualMotion = AgvMotionState.TURN_RIGHT; + else if (op === 'S') manualMotion = AgvMotionState.IDLE; + + setAgvState(s => ({ + ...s, + motionState: manualMotion, + runConfig: { + ...s.runConfig, + direction: targetDir + } + })); + + const isMoving = manualMotion !== AgvMotionState.IDLE; + setAgvBit('system1', SystemFlag1.agv_stop, !isMoving); + setAgvBit('system1', SystemFlag1.agv_run, isMoving); + + sendAck(cmd); + break; + + case 'SFR': // Reset Errors + setAgvState(s => ({ ...s, error: null, errorFlags: 0 })); + setAgvBit('system1', SystemFlag1.agv_run, false); + setAgvBit('system1', SystemFlag1.agv_stop, true); + break; + + case 'CBT': // Charging Command + if (dataStr.length > 4) { + const chargeCmd = dataStr[4]; + const isCharging = chargeCmd === 'I'; + setAgvBit('system1', SystemFlag1.Battery_charging, isCharging); + } + break; + + case 'CLF': // Lift / Magnet Control + const subCmd = dataStr.trim().toUpperCase(); + if (subCmd === 'UP0000') { + setAgvState(s => ({ ...s, liftStatus: 'UP' })); + } else if (subCmd === 'DN0000') { + setAgvState(s => ({ ...s, liftStatus: 'DOWN' })); + } else if (subCmd === 'STP000') { + setAgvState(s => ({ ...s, liftStatus: 'IDLE' })); + } else if (subCmd === 'ON0000') { + setAgvState(s => ({ ...s, magnetOn: true })); + } else if (subCmd === 'OFF000') { + setAgvState(s => ({ ...s, magnetOn: false })); + } + sendAck(cmd); + break; + + case 'CTL': // Turn Left 180 + handleTurn180('LEFT'); + sendAck(cmd); + break; + + case 'CTR': // Turn Right 180 + handleTurn180('RIGHT'); + sendAck(cmd); + break; + + case 'SSH': case 'SSM': case 'SSL': case 'SSS': case 'SHS': case 'SLS': case 'SPK': case 'SPM': + case 'SPL': case 'SPS': case 'SIK': case 'SIM': case 'SIH': case 'SIL': case 'SIS': case 'SDK': + case 'SDW': case 'SDL': case 'SDS': case 'SRS': case 'SCK': case 'SSK': case 'STT': case 'SSI': + case 'SMD': case 'SSC': case 'SPN': case 'SPH': case 'SCH': case 'SDH': case 'SDM': case 'SLB': + case 'SGS': + sendAck(cmd); + break; + + case 'ACK': + break; + } + }; + + const handleTurn180 = (direction: 'LEFT' | 'RIGHT') => { + setAgvState(s => ({ + ...s, + motionState: direction === 'LEFT' ? AgvMotionState.TURN_LEFT_180 : AgvMotionState.TURN_RIGHT_180, + targetRotation: direction === 'LEFT' ? s.rotation - 180 : s.rotation + 180 + })); + }; + + const autoSendData = useCallback(() => { + if (!agvConnected || !agvSerialRef.current || !agvSerialRef.current.connected) return; + // Use ref to get latest state without recreating function + const s = agvStateRef.current; + const barr = new Uint8Array(34); + barr[0] = 0x02; barr[1] = 0x53; barr[2] = 0x54; barr[3] = 0x53; + + const encodeHex = (val: number, len: number) => { + const hex = val.toString(16).toUpperCase().padStart(len, '0'); + return new TextEncoder().encode(hex); + }; + + const voltBytes = new TextEncoder().encode("255"); + barr.set(voltBytes, 4); + barr.set(encodeHex(s.system0, 4), 7); + barr.set(encodeHex(s.system1, 4), 11); + barr.set(encodeHex(s.errorFlags, 4), 15); + + const spdChar = s.runConfig.speedLevel.charCodeAt(0); + barr[19] = spdChar; + + const branchChar = s.runConfig.branch === 'LEFT' ? 'L'.charCodeAt(0) : s.runConfig.branch === 'RIGHT' ? 'R'.charCodeAt(0) : 'S'.charCodeAt(0); + barr[20] = branchChar; + + const dirChar = s.runConfig.direction === 'FWD' ? 'F'.charCodeAt(0) : 'B'.charCodeAt(0); + barr[21] = dirChar; + + barr[22] = s.sensorStatus.charCodeAt(0); + barr.set(encodeHex(s.signalFlags, 2), 23); + + barr[31] = '*'.charCodeAt(0); barr[32] = '*'.charCodeAt(0); barr[33] = 0x03; + + agvSerialRef.current.send(barr); + + // Log Transmission (Throttled log could be nice, but simple for now) + addLog('AGV', 'TX', `STS (AutoSend)`); + }, [agvConnected, addLog]); + + useEffect(() => { + if (agvConnected) { + const timer = setInterval(() => { + autoSendData(); + }, 500); + return () => clearInterval(timer); + } + }, [autoSendData, agvConnected]); + + // --- Main Logic: Sensors, Signals & Lift Physics --- + useEffect(() => { + // 1. Calculate Signals + let sig = 0; + if (agvState.sensorMark) sig |= (1 << AgvSignal.mark_sensor_1); + + // Front Line Sensor & Gate Out + if (!agvState.sensorLineFront) { + // Gate Out: Active when line is NOT detected + sig |= (1 << AgvSignal.front_gate_out); + } + + // Rear Sensor Gate Out + if (!agvState.sensorLineRear) { + // Gate Out: Active when line is NOT detected + sig |= (1 << AgvSignal.rear_sensor_out); + } + + // Lift Sensors + if (agvState.liftHeight <= 0) sig |= (1 << AgvSignal.lift_down_sensor); + if (agvState.liftHeight >= 100) sig |= (1 << AgvSignal.lift_up_sensor); + + // Magnet Relay + if (agvState.magnetOn) sig |= (1 << AgvSignal.magnet_relay); + + // 2. Calculate Error Flags + let err = agvState.errorFlags; + if (agvState.error === 'LINE_OUT') err |= (1 << AgvError.line_out_error); + else err &= ~(1 << AgvError.line_out_error); + + // 3. Calculate System Flags + let sys1 = agvState.system1; + const isAutoRun = agvState.motionState === AgvMotionState.RUNNING || agvState.motionState === AgvMotionState.MARK_STOPPING; + const isManualRun = [AgvMotionState.FORWARD, AgvMotionState.BACKWARD, AgvMotionState.TURN_LEFT, AgvMotionState.TURN_RIGHT, AgvMotionState.TURN_LEFT_180, AgvMotionState.TURN_RIGHT_180].includes(agvState.motionState); + + if (isAutoRun) { + sys1 |= (1 << SystemFlag1.agv_run); + sys1 &= ~(1 << SystemFlag1.agv_stop); + } else if (isManualRun) { + // Manual move: Ensure stop bit is OFF. Run bit can be ON to indicate active state (matches CRT logic) + sys1 |= (1 << SystemFlag1.agv_run); + sys1 &= ~(1 << SystemFlag1.agv_stop); + } else if (agvState.motionState === AgvMotionState.IDLE) { + sys1 &= ~(1 << SystemFlag1.agv_run); + sys1 |= (1 << SystemFlag1.agv_stop); + } + + if (sig !== agvState.signalFlags || err !== agvState.errorFlags || sys1 !== agvState.system1) { + setAgvState(s => ({ ...s, signalFlags: sig, errorFlags: err, system1: sys1 })); + } + }, [ + agvState.sensorMark, agvState.sensorLineFront, agvState.sensorLineRear, agvState.error, + agvState.motionState, agvState.signalFlags, agvState.errorFlags, agvState.system1, + agvState.detectedRfid, agvState.liftHeight, agvState.magnetOn + ]); + + // --- Lift Animation Physics Loop --- + useEffect(() => { + let animId: number; + const animateLift = () => { + setAgvState(prev => { + if (prev.liftStatus === 'IDLE') return prev; + + let newHeight = prev.liftHeight; + const step = 0.6; // Speed of lift (~3s for 0-100) + + if (prev.liftStatus === 'UP') { + newHeight += step; + if (newHeight >= 100) { + newHeight = 100; + return { ...prev, liftHeight: 100, liftStatus: 'IDLE' }; + } + } else if (prev.liftStatus === 'DOWN') { + newHeight -= step; + if (newHeight <= 0) { + newHeight = 0; + return { ...prev, liftHeight: 0, liftStatus: 'IDLE' }; + } + } + + return { ...prev, liftHeight: newHeight }; + }); + animId = requestAnimationFrame(animateLift); + }; + + animId = requestAnimationFrame(animateLift); + return () => cancelAnimationFrame(animId); + }, []); // Logic relies on functional state updates, so empty dependency is safe-ish, but let's be cleaner. + // Actually, requestAnimationFrame runs continuously. We just need to ensure we don't leak. + + + // --- BMS Protocol Logic --- + const bmsBufferRef = useRef([]); + + const handleBmsData = useCallback((data: Uint8Array) => { + // Log raw data for debugging visibility + const hex = Array.from(data).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '); + addLog('BMS', 'RX', `RAW: ${hex}`); + + for (let i = 0; i < data.length; i++) { + bmsBufferRef.current.push(data[i]); + } + + const buf = bmsBufferRef.current; + // Protocol: 7 bytes total. Start=0xDD, End=0x77. + while (buf.length >= 7) { + const startIdx = buf.indexOf(0xDD); + if (startIdx === -1) { + bmsBufferRef.current = []; + return; + } + if (startIdx > 0) { + buf.splice(0, startIdx); + } + + // Wait for end byte (not fully safe if 77 is inside payload, but standard for this packet size) + // Actually, fixed length is 34 for Status, 23 for Cells. + // But we receive potentially fragmented. + // Let's rely on fixed length per known command if we had one. + // Here we just look for 0x77. + const endIdx = buf.indexOf(0x77); + if (endIdx === -1) return; + + const packet = buf.splice(0, endIdx + 1); + processBmsPacket(packet); + } + }, [addLog]); + + const processBmsPacket = (packet: number[]) => { + const cmd = packet[2]; + // const hexStr = packet.map(b => b.toString(16).padStart(2,'0').toUpperCase()).join(' '); + + if (cmd === 0x03) { + sendBmsStatus(); + } else if (cmd === 0x04) { + sendBmsCellVoltage(); + } + }; + + const sendBmsStatus = () => { + if (!bmsSerialRef.current) return; + const s = agvStateRef.current; // Use Ref for latest state + const resp = new Uint8Array(34); + resp[0] = 0xDD; resp[1] = 0x03; resp[2] = 0x00; resp[3] = 0x1b; //arrary 3value 0->1b + + const totalV = Math.floor(s.cellVoltages.reduce((a,b)=>a+b, 0) * 100); + resp[4] = (totalV >> 8) & 0xFF; resp[5] = totalV & 0xFF; + + const remainAh = Math.floor(s.maxCapacity * (s.batteryLevel / 100)); + resp[8] = (remainAh >> 8) & 0xFF; resp[9] = remainAh & 0xFF; + const maxAh = s.maxCapacity; + resp[10] = (maxAh >> 8) & 0xFF; resp[11] = maxAh & 0xFF; + + resp[23] = Math.floor(s.batteryLevel); + + const t1 = Math.floor((s.batteryTemp * 10) + 2731); + const t2 = Math.floor((s.batteryTemp * 10) + 2731); + + resp[27] = (t1 >> 8) & 0xFF; resp[28] = t1 & 0xFF; + resp[29] = (t2 >> 8) & 0xFF; resp[30] = t2 & 0xFF; + + resp[31] = 0x00; //*magic numbee(skip) + resp[32] = 0x00; //*magic numbee(skip) + resp[33] = 0x77; + + bmsSerialRef.current.send(resp); + addLog('BMS', 'TX', 'DD ... 77 (Status)'); + }; + + const sendBmsCellVoltage = () => { + if (!bmsSerialRef.current) return; + const s = agvStateRef.current; // Use Ref for latest state + const resp = new Uint8Array(23); + resp[0] = 0xDD; resp[1] = 0x04; resp[2] = 0x00; resp[3] = 0x10; + let checksumSum = resp[3]; + + for (let i = 0; i < 8; i++) { + const v = Math.floor((s.cellVoltages[i] || 0) * 1000); + const idx = 4 + (i * 2); + const high = (v >> 8) & 0xFF; + const low = v & 0xFF; + resp[idx] = high; resp[idx + 1] = low; + checksumSum += high + low; + } + + const checksum = (0xFFFF - checksumSum + 1) & 0xFFFF; + resp[20] = (checksum >> 8) & 0xFF; resp[21] = checksum & 0xFF; + resp[22] = 0x77; + + bmsSerialRef.current.send(resp); + addLog('BMS', 'TX', 'DD ... 77 (Cells)'); + }; + + // --- ACS Protocol Logic (Implementing C# Logic) --- + const acsBufferRef = useRef([]); + const acsParseErrorsRef = useRef(0); + + const sendAcsPacket = useCallback((cmd: number, payload: number[] = []) => { + if (!acsSerialRef.current) { + addLog('ACS', 'ERROR', 'Not Connected'); + return; + } + + const id = 1; // Default AGV ID + const len = 1 + 1 + payload.length; // ID + CMD + DATA + const buffer = [0x02, len, id, cmd, ...payload]; + + // Calculate CRC over [LEN, ID, CMD, DATA] (indices 1 to end) + const crcData = buffer.slice(1); + const crc = calculateAcsCrc16(crcData); + + buffer.push(crc & 0xFF); // Low Byte + buffer.push((crc >> 8) & 0xFF); // High Byte + buffer.push(0x03); // ETX + + acsSerialRef.current.send(new Uint8Array(buffer)); + + const hex = buffer.map(b => b.toString(16).padStart(2,'0').toUpperCase()).join(' '); + addLog('ACS', 'TX', `CMD:${cmd.toString(16).toUpperCase()} [${hex}]`); + }, [addLog]); + + const handleAcsData = useCallback((data: Uint8Array) => { + for (let i = 0; i < data.length; i++) { + acsBufferRef.current.push(data[i]); + } + + const buf = acsBufferRef.current; + while (buf.length > 0) { + // 1. Find STX + const stxIndex = buf.indexOf(0x02); // STX + if (stxIndex === -1) { + buf.length = 0; // Clear junk + return; + } + // Remove garbage before STX + if (stxIndex > 0) { + buf.splice(0, stxIndex); + } + + // 2. Check minimal size (STX + Len + ID + Cmd + CRC + ETX) = 7 bytes if data is 0? + // Per C#: Length = 1(ID)+1(Cmd)+DataLen. + // Packet: STX(1) + Len(1) + Payload(Len) + CRC(2) + ETX(1) = 5 + Len. + if (buf.length < 2) return; // Need Length byte + + const lengthByte = buf[1]; + const totalPacketSize = lengthByte + 5; + + if (buf.length < totalPacketSize) return; // Wait for more data + + // 3. Extract and Verify + const packetData = buf.slice(0, totalPacketSize); + + // Verify ETX + if (packetData[totalPacketSize - 1] !== 0x03) { + addLog('ACS', 'ERROR', 'ETX Missing'); + buf.shift(); // Invalid packet, shift 1 and retry + continue; + } + + // Verify CRC + // CRC covers: [Length, ID, Command, Data...] (Skip STX, Exclude CRC bytes) + // Bytes to CRC: From index 1 to (1 + length) inclusive? + // C# Logic: CalculateCRC16(packetData.Skip(1).Take(length + 1)) + // packetData indices: 0=STX, 1=Len, 2=ID, 3=Cmd... + // We need indices 1 to (1 + length) inclusive. + // Total size is length + 5. + // CRC is at [total - 3, total - 2]. + + const dataForCrc = packetData.slice(1, lengthByte + 2); // slice end is exclusive + const calcCrc = calculateAcsCrc16(dataForCrc); + + const receivedCrc = (packetData[totalPacketSize - 2] << 8) | packetData[totalPacketSize - 3]; // Little Endian in C# BitConverter? + // C# BitConverter.ToUInt16(rawData, rawData.Length - 3). + // rawData ends with [CRC_L, CRC_H, ETX] usually? + // Default Little Endian. So index len-3 is Low, len-2 is High. + + if (receivedCrc !== 0xFFFF && calcCrc !== receivedCrc) { + const hex = packetData.map(b => b.toString(16).padStart(2,'0').toUpperCase()).join(' '); + addLog('ACS', 'ERROR', `CRC Fail: ${hex}`); + acsParseErrorsRef.current++; + if (acsParseErrorsRef.current > 3) buf.length = 0; // Reset on too many errors + else buf.shift(); + continue; + } + + acsParseErrorsRef.current = 0; + + // Parse Success + const id = packetData[2]; + const cmd = packetData[3]; + const payload = packetData.slice(4, totalPacketSize - 3); + + const hexStr = packetData.map(b => b.toString(16).padStart(2,'0').toUpperCase()).join(' '); + addLog('ACS', 'RX', `[ID:${id} CMD:${cmd}] ${hexStr}`); + + // Consume buffer + buf.splice(0, totalPacketSize); + } + }, [addLog]); + + // --- Connect/Disconnect Handlers --- + + const connectAgv = useCallback(async () => { + if (!agvSerialRef.current) { + agvSerialRef.current = new SerialPortHandler(handleAgvData); + } + try { + await agvSerialRef.current.connect(agvBaudRate); + setAgvConnected(true); + setAgvPortInfo(agvSerialRef.current.getPortInfo()); + addLog('AGV', 'INFO', `Connected at ${agvBaudRate} baud`); + } catch (err: any) { + if (err.message !== 'USER_CANCELLED') { + console.error(err); + addLog('AGV', 'ERROR', `Connection failed: ${err.message}`); + } + } + }, [agvBaudRate, handleAgvData, addLog]); + + const disconnectAgv = useCallback(async () => { + if (agvSerialRef.current) { + await agvSerialRef.current.disconnect(); + setAgvConnected(false); + setAgvPortInfo(null); + addLog('AGV', 'INFO', 'Disconnected'); + } + }, [addLog]); + + const connectBms = useCallback(async () => { + if (!bmsSerialRef.current) { + bmsSerialRef.current = new SerialPortHandler(handleBmsData); + } + try { + await bmsSerialRef.current.connect(bmsBaudRate); + setBmsConnected(true); + setBmsPortInfo(bmsSerialRef.current.getPortInfo()); + addLog('BMS', 'INFO', `Connected at ${bmsBaudRate} baud`); + } catch (err: any) { + if (err.message !== 'USER_CANCELLED') { + console.error(err); + addLog('BMS', 'ERROR', `Connection failed: ${err.message}`); + } + } + }, [bmsBaudRate, handleBmsData, addLog]); + + const disconnectBms = useCallback(async () => { + if (bmsSerialRef.current) { + await bmsSerialRef.current.disconnect(); + setBmsConnected(false); + setBmsPortInfo(null); + addLog('BMS', 'INFO', 'Disconnected'); + } + }, [addLog]); + + const connectAcs = useCallback(async () => { + if (!acsSerialRef.current) { + acsSerialRef.current = new SerialPortHandler(handleAcsData); + } + try { + await acsSerialRef.current.connect(acsBaudRate); + setAcsConnected(true); + setAcsPortInfo(acsSerialRef.current.getPortInfo()); + addLog('ACS', 'INFO', `Connected at ${acsBaudRate} baud`); + } catch (err: any) { + if (err.message !== 'USER_CANCELLED') { + console.error(err); + addLog('ACS', 'ERROR', `Connection failed: ${err.message}`); + } + } + }, [acsBaudRate, handleAcsData, addLog]); + + const disconnectAcs = useCallback(async () => { + if (acsSerialRef.current) { + await acsSerialRef.current.disconnect(); + setAcsConnected(false); + setAcsPortInfo(null); + addLog('ACS', 'INFO', 'Disconnected'); + } + }, [addLog]); + + // --- Map Loading Logic (Updated for New Structure) --- + const loadMapFromCSharp = useCallback((csharpData: CSharpMapData) => { + const newNodes: MapNode[] = []; + const newEdges: MapEdge[] = []; + const edgeSet = new Set(); + + // 1. Process Nodes + if (csharpData.Nodes && Array.isArray(csharpData.Nodes)) { + csharpData.Nodes.forEach((n) => { + const [xStr, yStr] = n.Position.split(',').map(s => s.trim()); + const x = parseFloat(xStr) || 0; + const y = parseFloat(yStr) || 0; + + // Map StationType to Internal NodeType + let internalType = n.StationType; + if (n.StationType === 2) internalType = NodeType.Charging; + else if (n.StationType === 3) internalType = NodeType.UnLoader; + + newNodes.push({ + id: n.Id, + x, y, + type: internalType, + name: n.Text || n.AliasName || "", + rfidId: n.RfidId, + connectedNodes: n.ConnectedNodes || [], + foreColor: n.NodeTextForeColor, + fontSize: n.NodeTextFontSize + }); + + if (n.ConnectedNodes && Array.isArray(n.ConnectedNodes)) { + n.ConnectedNodes.forEach((targetId) => { + const ids = [n.Id, targetId].sort(); + const key = ids.join('-'); + if (!edgeSet.has(key)) { + edgeSet.add(key); + newEdges.push({ + id: `edge_${key}`, + from: ids[0], + to: ids[1] + }); + } + }); + } + }); + } + + // 2. Process Labels (Convert to Nodes Type=Label) + if (csharpData.Labels && Array.isArray(csharpData.Labels)) { + csharpData.Labels.forEach(l => { + const [xStr, yStr] = l.Position.split(',').map(s => s.trim()); + newNodes.push({ + id: l.Id, + x: parseFloat(xStr)||0, y: parseFloat(yStr)||0, + type: NodeType.Label, + name: "", + rfidId: "", + connectedNodes: [], + labelText: l.Text, + foreColor: l.ForeColor, + backColor: l.BackColor, + fontSize: l.FontSize + }); + }); + } + + // 3. Process Images (Convert to Nodes Type=Image) + if (csharpData.Images && Array.isArray(csharpData.Images)) { + csharpData.Images.forEach(img => { + const [xStr, yStr] = img.Position.split(',').map(s => s.trim()); + newNodes.push({ + id: img.Id, + x: parseFloat(xStr)||0, y: parseFloat(yStr)||0, + type: NodeType.Image, + name: img.Name, + rfidId: "", + connectedNodes: [], + imageBase64: img.ImageBase64 + }); + }); + } + + // 4. Process Magnets + const newMagnets: MagnetLine[] = []; + if (csharpData.Magnets && Array.isArray(csharpData.Magnets)) { + csharpData.Magnets.forEach(m => { + const type = m.ControlPoint ? 'CURVE' : 'STRAIGHT'; + newMagnets.push({ + id: m.Id, + type: type, + p1: { x: m.P1.X, y: m.P1.Y }, + p2: { x: m.P2.X, y: m.P2.Y }, + controlPoint: m.ControlPoint ? { x: m.ControlPoint.X, y: m.ControlPoint.Y } : undefined + }); + }); + } + + // 5. Process Marks + const newMarks: FloorMark[] = []; + if (csharpData.Marks && Array.isArray(csharpData.Marks)) { + csharpData.Marks.forEach(mk => { + newMarks.push({ + id: mk.Id, + x: mk.X, + y: mk.Y, + rotation: mk.Rotation + }); + }); + } + + setMapData({ + nodes: newNodes, + edges: newEdges, + marks: newMarks, + magnets: newMagnets + }); + addLog('SYSTEM', 'INFO', `New Map loaded: ${newNodes.length} objects`); + + }, [addLog]); + + // --- Initialize Map from NewMap.json on Mount --- + useEffect(() => { + fetch('NewMap.json') + .then(response => { + if (!response.ok) throw new Error("Default map not found"); + return response.json(); + }) + .then((data: CSharpMapData) => { + loadMapFromCSharp(data); + }) + .catch(err => { + console.log("Could not load default map:", err); + }); + }, [loadMapFromCSharp]); + + // --- Map IO Logic --- + const processMapFile = (file: File) => { + const reader = new FileReader(); + reader.onload = (evt) => { + try { + const jsonStr = evt.target?.result as string; + const csharpData = JSON.parse(jsonStr) as CSharpMapData; + loadMapFromCSharp(csharpData); + } catch (err) { + console.error(err); + alert('Failed to parse map file.'); + } + }; + reader.readAsText(file); + }; + + const saveMap = () => { + const outNodes: any[] = []; + const outLabels: any[] = []; + const outImages: any[] = []; + + mapData.nodes.forEach(n => { + const pos = `${Math.round(n.x)}, ${Math.round(n.y)}`; + + if (n.type === NodeType.Label) { + outLabels.push({ + Id: n.id, + Type: 1, + Text: n.labelText || "Label", + Position: pos, + ForeColor: n.foreColor || "White", + BackColor: n.backColor || "Transparent", + FontFamily: "Arial", + FontSize: n.fontSize || 12, + FontStyle: 0, + Padding: 5 + }); + } else if (n.type === NodeType.Image) { + outImages.push({ + Id: n.id, + Type: 2, + Name: n.name || "Image", + Position: pos, + ImagePath: "", + ImageBase64: n.imageBase64 || "", + Scale: "1, 1", + Opacity: 1.0, + Rotation: 0.0 + }); + } else { + // Standard Node + // Map Internal NodeType back to StationType + let stType = n.type; + if (n.type === NodeType.Charging) stType = 2; // Cleaner + else if (n.type === NodeType.UnLoader) stType = 3; // Unloader + + // Re-calculate connected nodes + const connected = new Set(); + mapData.edges.forEach(e => { + if (e.from === n.id) connected.add(e.to); + if (e.to === n.id) connected.add(e.from); + }); + + outNodes.push({ + Id: n.id, + Text: n.name || "", + Position: pos, + Type: 0, // Always 0 for nodes + StationType: stType, + ConnectedNodes: Array.from(connected), + RfidId: n.rfidId || "", + NodeTextForeColor: n.foreColor, + NodeTextFontSize: n.fontSize || 7.0, + AliasName: "", + SpeedLimit: 0, + CanDocking: [1,2,3,4,5].includes(n.type), + DockDirection: n.type === NodeType.Charging || n.type === NodeType.ChargerStation ? 1 : 0, + CanTurnLeft: true, + CanTurnRight: true, + DisableCross: false, + IsActive: true + }); + } + }); + + const outMagnets = mapData.magnets.map(m => ({ + Id: m.id, + Type: 4, + P1: { X: m.p1.x, Y: m.p1.y }, + P2: { X: m.p2.x, Y: m.p2.y }, + ControlPoint: m.controlPoint ? { X: m.controlPoint.x, Y: m.controlPoint.y } : null + })); + + const outMarks = mapData.marks.map(mk => ({ + Id: mk.id, + Type: 3, + Position: `${Math.round(mk.x)}, ${Math.round(mk.y)}`, + X: mk.x, + Y: mk.y, + Rotation: mk.rotation + })); + + const exportData = { + Nodes: outNodes, + Labels: outLabels, + Images: outImages, + Magnets: outMagnets, + Marks: outMarks, + Settings: { + BackgroundColorArgb: -14671840, + ShowGrid: false + }, + CreatedDate: new Date().toISOString(), + Version: "1.3" + }; + + const json = JSON.stringify(exportData, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'NewMap.json'; + a.click(); + }; + + const loadMap = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'application/json'; + input.onchange = (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { + processMapFile(file); + } + }; + input.click(); + }; + + // --- Drag and Drop Handlers --- + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + const file = e.dataTransfer.files[0]; + if (file && file.name.endsWith('.json')) { + processMapFile(file); + } + }; + + // --- Real-time updates to Serial (AGV Events) --- + const prevSensors = useRef({ rfid: null as string|null, mark: false, line: false }); + useEffect(() => { + if (!agvConnected || !agvSerialRef.current) return; + + // Check RFID change + if (agvState.detectedRfid !== prevSensors.current.rfid) { + if (agvState.detectedRfid) { + sendTag(agvState.detectedRfid); + } + } + + prevSensors.current = { + rfid: agvState.detectedRfid, + mark: agvState.sensorMark, + line: agvState.sensorLineFront + }; + }, [agvState, agvConnected, sendTag]); + + return ( +
+ {/* 1. Far Left: Toolbar */} +
+ +
+ + {/* 2. Inner Left: Protocol Flags (Separated Sidebar) - Removed BMS from here */} +
+
+ +
+ + + {/* Middle 2: ACS Panel (Fixed) */} +
+ +
+ + +
+ + {/* 3. Center: Canvas & Bottom Panels */} +
+
+ addLog('SYSTEM', 'INFO', msg)} + /> +
+ + {/* Fixed Communication Panels (Now inside center column) */} +
+
+ l.source === 'AGV')} + isConnected={agvConnected} + onConnect={connectAgv} + onDisconnect={disconnectAgv} + onClear={() => clearLogs('AGV')} + baudRate={agvBaudRate} + setBaudRate={setAgvBaudRate} + portInfo={agvPortInfo} + colorClass="text-blue-400" + /> +
+
+ l.source === 'BMS')} + isConnected={bmsConnected} + onConnect={connectBms} + onDisconnect={disconnectBms} + onClear={() => clearLogs('BMS')} + baudRate={bmsBaudRate} + setBaudRate={setBmsBaudRate} + portInfo={bmsPortInfo} + colorClass="text-yellow-400" + /> +
+
+ l.source === 'ACS')} + isConnected={acsConnected} + onConnect={connectAcs} + onDisconnect={disconnectAcs} + onClear={() => clearLogs('ACS')} + baudRate={acsBaudRate} + setBaudRate={setAcsBaudRate} + portInfo={acsPortInfo} + colorClass="text-green-400" + /> +
+
+
+ + {/* 4. Right: Controls & BMS & ACS */} +
+ + {/* Top: Controls (Scrollable if needed) */} +
+ setAgvState(s => ({...s, motionState: m}))} + setLift={(h) => setAgvState(s => ({...s, liftHeight: h}))} + setRunConfig={(c) => setAgvState(s => ({...s, runConfig: c}))} + setError={(e) => setAgvState(s => ({...s, error: e}))} + onTurn180={handleTurn180} + setMagnet={(isOn) => setAgvState(s => ({...s, magnetOn: isOn}))} + setLiftStatus={(status) => setAgvState(s => ({...s, liftStatus: status}))} + setLidar={(isOn) => setAgvState(s => ({...s, lidarEnabled: isOn, sensorStatus: isOn ? '1' : '0'}))} + /> +
+ + {/* Middle 1: BMS Panel (Fixed) */} +
+ setAgvState(s => ({...s, batteryLevel: l}))} /> +
+ + {/* Bottom: System Logs (Fixed Height) */} +
+ l.source === 'SYSTEM')} onClear={() => clearLogs('SYSTEM')} /> +
+
+
+ ); +}; + +export default App; \ No newline at end of file diff --git a/NewMap.json b/NewMap.json new file mode 100644 index 0000000..2e25694 --- /dev/null +++ b/NewMap.json @@ -0,0 +1,1219 @@ +{ + "Nodes": [ + { + "Id": "N001", + "Text": "Unloader", + "Position": "99, 251", + "Type": 0, + "StationType": 3, + "ConnectedNodes": [ + "N002" + ], + "RfidId": "0001", + "NodeTextForeColor": "White", + "NodeTextFontSize": 30, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N002", + "Text": "", + "Position": "249, 250", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N001", + "N003" + ], + "RfidId": "0002", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N003", + "Text": "", + "Position": "350, 301", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N002", + "N011", + "N022", + "N031" + ], + "RfidId": "0003", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N006", + "Text": "", + "Position": "527, 254", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N007", + "N022", + "N023" + ], + "RfidId": "0013", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N007", + "Text": "", + "Position": "609, 227", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N006", + "N019" + ], + "RfidId": "0014", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N008", + "Text": "", + "Position": "275, 441", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N009", + "N031" + ], + "RfidId": "0009", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N009", + "Text": "", + "Position": "184, 466", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N008", + "N010" + ], + "RfidId": "0010", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N010", + "Text": "Cleaner", + "Position": "92, 465", + "Type": 0, + "StationType": 2, + "ConnectedNodes": [ + "N009" + ], + "RfidId": "0011", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 1, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N011", + "Text": "", + "Position": "450, 399", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N003", + "N012", + "N015", + "N031", + "N022" + ], + "RfidId": "0005", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N012", + "Text": "", + "Position": "549, 450", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N011", + "N013", + "N015" + ], + "RfidId": "0006", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N013", + "Text": "", + "Position": "616, 492", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N012", + "N014" + ], + "RfidId": "0007", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N014", + "Text": "Loader", + "Position": "670, 526", + "Type": 0, + "StationType": 1, + "ConnectedNodes": [ + "N013" + ], + "RfidId": "0008", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N019", + "Text": "Chg #1", + "Position": "668, 223", + "Type": 0, + "StationType": 5, + "ConnectedNodes": [ + "N007" + ], + "RfidId": "0015", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 1, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N022", + "Text": "", + "Position": "450, 300", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N003", + "N006", + "N011", + "N023", + "N031" + ], + "RfidId": "0012", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N023", + "Text": "", + "Position": "480, 183", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N006", + "N022", + "N024" + ], + "RfidId": "0016", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N024", + "Text": "", + "Position": "500, 135", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N023", + "N025" + ], + "RfidId": "0017", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N025", + "Text": "", + "Position": "573, 97", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N024", + "N026" + ], + "RfidId": "0018", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N026", + "Text": "Chg #2", + "Position": "649, 87", + "Type": 0, + "StationType": 5, + "ConnectedNodes": [ + "N025" + ], + "RfidId": "0019", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 1, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N015", + "Text": "", + "Position": "449, 499", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N011", + "N012", + "N016" + ], + "RfidId": "0037", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N016", + "Text": "", + "Position": "422, 537", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N015", + "N017" + ], + "RfidId": "0036", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N017", + "Text": "", + "Position": "380, 557", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N016", + "N018" + ], + "RfidId": "0035", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N018", + "Text": "", + "Position": "329, 561", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N017", + "N030", + "N005" + ], + "RfidId": "0034", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N005", + "Text": "", + "Position": "233, 561", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N018", + "N020", + "N029" + ], + "RfidId": "0033", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N020", + "Text": "", + "Position": "155, 560", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N005", + "N021", + "N028" + ], + "RfidId": "0032", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N021", + "Text": "", + "Position": "68, 558", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N020", + "N027" + ], + "RfidId": "0031", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N027", + "Text": "Buf #1", + "Position": "38, 637", + "Type": 0, + "StationType": 4, + "ConnectedNodes": [ + "N021" + ], + "RfidId": "0041", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N028", + "Text": "Buf #2", + "Position": "125, 639", + "Type": 0, + "StationType": 4, + "ConnectedNodes": [ + "N020" + ], + "RfidId": "0040", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N029", + "Text": "Buf #3", + "Position": "203, 635", + "Type": 0, + "StationType": 4, + "ConnectedNodes": [ + "N005" + ], + "RfidId": "0039", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N030", + "Text": "Buf #4", + "Position": "296, 638", + "Type": 0, + "StationType": 4, + "ConnectedNodes": [ + "N018" + ], + "RfidId": "0038", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N031", + "Text": "", + "Position": "350, 400", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N003", + "N008", + "N011", + "N022" + ], + "RfidId": "0030", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + } + ], + "Labels": [ + { + "Id": "LBL001", + "Type": 1, + "Text": "Amkor Technology Korea", + "Position": "180, 105", + "ForeColor": "White", + "BackColor": "MidnightBlue", + "FontFamily": "Arial", + "FontSize": 20, + "FontStyle": 0, + "Padding": 5 + } + ], + "Images": [ + { + "Id": "IMG001", + "Type": 2, + "Name": "Image", + "Position": "633, 310", + "ImagePath": "", + "ImageBase64": "", + "Scale": "1, 1", + "Opacity": 1, + "Rotation": 0 + } + ], + "Magnets": [ + { + "Id": "92130fcc-1d99-4a1c-99d6-7d48072d9a3f", + "Type": 4, + "P1": { + "X": 52, + "Y": 466 + }, + "P2": { + "X": 183, + "Y": 465 + }, + "ControlPoint": null + }, + { + "Id": "6d5f514a-c84d-42fd-951d-cc1836a02eb9", + "Type": 4, + "P1": { + "X": 315.7142857142857, + "Y": 562 + }, + "P2": { + "X": 449.7142857142857, + "Y": 399.2857142857143 + }, + "ControlPoint": { + "X": 485.39960039960044, + "Y": 568.185814185814 + } + }, + { + "Id": "fe5f3cc4-f995-4fa2-a55d-1bd77792c062", + "Type": 4, + "P1": { + "X": 449, + "Y": 498.6190476190476 + }, + "P2": { + "X": 549.2857142857142, + "Y": 449.4761904761904 + }, + "ControlPoint": { + "X": 469.6853146853147, + "Y": 417.5191475191474 + } + }, + { + "Id": "5a0edec2-7ac3-4c99-bbb4-8debde0c1d07", + "Type": 4, + "P1": { + "X": 317.14285714285717, + "Y": 562.7142857142857 + }, + "P2": { + "X": -8.270406275571691, + "Y": 556.7518162503355 + }, + "ControlPoint": null + }, + { + "Id": "def7c4b9-86db-42eb-aae6-0c6c9bedcc30", + "Type": 4, + "P1": { + "X": 38.24242424242431, + "Y": 675.8533133533133 + }, + "P2": { + "X": 40.39960039960053, + "Y": 561.0429570429569 + }, + "ControlPoint": null + }, + { + "Id": "624327ee-be0f-4373-b60a-786a93c1eabf", + "Type": 4, + "P1": { + "X": 124.90909090909095, + "Y": 676.51998001998 + }, + "P2": { + "X": 125.39960039960052, + "Y": 559.6620046620044 + }, + "ControlPoint": null + }, + { + "Id": "f1e885ae-55f7-42e9-b3aa-648541e97da0", + "Type": 4, + "P1": { + "X": 202.24242424242428, + "Y": 675.1866466866467 + }, + "P2": { + "X": 203.25674325674333, + "Y": 560.3286713286711 + }, + "ControlPoint": null + }, + { + "Id": "dc3e8061-2c99-4f24-ac9b-4020dd91fa8b", + "Type": 4, + "P1": { + "X": 296.9090909090909, + "Y": 675.1866466866467 + }, + "P2": { + "X": 295.39960039960044, + "Y": 562.4715284715284 + }, + "ControlPoint": null + }, + { + "Id": "e8242b99-ab7a-453f-b074-516cf7f8aa6b", + "Type": 4, + "P1": { + "X": 549.2857142857142, + "Y": 450.1428571428571 + }, + "P2": { + "X": 719, + "Y": 558 + }, + "ControlPoint": null + }, + { + "Id": "d9b18933-d211-4a46-9265-6d2543abc8f2", + "Type": 4, + "P1": { + "X": 349.85714285714283, + "Y": 399.71428571428567 + }, + "P2": { + "X": 449.7142857142857, + "Y": 399.2857142857143 + }, + "ControlPoint": { + "X": 400.39960039960044, + "Y": 349.6143856143856 + } + }, + { + "Id": "4ec4e1cd-2b6b-4e22-a0e3-1e91f83c90a6", + "Type": 4, + "P1": { + "X": 449.7142857142857, + "Y": 399.2857142857143 + }, + "P2": { + "X": 450.42857142857144, + "Y": 299.85714285714283 + }, + "ControlPoint": { + "X": 399.6853146853147, + "Y": 349.6143856143856 + } + }, + { + "Id": "c3de9569-3278-4b45-8c73-554e9a2241ad", + "Type": 4, + "P1": { + "X": 350.1428571428571, + "Y": 300.5714285714286 + }, + "P2": { + "X": 450.42857142857144, + "Y": 299.85714285714283 + }, + "ControlPoint": { + "X": 399.6853146853147, + "Y": 349.6143856143856 + } + }, + { + "Id": "c49475f5-5bae-49d1-aec9-8972b35f0624", + "Type": 4, + "P1": { + "X": 349.85714285714283, + "Y": 399.71428571428567 + }, + "P2": { + "X": 350.1428571428571, + "Y": 300.5714285714286 + }, + "ControlPoint": { + "X": 398.971028971029, + "Y": 349.6143856143856 + } + }, + { + "Id": "e47ba9a3-4678-4df6-bfbd-0f43cfd9dc40", + "Type": 4, + "P1": { + "X": 450, + "Y": 300 + }, + "P2": { + "X": 480, + "Y": 183 + }, + "ControlPoint": { + "X": 476, + "Y": 235 + } + }, + { + "Id": "d183c4a4-7de8-421a-bed2-4caa2e4b6e27", + "Type": 4, + "P1": { + "X": 480, + "Y": 183 + }, + "P2": { + "X": 700.2567432567432, + "Y": 90.47152847152849 + }, + "ControlPoint": { + "X": 493.88778460102014, + "Y": 76.24977555124613 + } + }, + { + "Id": "3e2ae683-f783-4001-a2a4-d56664ca89b8", + "Type": 4, + "P1": { + "X": 183, + "Y": 465 + }, + "P2": { + "X": 349.85714285714283, + "Y": 399.71428571428567 + }, + "ControlPoint": { + "X": 265.42857142857144, + "Y": 459.4597069597069 + } + }, + { + "Id": "b2b3d68a-2fe0-4bcd-b6de-0463a2b604e0", + "Type": 4, + "P1": { + "X": 350.1428571428571, + "Y": 300.5714285714286 + }, + "P2": { + "X": 41.113886113886245, + "Y": 266.75724275724275 + }, + "ControlPoint": { + "X": 223.25674325674333, + "Y": 201.75724275724275 + } + }, + { + "Id": "9bf4a200-eeb2-4a42-851c-1e596ac16c06", + "Type": 4, + "P1": { + "X": 450, + "Y": 300 + }, + "P2": { + "X": 716.7171740797905, + "Y": 226.11091587291276 + }, + "ControlPoint": { + "X": 550.92770039558, + "Y": 215.05828429396541 + } + }, + { + "Id": "5d340580-bb09-42d9-81ed-43e69f8921c3", + "Type": 4, + "P1": { + "X": 480, + "Y": 183 + }, + "P2": { + "X": 526.5263157894736, + "Y": 253.52631578947367 + }, + "ControlPoint": { + "X": 469.8750688166326, + "Y": 260.32144218870224 + } + }, + { + "Id": "f659dce6-5b29-44d7-9e51-e148dab3b02e", + "Type": 4, + "P1": { + "X": 350, + "Y": 301 + }, + "P2": { + "X": 450, + "Y": 399 + }, + "ControlPoint": null + }, + { + "Id": "10e46540-7a48-4f44-8b88-029c5a5115f1", + "Type": 4, + "P1": { + "X": 350, + "Y": 400 + }, + "P2": { + "X": 450, + "Y": 300 + }, + "ControlPoint": null + }, + { + "Id": "0417e191-5169-46be-ad45-607abdeae8a6", + "Type": 4, + "P1": { + "X": 450, + "Y": 399 + }, + "P2": { + "X": 549, + "Y": 450 + }, + "ControlPoint": null + }, + { + "Id": "26663018-221d-4ece-9dc0-7b40c19d31d8", + "Type": 4, + "P1": { + "X": 40.469394855230796, + "Y": 558.2519551312122 + }, + "P2": { + "X": 41.969394855230796, + "Y": 496.7519551312122 + }, + "ControlPoint": null + }, + { + "Id": "67eebd44-9a87-4298-be19-f8757c741b54", + "Type": 4, + "P1": { + "X": 125.96939485523076, + "Y": 563.2519551312122 + }, + "P2": { + "X": 125.96939485523076, + "Y": 495.7519551312122 + }, + "ControlPoint": null + }, + { + "Id": "5c4bdbc0-46b8-4e17-a6cc-43365d0f3bcb", + "Type": 4, + "P1": { + "X": 203.96939485523072, + "Y": 563.7519551312122 + }, + "P2": { + "X": 204.96939485523072, + "Y": 492.7519551312122 + }, + "ControlPoint": null + }, + { + "Id": "f67f7595-8f73-47e9-87be-4583239bb8e5", + "Type": 4, + "P1": { + "X": 296.9693948552307, + "Y": 564.7519551312122 + }, + "P2": { + "X": 295.4693948552307, + "Y": 493.2519551312122 + }, + "ControlPoint": null + } + ], + "Marks": [ + { + "Id": "2cb51787-c8cf-4ddb-97f0-b71f519d47dc", + "Type": 3, + "Position": "684, 539", + "X": 684, + "Y": 539, + "Rotation": 119.877727797857 + }, + { + "Id": "f704ebe0-1653-4559-b06f-1eaecafbefba", + "Type": 3, + "Position": "40, 559", + "X": 40, + "Y": 559, + "Rotation": 90 + }, + { + "Id": "d5b27365-79a2-4351-84c3-6767941ec0be", + "Type": 3, + "Position": "126, 560", + "X": 126, + "Y": 560, + "Rotation": 90 + }, + { + "Id": "0367cafb-9f85-4440-b6b4-c802a58e6181", + "Type": 3, + "Position": "203, 558", + "X": 203, + "Y": 558, + "Rotation": 89.2872271068898 + }, + { + "Id": "1f4ab2c9-07f8-4675-802d-9b4824b55198", + "Type": 3, + "Position": "296, 560", + "X": 296, + "Y": 560, + "Rotation": 88.40516772072262 + }, + { + "Id": "15fddfa4-ff74-48ff-b922-4aacdce1960b", + "Type": 3, + "Position": "81, 256", + "X": 81, + "Y": 256, + "Rotation": 74.50226651936697 + }, + { + "Id": "962bb671-6932-477d-9209-2c2a076cfb22", + "Type": 3, + "Position": "73, 466", + "X": 73, + "Y": 466, + "Rotation": 90 + }, + { + "Id": "cd9f8434-f223-4532-9b3e-5b44c738abbb", + "Type": 3, + "Position": "686, 196", + "X": 686, + "Y": 196, + "Rotation": 90 + }, + { + "Id": "61c6d1dd-6a39-4931-a530-9b44d2010139", + "Type": 3, + "Position": "669, 84", + "X": 669, + "Y": 84, + "Rotation": 90 + }, + { + "Id": "4b699847-36d4-471c-b990-4ad37967c2dc", + "Type": 3, + "Position": "204, 655", + "X": 204, + "Y": 655, + "Rotation": 0.3824344779617803 + }, + { + "Id": "a9f68317-f1c2-47d8-b029-348b5428be9f", + "Type": 3, + "Position": "296, 657", + "X": 296, + "Y": 657, + "Rotation": -1.3380194104322385 + }, + { + "Id": "fe227205-2a65-4ba9-bb4a-4efb4ed0a7b0", + "Type": 3, + "Position": "122, 657", + "X": 122, + "Y": 657, + "Rotation": 0.8431103833306963 + }, + { + "Id": "5dd29191-798c-480c-b066-7947bfcc4fb7", + "Type": 3, + "Position": "40, 657", + "X": 40, + "Y": 657, + "Rotation": 1.659829660758831 + }, + { + "Id": "649729f0-ff04-4e11-8869-f6a39d815427", + "Type": 3, + "Position": "39, 534", + "X": 39.4693948552308, + "Y": 533.7519551312122, + "Rotation": 0 + }, + { + "Id": "2bb9a821-f86b-4190-a182-64abe2c940ed", + "Type": 3, + "Position": "125, 534", + "X": 125.46939485523076, + "Y": 533.7519551312122, + "Rotation": 0 + }, + { + "Id": "821598e1-091a-4884-96fe-6ed5f43c4f62", + "Type": 3, + "Position": "203, 532", + "X": 203.46939485523072, + "Y": 532.2519551312122, + "Rotation": 0 + }, + { + "Id": "66c1bbee-89a8-45a9-b585-ddfd59768f6b", + "Type": 3, + "Position": "295, 533", + "X": 295.4693948552307, + "Y": 533.2519551312122, + "Rotation": 0 + } + ], + "Settings": { + "BackgroundColorArgb": -14671840, + "ShowGrid": false + }, + "CreatedDate": "2025-12-16T10:58:08.630Z", + "Version": "1.3" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d41eec1 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1vRAJl8k2mFhnuVvPiIQYjCVuYdZTmyiL + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/components/AcsControls.tsx b/components/AcsControls.tsx new file mode 100644 index 0000000..f222716 --- /dev/null +++ b/components/AcsControls.tsx @@ -0,0 +1,142 @@ +import React, { useState, useMemo } from 'react'; +import { Navigation, Package, Zap, ChevronRight, Hash, Type } from 'lucide-react'; +import { SimulationMap } from '../types'; + +interface AcsControlsProps { + onSend: (cmd: number, data?: number[]) => void; + isConnected: boolean; + mapData: SimulationMap; +} + +const AcsControls: React.FC = ({ onSend, isConnected, mapData }) => { + const [tagId, setTagId] = useState('0001'); + const [alias, setAlias] = useState('charger1'); + + const PRESET_ALIASES = [ + 'charger1', 'charger2', + 'loader', 'unloader', 'cleaner', + 'buffer1', 'buffer2', 'buffer3', 'buffer4', 'buffer5', 'buffer6' + ]; + + const availableTags = useMemo(() => { + // Extract unique, non-empty RFID tags from map nodes + const tags = mapData.nodes + .filter(n => n.rfidId && n.rfidId.trim() !== '') + .map(n => n.rfidId); + return Array.from(new Set(tags)).sort(); + }, [mapData]); + + const handleSend = (cmd: number, dataStr?: string, dataByte?: number) => { + if (!isConnected) return; + + let payload: number[] = []; + + if (dataStr !== undefined) { + // Convert string to ASCII bytes + for (let i = 0; i < dataStr.length; i++) { + payload.push(dataStr.charCodeAt(i)); + } + } else if (dataByte !== undefined) { + payload.push(dataByte); + } + + onSend(cmd, payload); + }; + + return ( +
+

+ ACS Control + EXT CMD +

+ + {/* Navigation Commands */} +
+ {/* Goto Tag */} +
+
+ + setTagId(e.target.value)} + className="w-full bg-gray-900 border border-gray-600 rounded py-1 pl-7 pr-2 text-xs text-white font-mono focus:border-green-500 outline-none" + placeholder="Tag ID" + /> + + {availableTags.map(tag => ( + +
+ +
+ + {/* Goto Alias */} +
+
+ + setAlias(e.target.value)} + className="w-full bg-gray-900 border border-gray-600 rounded py-1 pl-7 pr-2 text-xs text-white font-mono focus:border-green-500 outline-none" + placeholder="Alias Name" + /> + + {PRESET_ALIASES.map(item => ( + +
+ +
+
+ +
+ + {/* Action Buttons Grid */} +
+ + + + + +
+
+ ); +}; + +export default AcsControls; \ No newline at end of file diff --git a/components/AgvAutoRunControls.tsx b/components/AgvAutoRunControls.tsx new file mode 100644 index 0000000..bdf313c --- /dev/null +++ b/components/AgvAutoRunControls.tsx @@ -0,0 +1,104 @@ + +import React from 'react'; +import { Play, Square, ArrowLeft, ArrowRight, Radar } from 'lucide-react'; +import { AgvState, AgvRunConfig } from '../types'; + +interface AgvAutoRunControlsProps { + agvState: AgvState; + updateRunConfig: (key: keyof AgvRunConfig, value: any) => void; + toggleRun: () => void; + isRunning: boolean; + isError: boolean; + setLidar: (isOn: boolean) => void; +} + +const AgvAutoRunControls: React.FC = ({ + agvState, + updateRunConfig, + toggleRun, + isRunning, + isError, + setLidar, +}) => { + return ( +
+
+

Auto Run

+ + {isRunning ? 'Running' : 'Ready'} + +
+ +
+
+ Direction +
+ {['FWD', 'BWD'].map(dir => ( + + ))} +
+
+ +
+ Branch +
+ + + +
+
+ +
+ Speed +
+ {['L', 'M', 'H'].map(spd => ( + + ))} +
+
+ +
+ Lidar + +
+ + +
+
+ ); +}; + +export default AgvAutoRunControls; diff --git a/components/AgvControls.tsx b/components/AgvControls.tsx new file mode 100644 index 0000000..2ef25e5 --- /dev/null +++ b/components/AgvControls.tsx @@ -0,0 +1,167 @@ + +import React from 'react'; +import { StopCircle, Play, Square, AlertTriangle, ChevronsUp, ChevronsDown, Magnet, Radar, ArrowLeft, ArrowRight } from 'lucide-react'; +import { AgvState, AgvMotionState, AgvRunConfig } from '../types'; +import AgvManualControls from './AgvManualControls'; +import AgvAutoRunControls from './AgvAutoRunControls'; + +interface AgvControlsProps { + agvState: AgvState; + setMotion: (state: AgvMotionState) => void; + setLift: (val: number) => void; + setRunConfig: (config: AgvRunConfig) => void; + setError: (error: string | null) => void; + onTurn180: (direction: 'LEFT' | 'RIGHT') => void; + setMagnet: (isOn: boolean) => void; + setLiftStatus: (status: 'IDLE' | 'UP' | 'DOWN') => void; + setLidar: (isOn: boolean) => void; +} + +const AgvControls: React.FC = ({ agvState, setMotion, setLift, setRunConfig, setError, onTurn180, setMagnet, setLiftStatus, setLidar }) => { + const isRunning = agvState.motionState === AgvMotionState.RUNNING || agvState.motionState === AgvMotionState.MARK_STOPPING; + const isError = agvState.error !== null; + + const updateRunConfig = (key: keyof AgvRunConfig, value: any) => { + if (isError) return; + setRunConfig({ + ...agvState.runConfig, + [key]: value + }); + }; + + const toggleRun = () => { + if (isError) return; + + if (isRunning) { + setMotion(AgvMotionState.IDLE); + } else { + const isFwd = agvState.runConfig.direction === 'FWD'; + const hasLine = isFwd ? agvState.sensorLineFront : agvState.sensorLineRear; + + if (!hasLine) { + setError('LINE_OUT'); + return; + } + setMotion(AgvMotionState.RUNNING); + } + }; + + const handleMarkStop = () => { + if (agvState.motionState === AgvMotionState.RUNNING) { + setRunConfig({ + ...agvState.runConfig, + speedLevel: 'L' + }); + setMotion(AgvMotionState.MARK_STOPPING); + } + }; + + const resetError = () => { + setError(null); + }; + + const handleLiftSliderChange = (e: React.ChangeEvent) => { + if (agvState.liftStatus !== 'IDLE') { + setLiftStatus('IDLE'); + } + setLift(parseInt(e.target.value)); + }; + + return ( +
+ + {/* Error Overlay */} + {isError && ( +
+
+ + {agvState.error} +
+ +
+ )} + + {/* Manual Operation (분리된 콤포넌트) */} + + + {/* Lift & Magnet */} +
+
+ Lift Height + {Math.round(agvState.liftHeight)}% +
+ +
+ + + + + +
+ + +
+ +
+ + {/* Auto Run Controls */} + {/* Auto Run Controls (분리된 콤포넌트) */} + +
+ ); +}; + +export default AgvControls; diff --git a/components/AgvManualControls.tsx b/components/AgvManualControls.tsx new file mode 100644 index 0000000..bbaa07c --- /dev/null +++ b/components/AgvManualControls.tsx @@ -0,0 +1,119 @@ + +import React from 'react'; +import { ArrowUp, ArrowDown, RotateCcw, RotateCw, StopCircle, Disc } from 'lucide-react'; +import { AgvState, AgvMotionState, AgvRunConfig } from '../types'; + +interface AgvManualControlsProps { + agvState: AgvState; + setMotion: (state: AgvMotionState) => void; + updateRunConfig: (key: keyof AgvRunConfig, value: any) => void; + onTurn180: (direction: 'LEFT' | 'RIGHT') => void; + handleMarkStop: () => void; + isRunning: boolean; + isError: boolean; +} + +const AgvManualControls: React.FC = ({ + agvState, + setMotion, + updateRunConfig, + onTurn180, + handleMarkStop, + isRunning, + isError, +}) => { + const setManualMotion = (motion: AgvMotionState, direction?: 'FWD' | 'BWD') => { + setMotion(motion); + if (direction) { + updateRunConfig('direction', direction); + } + }; + + return ( +
+
+

Manual Operation

+
+ +
+
+
+ +
+ + + + + +
+ +
+
+ + {/* Extra Actions */} +
+ + + +
+
+
+ ); +}; + +export default AgvManualControls; diff --git a/components/AgvStatusPanel.tsx b/components/AgvStatusPanel.tsx new file mode 100644 index 0000000..a8f9c6e --- /dev/null +++ b/components/AgvStatusPanel.tsx @@ -0,0 +1,104 @@ + +import React from 'react'; +import { AgvState, AgvError, AgvSignal, SystemFlag0, SystemFlag1 } from '../types'; +import { CircleCheck, AlertOctagon, Cpu } from 'lucide-react'; + +interface AgvStatusPanelProps { + agvState: AgvState; +} + +const AgvStatusPanel: React.FC = ({ agvState }) => { + + const renderBit = (label: string, value: number, bitIndex: number, colorClass = 'bg-green-500') => { + const isOn = (value & (1 << bitIndex)) !== 0; + // 필터링에 의해 isOn이 true인 것만 전달되지만 스타일 유지를 위해 체크 로직 유지 + return ( +
+ {label.replace(/_/g, ' ')} +
+
+ ); + }; + + const getEnumKeys = (e: any) => { + return Object.keys(e).filter(k => typeof e[k as any] === "number"); + }; + + const getActiveKeys = (enumObj: any, value: number) => { + return getEnumKeys(enumObj).filter(key => (value & (1 << (enumObj[key as any] as number))) !== 0); + }; + + return ( +
+
+ AGV PROTOCOL FLAGS (ACTIVE) +
+ + {/* System 1 (Control State) */} +
+

+ SYSTEM 1 +

+
+ {getActiveKeys(SystemFlag1, agvState.system1).length > 0 ? ( + getActiveKeys(SystemFlag1, agvState.system1).map((key) => + renderBit(key, agvState.system1, SystemFlag1[key as any] as unknown as number, 'bg-blue-500') + ) + ) : ( + No active flags + )} +
+
+ + {/* Signals */} +
+

+ SIGNALS +

+
+ {getActiveKeys(AgvSignal, agvState.signalFlags).length > 0 ? ( + getActiveKeys(AgvSignal, agvState.signalFlags).map((key) => + renderBit(key, agvState.signalFlags, AgvSignal[key as any] as unknown as number, 'bg-yellow-500') + ) + ) : ( + No active signals + )} +
+
+ + {/* Errors */} +
+

+ ERRORS +

+
+ {getActiveKeys(AgvError, agvState.errorFlags).length > 0 ? ( + getActiveKeys(AgvError, agvState.errorFlags).map((key) => + renderBit(key, agvState.errorFlags, AgvError[key as any] as unknown as number, 'bg-red-600 shadow-[0_0_5px_rgba(220,38,38,0.5)]') + ) + ) : ( + None + )} +
+
+ + {/* System 0 (Hardware) */} +
+

+ SYSTEM 0 +

+
+ {getActiveKeys(SystemFlag0, agvState.system0).length > 0 ? ( + getActiveKeys(SystemFlag0, agvState.system0).map((key) => + renderBit(key, agvState.system0, SystemFlag0[key as any] as unknown as number, 'bg-cyan-500') + ) + ) : ( + No active flags + )} +
+
+
+ ); +}; + +export default AgvStatusPanel; diff --git a/components/BmsPanel.tsx b/components/BmsPanel.tsx new file mode 100644 index 0000000..1b4d9fd --- /dev/null +++ b/components/BmsPanel.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { BatteryCharging, Zap, Thermometer, Sliders } from 'lucide-react'; +import { AgvState } from '../types'; + +interface BmsPanelProps { + state: AgvState; + setBatteryLevel: (level: number) => void; +} + +const BmsPanel: React.FC = ({ state, setBatteryLevel }) => { + const currentCapacity = (state.maxCapacity * (state.batteryLevel / 100)).toFixed(1); + + return ( +
+

+ BMS Monitor + ID: 01 +

+ + {/* Main Info Grid */} +
+
+
Total Voltage
+
+ {(state.cellVoltages.reduce((a,b)=>a+b, 0)).toFixed(1)} V +
+
+
+
+ Temp +
+
+ {state.batteryTemp.toFixed(1)} °C +
+
+ +
+
Capacity (Ah)
+
+ {currentCapacity} + / + {state.maxCapacity} +
+
+
+
Level (%)
+
+ {state.batteryLevel.toFixed(1)}% +
+
+
+ + {/* Manual Slider */} +
+
+ Manual Adjust +
+ setBatteryLevel(Number(e.target.value))} + className="w-full h-1.5 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-green-500 hover:accent-green-400" + /> +
+ + {/* Cells */} +
+
Cell Voltages (V)
+
+ {state.cellVoltages.map((v, i) => ( +
3.5 ? 'bg-blue-900/30 border-blue-800 text-blue-300' : + 'bg-gray-700/30 border-gray-600 text-gray-300' + }`}> + {v.toFixed(3)} +
+ ))} +
+
+
+ ); +}; + +export default BmsPanel; diff --git a/components/ConnectionStatusBar.tsx b/components/ConnectionStatusBar.tsx new file mode 100644 index 0000000..1355e40 --- /dev/null +++ b/components/ConnectionStatusBar.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { Plug, Unplug, Activity, Battery } from 'lucide-react'; + +interface ConnectionStatusBarProps { + agvConnected: boolean; + bmsConnected: boolean; + onConnectAgv: () => void; + onDisconnectAgv: () => void; + onConnectBms: () => void; + onDisconnectBms: () => void; + agvBaudRate: number; + setAgvBaudRate: (rate: number) => void; + bmsBaudRate: number; + setBmsBaudRate: (rate: number) => void; +} + +const BAUD_RATES = [9600, 19200, 38400, 57600, 115200]; + +const ConnectionStatusBar: React.FC = ({ + agvConnected, + bmsConnected, + onConnectAgv, + onDisconnectAgv, + onConnectBms, + onDisconnectBms, + agvBaudRate, + setAgvBaudRate, + bmsBaudRate, + setBmsBaudRate, +}) => { + return ( +
+ + {/* AGV Connection Controls */} +
+
+ + + AGV + +
+ + +
+
+ +
+ SERIAL COMMUNICATION +
+ + {/* BMS Connection Controls */} +
+
+ + + BMS + +
+ + +
+
+
+ ); +}; + +export default ConnectionStatusBar; diff --git a/components/EditorToolbar.tsx b/components/EditorToolbar.tsx new file mode 100644 index 0000000..80a9c30 --- /dev/null +++ b/components/EditorToolbar.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { MousePointer2, GitCommitHorizontal, Radio, Disc, Eraser, Save, Upload, Minus, Spline } from 'lucide-react'; +import { ToolType } from '../types'; + +interface EditorToolbarProps { + activeTool: ToolType; + setTool: (t: ToolType) => void; + onSave: () => void; + onLoad: () => void; +} + +const EditorToolbar: React.FC = ({ activeTool, setTool, onSave, onLoad }) => { + const tools = [ + { id: ToolType.SELECT, icon: , label: 'Select/Move' }, + { id: ToolType.DRAW_LINE, icon: , label: 'Logical Connection (Graph)' }, + { id: ToolType.DRAW_MAGNET_STRAIGHT, icon: , label: 'Magnet Line (Straight)' }, + { id: ToolType.DRAW_MAGNET_CURVE, icon: , label: 'Magnet Line (Curve)' }, + { id: ToolType.ADD_RFID, icon: , label: 'Place RFID' }, + { id: ToolType.ADD_MARK, icon: , label: 'Place Mark' }, + { id: ToolType.ERASER, icon: , label: 'Delete' }, + ]; + + return ( +
+
Map Editor
+ {tools.map((tool) => ( + + ))} +
+ + +
+ ); +}; + +export default EditorToolbar; diff --git a/components/SerialConsole.tsx b/components/SerialConsole.tsx new file mode 100644 index 0000000..eab9350 --- /dev/null +++ b/components/SerialConsole.tsx @@ -0,0 +1,134 @@ +import React, { useRef, useEffect, useMemo } from 'react'; +import { LogEntry } from '../types'; +import { Terminal, Plug, Unplug, Settings2, Trash2, Usb } from 'lucide-react'; + +interface SerialConsoleProps { + title: string; + logs: LogEntry[]; + isConnected: boolean; + onConnect: () => void; + onDisconnect: () => void; + onClear: () => void; + baudRate: number; + setBaudRate: (rate: number) => void; + portInfo?: string | null; + colorClass?: string; // Text color for the source label +} + +const BAUD_RATES = [9600, 19200, 38400, 57600, 115200]; + +const SerialConsole: React.FC = ({ + title, + logs, + isConnected, + onConnect, + onDisconnect, + onClear, + baudRate, + setBaudRate, + portInfo, + colorClass = 'text-gray-400' +}) => { + const logContainerRef = useRef(null); + + // Memoize reversed logs to prevent unnecessary array operations on every render + const reversedLogs = useMemo(() => [...logs].reverse(), [logs]); + + // Force scroll to top whenever logs update to ensure the newest (top) is visible + useEffect(() => { + if (logContainerRef.current) { + logContainerRef.current.scrollTop = 0; + } + }, [reversedLogs]); + + return ( +
+ {/* Header with Connection Controls */} +
+
+
+ {title} +
+
+ + {isConnected && portInfo && ( +
+ + {portInfo.replace('ID:', '')} +
+ )} +
+ +
+ + + + +
+ + +
+
+ + {/* Log Body */} +
+ {reversedLogs.length === 0 && ( +
+ + No Data +
+ )} + + {reversedLogs.map((log, index) => ( +
+ {/* Row Number (1 = Newest) */} + {index + 1} + + {log.timestamp} + + {log.type === 'RX' ? 'RX' : log.type === 'TX' ? 'TX' : '--'} + + + {log.message} + +
+ ))} +
+
+ ); +}; + +export default SerialConsole; \ No newline at end of file diff --git a/components/SimulationCanvas.tsx b/components/SimulationCanvas.tsx new file mode 100644 index 0000000..4025249 --- /dev/null +++ b/components/SimulationCanvas.tsx @@ -0,0 +1,1561 @@ +import React, { useRef, useEffect, useState, useCallback } from 'react'; +import { SimulationMap, AgvState, ToolType, MapNode, MapEdge, MagnetLine, AgvMotionState, NodeType, FloorMark } from '../types'; +import { AGV_WIDTH, AGV_HEIGHT, SENSOR_OFFSET_FRONT, SENSOR_OFFSET_REAR, SENSOR_OFFSET_MARK, MAX_SPEED, TURN_SPEED, MARK_SEARCH_SPEED, SPEED_L, SPEED_M, SPEED_H, MAGNET_WIDTH, MAGNET_COLOR, GRID_SIZE } from '../constants'; +import { Trash2, X, Move } from 'lucide-react'; + +interface SimulationCanvasProps { + activeTool: ToolType; + mapData: SimulationMap; + setMapData: (data: SimulationMap | ((prev: SimulationMap) => SimulationMap)) => void; + agvState: AgvState; + setAgvState: (state: AgvState | ((prev: AgvState) => AgvState)) => void; + onLog: (msg: string) => void; +} + +interface HitObject { + type: 'NODE' | 'EDGE' | 'MAGNET' | 'MARK'; + id: string; + name: string; // For display + dist: number; +} + +const SimulationCanvas: React.FC = ({ activeTool, mapData, setMapData, agvState, setAgvState, onLog }) => { + const canvasRef = useRef(null); + const containerRef = useRef(null); + const [dimensions, setDimensions] = useState({ width: 800, height: 600 }); + + // --- View Transform (Zoom/Pan) --- + const viewRef = useRef({ x: 0, y: 0, scale: 1 }); + const [isPanning, setIsPanning] = useState(false); + const isSpacePressedRef = useRef(false); + + // Interaction State + const [dragStartPos, setDragStartPos] = useState<{x:number, y:number} | null>(null); + const [startNodeId, setStartNodeId] = useState(null); + + // Selection & Editing State + const [selectedItemId, setSelectedItemId] = useState(null); + // dragHandle: 'p1'|'p2'|'control' for magnets, 'mark_p1'|'mark_p2' for marks + const [dragHandle, setDragHandle] = useState(null); + + // Move/Drag State for Select Tool (Whole Object) + const [dragTarget, setDragTarget] = useState<{ + type: 'AGV' | 'NODE' | 'MAGNET' | 'MARK'; + id?: string; + startMouse: { x: number, y: number }; + initialObjState: any; + } | null>(null); + + // Curve Drawing State + const [curvePhase, setCurvePhase] = useState<0 | 1 | 2>(0); + const [tempMagnet, setTempMagnet] = useState | null>(null); + + // Eraser Context Menu State + const [contextMenu, setContextMenu] = useState<{ x: number, y: number, items: HitObject[] } | null>(null); + + // Image Cache + const imageCacheRef = useRef>({}); + + // Use ref for mouse position (Screen Coords) + const mouseScreenPosRef = useRef<{ x: number, y: number }>({ x: 0, y: 0 }); + // Use ref for mouse position (World Coords) + const mouseWorldPosRef = useRef<{ x: number, y: number }>({ x: 0, y: 0 }); + + // Refs for loop access + const mapDataRef = useRef(mapData); + const agvStateRef = useRef(agvState); + + // Track previous sensor position for intersection checking + const prevMarkSensorPosRef = useRef<{ x: number, y: number } | null>(null); + // Track the ID of the magnet currently being followed to prevent jumping + const lastMagnetIdRef = useRef(null); + // Throttle log + const logThrottleRef = useRef(0); + + // Sync refs + useEffect(() => { mapDataRef.current = mapData; }, [mapData]); + useEffect(() => { agvStateRef.current = agvState; }, [agvState]); + + // Handle Resize + useEffect(() => { + const updateSize = () => { + if (containerRef.current) { + setDimensions({ + width: containerRef.current.clientWidth, + height: containerRef.current.clientHeight + }); + } + }; + + updateSize(); + const observer = new ResizeObserver(updateSize); + if (containerRef.current) { + observer.observe(containerRef.current); + } + + return () => observer.disconnect(); + }, []); + + // Keyboard Event for Spacebar Panning + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.code === 'Space') isSpacePressedRef.current = true; + if (e.key === 'Escape') { + setSelectedItemId(null); + setDragHandle(null); + setDragTarget(null); + setContextMenu(null); + } + }; + const handleKeyUp = (e: KeyboardEvent) => { + if (e.code === 'Space') isSpacePressedRef.current = false; + }; + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + return () => { + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('keyup', handleKeyUp); + }; + }, []); + + // Reset tool states when tool changes + useEffect(() => { + setCurvePhase(0); + setTempMagnet(null); + setDragStartPos(null); + setStartNodeId(null); + setContextMenu(null); + setDragTarget(null); + setDragHandle(null); + // Note: We might want to keep selection when switching tools, but often resetting is safer + if (activeTool !== ToolType.SELECT) setSelectedItemId(null); + }, [activeTool]); + + // Preload Images + useEffect(() => { + const cache = imageCacheRef.current; + mapData.nodes.forEach(node => { + if (node.type === NodeType.Image && node.imageBase64 && !cache[node.id]) { + const img = new Image(); + let src = node.imageBase64; + if (!src.startsWith('data:image')) { + src = `data:image/png;base64,${src}`; + } + img.src = src; + cache[node.id] = img; + } + }); + }, [mapData]); + + // --- Coordinate Transformation Helpers --- + const screenToWorld = (screenX: number, screenY: number) => { + const t = viewRef.current; + return { + x: (screenX - t.x) / t.scale, + y: (screenY - t.y) / t.scale + }; + }; + + const worldToScreen = (worldX: number, worldY: number) => { + const t = viewRef.current; + return { + x: worldX * t.scale + t.x, + y: worldY * t.scale + t.y + }; + }; + + // --- Physics Helper Functions --- + + const rotatePoint = (px: number, py: number, cx: number, cy: number, angleDeg: number) => { + const rad = (angleDeg * Math.PI) / 180; + const cos = Math.cos(rad); + const sin = Math.sin(rad); + const dx = px - cx; + const dy = py - cy; + return { + x: cx + (dx * cos - dy * sin), + y: cy + (dx * sin + dy * cos), + }; + }; + + const getDistance = (p1: {x:number, y:number}, p2: {x:number, y:number}) => { + return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); + }; + + // Distance from point P to line segment VW + const distanceToSegment = (p: {x:number, y:number}, v: {x:number, y:number}, w: {x:number, y:number}) => { + const l2 = Math.pow(getDistance(v, w), 2); + if (l2 === 0) return getDistance(p, v); + let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2; + t = Math.max(0, Math.min(1, t)); + const projection = { x: v.x + t * (w.x - v.x), y: v.y + t * (w.y - v.y) }; + return getDistance(p, projection); + }; + + // Get closest point on segment + const getClosestPointOnSegment = (p: {x:number, y:number}, v: {x:number, y:number}, w: {x:number, y:number}) => { + const l2 = Math.pow(getDistance(v, w), 2); + if (l2 === 0) return v; + let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2; + t = Math.max(0, Math.min(1, t)); + return { x: v.x + t * (w.x - v.x), y: v.y + t * (w.y - v.y) }; + }; + + // Quadratic Bezier Point Calculation + const getBezierPoint = (t: number, p0: {x:number, y:number}, p1: {x:number, y:number}, p2: {x:number, y:number}) => { + const u = 1 - t; + const tt = t * t; + const uu = u * u; + + return { + x: (uu * p0.x) + (2 * u * t * p1.x) + (tt * p2.x), + y: (uu * p0.y) + (2 * u * t * p1.y) + (tt * p2.y) + }; + }; + + // Calculate derivative (tangent vector) at t for Quadratic Bezier + const getBezierDerivative = (t: number, p0: {x:number, y:number}, p1: {x:number, y:number}, p2: {x:number, y:number}) => { + const u = 1 - t; + // B'(t) = 2(1-t)(P1 - P0) + 2t(P2 - P1) + const dx = 2 * u * (p1.x - p0.x) + 2 * t * (p2.x - p1.x); + const dy = 2 * u * (p1.y - p0.y) + 2 * t * (p2.y - p1.y); + return { x: dx, y: dy }; + }; + + // Approximate distance to a Quadratic Bezier Curve by subdividing it + // Now uses 50 segments for higher resolution to prevent "warping" + const distanceToBezier = (p: {x:number, y:number}, p0: {x:number, y:number}, p1: {x:number, y:number}, p2: {x:number, y:number}, segments = 50) => { + let minDist = Number.MAX_VALUE; + let prevPoint = p0; + let closestProj = p0; + let bestT = 0; + + // Scan segments to find approximate closest point + for (let i = 1; i <= segments; i++) { + const t = i / segments; + const currPoint = getBezierPoint(t, p0, p1, p2); + const dist = distanceToSegment(p, prevPoint, currPoint); + + if (dist < minDist) { + minDist = dist; + // Get linear projection on this segment to approximate t + const proj = getClosestPointOnSegment(p, prevPoint, currPoint); + closestProj = proj; + + // Approximate t at projection (Lerp) + const segLen = getDistance(prevPoint, currPoint); + const distOnSeg = getDistance(prevPoint, proj); + const tStep = 1 / segments; + const tStart = (i - 1) / segments; + bestT = tStart + (distOnSeg / segLen) * tStep; + } + prevPoint = currPoint; + } + + // Calculate Exact Tangent using Derivative at bestT + const deriv = getBezierDerivative(bestT, p0, p1, p2); + const tangentAngle = Math.atan2(deriv.y, deriv.x) * (180 / Math.PI); + + return { dist: minDist, proj: closestProj, angle: tangentAngle }; + }; + + const normalizeAngle = (angle: number) => { + let a = angle % 360; + if (a > 180) a -= 360; + if (a <= -180) a += 360; + return a; + }; + + const doSegmentsIntersect = (p1: {x:number, y:number}, p2: {x:number, y:number}, p3: {x:number, y:number}, p4: {x:number, y:number}) => { + const ccw = (a:{x:number,y:number}, b:{x:number,y:number}, c:{x:number,y:number}) => (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x); + return (ccw(p1, p2, p3) !== ccw(p1, p2, p4)) && (ccw(p3, p4, p1) !== ccw(p3, p4, p2)); + }; + + // Helper to find closest magnet and its angle + const getClosestMagnetInfo = (x: number, y: number) => { + const map = mapDataRef.current; + const p = { x, y }; + let closestMag = null; + let minDist = 30; // Search radius + let angle = 0; + + map.magnets.forEach(mag => { + let dist = 999; + let magAngle = 0; + + if (mag.type === 'STRAIGHT') { + dist = distanceToSegment(p, mag.p1, mag.p2); + magAngle = Math.atan2(mag.p2.y - mag.p1.y, mag.p2.x - mag.p1.x) * (180/Math.PI); + } else if (mag.type === 'CURVE' && mag.controlPoint) { + const res = distanceToBezier(p, mag.p1, mag.controlPoint, mag.p2); + dist = res.dist; + magAngle = res.angle; + } + + if (dist < minDist) { + minDist = dist; + closestMag = mag; + angle = magAngle; + } + }); + return { magnet: closestMag, angle }; + }; + + // --- Hit Test Logic --- + const getObjectsAt = (x: number, y: number, tolerance = 15): HitObject[] => { + const hits: HitObject[] = []; + const map = mapDataRef.current; + const p = { x, y }; + + const worldTolerance = tolerance / viewRef.current.scale; + + // Type Priority Map: Smaller number = Higher Priority + const typePriority: Record = { + 'NODE': 1, + 'MARK': 2, + 'EDGE': 3, + 'MAGNET': 4 + }; + + // 1. Nodes + map.nodes.forEach(node => { + if (getDistance(p, node) < worldTolerance) { + hits.push({ type: 'NODE', id: node.id, name: node.name || node.id, dist: getDistance(p, node) }); + } + }); + + // 2. Marks (Calculate as segment for better hit detection) + map.marks.forEach(mark => { + // Visual width is 50px (-25 to 25). + const rad = (mark.rotation * Math.PI) / 180; + const dx = Math.cos(rad) * 25; + const dy = Math.sin(rad) * 25; + const p1 = { x: mark.x - dx, y: mark.y - dy }; + const p2 = { x: mark.x + dx, y: mark.y + dy }; + + const dist = distanceToSegment(p, p1, p2); + + if (dist < worldTolerance) { + hits.push({ type: 'MARK', id: mark.id, name: 'Mark', dist }); + } + }); + + // 3. Magnets + map.magnets.forEach(mag => { + let dist = 999; + if (mag.type === 'STRAIGHT') { + dist = distanceToSegment(p, mag.p1, mag.p2); + } else if (mag.type === 'CURVE' && mag.controlPoint) { + dist = distanceToBezier(p, mag.p1, mag.controlPoint, mag.p2).dist; + } + if (dist < worldTolerance) { + hits.push({ type: 'MAGNET', id: mag.id, name: `Magnet (${mag.type})`, dist }); + } + }); + + // 4. Edges + map.edges.forEach(edge => { + const start = map.nodes.find(n => n.id === edge.from); + const end = map.nodes.find(n => n.id === edge.to); + if (start && end) { + const dist = distanceToSegment(p, start, end); + if (dist < worldTolerance / 2) { + hits.push({ type: 'EDGE', id: edge.id, name: `Line ${edge.from}-${edge.to}`, dist }); + } + } + }); + + // Sort by Priority first, then Distance + return hits.sort((a,b) => { + const prioDiff = typePriority[a.type] - typePriority[b.type]; + if (prioDiff !== 0) return prioDiff; + return a.dist - b.dist; + }); + }; + + const deleteObject = (item: HitObject) => { + if (item.id === selectedItemId) setSelectedItemId(null); // Clear selection if deleted + setMapData(prev => { + const next = { ...prev }; + if (item.type === 'NODE') { + next.nodes = prev.nodes.filter(n => n.id !== item.id); + next.edges = prev.edges.filter(e => e.from !== item.id && e.to !== item.id); + } else if (item.type === 'MARK') { + next.marks = prev.marks.filter(m => m.id !== item.id); + } else if (item.type === 'MAGNET') { + next.magnets = prev.magnets.filter(m => m.id !== item.id); + } else if (item.type === 'EDGE') { + next.edges = prev.edges.filter(e => e.id !== item.id); + } + return next; + }); + setContextMenu(null); + }; + + // --- Main Simulation Loop --- + useEffect(() => { + let animationFrameId: number; + + const updatePhysics = () => { + const state = { ...agvStateRef.current }; + const map = mapDataRef.current; + let hasChanged = false; + logThrottleRef.current++; + + // Update sensors + const frontSensor = rotatePoint( + state.x + SENSOR_OFFSET_FRONT.x, + state.y + SENSOR_OFFSET_FRONT.y, + state.x, state.y, state.rotation + ); + const rearSensor = rotatePoint( + state.x + SENSOR_OFFSET_REAR.x, + state.y + SENSOR_OFFSET_REAR.y, + state.x, state.y, state.rotation + ); + const markSensor = rotatePoint( + state.x + SENSOR_OFFSET_MARK.x, + state.y + SENSOR_OFFSET_MARK.y, + state.x, state.y, state.rotation + ); + + if (!prevMarkSensorPosRef.current) prevMarkSensorPosRef.current = markSensor; + + let lineFrontDetected = false; + let lineRearDetected = false; + + // FIXED: Very tight threshold to ensure the sensor must physically be ON the line + // Reduced from 12 to 8 to prevent "grazing" false positives during tight turns + const DETECTION_THRESHOLD = 8; + const SEARCH_RADIUS = 30; + + // 1. Check Strict Sensor Status (for UI/State) + map.magnets.forEach(mag => { + let distFront = 999; + let distRear = 999; + + if (mag.type === 'STRAIGHT') { + distFront = distanceToSegment(frontSensor, mag.p1, mag.p2); + distRear = distanceToSegment(rearSensor, mag.p1, mag.p2); + } else if (mag.type === 'CURVE' && mag.controlPoint) { + distFront = distanceToBezier(frontSensor, mag.p1, mag.controlPoint, mag.p2).dist; + distRear = distanceToBezier(rearSensor, mag.p1, mag.controlPoint, mag.p2).dist; + } + + if (distFront < DETECTION_THRESHOLD) lineFrontDetected = true; + if (distRear < DETECTION_THRESHOLD) lineRearDetected = true; + }); + + // Handle Line Out Logic + const isAutoMove = state.motionState === AgvMotionState.RUNNING || state.motionState === AgvMotionState.MARK_STOPPING; + + if (state.motionState !== AgvMotionState.IDLE && !state.error) { + hasChanged = true; + let moveSpeed = state.speed; + + if (state.motionState === AgvMotionState.RUNNING) { + moveSpeed = state.runConfig.speedLevel === 'L' ? SPEED_L : state.runConfig.speedLevel === 'M' ? SPEED_M : SPEED_H; + } else if (state.motionState === AgvMotionState.MARK_STOPPING) { + moveSpeed = Math.min(state.speed, MARK_SEARCH_SPEED); + } + + // --- LINE TRACING --- + if (isAutoMove) { + const isFwd = state.runConfig.direction === 'FWD'; + const sensorPos = isFwd ? frontSensor : rearSensor; + + let bestMagnet: { mag: MagnetLine, dist: number, targetAngle: number, proj: {x:number, y:number} } | null = null; + + // Collect all candidate lines + const potentialLines: { mag: MagnetLine, dist: number, proj: {x:number, y:number}, targetAngle: number, angleDiff: number, category: string, isSticky: boolean, isBehind: boolean, isConnected: boolean, isSharpTurn: boolean }[] = []; + + map.magnets.forEach(mag => { + let dist = 999; + let proj = { x: 0, y: 0 }; + let tangentAngle = 0; + + if (mag.type === 'STRAIGHT') { + dist = distanceToSegment(sensorPos, mag.p1, mag.p2); + proj = getClosestPointOnSegment(sensorPos, mag.p1, mag.p2); + tangentAngle = Math.atan2(mag.p2.y - mag.p1.y, mag.p2.x - mag.p1.x) * (180/Math.PI); + } else if (mag.type === 'CURVE' && mag.controlPoint) { + const res = distanceToBezier(sensorPos, mag.p1, mag.controlPoint, mag.p2); + dist = res.dist; + proj = res.proj; + tangentAngle = res.angle; + } + + if (dist < SEARCH_RADIUS) { + let heading = state.rotation; + if (!isFwd) heading += 180; + heading = normalizeAngle(heading); + + // Align tangent to heading for basic diff calc + let alignedTangent = tangentAngle; + if (mag.type === 'STRAIGHT') { + let diff = normalizeAngle(tangentAngle - heading); + if (Math.abs(diff) > 90) alignedTangent = normalizeAngle(tangentAngle + 180); + } else { + let diff = normalizeAngle(tangentAngle - heading); + if (Math.abs(diff) > 90) alignedTangent = normalizeAngle(tangentAngle + 180); + } + + let diff = normalizeAngle(alignedTangent - heading); + + // --- Node-based Lookahead / Category --- + let comparisonAngle = alignedTangent; + const d1 = getDistance(sensorPos, mag.p1); + const d2 = getDistance(sensorPos, mag.p2); + const farEnd = d1 > d2 ? mag.p1 : mag.p2; // Heuristic for segment end + const targetNode = map.nodes.find(n => getDistance(n, farEnd) < 40); + + if (targetNode) { + const nodeAngle = Math.atan2(targetNode.y - sensorPos.y, targetNode.x - sensorPos.x) * (180 / Math.PI); + comparisonAngle = normalizeAngle(nodeAngle); + } + + let finalDiff = normalizeAngle(comparisonAngle - heading); + let category = 'STRAIGHT'; + if (finalDiff < -45) category = 'LEFT'; + else if (finalDiff > 45) category = 'RIGHT'; + + // --- Behind Check --- + const dx = proj.x - sensorPos.x; + const dy = proj.y - sensorPos.y; + const hRad = (heading * Math.PI) / 180; + const hx = Math.cos(hRad); + const hy = Math.sin(hRad); + + let isSticky = lastMagnetIdRef.current === mag.id; + + // Release Sticky if End of Line Reached + if (isSticky) { + const distToP1 = getDistance(sensorPos, mag.p1); + const distToP2 = getDistance(sensorPos, mag.p2); + if (distToP1 < 25 || distToP2 < 25) { + isSticky = false; + } + } + + // Dot product check + const behindThreshold = isSticky ? -25 : -5; + const isBehind = (dx * hx + dy * hy) < behindThreshold; + + // --- STRICT CONNECTIVITY CHECK --- + const distToEndpoint = Math.min(getDistance(sensorPos, mag.p1), getDistance(sensorPos, mag.p2)); + const isConnected = distToEndpoint < 30; + + // --- PHYSICAL CONSTRAINT (SHARP TURN) --- + // If deflection is > 35 degrees (Internal angle < 145), it is too sharp for normal line tracking + // This forces the AGV to ignore 90-degree junctions unless there is a curve magnet + const isSharpTurn = Math.abs(diff) > 35; + + potentialLines.push({ mag, dist, proj, targetAngle: alignedTangent, angleDiff: diff, category, isSticky, isBehind, isConnected, isSharpTurn }); + } + }); + + // Priority Sort + const branchMode = state.runConfig.branch; + + potentialLines.sort((a, b) => { + // 1. Behind Check (Absolute disqualifier usually) + if (a.isBehind !== b.isBehind) return a.isBehind ? 1 : -1; + + // 2. PHYSICAL CONSTRAINT: Disqualify Sharp Turns + // A sharp turn is physically impossible in motion, so we deprioritize it heavily. + if (a.isSharpTurn !== b.isSharpTurn) return a.isSharpTurn ? 1 : -1; + + const aValid = a.isSticky || a.isConnected; + const bValid = b.isSticky || b.isConnected; + + // 3. Validity Check: We prefer lines that are physically anchored here + if (aValid !== bValid) return aValid ? -1 : 1; + + // 4. Branch Matching (Only among valid lines) + if (aValid && bValid) { + const aMatch = a.category === branchMode; + const bMatch = b.category === branchMode; + if (aMatch !== bMatch) return aMatch ? -1 : 1; + } + + // 5. Stickiness (Stability bias) + if (a.isSticky !== b.isSticky) return a.isSticky ? -1 : 1; + + // 6. Connectivity (Prefer line starts over mid-lines) + if (a.isConnected !== b.isConnected) return a.isConnected ? -1 : 1; + + // 7. Distance + return a.dist - b.dist; + }); + + // Debug Logging (Throttled ~ 30 frames) + if (logThrottleRef.current % 30 === 0 && isAutoMove) { + const logMsg = `Trk: found=${potentialLines.length} | ` + + potentialLines.map(p => `[${p.mag.id.substring(0,4)} D:${Math.round(p.dist)} S:${p.isSticky ? 'Y' : 'N'} M:${p.category === branchMode ? 'Y':'N'} !${p.isSharpTurn ? 'SHARP' : 'OK'}]`).join(' '); + onLog(logMsg); + } + + if (potentialLines.length > 0) { + bestMagnet = potentialLines[0]; + lastMagnetIdRef.current = bestMagnet.mag.id; + } + + // *** LINE OUT CHECK INTEGRATED HERE *** + // Only error out if NO valid lines are found in search radius + if (!bestMagnet && isAutoMove) { + state.motionState = AgvMotionState.IDLE; + state.error = 'LINE_OUT'; + hasChanged = true; + onLog(`STOPPED: LINE_OUT. Sensors: F=${lineFrontDetected}, R=${lineRearDetected}`); + } else if (bestMagnet) { + // Adaptive Steering + const angleError = normalizeAngle(bestMagnet.targetAngle - (isFwd ? state.rotation : state.rotation + 180)); + + // Sharp turn handling - Reduced aggression to prevent jitter + const steerFactor = Math.abs(angleError) > 20 ? 0.1 : 0.05; + state.rotation += angleError * steerFactor; + + const driftX = bestMagnet.proj.x - sensorPos.x; + const driftY = bestMagnet.proj.y - sensorPos.y; + + // Position correction (Damping + Clamping) + // This prevents "Warping" when switching to lines that are found far away (e.g. 60px) + const driftFactor = 0.1; // Reduced from 0.3 for smoother path following + let moveX = driftX * driftFactor; + let moveY = driftY * driftFactor; + + // CLAMP: Max lateral snap per frame to prevent teleportation + const MAX_SNAP = 2.0; + const moveDist = Math.sqrt(moveX*moveX + moveY*moveY); + if (moveDist > MAX_SNAP) { + const ratio = MAX_SNAP / moveDist; + moveX *= ratio; + moveY *= ratio; + } + + state.x += moveX; + state.y += moveY; + + if (bestMagnet.mag.type === 'CURVE') { + // Extra rotation correction for curves is now handled better by Derivative angle + // But we keep a small correctional factor for drift + const newSensorX = sensorPos.x + driftX; + const newSensorY = sensorPos.y + driftY; + const otherSensor = isFwd ? rearSensor : frontSensor; + const dx = newSensorX - otherSensor.x; + const dy = newSensorY - otherSensor.y; + let newAngle = Math.atan2(dy, dx) * (180/Math.PI); + if (!isFwd) newAngle += 180; + let diff = normalizeAngle(newAngle - state.rotation); + state.rotation += diff * 0.1; // Reduced significantly as derivative is more accurate + } + + // Apply Movement + const rad = (state.rotation * Math.PI) / 180; + const dirMult = isFwd ? 1 : -1; + state.x += Math.cos(rad) * moveSpeed * dirMult; + state.y += Math.sin(rad) * moveSpeed * dirMult; + } + + } else { + // Manual Movements (unchanged) + const rad = (state.rotation * Math.PI) / 180; + if (state.motionState === AgvMotionState.FORWARD) { + state.x += Math.cos(rad) * moveSpeed; + state.y += Math.sin(rad) * moveSpeed; + } else if (state.motionState === AgvMotionState.BACKWARD) { + state.x -= Math.cos(rad) * moveSpeed; + state.y -= Math.sin(rad) * moveSpeed; + } else if (state.motionState === AgvMotionState.TURN_LEFT) state.rotation -= TURN_SPEED; + else if (state.motionState === AgvMotionState.TURN_RIGHT) state.rotation += TURN_SPEED; + else if (state.motionState === AgvMotionState.TURN_LEFT_180) { + state.rotation -= TURN_SPEED; + // 180 Turn Logic (L-Turn = CCW): + // Stop when REAR sensor detects a new line (e.g. 90 degree cross), but ignore first ~60 degrees to clear original line. + + const startRot = state.targetRotation !== null ? state.targetRotation + 180 : state.rotation; + const degreesTurned = startRot - state.rotation; // Positive increasing + + const ignoreSensors = degreesTurned < 60; // Increased from 40 to 60 to prevent early false detection + + // L-Turn: Check REAR sensor for stop condition + const stopCondition = !ignoreSensors && lineRearDetected; + const reachedLimit = (state.targetRotation !== null && state.rotation <= state.targetRotation); + + // Debug Log + if (logThrottleRef.current % 10 === 0) { + onLog(`Turn180 [L]: Deg=${degreesTurned.toFixed(1)}/180 Ignore=${ignoreSensors ? 'Y':'N'} RearLine=${lineRearDetected ? 'YES':'NO'}`); + } + + if (stopCondition || reachedLimit) { + if (reachedLimit && state.targetRotation !== null) state.rotation = state.targetRotation; + state.motionState = AgvMotionState.IDLE; state.targetRotation = null; + onLog(`Turn180 [L] Complete. Reason: ${stopCondition ? 'Rear Sensor Hit' : 'Angle Reached'}`); + } + } else if (state.motionState === AgvMotionState.TURN_RIGHT_180) { + state.rotation += TURN_SPEED; + // 180 Turn Logic (R-Turn = CW): + // Stop when FRONT sensor detects a new line, but ignore first ~60 degrees. + + const startRot = state.targetRotation !== null ? state.targetRotation - 180 : state.rotation; + const degreesTurned = state.rotation - startRot; // Positive increasing + + const ignoreSensors = degreesTurned < 60; // Increased from 40 to 60 + + // R-Turn: Check FRONT sensor for stop condition + const stopCondition = !ignoreSensors && lineFrontDetected; + const reachedLimit = (state.targetRotation !== null && state.rotation >= state.targetRotation); + + // Debug Log + if (logThrottleRef.current % 10 === 0) { + onLog(`Turn180 [R]: Deg=${degreesTurned.toFixed(1)}/180 Ignore=${ignoreSensors ? 'Y':'N'} FrontLine=${lineFrontDetected ? 'YES':'NO'}`); + } + + if (stopCondition || reachedLimit) { + if (reachedLimit && state.targetRotation !== null) state.rotation = state.targetRotation; + state.motionState = AgvMotionState.IDLE; state.targetRotation = null; + onLog(`Turn180 [R] Complete. Reason: ${stopCondition ? 'Front Sensor Hit' : 'Angle Reached'}`); + } + } + } + } + + state.sensorLineFront = lineFrontDetected; + state.sensorLineRear = lineRearDetected; + + let rfidFound: string | null = null; + map.nodes.forEach(node => { if (node.rfidId && getDistance({x: state.x, y: state.y}, node) < 15) rfidFound = node.rfidId; }); + state.detectedRfid = rfidFound; + + let markFound = false; + const prevSensorPos = prevMarkSensorPosRef.current; + if (prevSensorPos) { + map.marks.forEach(mark => { + const rad = (mark.rotation * Math.PI) / 180; + const dx = Math.cos(rad) * 25; const dy = Math.sin(rad) * 25; + const ms = { x: mark.x - dx, y: mark.y - dy }; const me = { x: mark.x + dx, y: mark.y + dy }; + + // Check 1: Dynamic Crossing (Intersection) + if (doSegmentsIntersect(prevSensorPos, markSensor, ms, me)) { + markFound = true; + } + + // Check 2: Static Proximity (Distance to line segment) + // This ensures sensor stays ON when sitting on the mark + if (!markFound) { + const distToMarkLine = distanceToSegment(markSensor, ms, me); + // If within 10px of the mark tape (tape width is technically line width but visually 3px, sensor range is wider) + if (distToMarkLine < 10) { + markFound = true; + } + } + }); + } + prevMarkSensorPosRef.current = { x: markSensor.x, y: markSensor.y }; + state.sensorMark = markFound; + if (markFound && state.motionState === AgvMotionState.MARK_STOPPING) { state.motionState = AgvMotionState.IDLE; hasChanged = true; } + + if (hasChanged || state.sensorLineFront !== agvStateRef.current.sensorLineFront || state.sensorMark !== agvStateRef.current.sensorMark || state.error !== agvStateRef.current.error || state.rotation !== agvStateRef.current.rotation) { + agvStateRef.current = state; + setAgvState(state); + } + }; + + const draw = () => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + // Clear Screen + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // --- Apply Zoom & Pan --- + const t = viewRef.current; + ctx.save(); + ctx.translate(t.x, t.y); + ctx.scale(t.scale, t.scale); + + // --- Draw Infinite Grid --- + const visibleLeft = -t.x / t.scale; + const visibleTop = -t.y / t.scale; + const visibleRight = (canvas.width - t.x) / t.scale; + const visibleBottom = (canvas.height - t.y) / t.scale; + + const startX = Math.floor(visibleLeft / GRID_SIZE) * GRID_SIZE; + const endX = Math.floor(visibleRight / GRID_SIZE) * GRID_SIZE + GRID_SIZE; + const startY = Math.floor(visibleTop / GRID_SIZE) * GRID_SIZE; + const endY = Math.floor(visibleBottom / GRID_SIZE) * GRID_SIZE + GRID_SIZE; + + ctx.strokeStyle = '#374151'; + ctx.lineWidth = 1; + ctx.beginPath(); + // Verticals + for (let x = startX; x <= endX; x += GRID_SIZE) { + ctx.moveTo(x, startY); ctx.lineTo(x, endY); + } + // Horizontals + for (let y = startY; y <= endY; y += GRID_SIZE) { + ctx.moveTo(startX, y); ctx.lineTo(endX, y); + } + ctx.stroke(); + + const map = mapDataRef.current; + + // 1. Logical Edges (Thin Gray) + ctx.strokeStyle = '#4B5563'; + ctx.lineWidth = 1; + ctx.setLineDash([2, 2]); + map.edges.forEach(edge => { + const start = map.nodes.find(n => n.id === edge.from); + const end = map.nodes.find(n => n.id === edge.to); + if (start && end) { + ctx.beginPath(); ctx.moveTo(start.x, start.y); ctx.lineTo(end.x, end.y); ctx.stroke(); + } + }); + ctx.setLineDash([]); + + // 2. Magnet Lines (Wide, Transparent Blue) + ctx.lineWidth = MAGNET_WIDTH; + ctx.lineCap = 'round'; + map.magnets.forEach(mag => { + const isSelected = mag.id === selectedItemId; + ctx.strokeStyle = isSelected ? 'rgba(59, 130, 246, 0.8)' : MAGNET_COLOR; // Brighter if selected + + ctx.beginPath(); + ctx.moveTo(mag.p1.x, mag.p1.y); + if (mag.type === 'STRAIGHT') { + ctx.lineTo(mag.p2.x, mag.p2.y); + } else if (mag.type === 'CURVE' && mag.controlPoint) { + ctx.quadraticCurveTo(mag.controlPoint.x, mag.controlPoint.y, mag.p2.x, mag.p2.y); + } + ctx.stroke(); + + // Draw Controls for Selected Curve + if (isSelected && mag.type === 'CURVE' && mag.controlPoint) { + // Helper Lines + ctx.strokeStyle = 'rgba(255, 255, 0, 0.5)'; + ctx.lineWidth = 1; + ctx.setLineDash([3, 3]); + ctx.beginPath(); + ctx.moveTo(mag.p1.x, mag.p1.y); + ctx.lineTo(mag.controlPoint.x, mag.controlPoint.y); + ctx.lineTo(mag.p2.x, mag.p2.y); + ctx.stroke(); + ctx.setLineDash([]); + + // Control Points + const drawHandle = (p: {x:number, y:number}, color: string) => { + ctx.fillStyle = color; + ctx.beginPath(); ctx.arc(p.x, p.y, 5 / t.scale, 0, Math.PI*2); ctx.fill(); // Scale handle size + ctx.strokeStyle = '#FFF'; ctx.lineWidth = 1; ctx.stroke(); + }; + + drawHandle(mag.p1, '#3B82F6'); // Start (Blue) + drawHandle(mag.p2, '#3B82F6'); // End (Blue) + drawHandle(mag.controlPoint, '#EAB308'); // Control (Yellow) + } else if (isSelected && mag.type === 'STRAIGHT') { + // Simple handles for straight lines + const drawHandle = (p: {x:number, y:number}, color: string) => { + ctx.fillStyle = color; + ctx.beginPath(); ctx.arc(p.x, p.y, 5 / t.scale, 0, Math.PI*2); ctx.fill(); + ctx.strokeStyle = '#FFF'; ctx.lineWidth = 1; ctx.stroke(); + }; + drawHandle(mag.p1, '#3B82F6'); + drawHandle(mag.p2, '#3B82F6'); + } + }); + + // Nodes (Unchanged) + const images = imageCacheRef.current; + map.nodes.forEach(node => { + if (node.type === NodeType.Image && images[node.id]) { + const img = images[node.id]; + if (img.complete) { + const w = 100; const h = (img.height / img.width) * w; + ctx.drawImage(img, node.x - w/2, node.y - h/2, w, h); + } + return; + } + if (node.type === NodeType.Label && node.labelText) { + ctx.font = 'bold 20px Arial'; + const tm = ctx.measureText(node.labelText); + const w = tm.width + 20; const h = 30; + if (node.backColor !== 'Transparent') { ctx.fillStyle = node.backColor!; ctx.fillRect(node.x - w/2, node.y - h/2, w, h); } + ctx.fillStyle = node.foreColor || 'White'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(node.labelText, node.x, node.y); + return; + } + let color = '#4B5563'; + if (node.type === NodeType.Loader || node.type === NodeType.UnLoader) color = 'green'; + else if (node.type === NodeType.Charging || node.type === NodeType.ChargerStation) color = 'magenta'; + else if (node.type === NodeType.Buffer) color = 'green'; + else if (node.displayColor) color = node.displayColor; + + // Highlight Selected Node + if (node.id === selectedItemId) { + ctx.strokeStyle = '#FDE047'; ctx.lineWidth = 2; + ctx.strokeRect(node.x - 10, node.y - 10, 20, 20); + } + + ctx.fillStyle = color; + if (node.type !== NodeType.Normal) ctx.fillRect(node.x - 6, node.y - 6, 12, 12); + else { ctx.beginPath(); ctx.arc(node.x, node.y, 4, 0, Math.PI * 2); ctx.fill(); } + ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic'; ctx.font = '10px monospace'; ctx.fillStyle = '#9CA3AF'; + const topText = node.rfidId ? `[${node.rfidId}]` : node.id; ctx.fillText(topText, node.x, node.y - 10); + if (node.name) { ctx.font = 'bold 10px Arial'; ctx.fillStyle = '#FFFFFF'; ctx.fillText(node.name, node.x, node.y + 18); } + }); + + // Marks + ctx.strokeStyle = '#FCD34D'; ctx.lineWidth = 3; + map.marks.forEach(mark => { + if (mark.id === selectedItemId) { + ctx.shadowColor = '#FDE047'; ctx.shadowBlur = 10; + } else { + ctx.shadowBlur = 0; + } + ctx.save(); ctx.translate(mark.x, mark.y); ctx.rotate((mark.rotation * Math.PI) / 180); + ctx.beginPath(); ctx.moveTo(-25, 0); ctx.lineTo(25, 0); ctx.stroke(); ctx.restore(); + ctx.shadowBlur = 0; + + // Draw handles if selected + if (mark.id === selectedItemId) { + + // Re-apply transform for simple drawing in local space + ctx.save(); ctx.translate(mark.x, mark.y); ctx.rotate((mark.rotation * Math.PI) / 180); + + // Handles + const drawHandle = (x: number, y: number) => { + ctx.fillStyle = '#EAB308'; + ctx.beginPath(); ctx.arc(x, y, 4 / t.scale, 0, Math.PI*2); ctx.fill(); + ctx.strokeStyle = '#FFF'; ctx.lineWidth = 1; ctx.stroke(); + }; + drawHandle(-25, 0); // Start + drawHandle(25, 0); // End + + ctx.restore(); + } + }); + + // Drawing Helpers + + // -- Straight Magnet -- + if (activeTool === ToolType.DRAW_MAGNET_STRAIGHT && dragStartPos) { + ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.lineWidth = MAGNET_WIDTH; + ctx.beginPath(); + ctx.moveTo(dragStartPos.x, dragStartPos.y); + + // Use snapped mouse pos for preview + const curr = getSnappedPos(mouseWorldPosRef.current.x, mouseWorldPosRef.current.y); + ctx.lineTo(curr.x, curr.y); + ctx.stroke(); + } + + // -- Logical Line -- + if (activeTool === ToolType.DRAW_LINE && dragStartPos) { + ctx.lineWidth = 1; ctx.strokeStyle = '#60A5FA'; ctx.setLineDash([5, 5]); + ctx.beginPath(); + ctx.moveTo(dragStartPos.x, dragStartPos.y); + const curr = getSnappedPos(mouseWorldPosRef.current.x, mouseWorldPosRef.current.y); + ctx.lineTo(curr.x, curr.y); + ctx.stroke(); + ctx.setLineDash([]); + } + + // -- Curve Magnet (3-Step Drawing) -- + if (activeTool === ToolType.DRAW_MAGNET_CURVE && tempMagnet) { + const p1 = tempMagnet.p1!; + const p2 = tempMagnet.p2 || getSnappedPos(mouseWorldPosRef.current.x, mouseWorldPosRef.current.y); + + // Visualize Construction + ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.lineWidth = 1; + ctx.setLineDash([2, 2]); + ctx.beginPath(); + ctx.moveTo(p1.x, p1.y); + + let control = tempMagnet.controlPoint; + if (curvePhase === 1) { + // In phase 1, control is midpoint of straight line + control = { x: (p1.x + p2.x)/2, y: (p1.y + p2.y)/2 }; + ctx.lineTo(p2.x, p2.y); + } else if (curvePhase === 2) { + // In phase 2, control follows mouse (no snapping for control point usually feels smoother, but can apply if needed) + // Let's use raw mouse for control point smooth dragging + control = mouseWorldPosRef.current; + ctx.lineTo(control.x, control.y); + ctx.lineTo(p2.x, p2.y); + } + ctx.stroke(); + ctx.setLineDash([]); + + // Control Point Handle + if (control) { + ctx.fillStyle = '#60A5FA'; + ctx.beginPath(); ctx.arc(control.x, control.y, 4, 0, Math.PI * 2); ctx.fill(); + } + + // Render Final Curve Preview + ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'; + ctx.lineWidth = MAGNET_WIDTH; + ctx.beginPath(); + ctx.moveTo(p1.x, p1.y); + if (control) { + ctx.quadraticCurveTo(control.x, control.y, p2.x, p2.y); + } else { + ctx.lineTo(p2.x, p2.y); + } + ctx.stroke(); + } + + // AGV Drawing (Same as before) + const state = agvStateRef.current; + ctx.save(); ctx.translate(state.x, state.y); ctx.rotate((state.rotation * Math.PI) / 180); + + // Draw LiDAR Scanner if active (Directional: Front or Rear based on Motor Direction) + if (state.lidarEnabled) { + const isFwd = state.runConfig.direction === 'FWD'; + const sensorPos = isFwd ? SENSOR_OFFSET_FRONT : SENSOR_OFFSET_REAR; + + ctx.save(); + // Arc Scanner + ctx.fillStyle = 'rgba(6, 182, 212, 0.2)'; // Cyan transparent + ctx.strokeStyle = 'rgba(6, 182, 212, 0.5)'; + ctx.lineWidth = 1; + ctx.beginPath(); + + const baseAngle = isFwd ? 0 : Math.PI; // 0 for Front, 180 for Rear + + ctx.moveTo(sensorPos.x, sensorPos.y); + ctx.arc(sensorPos.x, sensorPos.y, 60, baseAngle - Math.PI/6, baseAngle + Math.PI/6); // +/- 30 degrees + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // Scanning line animation + const now = Date.now(); + const scanOffset = Math.sin(now / 200) * (Math.PI/6); + const currentAngle = baseAngle + scanOffset; + + ctx.beginPath(); + ctx.moveTo(sensorPos.x, sensorPos.y); + ctx.lineTo(sensorPos.x + Math.cos(currentAngle)*60, sensorPos.y + Math.sin(currentAngle)*60); + ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.stroke(); + + ctx.restore(); + } + + ctx.fillStyle = '#1F2937'; ctx.strokeStyle = '#3B82F6'; ctx.lineWidth = 2; + ctx.fillRect(-AGV_WIDTH / 2, -AGV_HEIGHT / 2, AGV_WIDTH, AGV_HEIGHT); ctx.strokeRect(-AGV_WIDTH / 2, -AGV_HEIGHT / 2, AGV_WIDTH, AGV_HEIGHT); + if (state.error) { ctx.fillStyle = '#EF4444'; ctx.beginPath(); ctx.arc(0, 0, 15, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#FFF'; ctx.font = 'bold 8px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('ERR', 0, 0); } + ctx.fillStyle = '#000'; ctx.fillRect(-AGV_WIDTH / 2 - 4, -10, 4, 20); ctx.fillRect(AGV_WIDTH / 2, -10, 4, 20); + ctx.fillStyle = '#10B981'; ctx.fillRect(AGV_HEIGHT / 2 - 10, -AGV_WIDTH / 2 + 5, 10, AGV_WIDTH - 10); + ctx.fillStyle = '#EF4444'; ctx.fillRect(-AGV_HEIGHT / 2 + 5, -AGV_WIDTH / 2 + 10, 15, 20); + ctx.fillStyle = '#FFF'; ctx.font = '10px Arial'; ctx.textBaseline = 'alphabetic'; ctx.fillText(`Z:${state.liftHeight}`, -AGV_HEIGHT / 2 + 12, -AGV_WIDTH / 2 + 25); + + // Draw Status Info Panel (Rear) + ctx.save(); + ctx.translate(-12, 0); // Move towards rear center + + // Panel Background + ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'; + ctx.strokeStyle = '#374151'; + ctx.lineWidth = 1; + ctx.fillRect(-10, -18, 20, 36); + ctx.strokeRect(-10, -18, 20, 36); + + // Speed (Top) + const spd = state.runConfig.speedLevel; + ctx.fillStyle = spd === 'H' ? '#F87171' : spd === 'M' ? '#FACC15' : '#4ADE80'; // Red/Yellow/Green + ctx.font = 'bold 12px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(spd, 0, -8); + + // Divider + ctx.strokeStyle = '#4B5563'; + ctx.beginPath(); ctx.moveTo(-8, 0); ctx.lineTo(8, 0); ctx.stroke(); + + // Branch (Bottom) + ctx.fillStyle = '#FFF'; + const br = state.runConfig.branch; + if (br === 'LEFT') { + ctx.save(); ctx.translate(0, 10); ctx.rotate(-Math.PI/2); ctx.fillText('➜', 0, 0); ctx.restore(); + } else if (br === 'RIGHT') { + ctx.save(); ctx.translate(0, 10); ctx.rotate(Math.PI/2); ctx.fillText('➜', 0, 0); ctx.restore(); + } else { + ctx.fillText('➜', 0, 10); + } + ctx.restore(); + + // Draw Magnet Key Icon if ON (Shifted forward to avoid status panel) + if (state.magnetOn) { + ctx.save(); + ctx.translate(15, -6); // Position shifted forward + ctx.fillStyle = '#FACC15'; // Yellow/Gold + ctx.strokeStyle = '#FACC15'; + ctx.lineWidth = 1.5; + + // Key Head (Circle) + ctx.beginPath(); + ctx.arc(0, -3, 3.5, 0, Math.PI*2); + ctx.stroke(); + // Key Hole + ctx.beginPath(); ctx.arc(0, -3, 1, 0, Math.PI*2); ctx.fill(); + + // Key Shaft + ctx.beginPath(); + ctx.moveTo(0, 0.5); + ctx.lineTo(0, 8); + + // Key Teeth + ctx.moveTo(0, 4); ctx.lineTo(3, 4); + ctx.moveTo(0, 6.5); ctx.lineTo(3, 6.5); + ctx.stroke(); + ctx.restore(); + } + + // Sensors + ctx.fillStyle = state.sensorLineFront ? '#F87171' : '#4B5563'; ctx.beginPath(); ctx.arc(SENSOR_OFFSET_FRONT.x, SENSOR_OFFSET_FRONT.y, 3, 0, Math.PI * 2); ctx.fill(); + ctx.fillStyle = state.sensorLineRear ? '#F87171' : '#4B5563'; ctx.beginPath(); ctx.arc(SENSOR_OFFSET_REAR.x, SENSOR_OFFSET_REAR.y, 3, 0, Math.PI * 2); ctx.fill(); + ctx.fillStyle = state.sensorMark ? '#FFFF00' : '#4B5563'; ctx.beginPath(); ctx.arc(SENSOR_OFFSET_MARK.x, SENSOR_OFFSET_MARK.y, state.sensorMark ? 6 : 4, 0, Math.PI * 2); ctx.fill(); if(state.sensorMark) { ctx.strokeStyle='#FFF'; ctx.stroke(); } + + if (state.motionState === AgvMotionState.RUNNING) { + const activeOffset = state.runConfig.direction === 'FWD' ? SENSOR_OFFSET_FRONT : SENSOR_OFFSET_REAR; + ctx.strokeStyle = '#00FF00'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(activeOffset.x, activeOffset.y, 6, 0, Math.PI * 2); ctx.stroke(); + } + ctx.strokeStyle = '#FFF'; ctx.beginPath(); ctx.moveTo(-10, 0); ctx.lineTo(10, 0); ctx.lineTo(5, -5); ctx.moveTo(10, 0); ctx.lineTo(5, 5); ctx.stroke(); + ctx.restore(); + + // Restore Transform + ctx.restore(); + + animationFrameId = requestAnimationFrame(draw); + }; + + const interval = setInterval(updatePhysics, 16); + animationFrameId = requestAnimationFrame(draw); + return () => { clearInterval(interval); cancelAnimationFrame(animationFrameId); }; + }, [activeTool, dragStartPos, tempMagnet, curvePhase, selectedItemId, dimensions]); // Re-draw when dimensions change + + // --- Interaction Handlers --- + + const snapToGrid = (val: number) => Math.round(val / (GRID_SIZE / 2)) * (GRID_SIZE / 2); // Snap to half-grid + + // Intelligent Snapping: Prefer Nodes, else Raw Position + const getSnappedPos = (rawX: number, rawY: number) => { + const map = mapDataRef.current; + const SNAP_DIST = 20; + + // 1. Try snapping to Nodes + let closestNode: MapNode | null = null; + let minDist = SNAP_DIST; + + map.nodes.forEach(node => { + const d = Math.sqrt(Math.pow(node.x - rawX, 2) + Math.pow(node.y - rawY, 2)); + if (d < minDist) { + minDist = d; + closestNode = node; + } + }); + + if (closestNode) { + return { x: closestNode.x, y: closestNode.y }; + } + + // 2. Fallback to Raw (No Grid Snap as per user request) + return { x: rawX, y: rawY }; + }; + + const handleWheel = (e: React.WheelEvent) => { + e.preventDefault(); + const rect = canvasRef.current?.getBoundingClientRect(); + if (!rect) return; + + const screenX = e.clientX - rect.left; + const screenY = e.clientY - rect.top; + + // Zoom Logic + const zoomIntensity = 0.1; + const delta = e.deltaY < 0 ? 1 : -1; + let newScale = viewRef.current.scale + delta * zoomIntensity; + newScale = Math.min(Math.max(0.2, newScale), 5); // Limit zoom + + // World pos before zoom + const worldX = (screenX - viewRef.current.x) / viewRef.current.scale; + const worldY = (screenY - viewRef.current.y) / viewRef.current.scale; + + // Adjust offset so world pos remains at screen pos + const newX = screenX - worldX * newScale; + const newY = screenY - worldY * newScale; + + viewRef.current = { x: newX, y: newY, scale: newScale }; + }; + + const handleMouseMove = (e: React.MouseEvent) => { + const rect = canvasRef.current?.getBoundingClientRect(); + if (!rect) return; + const screenX = e.clientX - rect.left; + const screenY = e.clientY - rect.top; + + mouseScreenPosRef.current = { x: screenX, y: screenY }; + const worldPos = screenToWorld(screenX, screenY); + mouseWorldPosRef.current = worldPos; + + // Pan + if (isPanning) { + viewRef.current = { + ...viewRef.current, + x: viewRef.current.x + e.movementX, + y: viewRef.current.y + e.movementY + }; + return; + } + + // Drag Handle (Editing) + if (dragHandle && selectedItemId) { + const snapped = getSnappedPos(worldPos.x, worldPos.y); + setMapData(prev => { + const next = { ...prev }; + if (dragHandle === 'p1' || dragHandle === 'p2' || dragHandle === 'control') { + const magIndex = next.magnets.findIndex(m => m.id === selectedItemId); + if (magIndex !== -1) { + const newMags = [...next.magnets]; + const mag = { ...newMags[magIndex] }; + if (dragHandle === 'p1') mag.p1 = snapped; + else if (dragHandle === 'p2') mag.p2 = snapped; + else if (dragHandle === 'control') mag.controlPoint = worldPos; // No snap for control + newMags[magIndex] = mag; + next.magnets = newMags; + } + } else if (dragHandle === 'mark_p1' || dragHandle === 'mark_p2') { + // Rotate Mark + const markIndex = next.marks.findIndex(m => m.id === selectedItemId); + if (markIndex !== -1) { + const newMarks = [...next.marks]; + const mark = { ...newMarks[markIndex] }; + const dx = worldPos.x - mark.x; + const dy = worldPos.y - mark.y; + let angle = Math.atan2(dy, dx) * (180 / Math.PI); + if (dragHandle === 'mark_p1') angle += 180; + mark.rotation = normalizeAngle(angle); + newMarks[markIndex] = mark; + next.marks = newMarks; + } + } + return next; + }); + return; + } + + // Drag Object (Moving) + if (dragTarget && dragTarget.initialObjState) { + const dx = worldPos.x - dragTarget.startMouse.x; + const dy = worldPos.y - dragTarget.startMouse.y; + + if (dragTarget.type === 'AGV') { + setAgvState(prev => ({ + ...prev, + x: dragTarget.initialObjState.x + dx, + y: dragTarget.initialObjState.y + dy + })); + } else if (dragTarget.type === 'NODE' && dragTarget.id) { + const snapped = getSnappedPos(dragTarget.initialObjState.x + dx, dragTarget.initialObjState.y + dy); + setMapData(prev => ({ + ...prev, + nodes: prev.nodes.map(n => n.id === dragTarget.id ? { ...n, x: snapped.x, y: snapped.y } : n) + })); + } else if (dragTarget.type === 'MARK' && dragTarget.id) { + setMapData(prev => ({ + ...prev, + marks: prev.marks.map(m => m.id === dragTarget.id ? { ...m, x: dragTarget.initialObjState.x + dx, y: dragTarget.initialObjState.y + dy } : m) + })); + } else if (dragTarget.type === 'MAGNET' && dragTarget.id) { + const m0 = dragTarget.initialObjState as MagnetLine; + const dxw = worldPos.x - dragTarget.startMouse.x; // World delta + const dyw = worldPos.y - dragTarget.startMouse.y; + + setMapData(prev => ({ + ...prev, + magnets: prev.magnets.map(m => { + if (m.id === dragTarget.id) { + const newM = { ...m }; + newM.p1 = { x: m0.p1.x + dxw, y: m0.p1.y + dyw }; + newM.p2 = { x: m0.p2.x + dxw, y: m0.p2.y + dyw }; + if (m0.controlPoint) newM.controlPoint = { x: m0.controlPoint.x + dxw, y: m0.controlPoint.y + dyw }; + return newM; + } + return m; + }) + })); + } + return; + } + }; + + const handleMouseUp = (e: React.MouseEvent) => { + setIsPanning(false); + setDragHandle(null); + setDragTarget(null); + + // Finish DRAW_LINE + if (activeTool === ToolType.DRAW_LINE && dragStartPos && startNodeId) { + const { x, y } = mouseWorldPosRef.current; + const existingNode = mapData.nodes.find(n => getDistance(n, {x, y}) < 20); + if (existingNode && existingNode.id !== startNodeId) { + const edgeExists = mapData.edges.some(e => + (e.from === startNodeId && e.to === existingNode.id) || + (e.from === existingNode.id && e.to === startNodeId) + ); + if (!edgeExists) { + const newEdge: MapEdge = { + id: crypto.randomUUID(), + from: startNodeId, + to: existingNode.id + }; + setMapData(prev => ({ ...prev, edges: [...prev.edges, newEdge] })); + } + } + } + + // Finish DRAW_MAGNET_STRAIGHT + if (activeTool === ToolType.DRAW_MAGNET_STRAIGHT && dragStartPos) { + const p1 = dragStartPos; + const p2 = getSnappedPos(mouseWorldPosRef.current.x, mouseWorldPosRef.current.y); + if (getDistance(p1, p2) > 5) { + const newMag: MagnetLine = { + id: crypto.randomUUID(), + type: 'STRAIGHT', + p1, + p2 + }; + setMapData(prev => ({ ...prev, magnets: [...prev.magnets, newMag] })); + } + } + + setDragStartPos(null); + setStartNodeId(null); + }; + + const handleMouseDown = (e: React.MouseEvent) => { + if (contextMenu) { + setContextMenu(null); + return; + } + + const rect = canvasRef.current?.getBoundingClientRect(); + if (!rect) return; + const screenX = e.clientX - rect.left; + const screenY = e.clientY - rect.top; + + if (e.button === 1 || (e.button === 0 && isSpacePressedRef.current)) { + setIsPanning(true); + return; + } + + const { x, y } = screenToWorld(screenX, screenY); + const worldPos = { x, y }; + + if (activeTool === ToolType.ERASER) { + const hits = getObjectsAt(x, y); + if (hits.length === 1) { + deleteObject(hits[0]); + } else if (hits.length > 1) { + setContextMenu({ x: screenX, y: screenY, items: hits }); + } + return; + } + + if (activeTool === ToolType.SELECT) { + // Check handles + const worldTolerance = 15 / viewRef.current.scale; + + if (selectedItemId) { + const selectedMag = mapData.magnets.find(m => m.id === selectedItemId); + if (selectedMag) { + if (getDistance(worldPos, selectedMag.p1) < worldTolerance) { setDragHandle('p1'); return; } + if (getDistance(worldPos, selectedMag.p2) < worldTolerance) { setDragHandle('p2'); return; } + if (selectedMag.type === 'CURVE' && selectedMag.controlPoint && getDistance(worldPos, selectedMag.controlPoint) < worldTolerance) { setDragHandle('control'); return; } + } + const selectedMark = mapData.marks.find(m => m.id === selectedItemId); + if (selectedMark) { + const rad = (selectedMark.rotation * Math.PI) / 180; + const dx = Math.cos(rad) * 25; const dy = Math.sin(rad) * 25; + const p1 = { x: selectedMark.x - dx, y: selectedMark.y - dy }; + const p2 = { x: selectedMark.x + dx, y: selectedMark.y + dy }; + if (getDistance(worldPos, p1) < worldTolerance) { setDragHandle('mark_p1'); return; } + if (getDistance(worldPos, p2) < worldTolerance) { setDragHandle('mark_p2'); return; } + } + } + + // AGV + const agvDist = getDistance(worldPos, agvState); + if (agvDist < 30) { + setDragTarget({ type: 'AGV', startMouse: worldPos, initialObjState: { x: agvState.x, y: agvState.y } }); + setSelectedItemId(null); + return; + } + + // Map Objects + const hits = getObjectsAt(x, y); + const movableHits = hits.filter(h => h.type !== 'EDGE'); + if (movableHits.length > 0) { + const hit = movableHits[0]; + setSelectedItemId(hit.id); + let initialObjState = null; + if (hit.type === 'NODE') { + const n = mapData.nodes.find(o => o.id === hit.id); + if (n) initialObjState = { x: n.x, y: n.y }; + } else if (hit.type === 'MARK') { + const m = mapData.marks.find(o => o.id === hit.id); + if (m) initialObjState = { x: m.x, y: m.y }; + } else if (hit.type === 'MAGNET') { + const m = mapData.magnets.find(o => o.id === hit.id); + if (m) initialObjState = JSON.parse(JSON.stringify(m)); + } + if (initialObjState) { + setDragTarget({ type: hit.type as any, id: hit.id, startMouse: worldPos, initialObjState }); + } + } else { + setSelectedItemId(null); + } + return; + } + + if (activeTool === ToolType.DRAW_MAGNET_CURVE) { + if (curvePhase === 0) { + const start = getSnappedPos(x, y); + setTempMagnet({ id: crypto.randomUUID(), type: 'CURVE', p1: start, p2: start, controlPoint: start }); + setCurvePhase(1); + } else if (curvePhase === 2) { + // Finish curve + if (tempMagnet && tempMagnet.p1 && tempMagnet.p2) { + const finalMagnet: MagnetLine = { + id: tempMagnet.id!, + type: 'CURVE', + p1: tempMagnet.p1, + p2: tempMagnet.p2, + controlPoint: mouseWorldPosRef.current + }; + setMapData(prev => ({ ...prev, magnets: [...prev.magnets, finalMagnet] })); + setTempMagnet(null); + setCurvePhase(0); + } + } + return; + } + + if (activeTool === ToolType.DRAW_LINE) { + const hits = getObjectsAt(x, y); + const nodeHit = hits.find(h => h.type === 'NODE'); + if (nodeHit) { + const n = mapData.nodes.find(n => n.id === nodeHit.id); + if (n) { + setDragStartPos({ x: n.x, y: n.y }); + setStartNodeId(n.id); + } + } else { + const snapped = getSnappedPos(x, y); + const newNode: MapNode = { + id: `N${Date.now()}`, + x: snapped.x, y: snapped.y, + type: 0, + name: "", + rfidId: "", + connectedNodes: [] + }; + setMapData(prev => ({ ...prev, nodes: [...prev.nodes, newNode] })); + } + return; + } + + if (activeTool === ToolType.DRAW_MAGNET_STRAIGHT) { + const snapped = getSnappedPos(x, y); + setDragStartPos(snapped); + return; + } + + if (activeTool === ToolType.ADD_MARK) { + const snapped = getSnappedPos(x, y); + const newMark: FloorMark = { + id: crypto.randomUUID(), + x: snapped.x, + y: snapped.y, + rotation: 0 + }; + setMapData(prev => ({ ...prev, marks: [...prev.marks, newMark] })); + return; + } + + if (activeTool === ToolType.ADD_RFID) { + const hits = getObjectsAt(x, y); + const nodeHit = hits.find(h => h.type === 'NODE'); + if (nodeHit) { + const rfid = prompt("Enter RFID ID:", "12345"); + if (rfid) { + setMapData(prev => ({ + ...prev, + nodes: prev.nodes.map(n => n.id === nodeHit.id ? { ...n, rfidId: rfid } : n) + })); + } + } + } + }; + + return ( +
+ setIsPanning(false)} + className="block bg-gray-950 outline-none w-full h-full" + /> + + {/* HUD Info */} +
+ Pos: {Math.round(agvState.x)}, {Math.round(agvState.y)} | Rot: {Math.round(agvState.rotation)}° + | Zoom: {Math.round(viewRef.current.scale * 100)}% + {activeTool === ToolType.DRAW_MAGNET_CURVE && curvePhase === 1 && Drag to set Endpoint} + {activeTool === ToolType.DRAW_MAGNET_CURVE && curvePhase === 2 && Move mouse to bend curve, Click to finish} +
+ Middle Click / Space+Drag to Pan + {selectedItemId && Object Selected (Drag points to edit)} +
+
+ + {/* Context Menu for Eraser */} + {contextMenu && ( +
+
+ Select object to delete + +
+
    + {contextMenu.items.map((item, idx) => ( +
  • deleteObject(item)} + className="px-2 py-1.5 hover:bg-red-900/50 hover:text-red-200 cursor-pointer flex items-center gap-2" + > + + {item.name} + {Math.round(item.dist)}px +
  • + ))} +
+
+ )} +
+ ); +}; + +export default SimulationCanvas; \ No newline at end of file diff --git a/components/SystemLogPanel.tsx b/components/SystemLogPanel.tsx new file mode 100644 index 0000000..ae5b6a6 --- /dev/null +++ b/components/SystemLogPanel.tsx @@ -0,0 +1,69 @@ +import React, { useRef, useEffect, useMemo } from 'react'; +import { LogEntry } from '../types'; +import { Terminal, Trash2, Info } from 'lucide-react'; + +interface SystemLogPanelProps { + logs: LogEntry[]; + onClear: () => void; +} + +const SystemLogPanel: React.FC = ({ logs, onClear }) => { + const logContainerRef = useRef(null); + + const reversedLogs = useMemo(() => [...logs].reverse(), [logs]); + + // Force scroll to top + useEffect(() => { + if (logContainerRef.current) { + logContainerRef.current.scrollTop = 0; + } + }, [reversedLogs]); + + return ( +
+ {/* Header */} +
+
+
+ SYSTEM LOG +
+
+ + +
+ + {/* Log Body */} +
+ {reversedLogs.length === 0 && ( +
+ No System Activity +
+ )} + + {reversedLogs.map((log, index) => ( +
+ {index + 1} + {log.timestamp} + + {log.message} + +
+ ))} +
+
+ ); +}; + +export default SystemLogPanel; \ No newline at end of file diff --git a/constants.ts b/constants.ts new file mode 100644 index 0000000..bcc5887 --- /dev/null +++ b/constants.ts @@ -0,0 +1,32 @@ + +export const AGV_WIDTH = 40; +export const AGV_HEIGHT = 60; // Length +export const WHEEL_RADIUS = 8; +export const MAX_SPEED = 2.0; +export const TURN_SPEED = 1.5; +export const MARK_SEARCH_SPEED = 0.5; +export const GRID_SIZE = 50; + +// Magnet Line Visuals +export const MAGNET_WIDTH = 10; +export const MAGNET_COLOR = 'rgba(59, 130, 246, 0.4)'; // Transparent Blue +export const MAGNET_COLOR_ACTIVE = 'rgba(59, 130, 246, 0.6)'; + +// Run Mode Speeds +export const SPEED_L = 0.5; +export const SPEED_M = 1.5; +export const SPEED_H = 3.0; + +// Sensor positions relative to center (0,0) +// AGV faces "Right" (0 deg) in local calculation space before rotation +export const SENSOR_OFFSET_FRONT = { x: 28, y: 0 }; +export const SENSOR_OFFSET_REAR = { x: -28, y: 0 }; +// Mark sensor: Shifted left relative to forward direction +export const SENSOR_OFFSET_MARK = { x: 0, y: -12 }; + +export const INITIAL_MAP = { + nodes: [], + edges: [], + magnets: [], + marks: [], +}; \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..6065966 --- /dev/null +++ b/index.html @@ -0,0 +1,41 @@ + + + + + + AGV Simulator + + + + + + +
+ + + \ No newline at end of file diff --git a/index.tsx b/index.tsx new file mode 100644 index 0000000..6ca5361 --- /dev/null +++ b/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error("Could not find root element to mount to"); +} + +const root = ReactDOM.createRoot(rootElement); +root.render( + + + +); \ No newline at end of file diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..9ae3226 --- /dev/null +++ b/metadata.json @@ -0,0 +1,7 @@ +{ + "description": "Generated by Gemini.", + "requestFramePermissions": [ + "serial" + ], + "name": "AGV Emulator" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f67d3d3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1771 @@ +{ + "name": "agv-emulator", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agv-emulator", + "version": "0.0.0", + "dependencies": { + "lucide-react": "^0.561.0", + "react": "^19.2.3", + "react-dom": "^19.2.3" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", + "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", + "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", + "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", + "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", + "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", + "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", + "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", + "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", + "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz", + "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", + "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", + "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", + "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", + "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", + "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", + "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", + "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", + "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", + "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", + "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz", + "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz", + "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.10.tgz", + "integrity": "sha512-2VIKvDx8Z1a9rTB2eCkdPE5nSe28XnA+qivGnWHoB40hMMt/h1hSz0960Zqsn6ZyxWXUie0EBdElKv8may20AA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.561.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.561.0.tgz", + "integrity": "sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz", + "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.5", + "@rollup/rollup-android-arm64": "4.53.5", + "@rollup/rollup-darwin-arm64": "4.53.5", + "@rollup/rollup-darwin-x64": "4.53.5", + "@rollup/rollup-freebsd-arm64": "4.53.5", + "@rollup/rollup-freebsd-x64": "4.53.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", + "@rollup/rollup-linux-arm-musleabihf": "4.53.5", + "@rollup/rollup-linux-arm64-gnu": "4.53.5", + "@rollup/rollup-linux-arm64-musl": "4.53.5", + "@rollup/rollup-linux-loong64-gnu": "4.53.5", + "@rollup/rollup-linux-ppc64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-musl": "4.53.5", + "@rollup/rollup-linux-s390x-gnu": "4.53.5", + "@rollup/rollup-linux-x64-gnu": "4.53.5", + "@rollup/rollup-linux-x64-musl": "4.53.5", + "@rollup/rollup-openharmony-arm64": "4.53.5", + "@rollup/rollup-win32-arm64-msvc": "4.53.5", + "@rollup/rollup-win32-ia32-msvc": "4.53.5", + "@rollup/rollup-win32-x64-gnu": "4.53.5", + "@rollup/rollup-win32-x64-msvc": "4.53.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cc108b4 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "agv-emulator", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react-dom": "^19.2.3", + "lucide-react": "^0.561.0", + "react": "^19.2.3" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} diff --git a/services/serialService.ts b/services/serialService.ts new file mode 100644 index 0000000..b8e4bf2 --- /dev/null +++ b/services/serialService.ts @@ -0,0 +1,124 @@ +export type SerialDataCallback = (data: Uint8Array) => void; + +// Define Web Serial API types +interface SerialPortInfo { + usbVendorId?: number; + usbProductId?: number; +} + +interface SerialPort { + open(options: { baudRate: number }): Promise; + close(): Promise; + getInfo(): SerialPortInfo; + readable: ReadableStream | null; + writable: WritableStream | null; +} + +declare global { + interface Navigator { + serial: { + requestPort(options?: any): Promise; + }; + } +} + +export class SerialPortHandler { + private port: SerialPort | null = null; + private reader: ReadableStreamDefaultReader | null = null; + private writer: WritableStreamDefaultWriter | null = null; + private isConnected: boolean = false; + private onData: SerialDataCallback; + + constructor(onData: SerialDataCallback) { + this.onData = onData; + } + + async connect(baudRate: number = 9600) { + if (!navigator.serial) { + throw new Error('Web Serial API not supported in this browser.'); + } + + try { + this.port = await navigator.serial.requestPort(); + await this.port.open({ baudRate }); + + if (this.port.readable) { + this.reader = this.port.readable.getReader(); + } + + if (this.port.writable) { + this.writer = this.port.writable.getWriter(); + } + + this.isConnected = true; + this.readLoop(); + } catch (error: any) { + if (error.name === 'NotFoundError') { + throw new Error('USER_CANCELLED'); + } + console.error('Serial connection failed:', error); + this.disconnect(); + throw error; + } + } + + async disconnect() { + if (this.reader) { + await this.reader.cancel(); + this.reader.releaseLock(); + } + if (this.writer) { + await this.writer.close(); + this.writer.releaseLock(); + } + if (this.port) { + await this.port.close(); + } + this.isConnected = false; + this.port = null; + } + + async send(data: string | Uint8Array) { + if (this.writer && this.isConnected) { + let payload: Uint8Array; + if (typeof data === 'string') { + payload = new TextEncoder().encode(data + '\n'); + } else { + payload = data; + } + await this.writer.write(payload); + } + } + + getPortInfo(): string | null { + if (this.port) { + const info = this.port.getInfo(); + if (info.usbVendorId && info.usbProductId) { + return `ID:${info.usbVendorId.toString(16).padStart(4,'0').toUpperCase()}:${info.usbProductId.toString(16).padStart(4,'0').toUpperCase()}`; + } + return "USB Device"; + } + return null; + } + + private async readLoop() { + while (true) { + try { + const { value, done } = await this.reader!.read(); + if (done) { + break; + } + if (value) { + this.onData(value); + } + } catch (error) { + console.error('Serial read error:', error); + break; + } + } + } + + get connected() { + return this.isConnected; + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2c6eed5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + "types": [ + "node" + ], + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "allowJs": true, + "jsx": "react-jsx", + "paths": { + "@/*": [ + "./*" + ] + }, + "allowImportingTsExtensions": true, + "noEmit": true + } +} \ No newline at end of file diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..66a6dc8 --- /dev/null +++ b/types.ts @@ -0,0 +1,282 @@ + + +export enum ToolType { + SELECT = 'SELECT', + DRAW_LINE = 'DRAW_LINE', // Logical Edge + DRAW_MAGNET_STRAIGHT = 'DRAW_MAGNET_STRAIGHT', // Physical Line + DRAW_MAGNET_CURVE = 'DRAW_MAGNET_CURVE', // Physical Curve + ADD_RFID = 'ADD_RFID', + ADD_MARK = 'ADD_MARK', + ERASER = 'ERASER', +} + +export interface Point { + x: number; + y: number; +} + +// C# MapNode Type Enum (Internal use) +export enum NodeType { + Normal = 0, + Loader = 1, + UnLoader = 2, + Charging = 3, // Used for Cleaner in new map + Buffer = 4, + ChargerStation = 5, + Label = 6, + Image = 7 +} + +// React Internal Node Structure +export interface MapNode extends Point { + id: string; // NodeId + type: number; // NodeType (Internal enum) + name: string; + rfidId: string; // RFID is property of Node + connectedNodes: string[]; // Keep track for export + + // Visual props + labelText?: string; + foreColor?: string; + backColor?: string; + imageBase64?: string; + displayColor?: string; + + // New props preservation + fontSize?: number; +} + +export interface MapEdge { + id: string; + from: string; + to: string; +} + +// Physical Magnet Line for AGV to follow +export interface MagnetLine { + id: string; + type: 'STRAIGHT' | 'CURVE'; + p1: Point; + p2: Point; + controlPoint?: Point; // Required for CURVE (Quadratic Bezier control point) +} + +// Mark Sensor now needs rotation to be "crossed" on the line +export interface FloorMark extends Point { + id: string; + rotation: number; // Degrees +} + +export interface SimulationMap { + nodes: MapNode[]; + edges: MapEdge[]; // Logical Graph Connections + magnets: MagnetLine[]; // Physical Guide Tape + marks: FloorMark[]; +} + +// --- C# Data Transfer Objects (New JSON Structure) --- + +export interface CSharpNode { + Id: string; + Text: string; + Position: string; // "x, y" + Type: number; // Usually 0 for nodes + StationType: number; // Functional type + ConnectedNodes: string[]; + RfidId: string; + NodeTextForeColor?: string; + NodeTextFontSize?: number; + // Flags (Optional for TS if not used logic-side, but good to preserve) + CanDocking?: boolean; + DockDirection?: number; + CanTurnLeft?: boolean; + CanTurnRight?: boolean; + DisableCross?: boolean; + IsActive?: boolean; + SpeedLimit?: number; + AliasName?: string; +} + +export interface CSharpLabel { + Id: string; + Type: number; // 1 + Text: string; + Position: string; + ForeColor: string; + BackColor: string; + FontFamily?: string; + FontSize?: number; + FontStyle?: number; + Padding?: number; +} + +export interface CSharpImage { + Id: string; + Type: number; // 2 + Name: string; + Position: string; + ImagePath?: string; + ImageBase64?: string; + Scale?: string; + Opacity?: number; + Rotation?: number; +} + +export interface CSharpMagnet { + Id: string; + Type: number; // 4 + P1: { X: number; Y: number }; + P2: { X: number; Y: number }; + ControlPoint?: { X: number; Y: number } | null; +} + +export interface CSharpMark { + Id: string; + Type: number; // 3 + Position: string; + X: number; + Y: number; + Rotation: number; +} + +export interface CSharpMapData { + Nodes: CSharpNode[]; + Labels?: CSharpLabel[]; + Images?: CSharpImage[]; + Magnets: CSharpMagnet[]; + Marks: CSharpMark[]; + Settings?: any; + Version: string; + CreatedDate: string; +} + +export enum AgvMotionState { + IDLE = 'IDLE', + FORWARD = 'FORWARD', + BACKWARD = 'BACKWARD', + TURN_LEFT = 'TURN_LEFT', + TURN_RIGHT = 'TURN_RIGHT', + TURN_LEFT_180 = 'TURN_LEFT_180', + TURN_RIGHT_180 = 'TURN_RIGHT_180', + MARK_STOPPING = 'MARK_STOPPING', + RUNNING = 'RUNNING', +} + +export interface AgvRunConfig { + direction: 'FWD' | 'BWD'; + branch: 'STRAIGHT' | 'LEFT' | 'RIGHT'; + speedLevel: 'L' | 'M' | 'H'; +} + +// --- Protocol Enums (Matched with C# DevAGV.cs) --- + +export enum AgvError { + Emergency = 0, + Overcurrent = 1, + Charger_run_error = 2, + Charger_pos_error = 3, + line_out_error = 4, + runerror_by_no_magent_line = 5, + agv_system_error=6, + battery_low_voltage=7, + lift_time_over=9, + lift_driver_ocr=10, + lift_driver_emg = 11, + arrive_ctl_comm_error = 12, + door_ctl_comm_error = 13, + charger_comm_error = 14, + cross_ctrl_comm_error = 15, +} + +export enum AgvSignal { + front_gate_out = 0, + rear_sensor_out = 1, + mark_sensor_1 = 2, + mark_sensor_2 = 3, + lift_down_sensor = 4, + lift_up_sensor = 5, + magnet_relay = 6, + charger_align_sensor = 7, + front_center_sensor = 8, +} + +export enum SystemFlag0 { + Memory_RW_State = 5, + EXT_IO_Conn_State = 6, + RFID_Conn_State = 7, + M5E_Module_Run_State = 8, + Front_Ultrasonic_Conn_State = 9, + Front_Untrasonic_Sensor_State = 10, + Side_Ultrasonic_Conn_State = 11, + Side_Ultrasonic_Sensor_State = 12, + Front_Guide_Sensor_State = 13, + Rear_Guide_Sensor_State = 14, + Battery_Level_Check = 15 +} + +export enum SystemFlag1 { + Side_Detect_Ignore = 3, + Melody_check = 4, + Mark2_check = 5, + Mark1_check = 6, + gateout_check = 7, + Battery_charging = 8, + re_Start = 9, + front_detect_ignore = 10, + front_detect_check = 11, + stop_by_front_detect = 12, + stop_by_cross_in = 13, + agv_stop = 14, + agv_run = 15 +} + +export interface AgvState { + x: number; + y: number; + rotation: number; + targetRotation: number | null; + speed: number; + liftHeight: number; + motionState: AgvMotionState; + runConfig: AgvRunConfig; + error: string | null; + sensorStatus: string; // Corresponds to sts_sensor in C# + + // Physical Sensors + detectedRfid: string | null; + sensorLineFront: boolean; + sensorLineRear: boolean; + sensorMark: boolean; + + // New features + magnetOn: boolean; + liftStatus: 'IDLE' | 'UP' | 'DOWN'; + lidarEnabled: boolean; // 1=ON, 0=OFF + + // Protocol Flags (Integers representing bitmaps) + system0: number; + system1: number; + errorFlags: number; + signalFlags: number; + + // Battery + batteryLevel: number; // Percentage 0-100 + maxCapacity: number; // Ah (Total Capacity) + batteryTemp: number; + cellVoltages: number[]; +} + +export interface LogEntry { + id: string; + timestamp: string; + type: 'INFO' | 'RX' | 'TX' | 'ERROR'; + source: 'AGV' | 'BMS' | 'ACS' | 'SYSTEM'; + message: string; +} + +export interface AcsPacket { + id: number; + command: number; + data: number[]; + valid: boolean; +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..ee5fb8d --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,23 @@ +import path from 'path'; +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, '.', ''); + return { + server: { + port: 3000, + host: '0.0.0.0', + }, + plugins: [react()], + define: { + 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + } + } + }; +});