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 AgvStatusPanel from './components/AgvStatusPanel';
|
||||||
import AgvAutoRunControls from './components/AgvAutoRunControls';
|
import AgvAutoRunControls from './components/AgvAutoRunControls';
|
||||||
import SystemLogPanel from './components/SystemLogPanel';
|
import SystemLogPanel from './components/SystemLogPanel';
|
||||||
|
import PropertyPanel from './components/PropertyPanel';
|
||||||
import { SerialPortHandler } from './services/serialService';
|
import { SerialPortHandler } from './services/serialService';
|
||||||
|
|
||||||
// --- ACS CRC16 Table Generation (Matches C# Logic) ---
|
// --- ACS CRC16 Table Generation (Matches C# Logic) ---
|
||||||
@@ -43,6 +44,7 @@ const calculateAcsCrc16 = (data: number[] | Uint8Array): number => {
|
|||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
// --- State ---
|
// --- State ---
|
||||||
const [activeTool, setActiveTool] = useState<ToolType>(ToolType.SELECT);
|
const [activeTool, setActiveTool] = useState<ToolType>(ToolType.SELECT);
|
||||||
|
const [selectedItemIds, setSelectedItemIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
// Map Data
|
// Map Data
|
||||||
const [mapData, setMapData] = useState<SimulationMap>(INITIAL_MAP);
|
const [mapData, setMapData] = useState<SimulationMap>(INITIAL_MAP);
|
||||||
@@ -123,6 +125,20 @@ const App: React.FC = () => {
|
|||||||
const [bmsPortInfo, setBmsPortInfo] = useState<string | null>(null);
|
const [bmsPortInfo, setBmsPortInfo] = useState<string | null>(null);
|
||||||
const [acsPortInfo, setAcsPortInfo] = 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
|
// Serial Configuration
|
||||||
const [agvBaudRate, setAgvBaudRate] = useState(57600);
|
const [agvBaudRate, setAgvBaudRate] = useState(57600);
|
||||||
const [bmsBaudRate, setBmsBaudRate] = useState(9600);
|
const [bmsBaudRate, setBmsBaudRate] = useState(9600);
|
||||||
@@ -443,6 +459,11 @@ const App: React.FC = () => {
|
|||||||
barr[22] = s.sensorStatus.charCodeAt(0);
|
barr[22] = s.sensorStatus.charCodeAt(0);
|
||||||
barr.set(encodeHex(s.signalFlags, 2), 23);
|
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;
|
barr[31] = '*'.charCodeAt(0); barr[32] = '*'.charCodeAt(0); barr[33] = 0x03;
|
||||||
|
|
||||||
agvSerialRef.current.send(barr);
|
agvSerialRef.current.send(barr);
|
||||||
@@ -1206,6 +1227,13 @@ const App: React.FC = () => {
|
|||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
>
|
>
|
||||||
|
{/* Overlay: Property Panel (Floating) */}
|
||||||
|
<PropertyPanel
|
||||||
|
selectedItemIds={selectedItemIds}
|
||||||
|
mapData={mapData}
|
||||||
|
onUpdate={handlePropertyUpdate}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 1. Far Left: Toolbar */}
|
{/* 1. Far Left: Toolbar */}
|
||||||
<div className="w-16 p-2 border-r border-gray-800 flex flex-col items-center shrink-0">
|
<div className="w-16 p-2 border-r border-gray-800 flex flex-col items-center shrink-0">
|
||||||
<EditorToolbar
|
<EditorToolbar
|
||||||
@@ -1271,6 +1299,7 @@ const App: React.FC = () => {
|
|||||||
agvState={agvState}
|
agvState={agvState}
|
||||||
setAgvState={setAgvState}
|
setAgvState={setAgvState}
|
||||||
onLog={(msg) => addLog('SYSTEM', 'INFO', msg)}
|
onLog={(msg) => addLog('SYSTEM', 'INFO', msg)}
|
||||||
|
onSelectionChange={setSelectedItemIds}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const AgvStatusPanel: React.FC<AgvStatusPanelProps> = ({ agvState }) => {
|
|||||||
const isOn = (value & (1 << bitIndex)) !== 0;
|
const isOn = (value & (1 << bitIndex)) !== 0;
|
||||||
// 필터링에 의해 isOn이 true인 것만 전달되지만 스타일 유지를 위해 체크 로직 유지
|
// 필터링에 의해 isOn이 true인 것만 전달되지만 스타일 유지를 위해 체크 로직 유지
|
||||||
return (
|
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>
|
<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 className={`w-2.5 h-2.5 rounded-full flex-shrink-0 ${isOn ? colorClass : 'bg-gray-700'} shadow-sm`} />
|
||||||
</div>
|
</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;
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -47,7 +47,7 @@ export class SerialPortHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.port.writable) {
|
if (this.port.writable) {
|
||||||
this.writer = this.port.writable.getWriter();
|
this.writer = this.port.writable.getWriter();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isConnected = true;
|
this.isConnected = true;
|
||||||
@@ -86,19 +86,24 @@ export class SerialPortHandler {
|
|||||||
} else {
|
} else {
|
||||||
payload = data;
|
payload = data;
|
||||||
}
|
}
|
||||||
await this.writer.write(payload);
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPortInfo(): string | null {
|
getPortInfo(): string | null {
|
||||||
if (this.port) {
|
if (this.port) {
|
||||||
const info = this.port.getInfo();
|
const info = this.port.getInfo();
|
||||||
if (info.usbVendorId && info.usbProductId) {
|
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";
|
|
||||||
}
|
}
|
||||||
return null;
|
return "USB Device";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async readLoop() {
|
private async readLoop() {
|
||||||
|
|||||||
12
types.ts
12
types.ts
@@ -44,6 +44,18 @@ export interface MapNode extends Point {
|
|||||||
|
|
||||||
// New props preservation
|
// New props preservation
|
||||||
fontSize?: number;
|
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 {
|
export interface MapEdge {
|
||||||
|
|||||||
Reference in New Issue
Block a user