38 KiB
LayoutEditor Implementation - Development Log
Last Updated: 2024-12-03
📋 Overview
Tài liệu này ghi lại quá trình xây dựng LayoutEditor - trang chỉnh sửa bản đồ cho robot AGV/AMR. LayoutEditor cho phép người dùng vẽ và chỉnh sửa nodes, edges, stations trên canvas SVG với background image.
Note: Quá trình xây dựng LayoutManager đã được lưu riêng trước đó.
📐 Design Specifications (From Initial Discussion)
1. Coordinate System
| Item | Value |
|---|---|
| Web Origin | Top-Left (0,0) |
| Layout Origin | Bottom-Left (0,0) |
| Transform | WebY = ImageHeight - LayoutY |
| Mouse Display | World Coordinates (meters) - góc trên trái |
2. SVG Layers (Bottom → Top)
- Background Image (SLAM Map)
- Grid
- Edges + Trajectories
- Trajectory Control Points (khi edit)
- Nodes
- Station Overlays
- Selection Highlights
- Temporary Drawing (create edge preview)
3. Node Properties
| Property | Editable | Type |
|---|---|---|
| NodeId | ✅ | string |
| NodeName | ✅ | string |
| NodeDescription | ✅ | string |
| X, Y | ✅ | double (meters) |
| MapId | ✅ | string? |
| Station Info | ❌ (read-only) | display |
| VehicleProperties | ✅ | per VehicleType |
4. Edge Properties
| Property | Editable | Type |
|---|---|---|
| EdgeId | ✅ | string |
| EdgeName | ✅ | string |
| EdgeDescription | ✅ | string |
| StartNodeId | ❌ | Guid (read-only) |
| EndNodeId | ❌ | Guid (read-only) |
| Length | ❌ | double (auto-calc) |
| VehicleProperties | ✅ | per VehicleType |
5. Edge Vehicle Properties
| Property | Type |
|---|---|
| VehicleOrientation | double? |
| OrientationType | enum (GLOBAL, TANGENTIAL) |
| RotationAllowed | bool? |
| RotationAtStartNodeAllowed | enum (NONE, CCW, CW, BOTH) |
| RotationAtEndNodeAllowed | enum (NONE, CCW, CW, BOTH) |
| MaxSpeed, MaxRotationSpeed | double? |
| MinHeight, MaxHeight | double? |
| LoadRestriction_* | bool?, string? |
| Trajectory | JSON (NURBS) |
6. Node Vehicle Properties
| Property | Type |
|---|---|
| Theta | double? (radians) |
| Actions | JSON (Form Builder) |
7. Display Settings
| Setting | Default | Persist |
|---|---|---|
| Show Edge Names | ✅ | Session |
| Show Node Names | ✅ | Session |
| Show Grid | ✅ | Session |
| Show Background | ✅ | Session |
| Grid Spacing | 1.0m | Session |
| Selected VehicleType | First | Session |
8. Visual Representations
| Element | Style |
|---|---|
| Node (normal) | Circle, scale with zoom |
| Node (with Station) | Circle, different color (green) |
| Edge | Line with arrow at end |
| Edge Direction | Arrow at EndNode |
| Edge Name | Text above, center of edge |
| Node Name | Text below, center of node |
| Selection | Highlight ring/glow |
| Trajectory | NURBS curve (selected VehicleType only) |
| Control Points | Small circles (when editing) |
| Create Edge Preview | Dashed line from node1 to mouse (Option B) |
9. Operations
| Operation | Behavior |
|---|---|
| Select | Click = select single, Ctrl+Click = add to selection |
| Scanner | Drag rectangle to multi-select (objects completely within rectangle - Option C) |
| CreateEdge 1-Way | Click node1 → node2, creates 1 edge |
| CreateEdge 2-Way | Click node1 → node2, creates 2 edges (A→B, B→A) |
| Copy | Duplicate nodes with new IDs, duplicate edges, NO stations |
| Move | Drag selected nodes (no snap to grid) |
| Merge | Combine selected nodes at center position |
| Split | Split 1 node into N nodes (N = edge count), auto offset |
| Align H-Left | Align selected nodes to leftmost X |
| Align H-Right | Align selected nodes to rightmost X |
| Align H-Center | Align selected nodes to average X |
| Align V-Top | Align selected nodes to topmost Y |
| Align V-Bottom | Align selected nodes to bottommost Y |
| Align V-Center | Align selected nodes to average Y |
10. Undo/Redo
| Supported | Not Supported |
|---|---|
| Move operations | Create/Delete |
| Alignment operations | Property changes |
11. Keyboard Shortcuts
| Key | Action |
|---|---|
| Ctrl+Z | Undo |
| Ctrl+Y | Redo |
| Ctrl+S | Save |
| Ctrl+C | Copy selected nodes/edges |
| Ctrl+M | Move mode (if nodes selected) |
| Delete | Delete selected |
| Escape | Cancel current operation (CreateEdge, Copy) |
12. Viewport
| Setting | Value |
|---|---|
| Initial | Zoom to fit image bounds |
| Pan | Middle mouse drag OR toolbar button |
| Zoom | Mouse wheel OR toolbar buttons (zoom at cursor position) |
| Fit | Toolbar button |
13. Trajectory Editor
| Setting | Value |
|---|---|
| Display | Only when edge selected (single select mode, not multi-select) |
| VehicleType | Show trajectory of selected VehicleType |
| Degree | User selectable (1, 2, 3) |
| Control Points | Managed via Right Panel buttons |
| Edit | Drag control points on canvas |
| Default Creation | Auto-generate: 2 control points tại StartNode và EndNode (straight line) - Option A |
14. Trajectory Degree Change Logic
Khi TĂNG Degree (thêm control points):
- Degree 1 → Degree 2: Thêm 1 point tại giữa curve (t = 0.5)
- Degree 2 → Degree 3: Thêm 1 point tại t = 0.33 hoặc t = 0.67
- Chèn điểm mới ở giữa 2 điểm có khoảng cách lớn nhất để giữ biên dạng ít thay đổi
Khi GIẢM Degree (xóa control points):
- Degree 3 → Degree 2: Giữ P0 và P_last, tính P_middle = weighted average của các control points cũ
- Degree 2 → Degree 1: Chỉ giữ P0 và P_last, curve thành đường thẳng
- Giảm từ n points xuống n-1 points bằng cách tính trung bình các cặp điểm liền kề
15. Actions Form Builder
| Feature | Behavior |
|---|---|
| ActionType | Dropdown (VehicleType ActionDefaults) + Custom text input |
| Parameters | Key-Value pairs, add/remove individual parameters |
| Defaults | Pre-filled from VehicleType ActionDefaults, có thể thêm/xóa tiếp |
| Validation | JSON syntax + structure |
16. CheckLayout Validations
| Check | Description |
|---|---|
| Edge Min Length | Edge length < EdgeMinLengthCreate in settings |
| (More to be added) | ... |
17. Toolbar Order (Final)
[Scanner][CreateEdge 1-Way▼][Select] │ [Zoom][Fit] │
[H-Left][H-Right][H-Center][V-Top][V-Bottom][V-Center] [Copy][Move] │
[Merge][Split] │
VehicleType: [AMR-T800 ▼] │
[Undo][Redo] [Save] [Delete] [Check] [Exit]
Note: Display Options và Grid Spacing đã được chuyển vào Settings Tab (không còn trên Toolbar).
18. Exit Button
- Behavior: Quay về LayoutManager page (Option A)
19. Component Structure
RobotNet10.MapEditor/
├─ Components/
│ └─ LayoutEditor/
│ ├─ LayoutEditorComponent.razor ← Main container
│ ├─ LayoutEditorComponent.razor.css
│ ├─ EditorToolbar.razor ← Toolbar with all buttons
│ ├─ EditorToolbar.razor.css
│ ├─ MousePositionDisplay.razor ← World coordinates display
│ ├─ SvgEditorCanvas.razor ← Main SVG canvas
│ ├─ SvgEditorCanvas.razor.css
│ ├─ RightPanel/
│ │ ├─ EditorRightPanel.razor ← Container for tabs
│ │ ├─ PropertiesTab.razor ← Selected object properties
│ │ ├─ NodePropertiesEditor.razor
│ │ ├─ EdgePropertiesEditor.razor
│ │ ├─ VehiclePropertiesEditor.razor
│ │ ├─ TrajectoryEditor.razor ← NURBS control points
│ │ └─ SettingsTab.razor ← Layout level settings
│ └─ Dialogs/
│ ├─ ActionsFormDialog.razor ← Actions form builder
│ ├─ CheckLayoutResultDialog.razor ← Validation results
│ └─ UnsavedChangesDialog.razor ← Confirm leave
│
├─ Services/
│ └─ State/
│ ├─ LayoutEditorState.cs ← Centralized state
│ ├─ EditorCommand.cs ← For Undo/Redo
│ └─ EditorMode.cs ← Enum: Select, CreateEdge, etc.
│
└─ wwwroot/
└─ js/
└─ svgEditor.js ← JavaScript for SVG interactions
20. Host Page
RobotNet10.RobotApp.Client/
└─ Pages/
└─ LayoutEditor.razor ← Host page with route
@page "/layout-editor/{LevelId:guid}"
❓ Key Design Decisions (Q&A Summary)
Q8: Trajectory Display
- Q8.1: Hiển thị khi edge được select ở chế độ edit (không phải multi-select ở scanner)
- Q8.2: Option B - Trajectory chỉ hiển thị cho VehicleType được chọn
- Q8.3: Degree có thể thay đổi (1, 2, 3), số lượng control points tương ứng với degree
Q9: Node Properties
- Q9.1: Option B - Station info hiển thị trong Node Properties Tab
Q10: Edge/Node Names
- Q10.1: Option D - Edge names hiển thị ở trên, center của edge
- Q10.2: Scale với zoom level
Q11: Station Info Display
- Q11.1: Option B - Station info trong Node Properties Tab
- Q11.2: Tab Node Properties, trong đó sẽ có cả thông tin của Station
Q12: Actions Form Builder
- Q12.1: Option C - VehicleType có ActionDefaults, dropdown hiển thị defaults + cho phép nhập custom text
- Q12.2: Có thể thêm/xóa từng parameter. Nếu ActionDefault đã có parameters thì sẽ thêm/xóa tiếp vào những parameters default đó
Q13: Keyboard Shortcuts
- Q13.1: Có các shortcuts: Ctrl+Z (Undo), Ctrl+Y (Redo), Ctrl+S (Save), Delete (Delete selected)
Q14: Selection Behavior
- Q14.1: Option A - Nếu chỉ Click thì sẽ select sang đối tượng mới và unselect đối tượng cũ
Q15: Box Select
- Q15.1: Option A - Box select trong Scanner mode
Q16: NURBS Degree Change
- Q16.1: Khi thay đổi Degree, control points được thêm/xóa tự động với logic giữ biên dạng ít thay đổi nhất (xem section 14 ở trên)
Q17: Default Trajectory
- Q17.1: Option A - Auto-generate: 2 control points tại StartNode và EndNode (straight line)
Q18: Box Select Behavior
- Q18.1: Option C - Objects hoàn toàn nằm trong rectangle
Q19: Exit Button
- Q19: Option A - Quay về LayoutManager page
Q20: Create Edge Preview
- Q20: Option B - Đường nét liền màu khác (e.g., gray) từ node đầu tiên đến vị trí mouse
🎯 Implementation Phases
Phase 1: Foundation (Core Structure) ✅ COMPLETE
Status: Hoàn thành trong cuộc hội thoại ban đầu
Deliverables:
LayoutEditorComponent.razor- Main container với left canvas, right panelEditorToolbar.razor- Full toolbar với tất cả buttonsLayoutEditorState.cs- Centralized state management- Host page:
RobotApp.Client/Pages/LayoutEditor.razorvới route/layout-editor/{LevelId:guid} - Navigation từ LayoutManager page
Key Features:
- State management với data loading (Level, Nodes, Edges, Stations, VehicleTypes)
- Selection management (single/multi-select)
- Viewport control structure (
ViewportStateclass) - Coordinate transforms (SVG ↔ World)
- Undo/Redo stack (Command pattern với
EditorCommandabstract class) - Editor modes enum (
Select,Scanner,CreateEdge1Way,CreateEdge2Way,Pan,Move,Copy)
State Management Structure:
LayoutEditorState- Centralized state với:- Data:
Level,Nodes,Edges,Stations,VehicleTypes - Selection:
SelectedNodeIds,SelectedEdgeIds - Viewport:
ViewportState(ViewBoxX, ViewBoxY, ViewBoxWidth, ViewBoxHeight, ZoomLevel) - Display options:
ShowEdgeNames,ShowNodeNames,ShowGrid,ShowBackgroundImage,GridSpacing - Undo/Redo:
UndoStack,RedoStack - Editor mode:
CurrentMode - Methods:
InitializeAsync(),Pan(),Zoom(),FitToScreen(),SelectNodes(),SelectEdges(), etc.
- Data:
Command Pattern:
EditorCommandabstract class vớiExecute()vàUndo()methodsMoveNodesCommand- Example command cho move operations- Commands được push vào
UndoStackkhi execute
Phase 2: SVG Canvas (Basic) ✅ COMPLETE
Status: Hoàn thành trong cuộc hội thoại ban đầu
Deliverables:
SvgEditorCanvas.razor- SVG rendering componentMousePositionDisplay.razor- World coordinates display (góc trên trái)svgEditor.js- JavaScript interop module
Key Features:
- Background image rendering (Y-axis flip để match world coordinates)
- Grid rendering (togglable, configurable spacing)
- Nodes rendering (circles, green color for nodes with stations)
- Edges rendering (lines with direction arrows at EndNode)
- Edge/Node names (togglable, scale with zoom level)
- Box select rectangle visualization (Scanner mode)
- Create edge preview line (dashed/gray line from start node to mouse)
- Mouse position tracking (world coordinates in meters, displayed top-left)
Coordinate System:
- World Coordinates: Bottom-left origin (0,0), Y increases upward (meters)
- SVG Coordinates: Top-left origin (0,0), Y increases downward (pixels)
- Transform:
svgY = physicalHeight - worldY(flip Y axis)
JavaScript Interop (svgEditor.js):
- Mouse events:
mousemove,mousedown,mouseup,wheel - Keyboard shortcuts:
keydown(Ctrl+Z/Y/S, Delete, Escape) - Coordinate conversion:
screenToSvg(),screenToSvgArray()(exported for Blazor) - SVG element reference management
Rendering Layers (implemented):
- Background Image (if available)
- Grid (if
ShowGrid= true) - Edges (with arrows and names if enabled)
- Nodes (circles with names if enabled)
- Selection highlights (rings around selected nodes)
- Box select rectangle (when in Scanner mode)
- Create edge preview line (when creating edge)
Event Handling:
OnMouseMove- Updates mouse position, handles box select, pan, create edge previewOnMouseDown- Handles selection, starts pan, starts create edgeOnMouseUp- Ends pan, completes box select, completes create edgeOnWheel- Handles zoom (with cursor position preservation)
Phase 3: Viewport Controls ⏳ IN PROGRESS
Status: Đã implement cơ bản, đang polish và fix bugs
3.1. Pan (Middle Mouse Drag) ✅ FIXED
Vấn đề ban đầu:
- Pan bị giật và không di chuyển đúng theo chuột
- Delta bị cộng dồn khi di chuyển chuột
Nguyên nhân:
- Tính delta từ điểm bắt đầu pan mỗi lần mouse move
- Khi ViewBox thay đổi sau mỗi lần pan, việc convert
panStartScreensang SVG cho giá trị khác - Gây ra việc cộng dồn delta
Giải pháp:
- Thay đổi từ tính delta từ điểm bắt đầu sang incremental delta
- Lưu
panLastScreen(vị trí screen của lần move trước) - Mỗi lần mouse move:
- Convert
panLastScreenvàcurrentScreensang SVG (dùng ViewBox hiện tại) - Tính delta =
lastSvg - currentSvg - Pan ViewBox theo delta
- Update
panLastScreen = currentScreen
- Convert
Code Changes:
// Thêm panLastScreen để track vị trí trước đó
private (double X, double Y)? panLastScreen;
// Trong OnMouseMove:
if (isPanning && panLastScreen.HasValue)
{
// Tính incremental delta từ lần move trước
_ = PanIncrementalAsync(panLastScreen.Value.X, panLastScreen.Value.Y, screenX, screenY);
panLastScreen = (screenX, screenY);
}
// Trong OnMouseDown (button == 1):
panStartScreen = (screenX, screenY);
panLastScreen = (screenX, screenY); // Initialize
JavaScript Changes:
- Thêm
screenToSvgArray()function để export cho Blazor OnMouseMovevàOnMouseDownnhận thêmscreenX, screenYparameters
3.2. Zoom (Mouse Wheel) ✅ FIXED
Vấn đề ban đầu:
- Zoom không giữ tọa độ mouse không đổi
- Zoom không chính xác tại vị trí cursor
Giải pháp:
- Tính tỷ lệ vị trí cursor trong viewBox hiện tại
- Giữ điểm cursor không đổi trong world coordinates khi zoom
- Logic:
// Tính ratio của cursor trong viewBox var ratioX = (svgCenterX - ViewBoxX) / ViewBoxWidth; var ratioY = (svgCenterY - ViewBoxY) / ViewBoxHeight; // New dimensions var newWidth = ViewBoxWidth / factor; var newHeight = ViewBoxHeight / factor; // Adjust ViewBox để giữ cursor tại cùng vị trí world ViewBoxX = svgCenterX - ratioX * newWidth; ViewBoxY = svgCenterY - ratioY * newHeight;
3.3. Grid Rendering ✅ FIXED
Thay đổi:
stroke="#999"(đậm hơn, từ #ccc)stroke-width="0.04"(đậm gấp đôi, từ 0.02)opacity="0.7"(rõ hơn, từ 0.5)- Thêm
stroke-dasharray="0.1,0.1"(nét đứt)
Phase 4: Selection ⏳ PARTIAL
Status: Cơ bản đã có, cần enhancement
Đã có:
- Single select (click)
- Multi-select (Ctrl+click)
- Box select (Scanner mode)
- Selection visual feedback
Cần làm:
- Enhancement và polish
- Better visual feedback
Phase 5: Create Edge ✅ COMPLETE
Status: Đã hoàn thành
Đã implement:
- CreateEdge 1-Way mode với API call (
CreateEdgeAsync) - CreateEdge 2-Way mode với API call (tạo 2 edges ngược chiều)
- Edge preview while creating (preview line + preview nodes)
- Smart node detection (tích hợp API - tự động tìm node trong
NodeProximityRadiushoặc tạo mới) - Preview node hiển thị tại vị trí click đầu tiên và vị trí mouse
- Escape key để cancel CreateEdge operation
API Integration:
POST /api/edges- Create edge với smart node detection- Backend tự động tìm hoặc tạo nodes tại tọa độ start/end
- Nếu điểm nằm trong
NodeProximityRadiuscủa node hiện có → kết nối với node đó - Nếu không → tạo node mới tại tọa độ đó
Visual Features:
- Preview line: dashed line từ start node đến mouse position
- Preview nodes: semi-transparent circles tại start và end positions
- 2-way edges: hiển thị thành 2 đường song song với offset
Phase 6: Edit Operations ⏳ PARTIAL
Status: Đã implement một phần
6.1. Move Nodes ✅ COMPLETE
- Trigger: Ctrl + Drag (hoặc Move mode với Ctrl+M)
- Behavior: Di chuyển node theo chuột, khi thả Ctrl thì giữ nguyên vị trí hiện tại
- Undo/Redo: Hỗ trợ qua
MoveNodesCommand - Change Tracking: Đánh dấu nodes đã modified để Save sau
6.2. Copy Nodes/Edges ✅ COMPLETE
- Trigger: Toolbar button hoặc Ctrl+C (khi có selection)
- Mode:
EditorMode.Copy- dedicated mode với preview - Behavior:
- Click và drag để định offset
- Preview nodes và edges tại vị trí mới
- Thả chuột để hoàn thành copy
- Escape để cancel
- API:
POST /api/layout-data/copy-nodes- Backend xử lý toàn bộ logic - Logic:
- Tạo tất cả nodes mới với offset trước (validate coordinates, copy vehicle properties)
- Sau đó tạo tất cả edges mới (sử dụng node ID mapping)
- Xử lý 2-way edges: copy cả forward và reverse edge
- Không copy stations
- Selection: Tự động select các nodes/edges mới sau khi copy
6.3. Delete Selected ✅ COMPLETE
- Trigger: Delete button hoặc Delete key
- API:
DELETE /api/edges/{id}hoặc batch delete - Behavior: Xóa edges, backend tự động xóa orphaned nodes
- Confirmation: Dialog xác nhận trước khi xóa
6.4. Merge Nodes ✅ COMPLETE
- Trigger: Toolbar button (khi có 2+ nodes selected)
- API:
POST /api/layout-data/merge-nodes - Behavior:
- Gộp tất cả selected nodes thành 1 node tại vị trí center
- Redirect tất cả edges đến node mới
- Gộp vehicle properties từ tất cả nodes
- Nếu nhiều nodes có stations → dialog chọn node giữ station
- Distance check: nếu nodes ngoài
NodeProximityRadius→ confirmation dialog
- Selection: Tự động select node mới sau khi merge
6.5. Split Node ✅ COMPLETE
- Trigger: Toolbar button (khi có 1 node selected với 2+ edges)
- API:
POST /api/layout-data/split-node - Behavior:
- Split 1 node thành N nodes (N = số edges connected)
- Mỗi edge được redirect đến node mới tương ứng
- Copy vehicle properties đến tất cả nodes mới
- Nếu node có station → dialog chọn node nhận station
- Validation: node phải có ít nhất 2 edges mới được split
- Selection: Tự động select tất cả nodes mới sau khi split
6.6. Alignment Functions ❌ NOT STARTED
- Status: Buttons đã có, chưa implement logic
Phase 7: Right Panel - Properties ⏳ PARTIAL
Status: UI đã có, cần tích hợp API
7.1. Settings Tab ✅ FIXED & ENHANCED
Thay đổi trong cuộc hội thoại này:
a) Chuyển Display Options về Settings Tab:
- Xóa Display Options khỏi Toolbar
- Thêm Display Options vào Settings Tab (sau Grid Settings)
- Bao gồm: Show Edge Names, Show Node Names, Show Grid, Show Background Map
- Là session state (không lưu vào database)
b) Auto-generation Settings - Editable:
- Chuyển từ read-only sang editable
- Thêm local state để quản lý giá trị chỉnh sửa:
nodeNameAutoGenerate(bool)edgeNameAutoGenerate(bool)edgeMinLengthCreate(double)nodeProximityRadius(double)
- Thêm nút "Save Settings" với loading indicator
- Tích hợp với API để lưu settings
c) Thứ tự sections:
- Grid Settings
- Display Options
- Auto-generation Settings (editable với Save button)
- Layout Level Info (read-only)
- Statistics
d) API Integration:
- Tạo
EditorSettingsInfomodel mới - Thêm
EditorSettingsproperty vàoUpdateLayoutLevelRequest - Cập nhật
LayoutService.UpdateLevelAsync()để xử lý editor settings - Settings được lưu vào database khi click Save
Code Structure:
// Local editable state
private bool nodeNameAutoGenerate;
private bool edgeNameAutoGenerate;
private double edgeMinLengthCreate;
private double nodeProximityRadius;
// Initialize từ settings khi component load
protected override void OnParametersSet()
{
if (State.Level?.EditorSettings != null)
{
var settings = State.Level.EditorSettings;
nodeNameAutoGenerate = settings.NodeNameAutoGenerate;
// ... initialize other fields
}
}
// Save to API
private async Task SaveEditorSettings()
{
var request = new UpdateLayoutLevelRequest
{
EditorSettings = new EditorSettingsInfo
{
NodeNameAutoGenerate = nodeNameAutoGenerate,
// ... other fields
}
};
var updatedLevel = await ApiService.UpdateLevelAsync(State.Level.Id, request);
State.Level = updatedLevel; // Update state
}
Đã có:
- Node properties editor (UI)
- Edge properties editor (UI)
- VehicleType dropdown
- Vehicle properties editor (UI)
- Station info display
Cần làm:
- Save properties to API
- Validation
- Error handling
Phase 8: Trajectory Editor ❌ NOT STARTED
Status: Chưa thực hiện
Cần implement:
- Display trajectory (NURBS curve)
- Control points visualization
- Drag control points
- Add/Remove control points
- Change degree (với logic giữ biên dạng ít thay đổi)
Phase 9: Actions Form Builder ❌ NOT STARTED
Status: Chưa thực hiện
Cần implement:
- Actions form dialog
- ActionType dropdown (VehicleType defaults + custom text)
- Parameters key-value editor
- Validation
Phase 10: Undo/Redo & Save ⏳ PARTIAL
Status: Structure đã có, Save API đã implement
Đã có:
- Command pattern structure
- Undo stack
- Redo stack
- Save button (UI)
- Save API:
POST /api/layout-data/save- Batch save với transaction - Change tracking:
_modifiedNodeIds,_modifiedEdgeIds HasUnsavedChangesflag
Save API Details:
- Endpoint:
POST /api/layout-data/save - Request:
SaveLayoutDataRequestvới:NodesToCreate,NodesToUpdate,NodesToDeleteEdgesToCreate,EdgesToUpdate,EdgesToDelete
- Conflict Handling: Force Overwrite (Option D)
- Transaction: Toàn bộ operation trong 1 transaction, rollback nếu có lỗi
- Response:
SaveLayoutDataResponsevới số lượng items created/updated
Change Tracking:
MarkNodeModified(Guid nodeId)- Đánh dấu node đã thay đổiMarkEdgeModified(Guid edgeId)- Đánh dấu edge đã thay đổiSaveAsync()- Thu thập tất cả modified/new/deleted items và gọi API- Note: Các API calls trực tiếp (CreateEdge, DeleteEdge, CopyNodes, MergeNodes, SplitNode) KHÔNG set
HasUnsavedChangesvì đã lưu vào database
Cần làm:
- Unsaved changes warning khi exit
- Undo/Redo cho các operations khác (Align, etc.)
Phase 11: Polish ⏳ PARTIAL
Status: Một phần đã có
Đã có:
- Keyboard shortcuts (Ctrl+Z/Y/S/C/M, Delete, Escape)
- Display options
- Settings tab (đã sửa và enhance)
- Exit button
- Text rendering với fontSize phụ thuộc ZoomLevel và Resolution
- 2-way edges visualization (2 đường song song)
- Box select trong Scanner mode (select cả nodes và edges)
Text Font Size:
- Formula:
fontSizeSVG = baseFontSizeWorld / (Resolution * ZoomLevel) - baseFontSizeWorld: 0.24 meters (cho node names và edge names)
- Resolution: meters per pixel (từ
EditorSettings.Resolution) - ZoomLevel: hệ số zoom hiện tại
- Result: Text có cùng kích thước visual với mọi Resolution, zoom theo ZoomLevel
Cần làm:
- CheckLayout validation logic
- Performance optimization
🏗️ Architecture Overview
Component Hierarchy
LayoutManager (click Edit)
→ Navigate to /layout-editor/{levelId}
→ LayoutEditorComponent
├── EditorToolbar (top)
├── SVG Canvas (left) + MousePositionDisplay
└── Right Panel
├── Properties Tab
│ ├── NodePropertiesEditor (single node)
│ └── EdgePropertiesEditor (single edge)
└── Settings Tab
├── Grid Settings
├── Display Options
├── Auto-generation Settings
├── Layout Level Info
└── Statistics
Data Flow
-
Initialization:
- User clicks "Edit" on LayoutManager
- Navigate to
/layout-editor/{levelId} LayoutEditorComponentloadsLayoutEditorState.InitializeAsync()fetches:- Level info (with EditorSettings)
- Layout data (Nodes, Edges, Stations)
- VehicleTypes
- Background image (if available)
- Viewport initialized to fit image bounds
-
User Interactions:
- Mouse events → JavaScript (
svgEditor.js) → Blazor (SvgEditorCanvas.razor) - State changes →
LayoutEditorState→ Notify components viaOnStateChangedevent - UI updates → Blazor re-renders affected components
- Mouse events → JavaScript (
-
Save Operations:
- User edits properties → Local state changes
- Click "Save Settings" → API call (
MapManagerApiService.UpdateLevelEditorSettingsAsync) - Backend updates database → Returns updated Level
- State updated → UI refreshed
State Management Pattern
- Centralized State:
LayoutEditorState(scoped service) - Event-driven: Components subscribe to
OnStateChangedevent - Immutable Updates: State methods return new state or update properties and notify
- Command Pattern: Undo/Redo via
EditorCommandabstract class
Coordinate Transformation
- World → SVG:
WorldToSvg(worldX, worldY)→(worldX, physicalHeight - worldY) - SVG → World:
SvgToWorld(svgX, svgY)→(svgX, physicalHeight - svgY) - Screen → SVG: JavaScript
screenToSvgArray(screenX, screenY)usinggetScreenCTM()
API Integration
- Service:
MapManagerApiService(injected) - Endpoints Used:
GET /api/layouts/{layoutId}/levels/{levelId}- Get level infoGET /api/layouts/{layoutId}/levels/{levelId}/data- Get layout dataPUT /api/layouts/{layoutId}/levels/{levelId}- Update level (including editor settings)GET /api/layouts/{layoutId}/levels/{levelId}/background-image- Get background imagePOST /api/edges- Create edge (with smart node detection)DELETE /api/edges/{id}- Delete edge (single)DELETE /api/edges/batch- Delete edges (batch)POST /api/layout-data/save- Batch save nodes/edges (transactional)POST /api/layout-data/copy-nodes- Copy nodes/edges with offsetPOST /api/layout-data/merge-nodes- Merge multiple nodes into onePOST /api/layout-data/split-node- Split one node into multiple nodes
🔧 Technical Details
Coordinate System
- World Coordinates: Bottom-left origin (0,0), Y increases upward, units in meters
- SVG Coordinates: Top-left origin (0,0), Y increases downward, units in pixels
- Transform:
svgY = physicalHeight - worldY(flip Y axis)
Pan Logic (Fixed)
- Sử dụng incremental delta thay vì delta từ điểm bắt đầu
- Lưu
panLastScreenđể track vị trí trước đó - Convert screen coordinates sang SVG mỗi lần để đảm bảo chính xác khi ViewBox thay đổi
Zoom Logic (Fixed)
- Zoom tại vị trí cursor
- Giữ tọa độ mouse không đổi trong world space
- Tính ratio của cursor trong viewBox và adjust ViewBox position
Grid Styling
- Stroke:
#999(đậm) - Stroke width:
0.04(đậm gấp đôi) - Opacity:
0.7(rõ) - Stroke dasharray:
0.1,0.1(nét đứt)
Text Rendering (Node/Edge Names)
- Font Size Formula:
fontSizeSVG = baseFontSizeWorld / (Resolution * ZoomLevel)baseFontSizeWorld: 0.24 metersResolution: meters per pixel (fromEditorSettings.Resolution)ZoomLevel: current zoom factor
- Result: Text có cùng kích thước visual với mọi Resolution, zoom theo ZoomLevel
- Color: Red (#f44336)
- Font: Segoe UI, font-weight 500
- Letter Spacing: -0.02em (characters gần nhau hơn)
- Position:
- Node names: Below node center
- Edge names: Above edge center
Settings Tab Structure
- Grid Settings (editable)
- Display Options (session state)
- Auto-generation Settings (editable với Save button)
- Layout Level Info (read-only)
- Statistics (read-only)
📊 Implementation Status Summary
| Phase | Status | Progress |
|---|---|---|
| Phase 1: Foundation | ✅ Complete | 100% |
| Phase 2: SVG Canvas | ✅ Complete | 100% |
| Phase 3: Viewport Controls | ✅ Complete | 100% |
| Phase 4: Selection | ⏳ Partial | ~80% |
| Phase 5: Create Edge | ✅ Complete | 100% |
| Phase 6: Edit Operations | ⏳ Partial | ~70% |
| Phase 7: Properties | ⏳ Partial | ~70% |
| Phase 8: Trajectory Editor | ❌ Not Started | 0% |
| Phase 9: Actions Form Builder | ❌ Not Started | 0% |
| Phase 10: Undo/Redo & Save | ⏳ Partial | ~70% |
| Phase 11: Polish | ⏳ Partial | ~60% |
Overall Progress: ~65-70%
🐛 Bugs Fixed in This Session
1. Pan Logic - Cộng Dồn Delta
Problem: Pan bị cộng dồn quãng đường di chuyển Root Cause: Tính delta từ điểm bắt đầu mỗi lần thay vì incremental Solution: Chuyển sang incremental delta từ lần move trước
2. Zoom Logic - Tọa Độ Mouse Thay Đổi
Problem: Zoom không giữ tọa độ mouse không đổi Solution: Tính ratio và adjust ViewBox để giữ cursor tại cùng vị trí world
3. Grid Styling
Problem: Grid quá nhạt và nét liền Solution: Đậm hơn, opacity cao hơn, thêm stroke-dasharray
4. Settings Tab - Display Options
Problem: Display Options ở Toolbar thay vì Settings Solution: Chuyển về Settings Tab, xóa khỏi Toolbar
5. Settings Tab - Auto-generation Settings Read-only
Problem: Auto-generation Settings chỉ hiển thị, không thể edit Solution: Làm editable với local state và Save button, tích hợp API
6. Create Edge - Smart Node Detection
Problem: CreateEdge không hoạt động, chỉ tạo được trên nodes đã có Solution:
- Backend API tự động detect nodes trong
NodeProximityRadiushoặc tạo mới - Frontend chỉ cần gửi world coordinates, backend xử lý logic
7. 2-Way Edges Visualization
Problem: 2-way edges hiển thị thành 1 line với 2 arrowheads Solution: Tính toán offset để hiển thị thành 2 đường song song riêng biệt
8. Text Font Size - Resolution Independence
Problem: Text size thay đổi khi Resolution thay đổi
Solution: Công thức fontSizeSVG = baseFontSizeWorld / (Resolution * ZoomLevel) để text có cùng kích thước visual với mọi Resolution
9. HasUnsavedChanges - Direct API Calls
Problem: HasUnsavedChanges được set sau các API calls trực tiếp (CreateEdge, DeleteEdge, etc.)
Solution: Xóa HasUnsavedChanges = true sau các API calls trực tiếp vì đã lưu vào database rồi
10. CopyNodesAsync - Refactor
Problem: Copy logic phức tạp ở frontend, dùng temp edges Solution:
- Refactor để backend xử lý toàn bộ logic
- Tạo nodes trực tiếp vào database (validate coordinates, copy vehicle properties)
- Tạo edges trực tiếp với node ID mapping
- Xử lý 2-way edges tự động
📝 Key Files Modified in This Session
Components:
Components/LayoutEditor/RightPanel/SettingsTab.razor- Editable settings với SaveComponents/LayoutEditor/SvgEditorCanvas.razor- Fixed pan/zoom logic, CreateEdge, Copy preview, text renderingComponents/LayoutEditor/EditorToolbar.razor- Removed Display Options, added Move mode button
Services:
Services/State/LayoutEditorState.cs- Fixed zoom logic, CreateEdgeAsync, DeleteEdgesAsync, CompleteCopyAsync, MergeNodesAsync, SplitNodeAsync, SaveAsync, change tracking
JavaScript:
wwwroot/js/svgEditor.js- Added screenToSvgArray export, screen coordinates, Ctrl+C/M handlers
Shared Models:
RobotNet10.MapEditor.Shared/Models/EditorSettingsInfo.cs- New modelRobotNet10.MapEditor.Shared/DTOs/Requests/UpdateLayoutLevelRequest.cs- Added EditorSettingsRobotNet10.MapEditor.Shared/DTOs/Requests/SaveLayoutDataRequest.cs- New modelRobotNet10.MapEditor.Shared/DTOs/Responses/SaveLayoutDataResponse.cs- New modelRobotNet10.MapEditor.Shared/DTOs/Requests/CopyNodesRequest.cs- New modelRobotNet10.MapEditor.Shared/DTOs/Responses/CopyNodesResponse.cs- New modelRobotNet10.MapEditor.Shared/DTOs/Requests/MergeNodesRequest.cs- New modelRobotNet10.MapEditor.Shared/DTOs/Requests/SplitNodeRequest.cs- New model
Backend:
Commons/RobotNet10.MapManager/Services/LayoutService.cs- Update editor settings logicCommons/RobotNet10.MapManager/Services/LayoutDataService.cs- SaveLayoutDataAsync, CopyNodesAsync (refactored), MergeNodesAsync, SplitNodeAsyncCommons/RobotNet10.MapManager/Controllers/LayoutDataController.cs- New endpoints: save, copy-nodes, merge-nodes, split-node
🎯 Next Steps
Immediate (Phase 3 completion):
- Test pan/zoom thoroughly
- Verify no accumulation issues
- Performance testing
Short-term (Phase 4-6):
- Enhance selection features (Shift+Click multi-select, box select edges)
- Implement Create Edge với API
- Implement Edit Operations (Move, Copy, Delete, Merge, Split)
- Implement Alignment functions (6 directions)
Medium-term (Phase 7-9):
- Properties save to API (via Save button - batch save)
- Trajectory Editor (NURBS)
- Actions Form Builder
Long-term (Phase 10-11):
- Save API với transaction và change tracking
- Complete Undo/Redo cho tất cả operations
- CheckLayout validation
- Performance optimization
- Additional polish
📚 References
- Database Design:
DATABASE_DESIGN_DISCUSSION.md - API Guide:
API_IMPLEMENTATION_GUIDE.md - LayoutManager Implementation:
LAYOUTMANAGER_TECHNICAL.md(đã lưu trước đó)
Last Updated: 2024-12-XX
Session Focus: CreateEdge, Copy/Move/Delete/Merge/Split operations, Save API, Text rendering with Resolution independence, CopyNodesAsync refactor