feat: Enhance PropertyPanel, add RFID auto-gen, and fix node connections
This commit is contained in:
29
App.tsx
29
App.tsx
@@ -10,6 +10,7 @@ import AcsControls from './components/AcsControls';
|
||||
import AgvStatusPanel from './components/AgvStatusPanel';
|
||||
import AgvAutoRunControls from './components/AgvAutoRunControls';
|
||||
import SystemLogPanel from './components/SystemLogPanel';
|
||||
import PropertyPanel from './components/PropertyPanel';
|
||||
import { SerialPortHandler } from './services/serialService';
|
||||
|
||||
// --- ACS CRC16 Table Generation (Matches C# Logic) ---
|
||||
@@ -43,6 +44,7 @@ const calculateAcsCrc16 = (data: number[] | Uint8Array): number => {
|
||||
const App: React.FC = () => {
|
||||
// --- State ---
|
||||
const [activeTool, setActiveTool] = useState<ToolType>(ToolType.SELECT);
|
||||
const [selectedItemIds, setSelectedItemIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Map Data
|
||||
const [mapData, setMapData] = useState<SimulationMap>(INITIAL_MAP);
|
||||
@@ -123,6 +125,20 @@ const App: React.FC = () => {
|
||||
const [bmsPortInfo, setBmsPortInfo] = useState<string | null>(null);
|
||||
const [acsPortInfo, setAcsPortInfo] = useState<string | null>(null);
|
||||
|
||||
const handlePropertyUpdate = (type: 'NODE' | 'MAGNET' | 'MARK', id: string, data: any) => {
|
||||
setMapData(prev => {
|
||||
const next = { ...prev };
|
||||
if (type === 'NODE') {
|
||||
next.nodes = prev.nodes.map(n => n.id === id ? { ...n, ...data } : n);
|
||||
} else if (type === 'MAGNET') {
|
||||
next.magnets = prev.magnets.map(m => m.id === id ? { ...m, ...data } : m);
|
||||
} else if (type === 'MARK') {
|
||||
next.marks = prev.marks.map(m => m.id === id ? { ...m, ...data } : m);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Serial Configuration
|
||||
const [agvBaudRate, setAgvBaudRate] = useState(57600);
|
||||
const [bmsBaudRate, setBmsBaudRate] = useState(9600);
|
||||
@@ -443,6 +459,11 @@ const App: React.FC = () => {
|
||||
barr[22] = s.sensorStatus.charCodeAt(0);
|
||||
barr.set(encodeHex(s.signalFlags, 2), 23);
|
||||
|
||||
// Fill bytes 25-30 with '0' padding
|
||||
for (let i = 25; i <= 30; i++) {
|
||||
barr[i] = 0x30; // '0'
|
||||
}
|
||||
|
||||
barr[31] = '*'.charCodeAt(0); barr[32] = '*'.charCodeAt(0); barr[33] = 0x03;
|
||||
|
||||
agvSerialRef.current.send(barr);
|
||||
@@ -1206,6 +1227,13 @@ const App: React.FC = () => {
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{/* Overlay: Property Panel (Floating) */}
|
||||
<PropertyPanel
|
||||
selectedItemIds={selectedItemIds}
|
||||
mapData={mapData}
|
||||
onUpdate={handlePropertyUpdate}
|
||||
/>
|
||||
|
||||
{/* 1. Far Left: Toolbar */}
|
||||
<div className="w-16 p-2 border-r border-gray-800 flex flex-col items-center shrink-0">
|
||||
<EditorToolbar
|
||||
@@ -1271,6 +1299,7 @@ const App: React.FC = () => {
|
||||
agvState={agvState}
|
||||
setAgvState={setAgvState}
|
||||
onLog={(msg) => addLog('SYSTEM', 'INFO', msg)}
|
||||
onSelectionChange={setSelectedItemIds}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const AgvStatusPanel: React.FC<AgvStatusPanelProps> = ({ agvState }) => {
|
||||
const isOn = (value & (1 << bitIndex)) !== 0;
|
||||
// 필터링에 의해 isOn이 true인 것만 전달되지만 스타일 유지를 위해 체크 로직 유지
|
||||
return (
|
||||
<div className="flex items-center justify-between text-[10px] py-0.5 border-b border-gray-800 last:border-0 animate-in fade-in slide-in-from-left-1 duration-200">
|
||||
<div key={label} className="flex items-center justify-between text-[10px] py-0.5 border-b border-gray-800 last:border-0 animate-in fade-in slide-in-from-left-1 duration-200">
|
||||
<span className="text-gray-300 truncate pr-2" title={label}>{label.replace(/_/g, ' ')}</span>
|
||||
<div className={`w-2.5 h-2.5 rounded-full flex-shrink-0 ${isOn ? colorClass : 'bg-gray-700'} shadow-sm`} />
|
||||
</div>
|
||||
|
||||
295
components/PropertyPanel.tsx
Normal file
295
components/PropertyPanel.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { SimulationMap, MapNode, MagnetLine, FloorMark } from '../types';
|
||||
import { Save, Wand2 } from 'lucide-react';
|
||||
|
||||
interface PropertyPanelProps {
|
||||
selectedItemIds: Set<string>;
|
||||
mapData: SimulationMap;
|
||||
onUpdate: (type: 'NODE' | 'MAGNET' | 'MARK', id: string, data: any) => void;
|
||||
}
|
||||
|
||||
const PropertyPanel: React.FC<PropertyPanelProps> = ({ selectedItemIds, mapData, onUpdate }) => {
|
||||
const [formData, setFormData] = useState<any>(null);
|
||||
const [position, setPosition] = useState({ x: 1100, y: 100 });
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const dragOffset = React.useRef({ x: 0, y: 0 });
|
||||
|
||||
// When selection changes, update form data
|
||||
useEffect(() => {
|
||||
if (selectedItemIds.size === 1) {
|
||||
const id = Array.from(selectedItemIds)[0];
|
||||
const node = mapData.nodes.find(n => n.id === id);
|
||||
const magnet = mapData.magnets.find(m => m.id === id);
|
||||
const mark = mapData.marks.find(m => m.id === id);
|
||||
|
||||
if (node) {
|
||||
setFormData({ ...node, formType: 'NODE' });
|
||||
} else if (magnet) {
|
||||
setFormData({ ...magnet, formType: 'MAGNET' });
|
||||
} else if (mark) {
|
||||
setFormData({ ...mark, formType: 'MARK' });
|
||||
} else {
|
||||
setFormData(null);
|
||||
}
|
||||
} else {
|
||||
setFormData(null);
|
||||
}
|
||||
}, [selectedItemIds, mapData.nodes, mapData.magnets, mapData.marks]);
|
||||
|
||||
const handleChange = (field: string, value: string | number | boolean) => {
|
||||
if (!formData) return;
|
||||
setFormData((prev: any) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!formData) return;
|
||||
|
||||
// Validate RFID Duplication
|
||||
if (formData.formType === 'NODE' && formData.rfidId && formData.rfidId !== '0000') {
|
||||
const isDuplicate = mapData.nodes.some(n =>
|
||||
n.rfidId === formData.rfidId && n.id !== formData.id
|
||||
);
|
||||
|
||||
if (isDuplicate) {
|
||||
alert(`Error: RFID "${formData.rfidId}" is already used by another node.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onUpdate(formData.formType, formData.id, formData);
|
||||
};
|
||||
|
||||
// Auto Generate RFID
|
||||
const handleAutoRfid = () => {
|
||||
const usedRfids = new Set(mapData.nodes.map(n => n.rfidId));
|
||||
let nextRfid = 1; // Start from 1, assuming 0000 is default/null
|
||||
while (nextRfid < 10000) {
|
||||
const candidate = nextRfid.toString().padStart(4, '0');
|
||||
if (!usedRfids.has(candidate)) {
|
||||
handleChange('rfidId', candidate);
|
||||
return;
|
||||
}
|
||||
nextRfid++;
|
||||
}
|
||||
alert('No available RFID found between 0001 and 9999');
|
||||
};
|
||||
|
||||
// Drag Logic
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
setIsDragging(true);
|
||||
dragOffset.current = {
|
||||
x: e.clientX - position.x,
|
||||
y: e.clientY - position.y
|
||||
};
|
||||
};
|
||||
|
||||
const handleMouseMove = React.useCallback((e: MouseEvent) => {
|
||||
if (isDragging) {
|
||||
setPosition({
|
||||
x: e.clientX - dragOffset.current.x,
|
||||
y: e.clientY - dragOffset.current.y
|
||||
});
|
||||
}
|
||||
}, [isDragging]);
|
||||
|
||||
const handleMouseUp = React.useCallback(() => {
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
} else {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isDragging, handleMouseMove, handleMouseUp]);
|
||||
|
||||
|
||||
if (!formData || selectedItemIds.size !== 1) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
zIndex: 100
|
||||
}}
|
||||
className="w-64 bg-gray-800 border border-gray-600 rounded-lg shadow-xl overflow-hidden flex flex-col"
|
||||
>
|
||||
{/* Header / Drag Handle */}
|
||||
<div
|
||||
onMouseDown={handleMouseDown}
|
||||
className="bg-gray-700 p-2 cursor-move flex items-center justify-between border-b border-gray-600 select-none"
|
||||
>
|
||||
<span className="text-gray-200 font-bold text-xs">Properties</span>
|
||||
<button
|
||||
onMouseDown={(e) => e.stopPropagation()} // Prevent drag when clicking button
|
||||
onClick={handleSave}
|
||||
className="flex items-center gap-1 bg-blue-600 hover:bg-blue-500 text-white px-2 py-0.5 rounded text-[10px] transition-colors"
|
||||
>
|
||||
<Save size={12} /> Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 flex flex-col gap-4 max-h-[400px] overflow-y-auto">
|
||||
{/* Common Fields */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs text-gray-400">ID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.id}
|
||||
disabled
|
||||
className="bg-gray-900 border border-gray-700 text-gray-400 text-sm p-1 rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.formType === 'NODE' && (
|
||||
<>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs text-gray-400">Name (Alias)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name || ''}
|
||||
onChange={(e) => handleChange('name', e.target.value)}
|
||||
className="bg-gray-700 border border-gray-600 text-gray-200 text-sm p-1 rounded focus:border-blue-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs text-gray-400">RFID ID</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={formData.rfidId || ''}
|
||||
onChange={(e) => handleChange('rfidId', e.target.value)}
|
||||
className="flex-1 bg-gray-700 border border-gray-600 text-gray-200 text-sm p-1 rounded focus:border-blue-500 outline-none font-mono"
|
||||
/>
|
||||
<button
|
||||
onClick={handleAutoRfid}
|
||||
title="Auto Generate Unique RFID"
|
||||
className="p-1 bg-gray-600 hover:bg-gray-500 text-white rounded transition-colors"
|
||||
>
|
||||
<Wand2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs text-gray-400">X</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.x}
|
||||
onChange={(e) => handleChange('x', parseFloat(e.target.value))}
|
||||
className="bg-gray-700 border border-gray-600 text-gray-200 text-sm p-1 rounded focus:border-blue-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs text-gray-400">Y</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.y}
|
||||
onChange={(e) => handleChange('y', parseFloat(e.target.value))}
|
||||
className="bg-gray-700 border border-gray-600 text-gray-200 text-sm p-1 rounded focus:border-blue-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-600 my-2 pt-2">
|
||||
<label className="text-xs font-bold text-gray-300 mb-2 block">Configuration</label>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 mb-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] text-gray-400">Station Type</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.stationType || 0}
|
||||
onChange={(e) => handleChange('stationType', parseInt(e.target.value))}
|
||||
className="bg-gray-700 border border-gray-600 text-gray-200 text-xs p-1 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] text-gray-400">Speed Limit</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.speedLimit || 0}
|
||||
onChange={(e) => handleChange('speedLimit', parseInt(e.target.value))}
|
||||
className="bg-gray-700 border border-gray-600 text-gray-200 text-xs p-1 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] text-gray-400">Dock Dir</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.dockDirection || 0}
|
||||
onChange={(e) => handleChange('dockDirection', parseInt(e.target.value))}
|
||||
className="bg-gray-700 border border-gray-600 text-gray-200 text-xs p-1 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] text-gray-400">Text Size</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.nodeTextFontSize || 12}
|
||||
onChange={(e) => handleChange('nodeTextFontSize', parseInt(e.target.value))}
|
||||
className="bg-gray-700 border border-gray-600 text-gray-200 text-xs p-1 rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 mb-2">
|
||||
<label className="text-[10px] text-gray-400">Text Color</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={formData.nodeTextForeColor || '#FFFFFF'}
|
||||
onChange={(e) => handleChange('nodeTextForeColor', e.target.value)}
|
||||
className="bg-transparent w-6 h-6 p-0 border-0"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.nodeTextForeColor || '#FFFFFF'}
|
||||
onChange={(e) => handleChange('nodeTextForeColor', e.target.value)}
|
||||
className="flex-1 bg-gray-700 border border-gray-600 text-gray-200 text-xs p-1 rounded font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
{[
|
||||
{ k: 'canDocking', l: 'Can Docking' },
|
||||
{ k: 'canTurnLeft', l: 'Can Turn Left' },
|
||||
{ k: 'canTurnRight', l: 'Can Turn Right' },
|
||||
{ k: 'disableCross', l: 'Disable Cross' },
|
||||
{ k: 'isActive', l: 'Is Active' },
|
||||
].map((item) => (
|
||||
<label key={item.k} className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!formData[item.k]}
|
||||
onChange={(e) => handleChange(item.k, e.target.checked ? true : false)}
|
||||
className="rounded bg-gray-700 border-gray-600 text-blue-500 focus:ring-0"
|
||||
/>
|
||||
<span className="text-xs text-gray-300">{item.l}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PropertyPanel;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ interface SimulationCanvasProps {
|
||||
agvState: AgvState;
|
||||
setAgvState: (state: AgvState | ((prev: AgvState) => AgvState)) => void;
|
||||
onLog: (msg: string) => void;
|
||||
onSelectionChange?: (selectedIds: Set<string>) => void;
|
||||
}
|
||||
|
||||
interface HitObject {
|
||||
@@ -19,7 +20,7 @@ interface HitObject {
|
||||
dist: number;
|
||||
}
|
||||
|
||||
const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData, setMapData, agvState, setAgvState, onLog }) => {
|
||||
const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData, setMapData, agvState, setAgvState, onLog, onSelectionChange }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [dimensions, setDimensions] = useState({ width: 800, height: 600 });
|
||||
@@ -30,20 +31,33 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
const isSpacePressedRef = useRef(false);
|
||||
|
||||
// Interaction State
|
||||
const [dragStartPos, setDragStartPos] = useState<{x:number, y:number} | null>(null);
|
||||
const [dragStartPos, setDragStartPos] = useState<{ x: number, y: number } | null>(null);
|
||||
const [startNodeId, setStartNodeId] = useState<string | null>(null);
|
||||
|
||||
// Selection & Editing State
|
||||
const [selectedItemId, setSelectedItemId] = useState<string | null>(null);
|
||||
// Changed: Support multiple items
|
||||
const [selectedItemIds, setSelectedItemIds] = useState<Set<string>>(new Set());
|
||||
// dragHandle: 'p1'|'p2'|'control' for magnets, 'mark_p1'|'mark_p2' for marks
|
||||
const [dragHandle, setDragHandle] = useState<string | null>(null);
|
||||
|
||||
// Box Selection State
|
||||
const [selectionBox, setSelectionBox] = useState<{ start: { x: number, y: number }, current: { x: number, y: number } } | null>(null);
|
||||
|
||||
// Notify parent of selection change
|
||||
useEffect(() => {
|
||||
if (onSelectionChange) {
|
||||
onSelectionChange(selectedItemIds);
|
||||
}
|
||||
}, [selectedItemIds, onSelectionChange]);
|
||||
|
||||
// Move/Drag State for Select Tool (Whole Object)
|
||||
const [dragTarget, setDragTarget] = useState<{
|
||||
type: 'AGV' | 'NODE' | 'MAGNET' | 'MARK';
|
||||
id?: string;
|
||||
type: 'MULTI' | 'AGV';
|
||||
startMouse: { x: number, y: number };
|
||||
initialObjState: any;
|
||||
// For MULTI: store map of {id: initialState}
|
||||
initialStates?: Record<string, { x: number, y: number, p1?: any, p2?: any, controlPoint?: any }>;
|
||||
// For AGV single move
|
||||
initialAgvState?: { x: number, y: number };
|
||||
} | null>(null);
|
||||
|
||||
// Curve Drawing State
|
||||
@@ -101,10 +115,11 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === 'Space') isSpacePressedRef.current = true;
|
||||
if (e.key === 'Escape') {
|
||||
setSelectedItemId(null);
|
||||
setSelectedItemIds(new Set());
|
||||
setDragHandle(null);
|
||||
setDragTarget(null);
|
||||
setContextMenu(null);
|
||||
setSelectionBox(null);
|
||||
}
|
||||
};
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
@@ -128,7 +143,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
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);
|
||||
if (activeTool !== ToolType.SELECT) setSelectedItemIds(new Set());
|
||||
}, [activeTool]);
|
||||
|
||||
// Preload Images
|
||||
@@ -178,12 +193,12 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
};
|
||||
};
|
||||
|
||||
const getDistance = (p1: {x:number, y:number}, p2: {x:number, y:number}) => {
|
||||
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 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;
|
||||
@@ -193,7 +208,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
};
|
||||
|
||||
// Get closest point on segment
|
||||
const getClosestPointOnSegment = (p: {x:number, y:number}, v: {x:number, y:number}, w: {x:number, y:number}) => {
|
||||
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;
|
||||
@@ -202,7 +217,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
};
|
||||
|
||||
// Quadratic Bezier Point Calculation
|
||||
const getBezierPoint = (t: number, p0: {x:number, y:number}, p1: {x:number, y:number}, p2: {x:number, y:number}) => {
|
||||
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;
|
||||
@@ -214,7 +229,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
};
|
||||
|
||||
// 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 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);
|
||||
@@ -224,7 +239,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
|
||||
// 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) => {
|
||||
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;
|
||||
@@ -266,8 +281,8 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
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);
|
||||
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));
|
||||
};
|
||||
|
||||
@@ -285,7 +300,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
|
||||
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);
|
||||
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;
|
||||
@@ -366,7 +381,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
});
|
||||
|
||||
// Sort by Priority first, then Distance
|
||||
return hits.sort((a,b) => {
|
||||
return hits.sort((a, b) => {
|
||||
const prioDiff = typePriority[a.type] - typePriority[b.type];
|
||||
if (prioDiff !== 0) return prioDiff;
|
||||
return a.dist - b.dist;
|
||||
@@ -374,19 +389,29 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
};
|
||||
|
||||
const deleteObject = (item: HitObject) => {
|
||||
if (item.id === selectedItemId) setSelectedItemId(null); // Clear selection if deleted
|
||||
// If the item clicked is part of selection, delete ALL selected.
|
||||
// If not part of selection, delete just that item.
|
||||
const idsToDelete = new Set<string>();
|
||||
if (selectedItemIds.has(item.id)) {
|
||||
selectedItemIds.forEach(id => idsToDelete.add(id));
|
||||
} else {
|
||||
idsToDelete.add(item.id);
|
||||
}
|
||||
|
||||
setSelectedItemIds(prev => {
|
||||
const next = new Set(prev);
|
||||
idsToDelete.forEach(id => next.delete(id));
|
||||
return next;
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
// Filter out all deleted IDs
|
||||
next.nodes = prev.nodes.filter(n => !idsToDelete.has(n.id));
|
||||
// Remove edges connected to deleted nodes
|
||||
next.edges = prev.edges.filter(e => !idsToDelete.has(e.id) && !idsToDelete.has(e.from) && !idsToDelete.has(e.to));
|
||||
next.marks = prev.marks.filter(m => !idsToDelete.has(m.id));
|
||||
next.magnets = prev.magnets.filter(m => !idsToDelete.has(m.id));
|
||||
return next;
|
||||
});
|
||||
setContextMenu(null);
|
||||
@@ -464,10 +489,10 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
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;
|
||||
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 }[] = [];
|
||||
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;
|
||||
@@ -477,7 +502,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
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);
|
||||
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;
|
||||
@@ -591,7 +616,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
// 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(' ');
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -626,7 +651,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
|
||||
// CLAMP: Max lateral snap per frame to prevent teleportation
|
||||
const MAX_SNAP = 2.0;
|
||||
const moveDist = Math.sqrt(moveX*moveX + moveY*moveY);
|
||||
const moveDist = Math.sqrt(moveX * moveX + moveY * moveY);
|
||||
if (moveDist > MAX_SNAP) {
|
||||
const ratio = MAX_SNAP / moveDist;
|
||||
moveX *= ratio;
|
||||
@@ -644,7 +669,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
const otherSensor = isFwd ? rearSensor : frontSensor;
|
||||
const dx = newSensorX - otherSensor.x;
|
||||
const dy = newSensorY - otherSensor.y;
|
||||
let newAngle = Math.atan2(dy, dx) * (180/Math.PI);
|
||||
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
|
||||
@@ -684,7 +709,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
|
||||
// Debug Log
|
||||
if (logThrottleRef.current % 10 === 0) {
|
||||
onLog(`Turn180 [L]: Deg=${degreesTurned.toFixed(1)}/180 Ignore=${ignoreSensors ? 'Y':'N'} RearLine=${lineRearDetected ? 'YES':'NO'}`);
|
||||
onLog(`Turn180 [L]: Deg=${degreesTurned.toFixed(1)}/180 Ignore=${ignoreSensors ? 'Y' : 'N'} RearLine=${lineRearDetected ? 'YES' : 'NO'}`);
|
||||
}
|
||||
|
||||
if (stopCondition || reachedLimit) {
|
||||
@@ -708,7 +733,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
|
||||
// Debug Log
|
||||
if (logThrottleRef.current % 10 === 0) {
|
||||
onLog(`Turn180 [R]: Deg=${degreesTurned.toFixed(1)}/180 Ignore=${ignoreSensors ? 'Y':'N'} FrontLine=${lineFrontDetected ? 'YES':'NO'}`);
|
||||
onLog(`Turn180 [R]: Deg=${degreesTurned.toFixed(1)}/180 Ignore=${ignoreSensors ? 'Y' : 'N'} FrontLine=${lineFrontDetected ? 'YES' : 'NO'}`);
|
||||
}
|
||||
|
||||
if (stopCondition || reachedLimit) {
|
||||
@@ -724,7 +749,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
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; });
|
||||
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;
|
||||
@@ -756,8 +781,15 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
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);
|
||||
setAgvState(prev => {
|
||||
// Conflict Resolution:
|
||||
// If the motionState has changed externally (e.g. STOP command from App.tsx), we must yield to it.
|
||||
// We compare the 'prev' (latest React state) with the 'agvStateRef.current' (state at start of frame).
|
||||
if (prev.motionState !== agvStateRef.current.motionState) {
|
||||
return prev;
|
||||
}
|
||||
return state;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -819,7 +851,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
ctx.lineWidth = MAGNET_WIDTH;
|
||||
ctx.lineCap = 'round';
|
||||
map.magnets.forEach(mag => {
|
||||
const isSelected = mag.id === selectedItemId;
|
||||
const isSelected = selectedItemIds.has(mag.id);
|
||||
ctx.strokeStyle = isSelected ? 'rgba(59, 130, 246, 0.8)' : MAGNET_COLOR; // Brighter if selected
|
||||
|
||||
ctx.beginPath();
|
||||
@@ -831,8 +863,9 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
// Draw Controls for Selected Curve
|
||||
if (isSelected && mag.type === 'CURVE' && mag.controlPoint) {
|
||||
// Draw Controls for Selected Curve (Only if Single Selection for Simplicity handles)
|
||||
if (isSelected && selectedItemIds.size === 1) {
|
||||
if (mag.type === 'CURVE' && mag.controlPoint) {
|
||||
// Helper Lines
|
||||
ctx.strokeStyle = 'rgba(255, 255, 0, 0.5)';
|
||||
ctx.lineWidth = 1;
|
||||
@@ -845,25 +878,25 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Control Points
|
||||
const drawHandle = (p: {x:number, y:number}, color: string) => {
|
||||
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.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') {
|
||||
} else if (mag.type === 'STRAIGHT') {
|
||||
// Simple handles for straight lines
|
||||
const drawHandle = (p: {x:number, y:number}, color: string) => {
|
||||
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.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)
|
||||
@@ -873,7 +906,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
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);
|
||||
ctx.drawImage(img, node.x - w / 2, node.y - h / 2, w, h);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -881,7 +914,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
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); }
|
||||
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;
|
||||
}
|
||||
@@ -892,7 +925,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
else if (node.displayColor) color = node.displayColor;
|
||||
|
||||
// Highlight Selected Node
|
||||
if (node.id === selectedItemId) {
|
||||
if (selectedItemIds.has(node.id)) {
|
||||
ctx.strokeStyle = '#FDE047'; ctx.lineWidth = 2;
|
||||
ctx.strokeRect(node.x - 10, node.y - 10, 20, 20);
|
||||
}
|
||||
@@ -908,7 +941,8 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
// Marks
|
||||
ctx.strokeStyle = '#FCD34D'; ctx.lineWidth = 3;
|
||||
map.marks.forEach(mark => {
|
||||
if (mark.id === selectedItemId) {
|
||||
const isSelected = selectedItemIds.has(mark.id);
|
||||
if (isSelected) {
|
||||
ctx.shadowColor = '#FDE047'; ctx.shadowBlur = 10;
|
||||
} else {
|
||||
ctx.shadowBlur = 0;
|
||||
@@ -918,7 +952,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Draw handles if selected
|
||||
if (mark.id === selectedItemId) {
|
||||
if (isSelected && selectedItemIds.size === 1) {
|
||||
|
||||
// Re-apply transform for simple drawing in local space
|
||||
ctx.save(); ctx.translate(mark.x, mark.y); ctx.rotate((mark.rotation * Math.PI) / 180);
|
||||
@@ -926,7 +960,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
// 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.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
|
||||
@@ -936,34 +970,48 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
}
|
||||
});
|
||||
|
||||
// Drawing Helpers
|
||||
// Selection Box
|
||||
if (selectionBox) {
|
||||
const minX = Math.min(selectionBox.start.x, selectionBox.current.x);
|
||||
const minY = Math.min(selectionBox.start.y, selectionBox.current.y);
|
||||
const w = Math.abs(selectionBox.current.x - selectionBox.start.x);
|
||||
const h = Math.abs(selectionBox.current.y - selectionBox.start.y);
|
||||
|
||||
// -- 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();
|
||||
ctx.fillStyle = 'rgba(59, 130, 246, 0.2)';
|
||||
ctx.strokeStyle = '#3B82F6';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.fillRect(minX, minY, w, h);
|
||||
ctx.strokeRect(minX, minY, w, h);
|
||||
}
|
||||
|
||||
// -- Logical Line --
|
||||
// Drawing Helpers
|
||||
// -- Logical Edge (Connection) --
|
||||
if (activeTool === ToolType.DRAW_LINE && dragStartPos) {
|
||||
ctx.lineWidth = 1; ctx.strokeStyle = '#60A5FA'; ctx.setLineDash([5, 5]);
|
||||
ctx.strokeStyle = '#9CA3AF'; // Gray
|
||||
ctx.lineWidth = 1;
|
||||
ctx.setLineDash([4, 4]); // Dashed
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(dragStartPos.x, dragStartPos.y);
|
||||
const curr = getSnappedPos(mouseWorldPosRef.current.x, mouseWorldPosRef.current.y);
|
||||
const curr = mouseWorldPosRef.current;
|
||||
ctx.lineTo(curr.x, curr.y);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
// -- Curve Magnet (3-Step Drawing) --
|
||||
// -- Straight Magnet --
|
||||
if (activeTool === ToolType.DRAW_MAGNET_STRAIGHT && dragStartPos) {
|
||||
// ... (Keep existing drawing logic)
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
|
||||
ctx.lineWidth = MAGNET_WIDTH;
|
||||
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();
|
||||
}
|
||||
if (activeTool === ToolType.DRAW_MAGNET_CURVE && tempMagnet) {
|
||||
// ... (Keep existing drawing logic - ensuring it's not lost)
|
||||
const p1 = tempMagnet.p1!;
|
||||
const p2 = tempMagnet.p2 || getSnappedPos(mouseWorldPosRef.current.x, mouseWorldPosRef.current.y);
|
||||
|
||||
@@ -976,12 +1024,9 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
|
||||
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 };
|
||||
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);
|
||||
@@ -989,13 +1034,11 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
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();
|
||||
@@ -1027,19 +1070,19 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
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.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 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.lineTo(sensorPos.x + Math.cos(currentAngle) * 60, sensorPos.y + Math.sin(currentAngle) * 60);
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
|
||||
ctx.stroke();
|
||||
|
||||
@@ -1081,9 +1124,9 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
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();
|
||||
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();
|
||||
ctx.save(); ctx.translate(0, 10); ctx.rotate(Math.PI / 2); ctx.fillText('➜', 0, 0); ctx.restore();
|
||||
} else {
|
||||
ctx.fillText('➜', 0, 10);
|
||||
}
|
||||
@@ -1099,10 +1142,10 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
|
||||
// Key Head (Circle)
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, -3, 3.5, 0, Math.PI*2);
|
||||
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();
|
||||
ctx.beginPath(); ctx.arc(0, -3, 1, 0, Math.PI * 2); ctx.fill();
|
||||
|
||||
// Key Shaft
|
||||
ctx.beginPath();
|
||||
@@ -1119,7 +1162,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
// 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(); }
|
||||
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;
|
||||
@@ -1137,7 +1180,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
const interval = setInterval(updatePhysics, 16);
|
||||
animationFrameId = requestAnimationFrame(draw);
|
||||
return () => { clearInterval(interval); cancelAnimationFrame(animationFrameId); };
|
||||
}, [activeTool, dragStartPos, tempMagnet, curvePhase, selectedItemId, dimensions]); // Re-draw when dimensions change
|
||||
}, [activeTool, dragStartPos, tempMagnet, curvePhase, selectedItemIds, dimensions]); // Re-draw when dimensions change
|
||||
|
||||
// --- Interaction Handlers ---
|
||||
|
||||
@@ -1213,13 +1256,20 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
return;
|
||||
}
|
||||
|
||||
// Drag Handle (Editing)
|
||||
if (dragHandle && selectedItemId) {
|
||||
// Box Selection Update
|
||||
if (selectionBox) {
|
||||
setSelectionBox(prev => prev ? { ...prev, current: worldPos } : null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Drag Handle (Editing - Only if single selection)
|
||||
if (dragHandle && selectedItemIds.size === 1) {
|
||||
const singleId = Array.from(selectedItemIds)[0];
|
||||
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);
|
||||
const magIndex = next.magnets.findIndex(m => m.id === singleId);
|
||||
if (magIndex !== -1) {
|
||||
const newMags = [...next.magnets];
|
||||
const mag = { ...newMags[magIndex] };
|
||||
@@ -1231,7 +1281,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
}
|
||||
} else if (dragHandle === 'mark_p1' || dragHandle === 'mark_p2') {
|
||||
// Rotate Mark
|
||||
const markIndex = next.marks.findIndex(m => m.id === selectedItemId);
|
||||
const markIndex = next.marks.findIndex(m => m.id === singleId);
|
||||
if (markIndex !== -1) {
|
||||
const newMarks = [...next.marks];
|
||||
const mark = { ...newMarks[markIndex] };
|
||||
@@ -1250,45 +1300,56 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
}
|
||||
|
||||
// Drag Object (Moving)
|
||||
if (dragTarget && dragTarget.initialObjState) {
|
||||
if (dragTarget) {
|
||||
const dx = worldPos.x - dragTarget.startMouse.x;
|
||||
const dy = worldPos.y - dragTarget.startMouse.y;
|
||||
|
||||
if (dragTarget.type === 'AGV') {
|
||||
if (dragTarget.type === 'AGV' && dragTarget.initialAgvState) {
|
||||
setAgvState(prev => ({
|
||||
...prev,
|
||||
x: dragTarget.initialObjState.x + dx,
|
||||
y: dragTarget.initialObjState.y + dy
|
||||
x: dragTarget.initialAgvState!.x + dx,
|
||||
y: dragTarget.initialAgvState!.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;
|
||||
} else if (dragTarget.type === 'MULTI' && dragTarget.initialStates) {
|
||||
const initialStates = dragTarget.initialStates;
|
||||
setMapData(prev => {
|
||||
const next = { ...prev };
|
||||
|
||||
setMapData(prev => ({
|
||||
...prev,
|
||||
magnets: prev.magnets.map(m => {
|
||||
if (m.id === dragTarget.id) {
|
||||
// Update Nodes
|
||||
next.nodes = prev.nodes.map(n => {
|
||||
const init = initialStates[n.id];
|
||||
if (init) {
|
||||
return { ...n, x: init.x + dx, y: init.y + dy };
|
||||
}
|
||||
return n;
|
||||
});
|
||||
|
||||
// Update Marks
|
||||
next.marks = prev.marks.map(m => {
|
||||
const init = initialStates[m.id];
|
||||
if (init) {
|
||||
return { ...m, x: init.x + dx, y: init.y + dy };
|
||||
}
|
||||
return m;
|
||||
});
|
||||
|
||||
// Update Magnets
|
||||
next.magnets = prev.magnets.map(m => {
|
||||
const init = initialStates[m.id];
|
||||
if (init) {
|
||||
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 };
|
||||
newM.p1 = { x: init.p1.x + dx, y: init.p1.y + dy };
|
||||
newM.p2 = { x: init.p2.x + dx, y: init.p2.y + dy };
|
||||
if (init.controlPoint && newM.controlPoint) {
|
||||
newM.controlPoint = { x: init.controlPoint.x + dx, y: init.controlPoint.y + dy };
|
||||
}
|
||||
return newM;
|
||||
}
|
||||
return m;
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
return next;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1299,10 +1360,43 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
setDragHandle(null);
|
||||
setDragTarget(null);
|
||||
|
||||
// Box Selection End
|
||||
if (selectionBox) {
|
||||
const minX = Math.min(selectionBox.start.x, selectionBox.current.x);
|
||||
const maxX = Math.max(selectionBox.start.x, selectionBox.current.x);
|
||||
const minY = Math.min(selectionBox.start.y, selectionBox.current.y);
|
||||
const maxY = Math.max(selectionBox.start.y, selectionBox.current.y);
|
||||
|
||||
// Avoid accidental clicks being box selections
|
||||
if (maxX - minX > 5 || maxY - minY > 5) {
|
||||
const newSelection = new Set<string>();
|
||||
if (e.shiftKey || e.ctrlKey) {
|
||||
selectedItemIds.forEach(id => newSelection.add(id));
|
||||
}
|
||||
|
||||
// Find items in box
|
||||
mapData.nodes.forEach(n => {
|
||||
if (n.x >= minX && n.x <= maxX && n.y >= minY && n.y <= maxY) newSelection.add(n.id);
|
||||
});
|
||||
mapData.marks.forEach(m => {
|
||||
if (m.x >= minX && m.x <= maxX && m.y >= minY && m.y <= maxY) newSelection.add(m.id);
|
||||
});
|
||||
mapData.magnets.forEach(m => {
|
||||
// Simple center point check for magnets for now
|
||||
const cx = (m.p1.x + m.p2.x) / 2;
|
||||
const cy = (m.p1.y + m.p2.y) / 2;
|
||||
if (cx >= minX && cx <= maxX && cy >= minY && cy <= maxY) newSelection.add(m.id);
|
||||
});
|
||||
|
||||
setSelectedItemIds(newSelection);
|
||||
}
|
||||
setSelectionBox(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);
|
||||
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) ||
|
||||
@@ -1334,6 +1428,8 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
setDragStartPos(null);
|
||||
setStartNodeId(null);
|
||||
};
|
||||
@@ -1367,18 +1463,50 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeTool === ToolType.ADD_RFID) {
|
||||
// Smart ID Generation
|
||||
const ids = mapData.nodes.map(n => parseInt(n.id)).filter(n => !isNaN(n));
|
||||
const maxId = ids.length > 0 ? Math.max(...ids) : 0;
|
||||
const newId = (maxId + 1).toString();
|
||||
|
||||
const newNode: MapNode = {
|
||||
id: newId,
|
||||
x: worldPos.x,
|
||||
y: worldPos.y,
|
||||
type: NodeType.Normal,
|
||||
name: '',
|
||||
rfidId: '0000', // Default RFID
|
||||
connectedNodes: [],
|
||||
|
||||
// Defaults
|
||||
stationType: 0,
|
||||
speedLimit: 0,
|
||||
canDocking: false,
|
||||
dockDirection: 0,
|
||||
canTurnLeft: false,
|
||||
canTurnRight: false,
|
||||
disableCross: false,
|
||||
isActive: true,
|
||||
nodeTextForeColor: '#FFFFFF',
|
||||
nodeTextFontSize: 12
|
||||
};
|
||||
setMapData(prev => ({ ...prev, nodes: [...prev.nodes, newNode] }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeTool === ToolType.SELECT) {
|
||||
// Check handles
|
||||
// Check handles (Only if SINGLE item selected for now, to avoid complexity)
|
||||
const worldTolerance = 15 / viewRef.current.scale;
|
||||
|
||||
if (selectedItemId) {
|
||||
const selectedMag = mapData.magnets.find(m => m.id === selectedItemId);
|
||||
if (selectedItemIds.size === 1) {
|
||||
const singleId = Array.from(selectedItemIds)[0];
|
||||
const selectedMag = mapData.magnets.find(m => m.id === singleId);
|
||||
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);
|
||||
const selectedMark = mapData.marks.find(m => m.id === singleId);
|
||||
if (selectedMark) {
|
||||
const rad = (selectedMark.rotation * Math.PI) / 180;
|
||||
const dx = Math.cos(rad) * 25; const dy = Math.sin(rad) * 25;
|
||||
@@ -1392,33 +1520,51 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
// AGV
|
||||
const agvDist = getDistance(worldPos, agvState);
|
||||
if (agvDist < 30) {
|
||||
setDragTarget({ type: 'AGV', startMouse: worldPos, initialObjState: { x: agvState.x, y: agvState.y } });
|
||||
setSelectedItemId(null);
|
||||
setDragTarget({ type: 'AGV', startMouse: worldPos, initialAgvState: { x: agvState.x, y: agvState.y } });
|
||||
setSelectedItemIds(new Set());
|
||||
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 });
|
||||
}
|
||||
const isMultiSelect = e.shiftKey || e.ctrlKey;
|
||||
|
||||
let newSelection = new Set(selectedItemIds);
|
||||
|
||||
if (isMultiSelect) {
|
||||
if (newSelection.has(hit.id)) newSelection.delete(hit.id);
|
||||
else newSelection.add(hit.id);
|
||||
} else {
|
||||
setSelectedItemId(null);
|
||||
if (!newSelection.has(hit.id)) {
|
||||
newSelection = new Set([hit.id]);
|
||||
}
|
||||
// If already selected, keep selection (to allow drag)
|
||||
}
|
||||
setSelectedItemIds(newSelection);
|
||||
|
||||
// Prepare Multi-Drag
|
||||
const initialStates: Record<string, any> = {};
|
||||
newSelection.forEach(id => {
|
||||
const n = mapData.nodes.find(o => o.id === id);
|
||||
if (n) initialStates[id] = { x: n.x, y: n.y };
|
||||
const m = mapData.marks.find(o => o.id === id);
|
||||
if (m) initialStates[id] = { x: m.x, y: m.y };
|
||||
const mag = mapData.magnets.find(o => o.id === id);
|
||||
if (mag) initialStates[id] = JSON.parse(JSON.stringify(mag));
|
||||
});
|
||||
|
||||
setDragTarget({ type: 'MULTI', startMouse: worldPos, initialStates });
|
||||
} else {
|
||||
// Click on empty space
|
||||
if (!e.shiftKey && !e.ctrlKey) {
|
||||
setSelectedItemIds(new Set());
|
||||
}
|
||||
// Start Box Selection
|
||||
setSelectionBox({ start: worldPos, current: worldPos });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1428,6 +1574,11 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
const start = getSnappedPos(x, y);
|
||||
setTempMagnet({ id: crypto.randomUUID(), type: 'CURVE', p1: start, p2: start, controlPoint: start });
|
||||
setCurvePhase(1);
|
||||
} else if (curvePhase === 1) {
|
||||
// Set Endpoint
|
||||
const end = getSnappedPos(x, y);
|
||||
setTempMagnet(prev => prev ? { ...prev, p2: end } : null);
|
||||
setCurvePhase(2);
|
||||
} else if (curvePhase === 2) {
|
||||
// Finish curve
|
||||
if (tempMagnet && tempMagnet.p1 && tempMagnet.p2) {
|
||||
@@ -1488,19 +1639,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
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 (
|
||||
@@ -1525,7 +1664,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
{activeTool === ToolType.DRAW_MAGNET_CURVE && curvePhase === 2 && <span className="text-yellow-400 ml-2">Move mouse to bend curve, Click to finish</span>}
|
||||
<div className="mt-1 flex items-center gap-1 text-[10px] opacity-70">
|
||||
<Move size={10} /> <span>Middle Click / Space+Drag to Pan</span>
|
||||
{selectedItemId && <span className="text-yellow-500 font-bold ml-2">Object Selected (Drag points to edit)</span>}
|
||||
{selectedItemIds.size > 0 && <span className="text-yellow-500 font-bold ml-2">{selectedItemIds.size} Object(s) Selected</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1537,7 +1676,7 @@ const SimulationCanvas: React.FC<SimulationCanvasProps> = ({ activeTool, mapData
|
||||
>
|
||||
<div className="flex items-center justify-between px-2 py-1 border-b border-gray-700 text-xs font-bold text-gray-400">
|
||||
<span>Select object to delete</span>
|
||||
<button onClick={() => setContextMenu(null)} className="hover:text-white"><X size={12}/></button>
|
||||
<button onClick={() => setContextMenu(null)} className="hover:text-white"><X size={12} /></button>
|
||||
</div>
|
||||
<ul className="text-xs">
|
||||
{contextMenu.items.map((item, idx) => (
|
||||
|
||||
@@ -86,7 +86,12 @@ export class SerialPortHandler {
|
||||
} else {
|
||||
payload = data;
|
||||
}
|
||||
try {
|
||||
await this.writer.write(payload);
|
||||
} catch (e) {
|
||||
console.error("Serial Send Error", e);
|
||||
throw e; // Re-throw to let caller know if needed, or handle gracefull
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +99,7 @@ export class SerialPortHandler {
|
||||
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 `ID:${info.usbVendorId.toString(16).padStart(4, '0').toUpperCase()}:${info.usbProductId.toString(16).padStart(4, '0').toUpperCase()}`;
|
||||
}
|
||||
return "USB Device";
|
||||
}
|
||||
|
||||
12
types.ts
12
types.ts
@@ -44,6 +44,18 @@ export interface MapNode extends Point {
|
||||
|
||||
// New props preservation
|
||||
fontSize?: number;
|
||||
|
||||
// Extended Properties (from C# Model)
|
||||
stationType?: number;
|
||||
speedLimit?: number;
|
||||
canDocking?: boolean;
|
||||
dockDirection?: number;
|
||||
canTurnLeft?: boolean;
|
||||
canTurnRight?: boolean;
|
||||
disableCross?: boolean;
|
||||
isActive?: boolean;
|
||||
nodeTextForeColor?: string;
|
||||
nodeTextFontSize?: number;
|
||||
}
|
||||
|
||||
export interface MapEdge {
|
||||
|
||||
Reference in New Issue
Block a user