---
url: https://docs.nemo12.com/architecture/sdd-010-lab-platform/figures.md
description: >-
  Vì sao hình trong câu hỏi và lab phải vẽ bằng thuật toán, không dùng ảnh
  bitmap, và các tầng hiện thực hình vẽ.
---

# SDD-010 · Hình vẽ bằng thuật toán

Một phần của [SDD-010](./index.md).

## 11. Hình vẽ bằng thuật toán (REQ-LRN-20 — SRC-114)

> **Luật: mọi hình trong nội dung học là SVG sinh từ tham số. Cấm file bitmap (JPEG/PNG).**

Lý do không phải dung lượng mà là **học được gì**: hình sinh từ tham số thì learner kéo được (đổi số đo → hình đổi theo, thấy quan hệ chứ không thuộc lòng), sửa nội dung không phải vẽ lại ảnh, và hình luôn khớp đúng số liệu trong đề. Đây cũng là triết lý zero-asset của §7.

* **Lưu**: bảng `item_figures` (migration 0038) — `figure_json` là một `FigureSpec` thuần dữ liệu, không phải mã. `/practice/start` trả kèm `figure` cho từng câu (bọc try/catch: thiếu bảng thì bài vẫn chạy, chỉ không có hình).
* **Dựng**: `apps/learn/src/figures/engine.tsx` — 10 archetype phủ 28 node hình học: cặp góc (đối đỉnh · phụ · bù · kề), góc đơn, hai đường song song + cát tuyến (so le trong · đồng vị · trong cùng phía), tam giác (thường · vuông · cân), đa giác n cạnh, đường tròn (bán kính · tiếp tuyến · góc nội tiếp · cung), diện tích (chữ nhật · bình hành · thang · tam giác), mặt phẳng toạ độ / vectơ, đối xứng trục, khối 3D. Mỗi hình có **thanh kéo** đổi tham số.
* **Gắn hình**: `scripts/attach-figures.mjs` quyết định theo hai tầng — node → dạng hình mặc định, rồi **từ khoá trong đề bài** chỉnh variant và số đo (ví dụ đề ghi "35°" thì hình vẽ đúng 35°). Node chưa có luật thì bỏ trống và báo ra, không đoán bừa. Hiện phủ **280/280 câu hình học của Toán**.
* **Kiểm**: `apps/learn/src/figures/engine.test.tsx` (27 test) kiểm **tính đúng của hình**, không chỉ render được: đối đỉnh phải hiện hai lần cùng số đo, bù của 120° phải ra 60°, 3–4 phải ra cạnh huyền 5.00, ngũ giác 540° và bát giác 1080°, góc nội tiếp bằng nửa góc ở tâm… và **không có thẻ `<img>` hay đuôi .png/.jpg nào** trong output. Hình sai còn tệ hơn không có hình.

### 11.1 Đường đi của engine hình: ba tầng, và chỗ dừng của tầng ba (SRC-494)

Chủ dự án 2026-08-21: "một mặt áp cơ chế SVG hiện nay sang các môn khác, mặt khác bổ sung các cơ chế
vẽ khác, dùng threejs hoặc engine khác mà bạn đề xuất". Phân xử:

**Tầng 1 · mở rộng chính engine SVG đang có (làm ngay, và đã làm).** Việc thiếu ở Lý, Hoá, Sinh phần
lớn KHÔNG cần công nghệ mới, chỉ cần thêm dạng hình. Đã thêm ba dạng mở đường: `bar-chart` (đọc bảng
và biểu đồ, số liệu thí nghiệm), `venn` (phân loại, giao hai nhóm), `force-body` (vật kèm các lực vẽ
thành mũi tên có nhãn). Chi phí gần bằng không: cùng file, cùng bảng, cùng cách kiểm, không thêm một
byte nào vào gói tải của learner chưa gặp dạng đó.

**Tầng 2 · dạng hình 2D chuyên môn còn thiếu (làm khi có câu hỏi cần).** Sơ đồ mạch điện, sơ đồ tia
sáng, công thức cấu tạo phẳng, hộp và râu (box plot). Vẫn là SVG tham số, vẫn nằm trong `engine.tsx`.
Thứ tự ưu tiên do dữ liệu quyết định: dạng nào có nhiều câu đang cần thì làm trước, không làm trước
cho đẹp danh sách.

**Tầng 3 · three.js.** Chủ dự án bác lập luận dung lượng của tôi ngày 2026-08-22: *"three.js mà
150KB thì nó quá nhẹ, chỉ bằng dung lượng một tấm ảnh. Dùng three.js cho các lab vật lý, hoá học,
sinh học. Tóm lại cần sinh động hoá các lab, không chỉ bằng three.js mà còn bằng thư viện khác."*

