RemixStudio

Hướng dẫn

Tab "Sơ đồ dây" — Tài liệu công cụ (chuẩn đối chiếu cho audit)

⚠️ 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

  1. Phím tắt canvas nằm ở WiringCanvasV2View.axaml.cs:513 (OnKeyDown). Handler bỏ qua hoàn toàn khi con trỏ đang ở trong TextBox/NumericUpDown/ComboBox — đang gõ số trong panel tham số thì phím tắt chết.
  2. 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.Shapes luôn rỗng.
  3. 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.
  4. 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".
  5. 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ó.
  6. Status bar (ToolStatusMessage) tự xoá sau 4 giây, trừ khi đang ở chế độ đặt DXF.
  7. ✅ ĐÃ SỬA: trước đây RailToolItem.Shortcut chỉ là chuỗi trang trí, OnKeyDown giữ 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"). Nay WiringCanvasShortcuts.RailKeys derive thẳng từ chính field Shortcut, và OnKeyDown chỉ tra 2 bảng DrawToolKeys + 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:

# 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 nhóm → dải LED Đợ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):

⚠️ 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:

  1. Badge "N ctrl · N strips · N LEDs" — mục 1d, lý do là bố cục.
  2. 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.
  3. 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:

  1. 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.
  2. Rê chuột — xem trước còn nguyên.
  3. 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.
  4. 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:

✅ ĐÃ 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):


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:

Đườ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ằ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.json gõ THẬT từng phím để chốt: V/L sống, M/D/P chết và D không xoá LED nào. Một tài liệu hứa D xoá đượ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 X thực tế XOÁ LED — tin tooltip là mất dữ liệu → ĐÃ SỬA: nút "Xóa kết nối" không còn quảng cáo phím nào (Shortcut: 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: 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ì → ĐÃ SỬA: mọi hint này đã gỡ khỏi resx (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: 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) → ĐÃ SỬA: hint 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 IsHitTestVisible=False) → ĐÃ 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) 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 — HandlePointerDown không forward tới tool service → CÓ THẬT nhưng KHÔNG SỬA THEO HƯỚNG ĐÓ (xem 12.1): SelectTool đú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 | ShowRulers là setting chết — thước luôn hiện, không tắt được → ĐÃ 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 | Chư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 | GridSpacing chỉ đúng khi bước lưới trên màn hình nằm trong [40,320] px → ĐÃ SỬA: DrawGrid 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 | FreeRect không có trong registry của WiringToolService → kích hoạt sẽ ném KeyNotFoundException → ĐÃ SỬA: đăng ký [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á.

Nguồn: docs/HDSD/SO-DO-DAY-CONG-CU.md trong kho mã — bản web sinh lúc dựng ảnh.