⚠️ CẢNH BÁO ĐỘ TƯƠI — chưa đối chiếu lại sau G2–G5. Tài liệu này sửa lần cuối trước đợt G2–G5 (hợp nhất hai đường render, chọn nhiều clip, keyframe automation, thư viện ảnh động). Kiểm bằng grep: nó không nhắc tới một tính năng nào trong số đó. Phần mô tả giao diện vì thế có thể đã lạc hậu. Khi nghi ngờ, tin test và kịch bản UiDrive trước.
Cập nhật theo code hiện tại: 2026-09-11 (RMP-88: X/H/T trần đã đổi chủ)
ĐÍNH CHÍNH 2026-09-11 — X / H / T trần đổi chủ (RMP-88)
Ba phím một chữ đi ngược phản xạ của mọi trình biên tập (X vô hại, H = Hand/pan, T = Text)
lại là ba lệnh phá hoại hoặc đảo cấu trúc ở đây: X arm cục tẩy LED, H lật ngang, T đảo chiều.
Người quen Illustrator/Figma gõ T để chèn chữ và thấy dải LED đảo chiều im lặng. Nay:
| Phím | Trước | Nay |
|---|---|---|
X |
arm Quét xoá LED | không làm gì — cục tẩy đòi Shift+X |
H |
Lật ngang | không làm gì — lật ngang đòi Shift+H |
T |
Đảo chiều dải | mở hộp Chữ → LED (như mọi trình biên tập) — đảo chiều đòi Shift+T |
Tooltip của mọi nút rail có phím nay dùng placeholder ({0}) do PlatformShortcuts.Describe điền lúc
chạy (⇧X trên macOS, Shift+X trên Windows). Bảng lệnh Ctrl+K derive cùng nguồn (CommandSeed.RailShortcutOf).
Kịch bản destructive-keys.json canh cả hai chiều: phím trần không làm gì / mở hộp Chữ, phím Shift làm việc cũ.
ĐÍNH CHÍNH 2026-08-27 — sáu lời hứa PHÍM TẮT trong tài liệu này đã SAI
Đợt rà đối chiếu tài liệu ↔ mã bắt được sáu chỗ nói sai về phím tắt và một loạt con số lệch. Ghi ra đây để người sau không "sửa lại" về bản cũ: những khẳng định dưới đây là SAI, và đã được thay bằng giá trị đọc thẳng từ mã.
| Chỗ | Bản cũ nói (SAI) | Sự thật trong mã |
|---|---|---|
| §3 #11 Chọn | M |
V — WiringRailLayout.cs:38, resx "Chọn (V)" |
| §3 #14 Xóa điểm | D / Delete / Backspace |
KHÔNG có phím — Shortcut = null (WiringRailLayout.cs:44) |
| §1 #5 Đường thẳng | P |
L — DrawToolKeys[Key.L], tooltip "Đường thẳng (L)" |
| §9 Xoá lựa chọn | D |
Delete — WiringCanvasV2View.axaml:415 |
| §9 Lật dọc | V |
KHÔNG có phím — V là công cụ CHỌN (axaml:401-403) |
| §8 Grid mặc định | 100 mm |
10 mm — CanvasSettings.cs:58 |
§11 (Bảng phím tắt đầy đủ) đã ĐÚNG từ audit 2026-08-12 — chính nó liệt M, D, P vào nhóm
"KHÔNG LÀM GÌ". Tài liệu vì thế tự mâu thuẫn nhiều tháng: §3/§9 hứa một đằng, §11 nói một
nẻo. Khi hai chỗ trong tệp này lệch nhau, tin §11 — nó derive từ đúng hai bảng mà OnKeyDown
tra thật.
Vì sao lớp lỗi này đáng một khối đính chính riêng: đây đúng thứ mà invariant §5 sinh ra để chặn.
Từng có nút đỏ "Xóa kết nối (X)" mà phím X thật ra XOÁ LED — tin lời hứa phím là mất dữ
liệu. Một tài liệu hứa D xoá được LED cũng nguy hiểm y như một tooltip hứa thế.
Con số đã sửa cùng đợt: §1 VẼ 10 → 13 nút · §4 NỐI DÂY: bỏ 2 dòng ma #26/#27 (đúng
7) · §7 TỆP 3 → 6 nút (thêm cả nút đỏ "THAY THẾ toàn bộ bản vẽ" trước đây không được
nhắc) · §9 menu 14 → 19 mục, trong đó 8 mục căn/phân bố chứ không phải 6 · OnKeyDown ở
:513 chứ không phải :268.
Ba cảnh báo ⚠️ đã hết hiệu lực và được gỡ: "bấm lại nút CHỌN → rail không nút nào sáng" (G17 đã
sửa) · "nút trùng #15≡#18, #13≡#27 vẫn còn" (G9 đã gỡ cả hai) · "FreeRect đã có trong registry,
chỉ cần thêm nút" (E8c đã gỡ member khỏi enum tool).
Về cột
#: nó là mã định danh bền, không phải thứ tự đọc. Các số khuyết (15, 17, 20, 21, 26, 27) là nút đã nghỉ hưu — số của chúng không được cấp lại, để tham chiếu chéo trong tệp này ("#13 ≡ #27", "#15 ≡ #18") vẫn đọc được. Nút mới vì thế lấy số nối tiếp cuối dãy (39–44) kể cả khi nó nằm giữa bảng.
Tài liệu này liệt kê mọi nút, mọi tương tác, mọi phím tắt của tab Sơ đồ dây, kèm: điều kiện tiên quyết, các bước thao tác cụ thể để tái hiện, kết quả mong đợi, và có undo/lưu được hay không.
Mọi câu ở đây rút từ code đã đọc (không suy đoán). Chỗ nào giao diện quảng cáo một đằng, code làm một nẻo được đánh dấu ⚠️ — đó chính là danh sách gap cần vá.
Nguồn: Services/WiringTools/WiringRailLayout.cs (rail), Views/Wiring/WiringCanvasV2View.axaml(.cs)
(toolbar, canvas, phím tắt, menu chuột phải), ViewModels/Wiring/WiringCanvasV2ViewModel*.cs (dispatch),
Services/WiringTools/*Tool.cs (tool), RemixStudio.Core/Localization/Strings.resx (tooltip).
0. Quy tắc chung
- Phím tắt canvas nằm ở
WiringCanvasV2View.axaml.cs:513(OnKeyDown). Handler bỏ qua hoàn toàn khi con trỏ đang ở trongTextBox/NumericUpDown/ComboBox— đang gõ số trong panel tham số thì phím tắt chết. - Vẽ hình chỉ sinh LED V2 khi diagram có ≥ 1 Controller. Không có controller → hình rơi vào nhánh
Diagram.Shapes(V1): vẫn vẽ, vẫn lưu, nhưng không có LED nào, "Tổng LED" vẫn 0. Mọi project do app tạo ra đều có sẵn Controller, nên trên thực tếDiagram.Shapesluôn rỗng. - Undo: mỗi lần gán
Diagram= 1 undo entry. Trong lúc kéo/co giãn/xoay, write-back bị chặn → 1 cử chỉ = đúng 1 undo entry. Thao tác chỉ đổi selection thì không undo được. - Nút rail là toggle về Select: bấm lại nút đang sáng → rail trả về
Select, không giữ tool modal "ma". - One-shot vs modal: nút one-shot chạy xong tự trả về
Select; nút modal sáng cho tới khi Esc/đổi tool/bấm lại chính nó. - Status bar (
ToolStatusMessage) tự xoá sau 4 giây, trừ khi đang ở chế độ đặt DXF. - ✅ ĐÃ SỬA: trước đây
RailToolItem.Shortcutchỉ là chuỗi trang trí,OnKeyDowngiữ một bảng switch riêng → tooltip và phím chạy lệch nhau (gốc rễ của blocker "X xoá LED"). NayWiringCanvasShortcuts.RailKeysderive thẳng từ chính fieldShortcut, vàOnKeyDownchỉ tra 2 bảngDrawToolKeys+RailKeys— phím quảng cáo và phím chạy không thể lệch nhau nữa. Trùng phím giữa 2 nút → throw ngay lúc khởi tạo (unit test chốt trước khi ship).
1. Nhóm VẼ (toolbar ngang) — 13 nút
Tất cả tool kéo-vẽ: kéo phải dài ≥ 5 world units, ngắn hơn = huỷ im lặng, không tạo gì.
✅ Overlay nổi trên canvas không còn nuốt pointer. Floating Param Panel, Canvas HUD và overlay "LED Pick Mode" đã tách phần chrome khỏi phần nội dung:
- vùng nền/viền/bóng không chặn drag xuống canvas
- control thật bên trong vẫn bấm được
- regression đã được khóa bằng
tools/UiDrive/scenarios/overlay-passthrough.json
| # | Nút | Phím thật | Ghi chú |
|---|---|---|---|
| 1 | Zigzag Ngang | 1 |
✅ khớp tooltip |
| 2 | Zigzag Dọc | 2 |
✅ |
| 3 | Răng cưa Ngang | 3 |
✅ |
| 4 | Răng cưa Dọc | 4 |
✅ |
| 5 | Đường thẳng | L |
✅ khớp tooltip "Đường thẳng (L)" (Strings.resx:38, WiringCanvasShortcuts.DrawToolKeys[Key.L]) |
| 6 | Gấp khúc | P |
✅ khớp tooltip "Gấp khúc (P)" (Strings.resx, Key.P) |
| 7 | Hình tròn | O |
✅ |
| 8 | Viền | 9 |
✅ |
| 9 | Xoắn ốc | không có | ✅ tooltip "Xoắn ốc" không hứa phím nào |
| 10 | Đa giác đều | không có | ✅ tooltip "Đa giác đều" không hứa phím nào |
| 11 | Ngôi sao | không có | ✅ tooltip "Ngôi sao" không hứa phím nào |
| 12 | Chữ → LED | T |
✅ (RMP-88) tooltip "Chữ → LED ({0})" điền phím lúc chạy; T là Text như mọi trình biên tập |
| 13 | Ảnh nền | không có | ✅ tooltip đã gỡ hint (G) |
13 nút, không phải 10. Ba tool E8c (Xoắn ốc / Đa giác đều / Ngôi sao) đã có nút thật trên thanh
ngang (WiringCanvasV2View.axaml:119, :127, :135) nhưng bảng này bỏ sót chúng nhiều tháng.
Cả ba không có phím tắt và tooltip cũng không hứa phím nào — đúng luật §5.
Các bước (tool kéo-vẽ, ví dụ Zigzag Ngang): bấm nút hoặc phím 1 → panel tham số hiện
(Bước ngang/dọc, mặc định 10/10 mm) → nhấn-giữ chuột trái tại world (200,200) → rê tới (600,400) → nhả.
Kết quả mong đợi: CountX = round(400/PitchX) = 40, CountY = round(200/PitchY) = 20;
tổng LED bị chặn trần 20.000 (vượt → status "Hình quá lớn … đã giảm còn …"). Tạo strip mới
trên controller đầu tiên; đầy cổng thì tự sang strip kế. Canvas hiện LED; status bar
"V2 · N ctrl · N strips · N LEDs"; HUD "Tổng LED" tăng. Undo được, lưu được.
✅ ĐÃ SỬA — nút "Áp dụng": ApplyParameters() nay re-apply thật cho hình vừa vẽ — gỡ các strip
hình đó tạo ra, build lại shape với LedConfig mới trên cùng bbox, đúng 1 undo entry. Chưa vẽ hình
nào, hoặc Diagram đã bị chỉnh bất kỳ kiểu gì sau lần vẽ (kéo/xoay/scale/xoá LED, undo, dán…) →
không đụng dữ liệu, chỉ báo qua status bar là tham số sẽ tự áp cho hình vẽ kế tiếp.
✅ ĐÃ SỬA — nút "Gốc" (↖ ◎ ↗ ↙ ↘): DetectOrigin (suy góc gốc từ hướng kéo chuột) đã bị gỡ. Góc gốc
nay lấy đúng từ panel (ParamOrigin → DefaultLedConfig.Origin): ◎ Center = điểm bấm là tâm hình
(bbox nở đối xứng), Line ◎ = điểm bấm là trung điểm đoạn.
ℹ️ WiringToolKind.FreeRect KHÔNG còn tồn tại (E8c) — member đã bị gỡ khỏi enum tool
(WiringToolKind.cs:20-27): nó không có nút, không có phím, không có mục trong bảng lệnh Ctrl+K, tức
không một đường nào của người dùng tới được nó; và nó trùng chức năng với Răng cưa Ngang
(ShapeGenerator map cả ShapeKind.SawtoothH lẫn ShapeKind.FreeRect vào cùng hàm GenerateFreeRect,
ShapeGenerator.cs:33/:36, mà SawtoothH thì CÓ nút + phím 3).
ShapeKind.FreeRect thì GIỮ NGUYÊN — nó là giá trị mặc định của ShapeDto.Kind trên đĩa
(ProjectDto.cs:331), xoá là phá file khách cũ.
Bản trước của mục này ghi "đã có trong registry… muốn phơi ra UI thì chỉ cần thêm nút" — câu đó đã
sai kể từ E8c: không còn gì để phơi ra, và đi thêm nút cho nó là dựng lại đúng cái nút trùng vừa gỡ.
Đường thẳng (Line): panel chỉ 1 ô "Bước LED (mm)". Shape neo tại điểm bấm đầu tiên (không phải bbox). Giữ Shift khi nhả = snap góc về bội số 45°.
Hình tròn (Circle): panel riêng — Số vòng (1–32), LED/tia (2–512), Góc bắt đầu, Góc quét,
checkbox Thuận chiều kim đồng hồ. Số LED = Rings × LedsPerSpoke do người dùng nhập (không suy từ bbox).
Chữ → LED: mở dialog (nhập chữ + pitch) → rasterize font 5×7 → đặt tại tâm viewport hiện tại.
1b. Băng "Gấp khúc" — ✅ ĐÃ VIỆT HOÁ (audit 2026-08-12)
Nút chốt của chế độ này ghi thẳng Apply, và hai tooltip nhét Apply vào giữa câu tiếng
Việt. Nhãn băng ghi Polyline. Cả Apply lẫn Polyline không nằm trong danh sách được
miễn của invariants §3 (16 tên effect + DMX/MIDI/DXF/PDF/LED/FPS/mm/ms/Universe).
| Chỗ | Trước | Nay |
|---|---|---|
| Nhãn băng | Polyline |
Gấp khúc |
| Nút chốt | Apply |
Tạo dải LED |
| Tooltip nút vẽ | "Polyline V2 — nhấn N điểm, Apply tạo Strip mới trên Controller đang chọn" | "Gấp khúc — nhấn N điểm rồi bấm "Tạo dải LED"; dải LED mới nằm trên bộ điều khiển đang chọn" |
| Tooltip băng | "Chế độ Polyline — nhấn các điểm rồi Apply" | "Chế độ Gấp khúc — nhấn các điểm rồi bấm "Tạo dải LED"" |
| Tooltip nút chốt | "Apply: tạo Nhóm + LED (…)" | "Tạo dải LED: sinh dải LED từ các điểm vừa nhấn (…)" |
Vì sao một động từ + tên thực thể, chứ không "Áp dụng": nó nói đúng việc nút làm (các điểm
→ một dải LED), và nó dùng lại đúng từ mà cả tab này đang dùng cho Strip — "Thêm dải LED trên
cổng bộ điều khiển", "Sửa dải LED", "Tách dải LED", "Gộp dải LED", "Chuyển LED sang dải LED khác".
"Áp dụng" thì mơ hồ (áp dụng cái gì vào đâu?) và trùng chữ
với nút "Áp dụng" của panel tham số — hai nút cùng chữ làm UiDrive.PressButton (lấy nút đầu
tiên khớp Contains) bấm nhầm.
⚠️ Một tiền đề trong brief audit là SAI, đã đo: brief dự đoán đổi thành "Áp dụng" sẽ làm
apply-params.json và apply-params-guard.json đỏ. Không tái hiện được — cả hai vẫn xanh
(cùng overlay-passthrough.json). Lý do: băng gắn IsVisible={Binding IsPolylineV2Mode}, và khi ẩn
thì các nút của nó không có mặt trong cây hình (đo bằng press "Hủy" lúc băng ẩn → "no button
reading 'Hủy'", danh sách nút hiện có không liệt kê nó). Ba kịch bản ấy không bao giờ vào chế độ
Gấp khúc nên không có gì để đụng. Nhưng va chạm là có thật ngay khi một kịch bản đi qua cả
hai mặt — và đó chính là kịch bản mới: đặt "Áp dụng" cho nút băng ⇒
wiring-polyline-banner-vietnamese.json đỏ 5 bước.
⚠️ Nút này đã ĐỔI CHỮ MỘT LẦN NỮA (2026-08-12, mục 1e): Tạo nhóm → Tạo dải LED. Lý do
nằm ở 1e — "nhóm" bị bỏ hẳn. Kịch bản wiring-polyline-banner-vietnamese.json vẫn bấm theo CHỮ
(không chuyển sang clickname) vì chữ trên nút chính là thứ nó canh: chuyển sang bấm theo tên
sẽ giết đúng bất biến "nút phải nói tiếng Việt".
Băng cố ý Ở LẠI sau khi bấm "Tạo dải LED" (CommitPolylineV2 dọn điểm nhưng giữ chế độ để vẽ
tiếp dải LED sau). Esc mới đóng băng.
1c. controller / strip trong chuỗi tiếng Việt — ✅ ĐÃ TRẢ NỢ (audit 2026-08-12)
Nợ ghi ở mục 1b ("~32 chuỗi") đã trả. Rà lại từ đầu, không tin con số cũ: 30 <value> trong
Strings.resx khớp controller|strip, trong đó 29 là từ thật (1 chuỗi chỉ dính vì tên trường
IndexInStrip), cộng 11 câu gõ cứng trong .cs và 5 chỗ sinh tên thực thể mà lưới
resx không thấy. Con số cũ thiếu đúng phần nằm ngoài resx.
Tên thực thể mặc định cũng là chữ khách đọc. Tab Trực tiếp hiện nguyên Controller 1 trong mục
"Bộ điều khiển đã liên kết" — tên ấy do ProjectFactory (3 nhánh dựng dự án), WiringV2Migrator
(DefaultControllerName) và StripLedOps ($"Strip P{port}") sinh ra, nay là "Bộ điều khiển N"
và "Dải LED P{cổng}" (chữ "Nhóm" ở đây đã bị mục 1e thay). Đã kiểm ba rủi ro trước khi đổi: (1) tệp .rfx2 đặt tên theo Controller.Id
chứ không theo Name → không đụng tên tệp bàn giao; (2) trần Rfx2Format.MaxNameBytes là 256 byte,
"Bộ điều khiển 1" chỉ ~19 byte; (3) tính idempotent của migration V1→V2 khoá theo Strips is not null
và DefaultControllerId = "ctrl_default", không theo Name. Dự án cũ giữ nguyên tên cũ — đây
không phải mutation trên tệp khách.
Từ được chọn, và vì sao:
| Khái niệm | Từ dùng | Bằng chứng |
|---|---|---|
Controller |
bộ điều khiển | 33 chuỗi đã dùng, giữ mọi vị trí đắt: mục "BỘ ĐIỀU KHIỂN", nút "+ Bộ điều khiển", tiêu đề hộp thoại. Không có từ nào cạnh tranh. |
Strip |
Đợt này chọn "nhóm" theo số đông chuỗi. Chủ dự án đã bác ngày 2026-08-12: "nhóm" là từ chung chung, dễ lẫn với việc gom nhóm bất kỳ (nhóm clip, nhóm hiệu ứng, nhóm nút trên rail), còn một Strip là một dải LED nối tiếp nhau. Xem mục 1e. |
⚠️ "mạch" KHÔNG phải "bộ điều khiển". "Mạch" là bo phần cứng thật; Controller là thực thể
trong sơ đồ. Câu "Bộ điều khiển đã liên kết mạch" nói về liên kết giữa hai thứ đó — dịch gộp là
làm câu ấy vô nghĩa.
Ngoại lệ ghi ở đây ("hai chỗ cố ý dùng dải chứ không nhóm") nay KHÔNG còn là ngoại lệ —
mục 1e đã đưa cả app về một tên. Chính hai chỗ đó là bằng chứng sớm nhất rằng "nhóm" đang thua:
wiring.tool-status.auto-connect.none-left / .all-failed phải nói "dải" chỉ để khỏi chửi nhau với
các chuỗi cùng hàm.
1d. Badge "N ctrl · N strips · N LEDs" — CỐ Ý ĐỂ NGUYÊN, và vì sao
WiringCanvasV2ViewModel.V2StatusLabel in thẳng "1 ctrl · 4 strips · 512 LEDs" lên status bar tab
Sơ đồ dây. Nó vi phạm §3 (ctrl, strips không nằm trong danh sách miễn), và đợt rà cũ không
đếm nó vì nó gõ cứng trong ViewModel, không nằm trong resx.
Đã thử việt hoá và đã ĐO — rồi hoàn nguyên. Đổi sang "1 bộ điều khiển · 4 nhóm · 512 LED"
(dài hơn ~6 ký tự) làm câu hướng dẫn của tool bị cắt sớm hơn ~4 chữ. Ảnh A/B cùng một bước
(wiring-quick-connect-context, bước 09, cửa sổ 1600px, dự án 4 cổng):
| Status bar đọc được | |
|---|---|
91e151a |
V2 1 ctrl · 4 strips · 512 LEDs │ Cổng 1: 128 · … · Tổng: 512 │ ⚠ Nối nhanh: 3 đoạn sắp nối — |
| Việt hoá badge | V2 1 bộ điều khiển · 4 nhóm · 512 LED │ Cổng 1: 128 · … · Tổng: 512 │ ⚠ Nối nhanh: 3 đoạn s |
Căn nguyên KHÔNG phải chữ tiếng Việt: status bar đã cắt sẵn ở 91e151a — hàng này không
xuống dòng, không có …, và ba khối (ô tick lớp phủ + badge + tóm tắt cổng) đã ăn hết bề ngang
trước khi câu hướng dẫn kịp hiện. Việt hoá chỉ làm lộ ra và làm nặng thêm cái đã hỏng.
Cắt mất "Enter/bấm lại để chốt, Esc để huỷ" tệ hơn hẳn một badge tiếng Anh: badge chỉ lặp lại ba con số mà HUD "Trạng thái Canvas" ngay trên đã ghi đủ bằng tiếng Việt, còn câu kia là cách người dùng biết phải bấm gì tiếp. Nên giữ nguyên badge, và ghi nợ đúng chỗ: muốn việt hoá badge thì phải sửa bố cục status bar trước (cho câu hướng dẫn một hàng riêng, hoặc bỏ badge vì trùng HUD). Đó là quyết định thiết kế, không phải việc của một bản vá localization.
Nợ ghi ở đây ("hai tên tiếng Việt song song cho Strip") ĐÃ TRẢ — xem mục 1e. Badge thì
vẫn để nguyên tiếng Anh: lý do ở trên là bố cục, không phải từ vựng, nên đợt 1e không đụng
vào nó. Đổi badge sang "1 bộ điều khiển · 4 dải LED · 512 LED" sẽ dài hơn cả phương án đã đo và
hỏng đúng chỗ cũ.
1e. Strip = "dải LED" — MỘT tên, chốt 2026-08-12
Chủ dự án chốt: Strip gọi là "dải LED", bản en gọi là "Strip". Lý do bác "nhóm": nó
chung chung — repo này đang dùng đúng chữ ấy cho bốn thứ khác (nhóm clip trên timeline,
nhóm hiệu ứng trong thư viện, nhóm nút trên rail, nhóm tham số), nên "nhóm" không gọi tên được cái
gì cả. Một Strip là một dải LED nối tiếp nhau — tên phải nói ra điều đó.
Đo trước khi đổi (tại 58a1f77): Strings.resx có 64 chỗ nói "nhóm" và 14 chỗ nói
"dải LED". Số đông không quyết định — nghĩa quyết định.
Trước (58a1f77) |
Nay | |
|---|---|---|
| Mục tiêu đề panel | NHÓM (khoá wiring.section.strips) |
DẢI LED — xem ghi chú dưới bảng |
| Nút thêm | + Nhóm |
+ Dải LED |
| 4 tooltip | Sửa/Xóa/Tách/Gộp nhóm | Sửa/Xóa/Tách/Gộp dải LED |
| Menu chuột phải | Chuyển LED sang nhóm khác |
Chuyển LED sang dải LED khác |
| Ô tick lớp phủ | Đường nhóm LED |
Đường dải LED |
| Nút chốt Gấp khúc | Tạo nhóm |
Tạo dải LED |
| Tên mặc định | Nhóm 1 · Nhóm P{cổng} · Group {n} |
Dải LED 1 · Dải LED P{cổng} · Dải LED {n} |
Bản en |
GROUPS / + Group / Create Group |
STRIPS / + Strip / Create Strip |
Ghi chú (rà lại 2026-08-12): dòng đầu bảng gọi SAI tên khoá, và khoá ấy nay đã bị xoá. Không có
mục tiêu đề panel nào đọc wiring.section.strips — grep toàn repo (.axaml, .cs, kịch bản
tools/UiDrive/*.json) không ra một tham chiếu nào, và panel thật
(src/RemixStudio/Views/Modules/WiringV2EntitiesPanelView.axaml) chỉ có tiêu đề BỘ ĐIỀU KHIỂN
(wiring.section.controllers) — dải LED nằm lồng bên trong cây bộ điều khiển nên không có tiêu
đề riêng. Chữ Dải LED người dùng thật sự nhìn thấy đến từ wiring.label.strips, ở bảng số liệu
trên canvas (WiringCanvasV2View.axaml, dòng "Dải LED · {số}"), không phải tiêu đề panel. Khoá
wiring.section.strips là khoá mồ côi và đã bị xoá khỏi cả hai .resx; mọi ô còn lại của bảng vẫn
đúng.
$"Group {n}" là chỗ đợt 1c bỏ sót: WiringCanvasV2ViewModel.EnsureStrip +
.Tools.cs đặt tên tiếng Anh cho mọi dải sinh ra từ vẽ hình — không nằm trong resx nên lưới
1c không thấy, mà đó lại là đường tạo dải phổ biến nhất.
Đã đo bề rộng, không cắt chữ ở đâu. "dải LED" dài gần gấp đôi "nhóm", nên rủi ro thật là
tràn panel 320px. Ảnh A/B cùng một bước (tab "Sơ đồ dây" #1, cửa sổ 1600px):
- Hàng nút
+ Bộ điều khiển+ Dải LEDvẫn thừa chỗ trên một hàng, không xuống dòng. - Băng Gấp khúc
Gấp khúc · 0 điểm · Tạo dải LED · Hủykết thúc đúng mép cũ — băng vốn được kéo giãn, chữ dài chỉ đẩyGấp khúcsang trái trong cùng khối. - Hàng ô tick dưới canvas và thanh trạng thái: không đổi bề rộng đáng kể.
⚠️ Một chỗ CÓ cắt chữ, và nó KHÔNG phải do đợt này: hàng dải trong bảng thực thể đọc
#1–60 (60 LI — cụt giữa chữ. Ảnh 58a1f77 cắt y hệt, cùng một chỗ. Căn nguyên là
V2StripRowVm.Stats ($"#{a}–{b} ({n} LEDs)", gõ cứng tiếng Anh) phải chen với 7 nút icon trong
cột Auto của hàng. Đó là hai nợ riêng — chuỗi Anh + bố cục hàng — và cả hai đều nằm ngoài một
bản vá đổi từ vựng. Ghi nợ, không tự rút "dải LED" thành "dải": đẻ ra tên thứ ba đúng là cái bug
đang đi dọn.
Ba chỗ để nguyên, và vì sao:
- Badge
"N ctrl · N strips · N LEDs"— mục 1d, lý do là bố cục. V2StripRowVm.Stats/V2PortRowVm.Stats("1 strip(s) · 60 LEDs") — cùng họ với badge: chuỗi gõ cứng ngoài resx, và chỗ hiển thị đang cắt chữ sẵn.- Chữ "dải" trơn trong câu văn (
"còn {1} dải không còn cổng nào chứa nổi","các LED của dải") — đó là lối nói tắt trong cùng một câu, không phải một tên thứ hai; nhét "dải LED" vào mọi vế làm câu tiếng Việt kêu như máy dịch.
Nghiệm thu: 4 kịch bản UiDrive bấm theo chữ cũ đã được sửa đích ngắm, không nới expect nào.
apply-effect-to-group.json chuyển sang clickname StripChoiceButton (x:Name mới trong
InspectorView.axaml) để lần sau đổi từ vựng không làm nó đỏ; wiring-polyline-banner-vietnamese.json
giữ press theo chữ vì chữ chính là thứ nó canh. 4 test đơn vị canh câu trạng thái công cụ
(WiringToolsPhase4Tests, FreeRouteToolTests) đã đỏ thật khi chuỗi đổi — đó là bằng chứng
chúng có khả năng đỏ — rồi được sửa cho khớp thế giới mới.
2. Nhóm SỬA (toolbar ngang)
| Nút | Tooltip | Phím thật |
|---|---|---|
| Hoàn tác | Ctrl+Z | ✅ Ctrl+Z (KeyBinding của Window, không phải canvas) |
| Làm lại | Ctrl+Shift+Z | ✅ khớp |
✅ ĐÃ SỬA (F4 audit fix): 2 nút Undo/Redo giờ bind qua UndoCommand/RedoCommand local của
WiringCanvasV2ViewModel (delegate tới IProjectStateService.Undo()/Redo()), CanExecute được
refresh mỗi khi _projectState.Changed fire. Không còn phụ thuộc $parent[Window].
2b. Rail dọc — tổng quan (mọi con số ở mục này đều CÓ CỔNG CANH)
Rail dọc: 31 nút, chia 6 nhóm — CHỌN (6) → NỐI DÂY (5) → ĐO (1) → BIẾN ĐỔI (4) → CĂN CHỈNH (9) → TỆP (6).
Thứ tự nhóm derive từ thứ tự xuất hiện trong WiringRailLayout.Items (Items.GroupBy giữ nguyên
thứ tự gặp đầu tiên); test WiringRailLayoutTests.TotalItems_Are31 và
WiringRailLayoutTests.Layout_HasSixSections_InWorkingOrder chốt cả hai con số.
scripts/docs-check.sh phép kiểm 5 đối chiếu 5 con số của tài liệu này với mã:
tổng rail (31) · số nhóm (6) · nhóm VẼ (13) · nhóm NỐI DÂY (5) · nhóm TỆP (6). Với ba nhóm có
bảng, cổng đối chiếu cả tiêu đề LẪN số dòng của bảng ngay dưới nó — vì một tiêu đề đúng nằm
trên một bảng sai là chuyện đã xảy ra thật ở đây: §4 từng ghi "7 nút" (đúng) bên trên một
bảng 9 dòng (sai, hai dòng ma #26/#27), và không cổng nào thấy. Cổng chỉ đọc tiêu đề là cổng
để bảng trôi vô hình.
Nhóm VẼ nằm trên toolbar ngang, không thuộc rail — 13 nút của nó đếm từ
WiringCanvasV2View.axaml, khối <!-- ===== Section: DRAW ===== -->.
3. Nhóm CHỌN (rail dọc) — 6 nút
| # | Nút | Loại | Điều kiện | Phím thật |
|---|---|---|---|---|
| 11 | Chọn | modal (mặc định) | — | V ✅ (WiringRailLayout.cs:38 Shortcut: "V"; resx "Chọn (V)") |
| 12 | Chọn tất cả | one-shot | có Diagram | Ctrl+A ✅ |
| 45 | Đảo ngược vùng chọn | one-shot | có Diagram | Ctrl+Shift+I ✅ |
| 13 | Thêm điểm | modal | phải có 1 LED đang chọn | A ✅ |
| 14 | Xóa điểm (đỏ) | one-shot | ≥1 LED đang chọn | KHÔNG có phím — DeletePoint.Shortcut = null (WiringRailLayout.cs:44) |
| 25 | Quét xoá LED (đỏ) | modal | có LED | Shift+X ✅ (RMP-88) tooltip nói thẳng nó xoá LED; X trần không làm gì |
Thêm điểm — các bước: bấm nút → click một LED có sẵn tại (0,0) (click này không tạo LED, chỉ chọn strip) → click canvas trống tại (50,0) → LED mới nối vào cuối strip đó. Esc để thoát. Kết quả: LED mới xuất hiện (đã snap theo bước LED); status "Đã thêm LED vào cuối strip"; undo được. Strip chạm trần cổng → "Không thêm được LED: strip đã đạt giới hạn LED của cổng".
✅ ĐÃ SỬA (G17) — bấm lại nút đang sáng trả rail về CHỌN, không còn cảnh "rail tối đèn mà tool
vẫn nạp đạn". WiringRailViewModel.Activate() (:113) làm
ActiveTool = IsActive(kind) ? Select : kind, nên luôn có đúng một nút sáng đúng tool đang chạy.
null từ đây chỉ còn một nghĩa: tool đang chạy là tool VẼ trên toolbar ngang, không nút rail nào
sở hữu nó.
4. Nhóm NỐI DÂY (rail dọc) — 5 nút
| # | Nút | Loại | Điều kiện | Phím tooltip | Phím THẬT |
|---|---|---|---|---|---|
| 18 | Nối (kéo-thả) | modal | ≥2 dải | C |
C ✅ (tích hợp nút Nối tự động tất cả trên HUD) |
| 23 | Nối từng điểm | modal | LED cùng strip | B |
B ✅ (B giờ chỉ có đúng 1 chủ) |
| 22 | Nối thẳng | modal | 2 LED cùng strip | E |
E ✅ |
| 24 | Đi dây tự do | modal | LED cùng strip | F |
F ✅ (kéo một mạch qua nhiều LED — khác B bấm từng điểm) |
| 19 | Xóa kết nối (đỏ) | modal liên tục | có dây liên-strip | (không quảng cáo) | — ✅ (hover đỏ, click ngắt dây liên tiếp, Esc thoát) |
Đúng 5 dòng. Nút Nối nhanh độc lập đã gộp thẳng vào Floating HUD của Nối dải (C) dưới dạng nút [⚡ Nối tự động tất cả], giúp loại bỏ hoàn toàn tình trạng bấm nút không có tác dụng khi toàn bộ dải đã được kết nối. Quét xóa LED (cục tẩy) đã chuyển về đúng nhóm CHỌN & SỬA ĐIỂM. Đi dây tự do (F) được TRẢ VỀ rail (RMP-87): nút của nó từng bị gỡ trong khi lớp công cụ, banner HUD, 8 test và bốn khoá dịch đều còn nguyên — tooltip rail.tool.wire-free-route.tooltip nằm mồ côi trong resx và vẫn ghi "Đi dây tự do (F)", tức quảng cáo một phím không chạy. Đếm trong mã: đúng 5 mục WiringRailSection.Wire (WiringRailLayout.cs), và scripts/docs-check.sh phép kiểm 5 đối chiếu thẳng con số này.
4a. "Nối nhanh" — MỘT nút, hành vi theo VÙNG CHỌN (audit 2026-08-12)
✅ ĐÃ SỬA — ba nút gộp làm một. Trước đây nhóm này có ba công cụ tự-nối đứng cạnh nhau, không nút nào có phím tắt, và nhãn không nói ra khác biệt:
| Nút cũ | Nó làm gì | Đi đâu rồi |
|---|---|---|
| Nối nhanh | nối cặp gần nhau nhất toàn bản vẽ | ⊂ ngữ cảnh "chưa chọn gì" (xâu tất cả bao trùm nối một cặp) |
| Nối khối | dải của LED đang chọn ↔ dải rời gần nhất | → ngữ cảnh "đang chọn 1 dải" |
| Tự nối LED rời | mỗi controller: xâu mọi dải rời | → ngữ cảnh "chưa chọn gì" |
Người dùng phải bấm thử mới biết nút nào làm gì — mà bấm thử ở nhóm này là đã đổi topology bản vẽ. Nay còn một nút, và ngữ cảnh là thứ người dùng đã nói ra bằng vùng chọn:
| Vùng chọn | Nó nối gì |
|---|---|
| chưa chọn gì | mỗi controller: xâu mọi dải rời vào chuỗi của cổng thấp nhất, thứ tự gần-nhất |
| 1 dải | dải đó ↔ dải rời gần nhất của cùng controller — đúng một đoạn |
| ≥2 dải | xâu chính các dải đang chọn với nhau, thứ tự gần-nhất; dải không được chọn không bị đụng |
Xem trước là ĐIỀU KIỆN, không phải trang trí. Một nút đổi hành vi theo trạng thái vô hình còn tệ hơn ba nút riêng — người dùng mất cả khả năng đoán. Nên nút này là modal:
- Bấm nút → canvas vẽ nét đứt xanh lá + mũi tên đúng những đoạn sắp tạo (LED cuối của dải nguồn → LED đầu của dải đích, y hệt chỗ sợi dây thật sẽ hiện ra). Status bar: "Nối nhanh: N đoạn sắp nối — bấm để nối, Esc huỷ". Chưa đụng gì vào bản vẽ, 0 undo entry.
- Rê chuột — xem trước còn nguyên.
- Bấm vào bản vẽ = đồng ý → nối, đúng 1 undo entry cho cả loạt (kể cả 5 đoạn). Đèn về Chọn.
- Esc = huỷ sạch, không đổi dữ liệu.
Màu cố ý khác dây thật (cam) và dây đang kéo tay (tím): ba thứ cùng là nét đứt, cùng màu thì "sắp nối" trông y hệt "đã nối" và người dùng bỏ qua cú bấm xác nhận vì tưởng xong rồi.
Bản vẽ đổi dưới chân kế hoạch ⇒ huỷ NGAY và nói ra. Kế hoạch dựng một lần lúc arm và mang
theo cả một Diagram đã nối sẵn; Ctrl+Z là KeyBinding của Window nên nó chạy cả khi công cụ
đang nuốt cú bấm trên canvas. Chốt kế hoạch cũ lúc đó = gán đè một bản vẽ dựng từ thế giới đã
chết, tức xoá sổ cú hoàn tác của người dùng. OnDiagramChanged huỷ chế độ, trả đèn rail về
Chọn và báo "Bản vẽ vừa thay đổi — kế hoạch nối cũ không còn đúng…".
Không có gì để nối ⇒ TỪ CHỐI kèm lý do thật và không arm một mode rỗng (§4 invariants). Ba lý do phân biệt được: chưa đủ 2 dải · vùng chọn không còn hàng xóm rời · các dải đang chọn đều đã nằm trong chuỗi (kèm lối thoát: "bỏ chọn rồi bấm lại để xâu mọi dải rời").
Command Palette cũng còn một mục ("Nối nhanh") — gộp nút mà để palette giữ ba lối vào thì công cụ vẫn là ba, chỉ khác chỗ nấp, và hai lối kia không có xem trước.
Bằng chứng: tools/UiDrive/scenarios/wiring-quick-connect-context.json (60 bước, chuột thật, cả hai
ngữ cảnh) · WiringCanvasV2ViewModelTests.QuickConnect* (11 test, trong đó
QuickConnectPreview_DrawsExactlyTheWiresThatWillAppear so toạ độ nét đứt với sợi dây thật
sau khi chốt) · WiringRailLayoutTests.MergedConnectTools_LeaveNoOrphanButton.
4b. Nhóm ĐO (rail dọc) — 1 nút (audit 2026-08-12)
| # | Nút | Loại | Điều kiện | Phím thật |
|---|---|---|---|---|
| 21 | Đo khoảng cách | modal | — | không có (hint U cũ là phím ma, đã gỡ) |
✅ ĐÃ SỬA — "Đo khoảng cách" ra khỏi nhóm NỐI DÂY. Nó không nối gì cả: nó ghi một đường đo
vào Diagram.Measurements để đưa cho thợ thi công (xem E8c ở mục 8). Nó là công cụ duy nhất của
nhóm cũ không đổi topology, mà lại ngồi lẫn giữa sáu công cụ mà một cú bấm nhầm sửa luôn sơ đồ.
Vì sao một nhóm MỚI chứ không nhét vào nhóm sẵn có: CHỌN đổi vùng chọn · BIẾN ĐỔI và CĂN CHỈNH đổi toạ độ LED · TỆP đọc/ghi tệp. Không nhóm nào mang nghĩa "đo / kiểm bản vẽ" — nhét bừa là dời chỗ sai lần thứ hai.
Vì sao ngay SAU nhóm NỐI DÂY, không đẩy xuống cuối rail: người quen tay vẫn thấy nó ở gần chỗ
cũ, và nhịp làm việc là vẽ → nối → đo kiểm → biến đổi → căn chỉnh → tệp. Thứ tự nhóm derive
từ thứ tự xuất hiện trong WiringRailLayout.Items.
Không có phím tắt, cố ý: hint U cũ đã bị gỡ vì là phím ma, và 17 phím quảng cáo = 17 phím
thật là con số có test chốt (WiringRailLayoutTests.AdvertisedKeys_AndLiveKeys_AreTheSameSeventeen).
Guard: WiringRailLayoutTests.MeasureTool_LivesInItsOwnSection_NotAmongTheWiringTools +
Layout_HasSixSections_InWorkingOrder (chốt cả thứ tự) ·
tools/UiDrive/scenarios/measure-survives.json (lái nút ở chỗ mới bằng chuột thật, kèm
railhit:Measure==yes — verb rail không kiểm nút có bấm tới được không).
Nối dây thẳng / Nối (kéo-thả) — các bước: bấm nút → nhấn-giữ trên LED CUỐI của strip A → rê (dây tím nét đứt bám theo) → nhả trên LED ĐẦU của strip B. Kết quả: strip B nối ngay sau A trên cùng cổng; undo được. Bị từ chối → status tiếng Việt cụ thể (vượt cap cổng / vòng chuỗi / tự nối). ⚠️ Nhấn chuột xuống ở LED không phải LED cuối → không có gì xảy ra và không một lời báo nào.
Xóa kết nối — các bước: nối 2 strip trước → bấm nút → click đúng lên đoạn dây giữa chúng. Kết quả: dây biến mất, phần chuỗi phía sau chuyển vào dải LED "Chưa kết nối"; mode tự tắt; undo được. Click trượt → "Không trúng dây nối nào", mode vẫn bật.
Quét xoá LED — các bước: bấm nút → nhấn-giữ và rê đè lên hàng LED → nhả. Hoặc click đúng 1 LED. Kết quả: mọi LED cách đường quét ≤ tolerance bị XOÁ; strip rỗng bị dọn luôn; undo được.
✅ ĐÃ SỬA — ba lỗi nguy hiểm nhất của nhóm này:
- Nút "Xóa kết nối" không còn hứa phím nào.
Xgiờ chỉ có đúng một chủ là nút quét xoá, và tooltip của chủ nói thẳng bản chất: "Quét xoá LED (X)". Tin tooltip không còn mất LED. - "Quét xoá kết nối" đã đổi tên thành "Quét xoá LED" — tên khớp việc nó thật sự làm.
- Key resx
rail.tool.draw-connect.tooltipđã có ("Nối (kéo-thả) (C)") → không còn hiện chuỗi key thô.
✅ ĐÃ SỬA (G9) — hai cặp nút trùng đã gỡ hẳn, không còn cặp nào. ConnectLine (#15) xoá, giữ
DrawConnect (#18, phím C); WireAddPoint (#27) xoá, giữ "Thêm điểm (A)" (#13) ở nhóm CHỌN.
Vì không còn 2 nút cho 1 tool nên hàm Canonical() — bịa ra chỉ để 2 nút sáng cùng nhau — cũng đã
biến mất. Bản trước của mục này ghi "#15 ≡ #18 vẫn còn" và "#13 ≡ #27 vẫn là 2 nút": cả hai
câu đều đã hết hiệu lực, #15 và #27 không còn tồn tại trong WiringRailLayout.Items.
5. Nhóm BIẾN ĐỔI (rail dọc)
| # | Nút | Điều kiện | Phím thật |
|---|---|---|---|
| 28 | Đảo hướng | ≥1 LED chọn | Shift+T ✅ (RMP-88; T trần = Chữ → LED) |
| 29 | Xoay 90° CW | ≥1 LED chọn | R ✅ |
| 30 | Lật ngang | ≥2 LED mới có tác dụng | Shift+H ✅ (RMP-88; H trần không làm gì) |
| 31 | Lật dọc | ≥2 LED | KHÔNG có phím — chỉ vào được bằng nút rail |
Cả 4 nút đều có nhánh V2 thật và chạy đúng. Undo được, lưu được.
✅ ĐÃ SỬA — ba cảnh báo cũ ở đây đã hết hiệu lực (đính chính trong audit 2026-08-12; một cảnh báo chết nằm lại còn hại hơn không có cảnh báo, vì người sau sẽ đi kiểm lại thứ đã lành):
- "Đảo hướng nhiều LED thuộc nhiều strip → chỉ strip đầu tiên bị đảo" — không còn đúng.
ContextReverseDirectiongomDistinctmọiStripIdcủa selection rồi xâu chuỗiReverseStriptrên diagram cục bộ,WriteBackToProjectStateđúng một lần ở cuối ⇒ đảo hết, và vẫn là một undo entry. - "Xoay với ≤1 LED → return im lặng" — không còn đúng.
ReportTransformNeedsTwo()báo "Cần ít nhất 2 LED để xoay/lật (1 LED xoay quanh chính nó không đổi vị trí)", và phân biệt với ca ≥2 LED mà op vẫn no-op hình học ("Thao tác không làm hình đổi gì"). Đo bằng chuột thật trong audit: bấm rail Lật ngang với đúng 1 LED ⇒ status hiện đúng câu đầu. - "Phím
Vtrần = Lật dọc" — không còn đúng.Vlà phím công cụ Chọn (WiringCanvasShortcuts.RailKeys[V] = Select); Lật dọc không quảng cáo phím nào.
6. Nhóm CĂN CHỈNH (rail dọc) — 9 nút 1 chạm trực tiếp
| # | Nút | Loại | Điều kiện (theo code) |
|---|---|---|---|
| 32 | Căn trái | one-shot | ≥2 đơn vị căn |
| 46 | Căn phải | one-shot | ≥2 đơn vị căn |
| 47 | Căn đỉnh | one-shot | ≥2 đơn vị căn |
| 48 | Căn đáy | one-shot | ≥2 đơn vị căn |
| 39 | Căn giữa ngang | one-shot | ≥2 đơn vị căn |
| 40 | Căn giữa dọc | one-shot | ≥2 đơn vị căn |
| 33 | Phân bố đều ngang | one-shot | ≥3 đơn vị căn |
| 34 | Phân bố đều dọc | one-shot | ≥3 đơn vị căn |
| 35 | Căn gốc | one-shot | ≥1 đơn vị căn |
✅ ĐÃ NÂNG CẤP THÀNH NÚT 1 CHẠM TRỰC TIẾP: Các nút Căn trái, Căn phải, Căn đỉnh, Căn đáy, Căn giữa ngang, Căn giữa dọc, Phân bố ngang, Phân bố dọc, Căn gốc hoạt động tức thì với 1 cú nhấp chuột (hoặc qua Context Menu), với biểu tượng Material sắc nét. Đọc SelectedV2LedIds và gọi V2LedTransformOps.Align / DistributeHorizontal / DistributeVertical / TranslateSelectionToOrigin. 1 lần chạy = 1 undo entry; selection đã thẳng hàng sẵn ⇒ không tạo undo entry rỗng.
Đơn vị căn (align unit): khi selection trải ≥2 strip ⇒ đơn vị là cả strip (tịnh tiến cứng cả khối — không sập nghìn LED về một cột). Khi selection nằm gọn trong 1 strip ⇒ đơn vị là từng LED.
⚠️ Per-LED bị TỪ CHỐI có lý do khi thao tác sẽ phá hình dạng người dùng vẽ (dồn LED chồng lên nhau, sập dải chéo/cong/xoay về một đường, xé cột-hàng của strip). Lúc đó status bar báo đúng lý do ("thao tác sẽ phá hình dạng dải LED… chọn LED thuộc nhiều dải để căn theo khối"), không còn báo sai "Chọn LED trước" trong khi người dùng đang chọn LED. Thiếu số lượng thì báo "cần ≥2 / ≥3", không báo "chưa chọn".
✅ ĐÃ LÀM (audit 2026-08-12) — "Căn giữa ngang" và "Căn giữa dọc" ra rail. Hai lệnh này chạy thật từ wave 3 nhưng chỉ tới được qua flyout của nút "Căn hàng", trong khi bốn hướng còn lại có nút riêng: sáu thao tác ngang hàng nhau mà hai mức độ dễ dùng. Nay chúng là nút thứ 5–6 của nhóm.
⚠️ Chúng KHÔNG mang Flyout — và đó là điều kiện, không phải chi tiết. Nút "Căn hàng" từng vừa
mang Flyout vừa mang Command, Avalonia chạy cả hai trên một cú bấm (xem đoạn dưới). Hai nút
căn giữa là one-shot thuần: bấm là căn, không menu nào mở ra. Guard:
WiringAlignTests.OnlyTheAlignMenuButton_CarriesAFlyout (khẳng định đúng một nút mang
HasAlignFlyout, tức đúng một nút được style gắn Flyout) +
tools/UiDrive/scenarios/wiring-align-center-buttons.json (bấm chuột thật rồi đòi menuitems==0
cho hai nút mới và menuitems>0 cho nút "Căn hàng" — hai lời khẳng định trái dấu cạnh nhau là
cách duy nhất phân biệt hai loại nút từ ngoài; kèm đo hình học: đầu cánh 1 dời từ x=704 sang
x=552, và Ctrl+Z trả nó về đúng chỗ).
✅ ĐÃ SỬA (audit 2026-08-12): nút "Căn hàng" nay chỉ MỞ MENU 6 hướng, đúng như tooltip hứa
("Căn hàng (bấm để chọn hướng)"). Trước đó nút vừa mang Flyout vừa mang Command, và Avalonia chạy
cả hai trên một cú bấm: menu mở ra đồng thời ExecuteAlignLeft() chạy. Đo được: một dải LED vẽ
ở x=400 nhảy về x=0 chỉ vì người dùng bấm để XEM có những hướng nào — bấm Esc bỏ menu cũng không lấy
lại được; chọn tiếp một hướng thì một cử chỉ đẻ HAI undo entry (invariant §4). Guard:
tools/UiDrive/scenarios/wiring-align-button-opens-menu.json (chuột thật) +
WiringAlignTests.AlignRailButton_Click_OpensMenuOnly_DoesNotAlignAnything.
7. Nhóm TỆP (rail dọc) — 6 nút
| # | Nút | Điều kiện | Ghi chú |
|---|---|---|---|
| 36 | Nhập DXF | file chứa LINE/CIRCLE/ARC/POLYLINE/POINT | |
| 37 | Xuất DXF | có Diagram | |
| 38 | Xuất PDF | có Diagram | |
| 42 | Xuất sơ đồ dây (.remixwire) |
có Diagram | E5 — sơ đồ dây độc lập |
| 43 | Nhập sơ đồ dây — gộp vào bản vẽ hiện tại | — | giữ nguyên những gì đang có, thêm vào |
| 44 | ⚠️ Nhập sơ đồ dây — THAY THẾ toàn bộ bản vẽ | — | nút đỏ (IsDangerous: true): xem dưới |
⚠️ #44 THAY CẢ BẢN VẼ. "Nhập sơ đồ dây — THAY THẾ toàn bộ bản vẽ"
(WiringRailLayout.cs:174-177) không gộp: nó bỏ đi bản vẽ đang mở và thay bằng nội dung tệp
.remixwire. Nó render màu đỏ đúng vì lý do đó, và tooltip viết hoa chữ THAY THẾ.
Cả ba nút E5 (#42–#44) đều KHÔNG có phím tắt, có chủ đích (Shortcut: null,
WiringRailLayout.cs:163-167): một phím một-ký-tự cho thao tác xoá sạch bản vẽ là đúng lớp lỗi
"X hứa xoá kết nối nhưng xoá LED" mà invariant §5 sinh ra để chặn. Vì RailKeys derive thẳng từ
Shortcut, null cũng có nghĩa tooltip không hứa phím nào — hai bên không thể lệch nhau.
Bản trước của mục này chỉ liệt 3 nút và bỏ sót cả ba nút E5, tức bỏ sót luôn nút phá dữ liệu nguy hiểm nhất của rail.
Nhập DXF — các bước: bấm nút → chọn file → app vào chế độ đặt DXF: rê chuột (overlay mờ bám theo), Ctrl+lăn chuột = chỉnh tỷ lệ, click trái = đặt, Esc = huỷ. Status bar hiện hướng dẫn thường trực (không tự tắt sau 4s). Đặt xong = 1 undo entry.
8. Tương tác canvas
| Hành động | Cách làm | Ghi chú |
|---|---|---|
| Pan | Giữ Space + kéo trái, hoặc chuột giữa | Không quảng cáo ở đâu |
| Zoom | Lăn chuột (1.15×/nấc), clamp [0.05, 10] | Neo theo con trỏ |
| Phóng to / thu nhỏ | + / - |
|
| 1:1 | Ctrl+1 |
Cũng reset Pan = (0,0) |
| Vừa khung | Ctrl+0 |
bbox toàn bộ + margin 10% |
| Phóng đến vùng chọn | nút HUD | clamp max 1.5× |
| Marquee | Tool Chọn → kéo trên nền trống | Ctrl = cộng, Shift = trừ, không modifier = thay thế |
| Kéo LED | Tool Chọn → click trúng LED rồi kéo | Ngưỡng 3 px màn hình; snap theo bước LED |
| Khung bao: di chuyển / co giãn / xoay | Kéo trong bbox / kéo 8 handle / kéo handle tròn phía trên | Cần ≥ 2 LED được chọn — chọn 1 LED thì chỉ tô sáng LED, không có bbox/handle |
| Huỷ cử chỉ đang dở | Esc |
Vứt preview, không ghi undo |
Snap hiện tại đã hợp nhất về một con số duy nhất: GridSpacing.
⚠️ Mặc định là 10 mm, KHÔNG phải 100 — CanvasSettings.DefaultGridSpacing = 10.0
(CanvasSettings.cs:58). 100 mm là lưới VẼ cũ, con số chỉ để nhìn; 10 mm là bước snap LED cũ
(LedPitch), con số đã điều khiển hình học. Hợp nhất hai thứ đó phải giữ nguyên hành vi hình học, nên
giá trị sống sót là 10. Chính CanvasSettings.cs:54-57 cảnh báo thẳng về cú nhầm này: lấy 100 thì
"cú kéo LED đầu tiên sẽ bắn LED đi tới 50 mm". Bảng dưới từng ghi 100 mm — đó là con số sai.
| Trường | Nguồn thật | Mặc định | Dùng cho |
|---|---|---|---|
| Grid size | CanvasSettings.GridSpacing (lưu theo diagram) |
10 mm | Kéo LED, kéo handle, điểm Gấp khúc, Thêm điểm, Dán/Nhân bản, và hiển thị lưới |
| Bước ngang/dọc trong panel tham số | tham số tạm của tool vẽ | 10/10 mm | Chỉ quyết định mật độ LED của hình đang vẽ |
Chỉnh bước snap/document grid: qua ô "Grid size (mm)" trong mục LƯỚI & BẮT DÍNH của Inspector.
Không còn trường LedPitchX/Y riêng cho tài liệu.
Lưới: GridMode = Lưới điểm / Lưới đường / Lưới kép. Tắt hoàn toàn khi ShowGrid = false.
GridSpacing hiện được tôn trọng ở mọi mức zoom; ngoại lệ duy nhất là khi lưới quá dày
(< 4 px trên màn hình) thì renderer không vẽ thay vì tự ý đổi sang bước khác.
Thước (rulers): mặc định HIỆN, tắt được bằng công tắc "Hiện thước" trong mục LƯỚI & BẮT DÍNH của Inspector. Trạng thái này là view preference theo người dùng, không dirty project, không tạo undo entry. Tắt ⇒ hàng/cột thước co về 0 px, canvas chiếm lại chỗ đó.
Đường chữ thập (crosshair): công tắc "Đường chữ thập" cùng mục Inspector — mặc định TẮT, cũng là view preference theo người dùng, không lưu vào tài liệu project. Bật ⇒ 2 vạch chạy theo con trỏ trên canvas + vạch chiếu lên cả hai thước. Con trỏ rời canvas hoặc release ngoài canvas ⇒ chữ thập phải biến mất ngay.
9. Menu chuột phải
19 mục, không phải 14 — đếm MenuItem trong WiringCanvasV2View.axaml:321-440. Trong đó
8 mục căn/phân bố, không phải 6: bảng cũ gộp "6–11" và bỏ sót Căn giữa ngang + Căn giữa dọc
(hai mục đã ra rail ở G22 nhưng vẫn có mặt trong menu này).
| # | Mục | Điều kiện hiện | Phím thật | Trạng thái |
|---|---|---|---|---|
| 1 | Sao chép | có selection | Ctrl+C |
✅ (LED V2) |
| 2 | Dán | luôn hiện (disable khi clipboard rỗng) | Ctrl+V |
✅ dán tại vị trí con trỏ, snap theo bước LED, tạo strip mới trên controller nguồn |
| 3 | Đảo chiều | có selection | Shift+T |
✅ (RMP-88) |
| 4 | Sắp xếp LED theo vị trí | có LED chọn | — | ✅ |
| 5 | Chuyển LED sang dải LED khác | có LED chọn | — | ✅ mở dialog chọn dải LED đích, hủy = không đổi |
| 6 | Căn trái | có selection | — | ✅ chạy thật trên V2 (xem mục 6) |
| 7 | Căn phải | có selection | — | ✅ |
| 8 | Căn trên | có selection | — | ✅ |
| 9 | Căn dưới | có selection | — | ✅ |
| 10 | Căn giữa ngang | có selection | — | ✅ (cũng có nút rail từ G22) |
| 11 | Căn giữa dọc | có selection | — | ✅ (cũng có nút rail từ G22) |
| 12 | Phân bố đều ngang | có selection | — | ✅ |
| 13 | Phân bố đều dọc | có selection | — | ✅ |
| 14 | Xoay 90° thuận chiều kim đồng hồ | có selection | R |
✅ |
| 15 | Lật ngang | có selection | Shift+H |
✅ (RMP-88) |
| 16 | Lật dọc | có selection | KHÔNG có phím | ✅ menu cố ý không quảng cáo phím nào |
| 17 | Nhân bản | có selection | Ctrl+D |
✅ lệch đúng 1 bước LED |
| 18 | Xóa lựa chọn (đỏ) | có selection | Delete |
✅ InputGesture="Delete"; Backspace cũng hoạt động |
| 19 | Xoá mọi đường đo | có đường đo (HasMeasurements) |
— | ✅ đường đo là dữ liệu (sống qua lưu/mở, vào DXF/PDF) nên phải có đường xoá |
⚠️ Hai chỗ bảng cũ nói sai phím — cùng lớp lỗi mà invariant §5 sinh ra để chặn:
- "Xóa lựa chọn" chạy
Delete, KHÔNG phảiD.InputGesture="Delete"(WiringCanvasV2View.axaml:415), và chính comment ngay trên đó ghi lý do: menu cũ quảng cáoDmàDkhông nằm trong bảng phím nào — người dùng bấmDvà không có gì xảy ra.WiringRailLayoutTests.cs:260chốtRailKeyskhông chứaKey.D. - "Lật dọc" KHÔNG có phím. Bảng cũ hứa
V, nhưngVlà công cụ CHỌN (RailKeys[Key.V] = Select) — bấmVđổi công cụ chứ không lật gì cả. Mục menu vì thế không mangInputGesture(WiringCanvasV2View.axaml:401-403, có comment nêu đúng lý do này). Chỉ Lật ngang có phím, và làShift+H(RMP-88).
Đường duy nhất để lật dọc: mục menu này, hoặc nút #31 ở nhóm BIẾN ĐỔI.
10. Status bar canvas
4 checkbox overlay (không lưu vào tài liệu, không undo — chúng ở preferences.json):
Viền bộ điều khiển · Đường dải LED · Chỉ số LED · Dò sơ đồ thực (xem 10b).
Badge V2 + tóm tắt cổng (đỏ khi tràn cổng). ✅ ĐÃ SỬA: chuỗi đếm LED đã qua resx
(wiring.status.leds-selected-suffix → "N LED được chọn"), không còn hardcode tiếng Anh trong XAML.
10b. Dò sơ đồ thực — overlay góc trên-PHẢI canvas, bấm mới hiện
Soi từng bóng một trên dây thật để đối chiếu với bố cục đang khai. Có vì người đi dây và người soạn hiệu ứng thường là hai người ở hai thời điểm: người đi dây có thể không cầm sơ đồ, đi theo hiện trường, thêm hoặc bớt điểm dọc đường.
Bật bằng checkbox "Dò sơ đồ thực" trên thanh trạng thái, mặc định TẮT. Trước audit
2026-08-12 bảng này gắn cứng vào HasDiagram — tức LUÔN hiện — và vì nó neo cùng ô lưới, cùng
góc trên-PHẢI với bảng Trạng thái Canvas nhưng khai báo SAU trong XAML, nó vẽ đè và ăn
luôn cú bấm: 5 nút phóng (thu nhỏ · 1:1 · phóng to · vừa khung hình · phóng vùng chọn) cùng
toàn bộ số liệu Tổng LED · Đã chọn · Bộ điều khiển · Dải · Phóng không cách nào chạm tới bằng
chuột. Nay hai bảng xếp chồng dọc trong một StackPanel nên không thể đè nhau nữa, và
lựa chọn bật/tắt là view-pref (0 undo, 0 dirty, ghi thẳng preferences.json lúc bấm).
Tắt bảng thì dừng dò luôn — ẩn bảng mà để runner chạy tiếp là bỏ mạch thật nhấp nháy không
còn lối tắt.
| Nút | Làm gì | Ghi chú |
|---|---|---|
| Theo con trỏ | rê chuột trên sơ đồ → bóng thật sáng theo | trả lời "điểm này trên màn hình là bóng nào ngoài đời" |
| Chạy lần lượt | đi hết tuyến, mỗi bóng dừng một khoảng | thứ tự là thứ tự DÂY (controller → cổng → chain → chỉ số), không phải thứ tự vẽ |
| Dừng mỗi bóng (ms) | nhịp của chế độ chạy lần lượt | kẹp trong [20, 60000] |
- Bật dò sẽ tự mở đường ra (
BeginDiagnosticTakeover) và trả lại đúng trạng thái cũ khi tắt. Không có bước đó thìLiveFrameGatenuốt sạch khung và không byte nào rời máy — lỗi đã xảy ra thật trên mạch. - TẮT TOÀN BỘ thắng. Blackout đang bật thì công cụ từ chối và nói lý do, không im lặng.
- Đi qua đường dựng khung của sản phẩm (một pixel sáng giữa nền tối), nên chạy với mọi controller chứ không riêng Remix4P.
Bằng chứng: src/RemixStudio.Core/Live/LedProbeRunner.cs ·
tools/UiDrive/scenarios/wiring-probe-real-layout.json (29 bước, chuột thật) ·
tools/UiDrive/scenarios/wiring-probe-panel-toggle.json (bật/tắt bảng, đọc THẲNG
preferences.json, và chứng minh 5 nút phóng bấm được cả khi bảng đang hiện) ·
tools/UiDrive/scenarios-hardware/probe-reaches-the-wire.json (đo tới tận dây).
11. Bảng phím tắt đầy đủ
Nguồn duy nhất: OnKeyDown — và nó chỉ tra 2 bảng WiringCanvasShortcuts.DrawToolKeys
(tool vẽ trên toolbar ngang) + WiringCanvasShortcuts.RailKeys (derive từ RailToolItem.Shortcut),
cộng vài tổ hợp Ctrl và Delete/Backspace/Enter. Không còn switch tay lệch với tooltip.
| Phím | Hành động | Có được quảng cáo đúng? |
|---|---|---|
Space (giữ) |
Pan | Không quảng cáo |
Esc |
Huỷ mode/cử chỉ | ✅ |
Ctrl+A |
Chọn tất cả | ✅ |
Ctrl+0 / Ctrl+1 |
Vừa khung / 1:1 | ✅ |
+ / - |
Zoom | ✅ |
Ctrl+C / Ctrl+V / Ctrl+D |
Sao chép / Dán / Nhân bản | ✅ |
V |
Tool Chọn | ✅ rail.tool.select.tooltip = "Chọn (V)" |
1 2 3 4 9 |
ZigzagH/V, Răng cưa H/V, Viền | ✅ |
L |
Vẽ đường thẳng | ✅ wiring.tool.line.tooltip = "Đường thẳng (L)" |
P |
Vẽ gấp khúc | ✅ wiring.tool.polyline-v2.tooltip = "Gấp khúc (P)" |
O |
Vẽ hình tròn | ✅ |
E |
Nối thẳng | ✅ |
B |
Nối từng điểm | ✅ (nút "Nối khối" đã gỡ hint B — B chỉ còn 1 chủ) |
Shift+X |
Quét xoá LED | ✅ tooltip "Quét xoá LED ({0})" điền ⇧X/Shift+X lúc chạy; nút "Xóa kết nối" đã hết hứa X |
F |
Nối tự do | ✅ |
A |
Thêm điểm | ✅ |
C |
Nối (kéo-thả) | ✅ "Nối (kéo-thả) (C)" |
T |
Chèn Chữ → LED (RMP-88) | ✅ tooltip "Chữ → LED ({0})" |
Shift+T |
Đảo chiều strip | ✅ (RMP-88) |
Shift+H |
Lật ngang | ✅ "Lật ngang ({0})" (RMP-88). Lật dọc KHÔNG có phím — V thuộc về công cụ Chọn; đường duy nhất là nút rail |
R |
Xoay 90° CW | ✅ |
Delete / Backspace |
Xoá lựa chọn | ✅ Menu ngữ cảnh quảng cáo đúng hai phím này |
Enter |
Chốt chuỗi Nối từng điểm | ✅ |
X H (trần) |
KHÔNG LÀM GÌ (RMP-88) | ✅ cục tẩy / lật ngang đòi Shift — gõ nhầm không phá gì |
M D S U G 5 8 |
KHÔNG LÀM GÌ | ✅ không tooltip nào quảng cáo chúng nữa — hết phím ma (P là Gấp khúc, đã liệt ở trên) |
Bảng này từng nói sai và đã được đính chính (audit 2026-08-12). Nó ghi
M= Chọn,P= vẽ đường thẳng,D= xoá lựa chọn,V= lật dọc — cả bốn đều là bảng phím CŨ, đã nghỉ hưu từ wave 2026-07. Bảng đang chạy làWiringCanvasShortcuts.DrawToolKeys+RailKeys(derive từRailToolItem.Shortcut), vàtools/UiDrive/scenarios/destructive-keys.jsongõ THẬT từng phím để chốt:V/Lsống,M/D/Pchết vàDkhông xoá LED nào. Một tài liệu hứaDxoá được LED là đúng lớp lỗi mà invariant §5 sinh ra để chặn — tin lời hứa phím là mất dữ liệu.
12. Danh sách gap ưu tiên (từ tài liệu này, chưa cần chạy audit)
| # | Gap | Mức |
|---|---|---|
| G1 | Nhóm CĂN CHỈNH (4 nút rail) + 8 mục Căn/Phân bố trong menu chuột phải đều CHẾT trên project V2 — luôn báo "Chọn LED trước" trong khi người dùng đang chọn LED → ĐÃ SỬA: có nhánh V2 thật (V2LedTransformOps.Align/Distribute*/TranslateSelectionToOrigin đọc SelectedV2LedIds); đơn vị căn = strip khi selection trải ≥2 strip, per-LED khi trong 1 strip; per-LED phá hình dạng thì từ chối kèm lý do đúng, không báo sai "chưa chọn" | ✅ |
| G2 | "Xóa kết nối (X)": nhấn → ĐÃ SỬA: nút "Xóa kết nối" không còn quảng cáo phím nào (X thực tế XOÁ LED — tin tooltip là mất dữ liệuShortcut: null, resx "Xóa kết nối"); RailKeys derive thẳng từ Shortcut nên tooltip không thể lệch phím chạy nữa | ✅ |
| G3 | "Quét xoá kết nối" đặt tên sai bản chất — nó xoá LED, không xoá kết nối → ĐÃ SỬA: đổi tên thành "Quét xoá LED (X)" trong cả 2 resx (vi/en) | ✅ |
| G4 | Tooltip nút "Nối (kéo-thả)" thiếu key resx → hiện chuỗi key thô trên UI → ĐÃ SỬA: rail.tool.draw-connect.tooltip = "Nối (kéo-thả) (C)" / "Connect (drag) (C)" | ✅ |
| G5 | Phím ma: → ĐÃ SỬA: mọi hint này đã gỡ khỏi resx (8 (Chữ→LED), G (Ảnh nền), S (Nối nhanh), U (Đo), 5 (FreeRect) — quảng cáo nhưng không làm gìwiring.tool.text/background.tooltip, rail.tool.quick-connect/measure.tooltip; key mồ côi wiring.tool.free-rect.tooltip bị xoá hẳn) | ✅ |
| G6 | Phím sai: → ĐÃ SỬA: hint L (tooltip "Nối dây thẳng" → thực tế vẽ hình), B (tooltip "Nối khối" → thực tế Nối từng điểm)L/B gỡ khỏi 2 nút đó; L GIỮ NGUYÊN trong DrawToolKeys cho tool vẽ Đường thẳng
(WiringCanvasShortcuts.DrawToolKeys[Key.L] = WiringToolKind.Line), và tooltip nói đúng
"Đường thẳng (L)" — thứ bị gỡ là hint L trên nút "Nối dây thẳng", không phải phím L.
(Bản trước của dòng này ghi "L gỡ luôn khỏi DrawToolKeys (chỉ còn P vẽ đường thẳng)" — sai:
P chưa bao giờ có trong bảng phím nào, xem §11.) B chỉ còn thuộc "Nối từng điểm (B)"; B chỉ còn thuộc "Nối từng điểm (B)" | ✅ |
| G7 | Floating Param Panel nuốt cú kéo ở góc trên-trái canvas (không → ĐÃ SỬA: 3 overlay nổi trên canvas (Param Panel, Canvas HUD, thanh LED-pick) tách chrome (nền/viền/bóng, IsHitTestVisible=False)IsHitTestVisible=False) khỏi nội dung (control vẫn bấm được; chữ/icon trang trí tắt hit-test qua style .OverlayContent). Không thể đặt IsHitTestVisible=False lên cả Border như 2 overlay kia vì Avalonia cắt cả cây con → NumericUpDown/Áp dụng/Gốc sẽ chết theo. Guard: tools/UiDrive/scenarios/overlay-passthrough.json (trước fix: 9/23 bước đỏ, có đúng thông báo "an overlay is swallowing it") | ✅ |
| G8 | Nút "Áp dụng" trong panel tham số không làm gì; nút "Gốc" bị hướng kéo ghi đè → ĐÃ SỬA: ApplyParameters() re-build hình vừa vẽ với LedConfig mới (1 undo entry, bỏ qua an toàn khi Diagram đã bị chỉnh); DetectOrigin đã gỡ — góc gốc lấy đúng từ panel | ✅ |
| G9 | Nút trùng chức năng: "Nối dây thẳng" ≡ "Nối (kéo-thả)"; "Thêm điểm" ≡ "Thêm điểm nối" → ĐÃ SỬA: gỡ 2 nút trùng. ConnectLine xoá (giữ DrawConnect — đường framework ConnectV2Tool + phím C; mode thủ công IsConnectV2Mode/HandleConnectV2Pointer* là bản sao từng dòng của IWiringToolHost.BeginConnect/UpdateConnect/EndConnect nên xoá luôn). WireAddPoint xoá (giữ nút SEL "Thêm điểm (A)") → hàm Canonical() (bịa ra để 2 nút sáng cùng nhau) không còn cần. Rail: 28 → 26 nút tại thời điểm sửa đó (tổng hôm nay lại là 28 — xem WiringRailLayoutTests.TotalItems_Are28) | ✅ |
| G10 | Shape V1 không click/kéo được — → CÓ THẬT nhưng KHÔNG SỬA THEO HƯỚNG ĐÓ (xem 12.1): HandlePointerDown không forward tới tool serviceSelectTool đúng là không bao giờ chạy (HandlePointerDown return ở mọi nhánh khi ActiveTool==Select, WiringCanvasV2ViewModel.cs:2472, không bao giờ tới _toolService.OnPointerDown ở :2480). Nhưng scene chưa bao giờ vẽ Diagram.Shapes — DrawShape chỉ được gọi cho _previewShape (SkiaWiringScene.cs:417). Nối dây cho SelectTool = cho phép kéo một vật vô hình. Lỗ THẬT nằm chỗ khác và đã bịt: CommitShape fallback V1 (:408) tạo Shape vô hình, không chọn được, không xoá được khi diagram không có controller — tới được bằng cách xoá controller duy nhất của project mới (ProjectFactory.BuildEmpty = 1 controller + 0 strip; RemoveController chỉ chặn khi còn strip trỏ vào). Giờ từ chối + báo lý do thay vì âm thầm nuốt nét vẽ | ✅ |
| G11 | "Chuyển LED sang nhóm khác" không có dialog — lấy đại strip đầu tiên → ĐÃ SỬA: ReassignLedStrip cũ lấy Strips.FirstOrDefault(s => s.Id != current) — trên sơ đồ 8 strip, LED rơi vào strip nào đứng đầu danh sách, user không chọn được và không biết nó đi đâu. Giờ mở hộp thoại chọn dải LED đích (PickStripDialog, đúng tiền lệ IDialogLauncher của Add/Edit/Merge Strip); danh sách loại trừ strip nguồn; hủy = không đổi gì + báo; 1 cử chỉ = 1 undo entry (test đo trên UndoStack thật). Sơ đồ <2 dải → không mở hộp thoại rỗng, báo "Cần ít nhất 2 dải LED để chuyển". Guard: tools/UiDrive/scenarios/move-led-to-group.json + move-led-single-group.json | ✅ |
| G12 | Khung bao cần ≥2 LED mới có handle, nhưng bbox vẫn vẽ khi chọn 1 LED → trông như hỏng → KHÔNG CÓ THẬT (xem 12.1): DrawSelectedV2Leds thoát ở if (count < 2) return; (SkiaWiringScene.cs:1087) TRƯỚC khi vẽ bbox/handle — chọn 1 LED chỉ tô sáng chấm LED, không hề có bbox hay handle nào. Scene và hit-test (HitTestV2LedHandle, :2139) khớp nhau. Không sửa gì; thêm test chốt để 2 nửa không lệch nhau về sau (WiringGridAndSelectionAdornerTests) | ✅ |
| G13 | → ĐÃ SỬA (wave F+G1): công tắc "Hiện thước" trong Inspector, mặc định vẫn HIỆN, lưu theo view preference người dùng | ✅ |
| G14 | ShowRulers là setting chết — thước luôn hiện, không tắt đượcChưa có crosshair chạy theo con trỏ chiếu lên thước → ĐÃ LÀM (wave F+G1): công tắc "Đường chữ thập", mặc định tắt, cũng là view preference người dùng | ✅ |
| G15 | → ĐÃ SỬA: GridSpacing chỉ đúng khi bước lưới trên màn hình nằm trong [40,320] pxDrawGrid trước đây âm thầm thay bằng auto-spacing ngoài khoảng đó — mà GridSpacing mặc định hồi đó là 100, nên chỉ cần zoom 0.3 là user không còn nhìn vào lưới 100 trong khi inspector vẫn ghi 100. (Mặc định hôm nay là 10 mm — CanvasSettings.DefaultGridSpacing = 10.0, CanvasSettings.cs:58; con số 100 trong dòng này là giá trị lịch sử, đừng đọc nó như giá trị hiện hành — xem mục 8.) Chọn tôn trọng setting ở MỌI mức zoom (GridMath.ResolveMajorSpacing); ngoại lệ duy nhất: lưới dày < 4px/màn hình thì không vẽ (trả null) — không vẽ gì là im lặng, vẽ sai số là nói dối. GridSpacing ≤ 0 = auto (UI không hứa con số nào) | ✅ |
| G16 | Chuỗi "{N} LED(s) selected" hardcode tiếng Anh trong XAML → ĐÃ SỬA: đi qua resx wiring.status.leds-selected-suffix ("N LED được chọn") | ✅ |
| G17 | Bấm lại nút đang sáng → rail không nút nào sáng dù tool vẫn hoạt động → ĐÃ SỬA: Activate() toggle về Select thay vì null; dispatcher rail disarm vô điều kiện (_toolService.Activate(Select) mỗi lần rail đổi) nên mọi đường về null cũng nhả tool; one-shot + Esc trả đèn về SEL. Bất biến: null chỉ còn nghĩa "tool VẼ trên toolbar ngang đang chạy". Kịch bản tệ nhất (cục tẩy còn nạp đạn, rail tối đèn, click sau xoá LED) có test riêng | ✅ |
| G18 | Nối dây: bấm vào LED không phải LED cuối → im lặng tuyệt đối, không báo gì → ĐÃ SỬA: BeginConnect/EndConnect trả false trần → không preview, không status, user không phân biệt được "tool hỏng" với "mình bấm sai chỗ". Giờ mọi lần từ chối đều nói ra luật: bấm trượt LED → "bắt đầu kéo từ LED CUỐI của một dải LED"; bấm LED giữa → "không phải LED cuối của dải LED 'Cánh 1' — kéo từ LED cuối (số 20)" (gọi tên đúng dải user vừa bấm); thả sai đích → "thả vào LED ĐẦU của một dải LED khác". Nối thành công thì xóa hint cũ (dây vẽ ra chính là phản hồi). Guard: tools/UiDrive/scenarios/connect-feedback.json | ✅ |
| G19 | "Đảo hướng" với nhiều strip → chỉ đảo strip đầu tiên, không báo → ĐÃ SỬA: đảo mọi strip có mặt trong selection (không từ chối: user quét chọn nhiều strip là đã nói rõ ý định). ReverseStrip thuần Diagram→Diagram nên xâu chuỗi N lần rồi WriteBackToProjectState() một lần → 1 cử chỉ = 1 undo entry (có test) | ✅ |
| G24 | Băng Polyline nói tiếng Anh: nút chốt ghi Apply, nhãn ghi Polyline, hai tooltip nhét Apply vào giữa câu tiếng Việt — §3 chỉ miễn 16 tên effect + token kỹ thuật → ĐÃ SỬA: Gấp khúc / Tạo dải LED (chữ nút đổi lần hai ở mục 1e). Xem mục 1b | ✅ |
| G23 | "Đo khoảng cách" nằm trong nhóm NỐI DÂY — nó không nối gì, nó ĐỌC bản vẽ, và là công cụ duy nhất của nhóm không đổi topology → ĐÃ SỬA: nhóm ĐO riêng, đặt ngay sau NỐI DÂY. Xem mục 4b | ✅ |
| G22 | "Căn giữa ngang/dọc" chỉ tới được qua flyout trong khi 4 hướng căn khác có nút rail riêng → ĐÃ LÀM: hai nút mới ở nhóm CĂN CHỈNH, one-shot thuần, KHÔNG mang Flyout (không tái lập khuôn f427c1a). Xem mục 6 | ✅ |
| G21 | Ba nút tự-nối không phân biệt được: "Nối nhanh" / "Nối khối" / "Tự nối LED rời" đứng cạnh nhau, không nút nào có phím tắt, nhãn không nói ra khác biệt ⇒ phải bấm thử mới biết — mà bấm thử ở nhóm này là đã đổi topology bản vẽ → ĐÃ SỬA: gộp còn một nút đọc vùng chọn, và vì nó đổi hành vi theo trạng thái nên nó phải cho xem trước: nét đứt xanh vẽ đúng các đoạn sắp tạo, bấm mới chốt, 1 cử chỉ = 1 undo entry, Esc huỷ sạch, hết việc thì từ chối kèm lý do thật. Xem mục 4a | ✅ |
| G20 | → ĐÃ SỬA: đăng ký FreeRect không có trong registry của WiringToolService → kích hoạt sẽ ném KeyNotFoundException[FreeRect] = new AddShapeTool(FreeRect) (ShapeKind + generator + MapToShapeKind đã có sẵn đầy đủ; chỉ thiếu đúng dòng registry). Chọn implement thay vì xoá member vì spec (mix_2026/05) vẫn liệt kê nó là tool vẽ. WiringToolServiceTests.Activate_AllToolKinds_Work bỏ continue né FreeRect và giờ ép mọi member phải resolve được CurrentTool → không member nào có thể là mìn nữa. ⚠ E8c đã vượt qua kết luận này: WiringToolKind.FreeRect sau đó bị gỡ khỏi enum (WiringToolKind.cs:20-27) vì không có lối vào nào cho người dùng và trùng chức năng với Răng cưa Ngang, nên không còn registry entry nào để nói tới. ShapeKind.FreeRect giữ nguyên (mặc định của ShapeDto.Kind trên đĩa). Xem mục 1 | ✅ |
12.1 Hai kết luận cần giải thích, không chỉ tick
G12 — báo cáo SAI, không có lỗi. Claim: "scene vẫn vẽ bbox khi chọn 1 LED trong khi hit-test đòi ≥2 → user thấy handle nhưng không nắm được". Đọc code thì DrawSelectedV2Leds thoát ở if (count < 2) return; (SkiaWiringScene.cs:1087) — dòng đó nằm trước toàn bộ đoạn vẽ bbox + 8 handle + rotate handle. Chọn 1 LED chỉ tô sáng chấm LED đó, không có affordance nào được vẽ ra cả. Scene và HitTestV2LedHandle (:2139) cùng đòi ≥2 LED → khớp nhau. Không sửa gì (sửa = đi thêm code vào chỗ không hỏng). Đã thêm test render 2 chiều (chọn 1 LED → 0 pixel màu handle; chọn 2 LED → có pixel) để nếu sau này ai đó gỡ điều kiện count < 2 ở một nửa thì nửa kia đỏ ngay.
G10 — có thật, nhưng "sửa" theo hướng đề xuất là sai. Đúng là SelectTool không bao giờ chạy: với ActiveTool==Select, HandlePointerDown return ở mọi nhánh (:2472) và không bao giờ tới _toolService.OnPointerDown (:2480). Nhưng đi sâu hơn một tầng thì scene chưa bao giờ vẽ Diagram.Shapes — DrawShape chỉ được gọi đúng một chỗ, cho _previewShape (SkiaWiringScene.cs:417). Nghĩa là nối SelectTool vào = cho user kéo-thả một vật vô hình. Đó là lý do không nối.
Lỗ thật — và đã bịt — là nhánh fallback V1 trong CommitShape (:408): khi diagram không có controller, nét vẽ được commit thành Shape nhét vào Diagram.Shapes → vô hình (không ai vẽ), không chọn được (SelectTool chết), không xoá được. Người dùng vẽ xong và mất trắng nét vẽ, không một lời báo. Và nó tới được thật: project mới (ProjectFactory.BuildEmpty) = 1 controller + 0 strip, mà RemoveController chỉ chặn khi còn strip trỏ vào → xoá controller duy nhất là hợp lệ → vẽ tiếp là rơi thẳng vào nhánh này. Giờ CommitShape từ chối + báo "Chưa có controller — thêm controller trước khi vẽ", không bao giờ sinh Shape V1 nữa.
Còn lại: SelectTool + bộ callback shape-drag/resize/rotate trong IWiringToolHost giờ là code chết trơ (không đường nào gọi tới, không dữ liệu nào để thao tác). Chưa xoá trong đợt này — nó chạm ~55 điểm (VM state, IWiringToolHost, bất biến "registry phủ mọi member WiringToolKind" mà WiringToolServiceTests đang chốt, và một mảng test V1 đang pin hành vi Shape). Đó là một PR xoá riêng, có chủ đích, không nên độn vào một PR sửa lỗi — nhưng nó nên được làm, và giờ đã có ghi chép rõ tại sao nó an toàn để xoá.