**Chủ dự án đúng, và tôi ghi lại chỗ mình sai** thay vì lặng lẽ sửa: Nemo12 cấm ảnh bitmap CHÍNH VÌ
hình sinh từ tham số vừa nhẹ vừa học được nhiều hơn. Lấy dung lượng ra chặn một engine tương tác
thật là dùng sai chính nguyên tắc ấy, nhất là khi 131KB nén chỉ tải cho lab nào cần và nằm ở Lab,
nơi learner đã sẵn sàng ngồi lâu. Xem [§11.2](#112-lab-sống-động-hơn-src-505) cho phần đã làm.

Hai lý do còn lại vẫn đứng, và chúng quyết định RANH GIỚI chứ không còn quyết định có hay không:

1. **Hình của CÂU HỎI là thứ liếc rồi trả lời.** learner nhìn vài giây rồi chọn đáp án. Một cảnh 3D
   xoay được mời gọi nghịch, tức nó cướp sự chú ý khỏi chính câu hỏi. Nên 3D vào **Lab**, nơi nghịch
   CHÍNH LÀ việc học, chứ không vào `item_figures`.
2. **3D phải KIẾM ĐƯỢC chỗ của nó.** Khối lập phương, hình trụ, hình nón đã đọc được ngay bằng phép
   chiếu SVG; dựng lại chúng bằng WebGL là đổi công lấy hiệu ứng. Thước đo: *xoay được thì learner
   thấy thêm điều gì mà hình phẳng không nói nổi?* Hình học phân tử trả lời được (góc 104,5 độ và
   cặp electron chưa liên kết), một khối lập phương thì không.

Có một ngoại lệ đáng cân nhắc sau: **đồ thị hàm số vẽ động** (kéo hệ số, thấy parabol đổi hình). Cái
này không cần three.js, chỉ cần cho `coordinate` nhận một biểu thức và một thanh kéo, tức vẫn tầng 1.

### 11.2 Lab sống động hơn (SRC-505)

Đã làm, chạy thật trên production:

* `explore/three/Molecule3D.tsx` — mô hình bi và que xoay được bằng chuột hoặc ngón tay, nạp động
  (`lazy(() => import(...))`), gói riêng 131KB nén nên learner chưa mở lab loại này thì không tải.
  Không asset ngoài: hình cầu và hình trụ là dạng dựng sẵn, màu theo quy ước CPK của sách giáo khoa.
  Liên kết đôi và ba vẽ thành các thanh SONG SONG để learner ĐẾM được bậc, không phải một thanh dày.
  Máy thiếu WebGL thì nói ra một câu, không để trang trắng.
* Archetype `molecule-3d` trong `explore/spec.ts`: dữ liệu là danh sách nguyên tử, liên kết và cặp
  electron chưa liên kết, nằm trong `lab_content.viz` như mọi archetype khác. Không có file model.
* Hai lab Hoá đã đổi sang nó (`chl-covalent` với H₂O, `chl-detergent` với CH₄), bản `picker` cũ giữ
  nguyên trong `viz_picker` để đổi lại được.

**Đang mở, xếp theo mức đáng làm.** Nguyên tắc chọn thư viện: mỗi thư viện phải trả lời được câu
"learner làm được gì mà trước đó không làm được", và nạp động cho đúng lab cần nó.

| Việc | Thư viện | Vì sao đáng | Tình trạng |
|---|---|---|---|
| Va chạm, con lắc, mặt phẳng nghiêng (Lý) | tự viết, không thêm thư viện | learner đổi khối lượng rồi THẤY hệ quả, thay vì thay số vào công thức | xong SRC-510 |
| Mạch điện lắp được (Lý) | SVG + một bộ giải mạch nhỏ tự viết | nối sai thì đèn không sáng, đó mới là phản hồi | xong SRC-510 |
| Mô hình phân tử cho Sinh (ADN, protein) | three.js, dùng lại đúng engine trên | xoắn kép chỉ hiểu được khi xoay | đang mở |
| Đồ thị hàm số kéo hệ số (Toán) | không cần thư viện, mở rộng `coordinate` của engine SVG | kéo a, b rồi thấy parabol đổi hình | xong SRC-510 |

#### 11.2a Bốn archetype MÔ PHỎNG — hình chạy bằng công thức thật (SRC-510)

Bốn archetype đã đóng hàng chờ trên, tất cả trong engine khám phá, **không thêm một thư viện nào**:

| Archetype | Learner làm gì | Máy tính gì |
|---|---|---|
| `pendulum` | kéo dài dây, đổi biên độ | chu kỳ **T = 2π√(L/g)** tính thật — nên đổi biên độ nhỏ thì T **không** đổi |
| `collision` | hai xe, chọn va chạm đàn hồi hay mềm | giải từ **bảo toàn động lượng** |
| `circuit` | nối bóng đèn nối tiếp hay song song | **định luật Ôm**: độ sáng vẽ từ dòng THẬT — nối tiếp thì mọi bóng cùng mờ, song song thì sáng như một bóng |
| `quadratic` | kéo a, b, c | parabol dựng lại từ hệ số |

Luật thiết kế, và đây là chỗ chúng khác mọi hình minh hoạ trước đó: **ba archetype đầu CHẠY MÔ
PHỎNG, không vẽ minh hoạ.** Giá trị của chúng nằm đúng ở những chỗ learner hay nói ngược (biên độ
đổi thì chu kỳ đổi; mắc nối tiếp thì đèn gần nguồn sáng hơn) — hình vẽ tay theo trực giác người soạn
sẽ tái tạo lại chính hiểu lầm ấy, còn công thức thì không.

Hệ quả cho kiểm thử: **test canh CON SỐ, không canh hình.** Mô phỏng vẽ đẹp mà ra số sai thì đang
dạy sai, tệ hơn là không có mô phỏng.

### 11.3 Hình phải cầm được ở chính chỗ nó nói (SRC-460, SRC-466, SRC-524)

Ba đợt sửa hình rời nhau nhưng cùng rút ra một luật chung, đặt ở đây làm chuẩn cho mọi archetype mới:

> **Thao tác phải nằm trên chính hình mang ý nghĩa, và mọi quy ước hình học của môn phải được tôn
> trọng — hình là thứ learner đọc để rút ra kết luận, không phải phần trang trí quanh câu hỏi.**

**Trục số kéo trực tiếp (SRC-460).** Bản cũ để điểm nằm trên trục nhưng bắt kéo ở **một thanh trượt
rời bên dưới**: hai hình cùng nói một giá trị mà thao tác ở hình kia, tức bắt trẻ tự dựng cầu nối
trong đầu trước khi học được điều lab định dạy. Nay:

* SVG của archetype number-line nhận **Pointer Events trực tiếp**: chạm hay kéo ở đâu trên trục thì
  điểm nhảy về **giá trị nguyên gần nhất**.
* `setPointerCapture` để kéo trượt ra ngoài khung vẫn bám — trẻ kéo bằng ngón tay thì đi chệch là
  chuyện thường, mất bám giữa chừng đọc thành "hỏng".
* Một **quầng mờ quanh điểm** nói "chỗ này cầm được" — bằng hình, không bằng chữ.
* **Thanh trượt rời bị bỏ.**
* A11y không mất: `role="slider"` trên SVG + một input ẩn (`sr-only`) để bàn phím và trình đọc màn
  hình vẫn chỉnh được giá trị.

**Trục số có mũi tên hai đầu, số 0 khác màu (SRC-466).** Trục vẽ **mũi tên ở cả hai đầu**, cùng màu
trục — nói "còn rất nhiều nữa về cả hai phía", chứ đoạn thẳng cụt ngụ ý dãy số dừng ở đó. Vạch 0
**cao hơn, dày hơn** và mang **màu đối tượng chính** (aqua `--fig-a1`), nhãn 0 to đậm cùng màu: 0 là
**mốc quy chiếu của cả trục**, mọi vạch cùng màu thì mỗi lần định vị mắt phải đếm lại từ nhãn. Áp ở
tầng archetype nên ăn cho **mọi lab dùng trục số, mọi môn**.

**Hình góc: đỉnh ở tâm, 90° vẽ dấu vuông (SRC-524).**

* **Đỉnh góc đặt ở giữa khung** (`x = 150` thay vì `x = 60`): đỉnh là chỗ mắt nhìn vào để đọc một
  góc, nên nó phải là tâm bố cục; đặt lệch trái thì ở 180° hai cạnh không trải đều được hai bên.
* **Đủ 90° thì cung tròn đổi thành dấu vuông.** Đây là **quy ước hình học, không phải trang trí**:
  cung nói "một góc nào đó", ô vuông nói "đúng 90°". Vẽ cung ở 90° là bỏ mất đúng điều lab muốn
  learner nhận ra.
* Bỏ dòng số đo dưới hình: khung chỉ cao **160** nên dòng đó nằm ngoài vùng nhìn thấy, mà số đo đã
  có sẵn ở nhãn thanh kéo.

**Chữ rò ra mặt trang** (cùng đợt SRC-524). Trong phần con của JSX, `/* … */` **không phải comment
mà là chữ** — một ghi chú mã nguồn kèm mã SRC đã lên tới production ở đầu trang Toàn cảnh. Không
cổng nào bắt được vì không test nào đọc chữ thừa trên trang. Nay có một test quét ba trang chính,
chặn cả `/*` lẫn chuỗi `SRC-` lọt ra giao diện.
