diff --git a/packages/preview/conic-toan/0.3.4/LICENSE b/packages/preview/conic-toan/0.3.4/LICENSE new file mode 100644 index 0000000000..1f65d592fb --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Kim Thu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/preview/conic-toan/0.3.4/README.md b/packages/preview/conic-toan/0.3.4/README.md new file mode 100644 index 0000000000..626c4a70e3 --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/README.md @@ -0,0 +1,2212 @@ +

conic-toan

+ +

+ Draw math figures, graphs and variation tables — and build exam papers.
+ A dependency-free Typst library for Vietnamese high-school mathematics. +

+ +

+ Solid geometry +

+ +## English overview + +`conic-toan` is a pure Typst library — **no external packages, no CeTZ** — for +authoring Vietnamese high-school mathematics material. Everything below is drawn +by the library itself and compiles offline. + +**What it does** + +- **Plane and solid geometry.** Triangles with their notable points and circles + (incircle, circumcircle, orthocentre, excircles), pyramids, prisms, frustums, + cones, cylinders and spheres, with hidden edges dashed automatically. +- **Function graphs.** Quadratic through rational functions, exponentials, + logarithms, trigonometry, conics, tangent lines, areas between curves and + feasible regions of linear systems. Labels carry *exact* values — a cubic's + extremum prints as a surd, not as `10.48`. +- **Variation tables from coefficients.** `bbt-bac-ba(1, 0, -3, 1)` produces the + full table for *y = x³ − 3x + 1*, covering every discriminant case. +- **Full worked solutions.** `khao-sat-ve-do-thi-ham-bac-ba(1, 0, -3, 1)` emits + the domain, derivative, limits, monotonicity, extrema, variation table and + graph — the entire textbook analysis from five numbers. +- **Statistics for the 2018 curriculum.** Frequency tables, histograms, ogives, + bar charts, box plots and pie charts, plus fourteen summary statistics for raw + and grouped data. +- **Solids of revolution.** A plane region, the rotation arrow, the resulting + solid and its volume integral, side by side. +- **One source, three PDFs.** The same question file renders as a printable exam, + a solution key, or 16:9 lecture slides. Eight question types, automatic answer + keys and answer tables. + +Function and parameter names are in unaccented Vietnamese (`doan`, `duong-tron`, +`bbt-bac-ba`), and the documentation below is in Vietnamese. + +**Quick start** + +```typst +#import "@preview/conic-toan:0.3.4": * + +#do-thi-bac-ba(1, 0, -3, 1) // graph with exact extrema +#bbt-bac-ba(1, 0, -3, 1) // matching variation table +#hinh-chop-luc-giac-deu() // hexagonal pyramid +``` + + + + + + + + + + + + + + +
Function graphsPlane geometry
Variation tablesSolids of revolution
Statistical chartsFeasible region
+ +One question source, rendered as lecture slides and as an exam paper: + +

+ Lecture slide
+ Exam page +

+ +Licensed under MIT. Full documentation in Vietnamese follows. + +--- + +# Hệ thống bài giảng trình chiếu Toán THPT bằng Typst + +Thuần Typst, **không phụ thuộc package nào** — biên dịch offline được ngay. + +## Cài đặt + +```typst +#import "@preview/conic-toan:0.3.4": * +``` + +Không cần tải gì thêm — Typst tự tải gói về khi biên dịch lần đầu. +Mã nguồn và các file mẫu: https://github.com/Thutran1891/conic-toan + +## Yêu cầu & biên dịch + +- Typst **0.13 trở lên** (cài trên Windows: `winget install --id Typst.Typst`). +- Biên dịch: `typst compile main.typ` — Xem trực tiếp khi soạn: `typst watch main.typ`. +- Khuyến nghị dùng VS Code + extension **Tinymist Typst** để xem trước tức thì. + +## Cấu trúc + +``` +baigiang.typ ← import MỘT file này là dùng được tất cả +main.typ ← bài giảng demo trình diễn toàn bộ tính năng +lib/ + slide.typ ← engine trình chiếu 16:9 + ve.typ ← engine vẽ hình lõi (hệ toạ độ, điểm, đoạn, cung, góc…) + hinh-phang.typ ← hình học phẳng + hinh-khong-gian.typ ← hình học không gian + do-thi.typ ← đồ thị hàm số + oxyz-toan.typ ← TÍNH TOÁN trong Oxyz (vectơ, mặt phẳng, đường, mặt cầu) + bang.typ ← bảng biến thiên, bảng xét dấu +``` + +Tạo bài giảng mới: copy `main.typ` thành `bai-1.typ` rồi sửa nội dung. + +## 1. Trình chiếu + +```typst +#import "@preview/conic-toan:0.3.4": * +#show math.equation.where(block: false): it => math.display(it) + +#show: bai-giang.with( + tieu-de: [CHƯƠNG I. KHẢO SÁT HÀM SỐ], + tieu-de-ngan: [Chương I. Khảo sát hàm số], // tên bài rút gọn — LUÔN hiện + // trên dải đầu trang mọi slide (none => dùng nguyên tieu-de) + phu-de: [Tiết 1: Sự đồng biến, nghịch biến], + gv: "Kim Thu", + ngay: "12/07/2026", + lop: "Lớp 12A1", // (tuỳ chọn) hiện trên TRANG BÌA + mon: "Giải tích", // (tuỳ chọn) môn/chương hiện trên TRANG BÌA + logo: image("logo.png"), // (tuỳ chọn) logo trường — PHẢI bọc image(...) + // Đặt ảnh CÙNG thư mục file .typ này. KHÔNG + // truyền chuỗi "logo.png" (image() chạy trong + // package sẽ tìm ảnh cạnh package -> không thấy). + // none = không có logo. + kieu-bia: 1, // KIỂU TRANG BÌA — 1..5 hoặc tên: + // 1 "toi-gian" Tối giản & Thanh lịch + // 2 "tre-trung" Trẻ trung & Sáng tạo + // 3 "co-dien" Chuẩn mực Học thuật Cổ điển + // 4 "chuyen-nghiep" Chuyên nghiệp & Khoa học + // 5 "ky-thuat" Tiêu chuẩn Tài liệu Kỹ thuật + // Mỗi kiểu có TÔNG MÀU riêng; đặt mau-chinh/ + // mau-nhan để GHI ĐÈ tông đó (đồng bộ cả bài). + mau-chinh: rgb("#0f4c81"), // đổi màu chủ đạo tuỳ ý (auto = theo kieu-bia) + nen: "kem", // nền slide: "trang" (mặc định) | "kem" + // | "xanh-nhat" | "luc-nhat" | "xam" + // hoặc màu TUỲ Ý, ví dụ các tông sáng đẹp: + // rgb("#fdf3e7") cam nhạt + // rgb("#fdf0f0") hồng nhạt + // rgb("#f6f0fb") tím nhạt + // rgb("#eefaf6") bạc hà + // rgb("#fbfbe8") vàng chanh nhạt + ti-le-chu: 1.0, // HỆ SỐ PHÓNG CỠ CHỮ THÂN NỘI DUNG (toàn file): + // 1.0 giữ nguyên; 1.1 to hơn 10%; 0.9 nhỏ đi 10%. + // Chỉ co giãn thân + khung (định nghĩa/ví dụ/lời + // giải…); thanh tiêu đề, header, footer giữ nguyên. + gian-dong: 1.0, // HỆ SỐ GIÃN DÒNG — khoảng cách giữa các dòng + // (toàn file): 1.0 = mốc mặc định (trình chiếu + // 0.62em · A4 bài giảng 0.6em · A4 đề thi 0.65em); + // 1.25 giãn thêm 25%; 0.9 thu lại 10%. TĂNG khi + // phân số / căn thức / chỉ số nhiều tầng làm hai + // dòng liền nhau dính (thậm chí chồng) vào nhau. + mau-cong-thuc: auto, // MÀU MỌI CÔNG THỨC trong $...$ (toàn file): + // auto (mặc định) = thừa kế màu chữ — thân đen, + // tiêu đề/bìa trắng. Đặt màu, vd rgb("#0f4c81"), + // để nhuộm TẤT CẢ công thức theo màu đó. +) + +#muc[1. Định nghĩa] // slide chuyển phần +#slide(tieu-de: [Khái niệm])[ ... ] // slide thường +#trang-cam-on() +``` + +Khung nội dung (tự đánh số ví dụ/luyện tập): +`#dinh-nghia[...]`, `#dinh-ly[...]`, `#tinh-chat[...]`, `#vi-du[...]`, +`#loi-giai[...]`, `#chu-y[...]`, `#ghi-nho[...]`, `#nhan-xet[...]`, `#luyen-tap[...]`. + +Bố cục: `#chia-cot(trai, phai)` hoặc `#chia-cot(a, b, c, ti-le: (2fr, 1fr, 1fr))`. +Các bước giải: `#buoc([Bước 1...], [Bước 2...])`. + +## 2. Vẽ hình tự do (ve.typ) + +Mọi hình vẽ nằm trong `#hinh(...)` với hệ toạ độ toán học (y hướng lên). +Để `h: auto` thì hai trục cùng tỉ lệ (nên giữ khi vẽ hình hình học). + +```typst +#hinh(w: 7cm, xmin: -1, xmax: 6, ymin: -1, ymax: 5, ctx => { + doan(ctx, (0, 0), (5, 0)) // đoạn thẳng + doan(ctx, (1, 1), (4, 3), dut: true, mau: red) // nét đứt + duong-gap-khuc(ctx, ((0,0), (1,2), (3,1), (5,4)), mau: blue, dut: true) // gấp khúc A-B-C-D, đứt thật từng đoạn + diem(ctx, (2, 3), ten: $M$, huong: "tren") // điểm + nhãn + mui-ten(ctx, (0, 0), (3, 2)) // mũi tên + mui-ten-2-dau(ctx, (0, -0.6), (5, -0.6), // mũi tên HAI ĐẦU: ghi số đo + ten: [5 cm], vach: true) // chữ nằm giữa thân + vecto(ctx, (0, 0), (2, 3), ten: $arrow(u)$) // vectơ có tên + vecto(ctx, (0, 0), (2, 3), dut: true) // vectơ nét đứt (cạnh khuất), đầu mũi tên vẫn liền + duong-tron(ctx, (3, 2), 1.5) // đường tròn + cung(ctx, (0, 0), 2, tu: 0deg, den: 90deg) // cung tròn + goc(ctx, (0,0), (5,0), (3,4), ten: $alpha$) // đánh dấu góc (cung + nhãn) + goc(ctx, (0,0), (5,0), (3,4), so-do: true, // tự ghi số đo góc (vd 60°) + to: rgb(255, 170, 0, 70)) // + tô màu hình quạt + goc(ctx, (0,0), (5,0), (3,4), vach: 2) // 2 vạch cắt ngang cung + goc-vuong(ctx, (0,0), (5,0), (0,4)) // ký hiệu vuông góc + ve-goc(ctx, (5,0), (0,0), (3,4), ten: $alpha$) // = goc, nhưng ĐỈNH ở GIỮA (lối TikZ) + ve-goc-vuong(ctx, (5,0), (0,0), (0,4)) // = goc-vuong, ĐỈNH ở GIỮA (lối TikZ) + danh-dau(ctx, (0,0), (5,0), so: 2) // 2 vạch "bằng nhau" + danh-dau(ctx, (0,0), (5,0), so: 2, nghieng: 25deg) // vạch CHÉO "//" + danh-dau(ctx, (0,0), (5,0), cheo: true) // dấu ✕ (chéo nhau) + ve-ham(ctx, x => x*x/4, mau: blue) // đồ thị hàm bất kỳ + to-vung(ctx, x => x*x/4, 0, 3) // tô miền dưới đồ thị + nhan(ctx, (3, 4), [chú thích], huong: "phai") +}) +``` + +Hướng nhãn: `"tren" | "duoi" | "trai" | "phai" | "tren-trai" | "tren-phai" | "duoi-trai" | "duoi-phai" | "giua"`. + +**Mũi tên hai đầu — ghi số đo (`mui-ten-2-dau`)** + +Kiểu đường ghi kích thước trong bản vẽ: hai đầu đều có mũi tên, số đo đặt ở +GIỮA thân (thân tự cắt chừa chỗ cho chữ, có nền lót nên nét không cắt qua chữ). + +```typst +mui-ten-2-dau(A, B, ten: [5 cm]) // chữ giữa thân +mui-ten-2-dau(A, B, ten: $2a$, vach: true) // + vạch chặn hai đầu +mui-ten-2-dau(A, B, ten: $sqrt(29)$, trong: false) // chữ đặt NGOÀI, trên đoạn +mui-ten-2-dau(A, B, ten: [dài], trong: false, ten-quay: true) // chữ nằm dọc +mui-ten-2-dau(A, B, ten: $h$, le: 3pt) // lùi hai đầu, hở khỏi vật đo +``` + +Đoạn ngắn không đủ chỗ chứa chữ thì hàm TỰ đưa chữ ra ngoài thân — không bao +giờ đè lên mũi tên. `nen: none` để bỏ nền lót, `dem:` nới chỗ hở quanh chữ. + +**Nhãn có phân số / căn không còn bị nét vẽ cắt ngang** + +Typst tính HỤT khung của công thức trong dòng: `measure($1/2$)` chỉ cho chiều +cao MỘT DÒNG CHỮ trong khi phân số vẽ ra cao gấp đôi và tràn cả trên lẫn dưới. +`nhan` (và mọi hàm gọi qua nó: nhãn đoạn, nhãn góc, nhãn sơ đồ cây, nhãn biểu +đồ…) nay đo thêm theo BIÊN NÉT CHỮ rồi bù đúng phần tràn đó. Chữ thường không +tràn nên bù 0 — bố cục các hình cũ giữ nguyên. + +> **Vị trí ĐỈNH góc — hai lối viết.** `goc`/`goc-vuong` đặt đỉnh làm đối số +> **ĐẦU**: `goc-vuong(O, A, B)` là góc vuông **tại O**. TikZ thì viết đỉnh ở +> **GIỮA** (`pic angle = A--O--B`), nên có thêm `ve-goc(A, O, B)` và +> `ve-goc-vuong(A, O, B)` cho ai quen lối đó — cùng vẽ góc **tại O**, cùng bộ +> tuỳ chọn (`r`, `ten`, `so-do`, `so-cung`, `vach`, `to`, `mau`, `day`, +> `cach-nhan`), dùng lẫn nhau trong một hình cũng được. Viết nhầm thứ tự sẽ +> KHÔNG báo lỗi, chỉ vẽ ký hiệu sai đỉnh — nên chọn một lối rồi giữ nguyên. + +### Đường xoắn ốc & góc lượng giác (07/2026) + +`xoan-oc` vẽ xoắn ốc Archimedes (bán kính tăng đều theo góc), quét được **nhiều +vòng**; `goc-luong-giac` là lối gọi tắt cho hình góc lượng giác $(OA, OM)$ của +SGK 11 — tự tính chiều quay và số vòng. + +```typst +#hinh(xmin: -3, xmax: 3, ymin: -3, ymax: 3, w: 6cm, ctx => { + let O = (0, 0) + let A = (2.2, 0) + let M = toa-cuc(O, 2.2, -70) + cac-doan((O, A), (O, M)) + cac-diem((O, $O$, "above-left"), (A, $A$, "above"), (M, $M$, "right")) + goc-luong-giac(O, A, M, chieu: "am", vong: 1) // (OA, OM) = -430° +}) +``` + +`goc-luong-giac(O, A, M, ...)` + +| Tham số | Ý nghĩa | +| ----------------------------------- | ------------------------------------------------------------------------------------------------- | +| `chieu` | `"duong"` (ngược kim đồng hồ, mặc định) · `"am"` (cùng kim đồng hồ) | +| `vong` | số vòng quay THÊM trước khi dừng ở tia OM (0, 1, 2, …) | +| `so-do` | `true` = tự ghi số đo (vd −430°) cạnh mũi tên; `ten:` được ưu tiên | +| `r`, `buoc`, `r-cuoi` | bán kính đầu, khoảng cách 2 vòng, bán kính cuối (`auto` = tính theo chiều dài tia) | +| `mau`, `day`, `dut`, `kich` | màu (mặc định`blue`), độ dày, nét đứt, cỡ đầu mũi tên | +| `huong`, `cach`, `mau-ten` | vị trí nhãn số đo quanh điểm cuối | + +`xoan-oc(O, tu: 0deg, den: 1080deg, r: 0.1, buoc: 0.85, mau: blue)` — dùng khi +cần xoắn ốc thuần: `den > tu` quay ngược kim đồng hồ, `den < tu` quay cùng kim +đồng hồ, `r-cuoi:` đặt bán kính cuối (nhỏ hơn `r` thì xoắn vào trong), +`mui-ten:` nhận `true`/`"cuoi"`/`"dau"`/`"ca-hai"`/`false`, `ten:` ghi nhãn ở +điểm cuối. + +### `cac-doan` — vẽ nhiều nét trong MỘT lệnh (kiểu `\draw` của TikZ) (07/2026) + +```typst +cac-doan(A, B) // một đoạn +cac-doan(A, B, C, D) // các điểm liền nhau -> gấp khúc A-B-C-D +cac-doan((A, B), (C, D)) // mỗi MẢNG điểm là một nét rời +cac-doan( + duong(B, C, D, S, dong: true), // nét khép kín + duong(A, B, dut: true), duong(A, D, dut: true), + duong(B, D, mau: red, dut: true, ten: $d$, tai: 0.35), + day: 1.1pt, // kiểu CHUNG cho mọi nét +) +``` + +`duong(...)` khai báo một nét có kiểu riêng (đè lên kiểu chung). Tuỳ chọn dùng +được ở cả `duong(...)` lẫn `cac-doan(...)`: `mau`, `day`, `dut`, `dong` (khép +kín), `to` (tô màu — tự khép kín), `mui-ten` (đầu mũi tên ở điểm cuối), và bộ +nhãn `ten`/`tai`/`huong`/`cach`/`ten-quay`/`mau-ten` (nhãn đặt tại tỉ lệ `tai` +tính theo TỔNG chiều dài nét). + +### `duong-luon` — đường cong uốn lượn (kiểu `.. controls ..` của TikZ) (07/2026) + +```typst +duong-luon(A, B, C, D) // trơn TỰ ĐỘNG qua các điểm neo +duong-luon(A, dieu-khien(c1, c2), B) // Bézier bậc ba: 2 điểm điều khiển +duong-luon( + (0,0), dieu-khien((1,2),(2,2)), + (3,0), dieu-khien((4,-2),(5,-2)), (6,0), // nhiều đoạn nối tiếp (sóng) + mau: teal, day: 1.4pt, mui-ten: true, +) +duong-luon(A, B, C, dong: true, to: blue.lighten(85%)) // khép kín + tô +``` + +Liệt kê các **điểm neo**; xen `dieu-khien(c1, c2)` giữa hai neo để đặt điểm điều +khiển như `.. controls (c1) and (c2) ..` của TikZ. Đoạn KHÔNG có `dieu-khien` thì +tự làm mềm bằng Catmull-Rom (đường trơn đi qua đúng các neo — khỏi tính tay). +`dieu-khien(c1)` = một điểm điều khiển (c₂ = c₁); để `auto` một thành phần thì +thành phần đó tự tính. Tuỳ chọn: `mau`, `day`, `dut`, `dong` (khép kín), `to` +(tô), `mui-ten`, `kich`, bộ nhãn `ten`/`tai`/`huong`/`cach`/`ten-quay`/`mau-ten` +(đặt theo tỉ lệ độ dài, như `cac-doan`), `n` (số mẫu mỗi đoạn — tăng cho mượt). +`diem-luon(...)` TRẢ VỀ mảng điểm mẫu (không vẽ) để tái dùng — ví dụ đưa vào +`nhan-cong` cho chữ bám đúng đường. + +### `nhan-cong` — nhãn chữ bám theo một đường cong (07/2026) + +```typst +// chữ chạy dọc nửa trên đường tròn lượng giác +nhan-cong(diem-cung((0,0), 2.2, 2.2, 160deg, 20deg), + "DUONG TRON LUONG GIAC", co: 13pt, mau: blue, can: "giua", tu: 0.5) +// chữ bám sóng Bézier (lấy đúng mẫu của đường đã vẽ) +let s = diem-luon((0,0), dieu-khien((1,2),(2,2)), (3,0), n: 24) +nhan-cong(s, "song song", co: 12pt, phia: "duoi") +``` + +Đặt từng ký tự dọc theo `duong` (mảng điểm toạ độ toán — lấy từ `diem-luon`, +`diem-cung`, `lay-mau`, hay bất kỳ dãy điểm nào), tự xoay tiếp tuyến và canh theo +độ dài cung. `chu` nên là chuỗi `"..."`. Tuỳ chọn: `tu` (vị trí bắt đầu theo tỉ +lệ 0..1), `can` (`"trai"`/`"giua"`/`"phai"`), `khoang` (giãn ký tự), `co` (cỡ +chữ), `mau`, `phia` (`"tren"`/`"duoi"`/`"giua"` so với đường), `cach` (hở chữ↔đường), +`dao: true` (đảo chiều khi đường vẽ từ phải sang trái, để chữ đọc xuôi). + +### `cac-diem` — chấm và đặt tên nhiều điểm trong MỘT lệnh (07/2026) + +```typst +cac-diem( + (A, $A$, "below-left"), (B, $B$, "below-right"), (C, $C$, "above"), + mau: blue, bk: 2.2pt, +) +``` + +Mỗi đối số là một điểm, viết theo một trong bốn dạng: + +- `A` — chỉ chấm, không nhãn; +- `(A, $A$)` — chấm + nhãn, hướng lấy theo `huong` chung; +- `(A, $A$, "left")` — thêm hướng riêng; +- `(A, $A$, "left", red)` — thêm màu riêng (áp cho cả chấm lẫn chữ). + +Tuỳ chọn chung: `mau`, `bk` (bán kính chấm), `huong`, `cach` (nhãn cách chấm), +`mau-ten` (`auto` = theo màu chấm). Lệnh `diem` cũng nhận thêm `cach` và +`mau-ten` như trên. + +### Tiếp tuyến của đồ thị tại một điểm (07/2026) + +```typst +tiep-tuyen(f, 1.5) // tiếp tuyến tại x = 1,5 +tiep-tuyen(f, 1.5, ten: auto, ten-diem: $M$, giong: true) // + phương trình +tiep-tuyen(f, (-1, 2), mau: green, dut: true) // nhiều tiếp điểm một lệnh +tiep-tuyen(f, 0, dai: 1.4, ten: $Delta$) // đoạn ngắn quanh tiếp điểm +let k = dao-ham(f, 1.5) // chỉ lấy hệ số góc +``` + +Hệ số góc tính bằng **đạo hàm số** nên dùng được cho mọi hàm viết bằng closure +(kể cả hàm hợp, hàm căn, lượng giác). Tuỳ chọn: `dai` (`auto` = kéo hết khung, +tự cắt gọn theo cửa sổ; hoặc nửa độ dài theo trục Ox), `cham`/`mau-cham` (chấm +tiếp điểm), `ten-diem`/`huong-diem`, `ten` (`auto` = tự ghi `y = kx + m` rút +gọn), `tai`/`huong-ten`/`cach`/`ten-quay` (vị trí & kiểu nhãn), `giong` (kẻ +đường gióng đứt từ tiếp điểm về hai trục), `mau`, `day`, `dut`. + +### Nhãn chú thích ngay trên đoạn (`doan`) (07/2026) + +```typst +doan(A, B, ten: [6 cm]) // giữa đoạn, tự đặt vuông góc +doan(A, B, ten: $h$, tai: 0.7, huong: "right", cach: 4pt) +doan(B, C, ten: [cạnh huyền], ten-quay: true) // chữ nằm dọc theo đoạn +``` + +- `tai`: vị trí nhãn theo tỉ lệ từ A đến B (0 = tại A, 1 = tại B, mặc định 0.5). +- `huong`: `auto` (mặc định) = vuông góc, phía trên đoạn — đoạn thẳng đứng thì + ra bên trái; hoặc tên hướng như trên, hoặc vectơ `(dx, dy)` tự chọn. +- `ten-quay: true`: chữ xoay theo đoạn và luôn đọc xuôi (không lộn ngược). +- `mau-ten`: mặc định lấy theo màu đoạn. + +File thử: `thu-cac-doan.typ`. + +Hàm tính toán: `trung-diem`, `khoang-cach`, `chia(A, B, t)` (t = tỉ lệ AM/AB, +ngoài [0, 1] là điểm trên đường kéo dài), `hinh-chieu(P, A, B)`, +`tam-ngoai-tiep`, `tam-noi-tiep`, `truc-tam(A, B, C)` (giao ba đường cao), +`tam-bang-tiep(A, B, C)` → `(J, r)` (tâm + bán kính đường tròn bàng tiếp +**trong góc A**; muốn góc B thì gọi `tam-bang-tiep(B, C, A)`), +`tiep-diem`, `giao-hai-duong-tron`, +`giao-duong-thang(A, B, C, D)` (giao 2 đường thẳng AB, CD), +`giao-duong-thang-duong-tron((A, B), (O, r))` (giao đường thẳng AB với đường +tròn (O; r) → MẢNG điểm: rỗng nếu không cắt, 1 điểm nếu tiếp xúc, 2 điểm nếu +cắt), `dung-diem(A, B, goc, r)` (dựng điểm M: tia AM là tia AB quay quanh A một +góc lượng giác `goc` (độ, dương ngược kim đồng hồ) và AM = `r`). + +Giao với đường cong (07/2026): `giao-ham(f, g, tu, den)` trả về MẢNG giao điểm +của 2 đồ thị y = f(x), y = g(x) trên [tu, den]; đường thẳng qua 2 điểm đổi thành +hàm bằng `ham-qua-2-diem(A, B)`; đường đứng x = k thì giao là `(k, f(k))`. +Ví dụ đầy đủ: `vi-du-ve-tu-do.typ`. + +### KHÔNG cần gõ `ctx` nữa (07/2026) + +Trong thân `#hinh(...)` và trong `them: ctx => ...` của **mọi** hình/đồ thị +dựng sẵn, các lệnh vẽ tự lấy khung hình đang vẽ — **bỏ hẳn `ctx`**: + +```typst +them: ctx => { + doan(M, N, mau: blue, dut: true) + ve-ham(g, mau: red) + diem(A, mau: red, ten: $A$) +} +``` + +- Lối cũ `doan(ctx, M, N)` **vẫn chạy y nguyên** (đối số đầu là `ctx` thì gọi + thẳng) — bài soạn cũ không phải sửa một chữ nào. +- **Ngoại lệ vẫn phải truyền `ctx`**: các hàm TRẢ VỀ GIÁ TRỊ chứ không vẽ — + `toa`, `toa-pt`, `toa-nguoc`, `goc-truc`, `ctx-quay`, `ctx-tinh-tien`. + Ví dụ nhãn nghiêng: `nhan(P, $d$, quay: goc-truc(ctx, A, B))`. +- Gọi lệnh vẽ ở NGOÀI khung hình sẽ báo lỗi rõ ràng: *"Hàm vẽ cần khung + hình: đặt lệnh trong thân #hinh(...) hoặc trong them: ctx => ..."*. +- Vẫn giữ `ve-voi(ctx)` (bộ hàm gắn sẵn ctx, lấy bằng + `let (doan, diem) = ve-voi(ctx)`) cho ai muốn đặt tên riêng, hoặc khi vẽ + trên khung đã quay: `ve-voi(ctx-quay(ctx, 30deg))`. + +Danh sách 49 lệnh được gọi ngầm: nguyên thuỷ của `ve.typ` +(`doan`, `diem`, `nhan`, `mui-ten`, `vecto`, `duong-cong`, `duong-tron`, +`elip`, `cung`, `goc`, `goc-vuong`, `ve-goc`, `ve-goc-vuong`, `danh-dau`, `ve-ham`, `to-vung`, +`gach-mien`, `gach-vung`, `ve-mien`, `mui-ten`, `dau-mui-ten`…), +hệ trục của `do-thi.typ` (`truc`, `luoi`, `vach-chia`, `he-truc`, `giong`, +`nhan-pi`), toàn bộ `hinh-phang.typ` (`tam-giac`, `duong-cao`, `tu-giac`…) +và nhóm Oxyz (`diem-oxyz`, `doan-oxyz`, `vecto-oxyz`, `giong-oxyz`). + +Các hàm KHÔNG cần `ctx` từ trước (`trung-diem`, `chia`, `hinh-chieu`, +`giao-ham`, `ham-qua-2-diem`, `so-toan`, `mien-tron`, `giao`/`hop`/`bu`…) +vẫn gọi thẳng như cũ. + +> `giao-ham` chỉ **TÍNH**, không vẽ ⇒ **không truyền `ctx`**; kết quả đem cho +> `diem`/`nhan` mới ra hình. Cận nhận cả 2 lối viết: `giao-ham(f, g, -3, 2)` +> hoặc `giao-ham(f, g, tu: -3, den: 2)`. Lỡ gõ `giao-ham(ctx, f, g, ...)` thì +> `ctx` được bỏ qua chứ không báo lỗi. Quét qua tiệm cận đứng cũng an toàn: +> chỗ hàm "nhảy" bị loại khỏi kết quả. +> +> ```typst +> them: ctx => { +> let f = x => (x + 2)/(x + 1) +> let g = x => x + 1 +> ve-ham(g, mau: red) +> for P in giao-ham(f, g, -3, 2) { diem(P, mau: red, bk: 2.2pt) } +> } +> ``` + +## 3. Hình phẳng (hinh-phang.typ) + +```typst +#hinh(w: 7cm, xmin: -1, xmax: 6, ymin: -1, ymax: 5, ctx => { + let (A, B, C) = ((0.5, 0), (5.5, 0), (3.5, 4)) + tam-giac(ctx, A, B, C) // nhãn đỉnh tự đặt + duong-cao(ctx, C, A, B, ten-chan: $H$) + trung-tuyen(ctx, A, B, C, ten-chan: $M$) + phan-giac(ctx, B, C, A, ten-chan: $D$) + phan-giac(ctx, B, C, A, vach: 1) // 1 vạch: hai góc BẰNG NHAU + phan-giac(ctx, B, C, A, so-cung: 2) // hoặc 2 cung đồng tâm + trung-truc(ctx, A, B) + duong-tron-ngoai-tiep(ctx, A, B, C) // tâm O tự tính + duong-tron-noi-tiep(ctx, A, B, C) +}) +``` + +**Đường tròn ngoại tiếp ĐA GIÁC** — `duong-tron-ngoai-tiep` nhận cả một MẢNG +đỉnh, không chỉ tam giác. Từ 4 đỉnh trở lên, tâm và bán kính khớp theo bình +phương bé nhất (đa giác nội tiếp được vẫn cho ĐÚNG đường tròn của nó). + +```typst +#let P = range(5).map(i => toa-cuc((0, 0), 1, 90 + i * 72)) // ngũ giác đều +#duong-tron-ngoai-tiep(P, canh: true, ban-kinh: true, ten-r: $R$) +#let (O, R) = tron-qua-diem(P) // TRẢ VỀ (tâm, bán kính), không vẽ +``` + +Nhãn tâm tự chọn phía THOÁNG nhất nên không rơi lên cạnh đa giác (`huong-tam:` +để chỉ định tay). `ban-kinh: true` cũng tự chọn đỉnh mà bán kính không nằm đè +lên cạnh — vd tam giác vuông có tâm ở giữa cạnh huyền thì bán kính vẽ tới đỉnh +góc vuông chứ không trùng cạnh huyền. + +**Khung vừa khít hình (`khung-vua`)** — đường tròn ngoại tiếp thường chìa ra +ngoài đa giác nên rất dễ tràn khỏi `#hinh`. `khung-vua` trả về cửa sổ toạ độ +vừa khít, rải thẳng vào `#hinh`: + +```typst +#let (O, R) = tron-qua-diem((A, B, C)) +#hinh(w: 5cm, ..khung-vua((A, B, C), (O, R)), ctx => { + tam-giac(ctx, A, B, C) + duong-tron-ngoai-tiep(ctx, A, B, C, ban-kinh: true, ten-r: $R$) +}) +``` + +Mỗi đối số là một ĐIỂM `(x, y)`, một ĐƯỜNG TRÒN `(tâm, bán kính)`, hoặc một +MẢNG gồm các thứ đó; `le:` chừa lề theo tỉ lệ cạnh lớn (mặc định `0.12`). + +Sẵn có thêm: `tu-giac`, `hinh-binh-hanh` (biết 3 đỉnh), `hinh-chu-nhat`, +`hinh-thang`, `tiep-tuyen-tu-diem(ctx, O, r, M)` (hai tiếp tuyến + góc vuông), +`tiep-tuyen-tai-diem(O, r, M, dai: auto, dut: false)` (tiếp tuyến TẠI điểm M +trên đường tròn — đoạn qua M vuông góc OM; `dai` = nửa độ dài đoạn), +`duong-tron-luong-giac(so-do: 55deg)` (hình hoàn chỉnh, tự tạo khung). + +Nhãn theo GÓC LƯỢNG GIÁC — `nhan-goc(..muc)` đặt nhãn nhiều điểm, phía đặt nhãn +xác định bằng góc (thay tên hướng): mỗi mục `(P, nội-dung, goc)` / +`(P, nội-dung, goc, ban-kinh)` / `(P, nội-dung, goc, ban-kinh, mau)`, `goc` số +trần = độ (dương ngược kim đồng hồ), `ban-kinh` là độ dài trang (điểm→nhãn). +Ví dụ `nhan-goc((A, $A$, 0), (B, $B$, 120, 8pt), (C, $C$, 240, 8pt, red))`. + +`duong-tron-ngoai-tiep` và `duong-tron-noi-tiep` nhận thêm `dut: true` để vẽ +đường tròn bằng NÉT ĐỨT. File thử các hàm này: `thu-hinh-tron-moi.typ`. + +**Trực tâm & đường tròn bàng tiếp (07/2026)** — trọn bộ công cụ tam giác: + +```typst +ve-truc-tam(A, B, C) // 3 đường cao + ký hiệu vuông góc + H +ve-truc-tam(A, B, C, canh: false, // không vẽ lại cạnh tam giác + ten: $H$, ten-chan: ($H_A$, $H_B$, $H_C$)) +duong-tron-bang-tiep(A, B, C) // bàng tiếp TRONG GÓC A +duong-tron-bang-tiep(A, B, C, ban-kinh: true, // + bán kính tới BC + ten-tam: $J_A$, ten-tiep: ($X$, $Y$, $Z$)) +duong-tron-bang-tiep(B, C, A) // bàng tiếp trong góc B +``` + +`ve-truc-tam` tự kéo dài cạnh bằng nét đứt khi tam giác tù (chân đường cao rơi +ra ngoài cạnh); `duong-tron-bang-tiep` tự kéo dài AB, AC tới tiếp điểm +(`keo-dai: false` để tắt). + +Tam giác đặc biệt (tự vẽ ký hiệu vuông góc, vạch cạnh bằng nhau): + +```typst +tam-giac-deu(ctx, A, canh) // đều +tam-giac-vuong(ctx, A, a, b) // vuông tại A, hai cạnh a × b +tam-giac-can(ctx, A, canh-day, chieu-cao) // cân tại C +tam-giac-vuong-can(ctx, A, canh) // vuông cân tại A +``` + +## 4. Hình không gian (hinh-khong-gian.typ) + +Các hình hoàn chỉnh, nét khuất tự vẽ đứt theo quy ước SGK: + +```typst +#hinh-chop-tam-giac() // S.ABC +#hinh-chop-tu-giac(duong-cao: "tam", duong-cheo: true) // chóp đều S.ABCD +#hinh-chop-tu-giac-thuong(duong-cheo: true) // chóp tứ giác thường (góc nhìn khác) +#hinh-chop-day-hinh-thang(duong-cheo: true) // chóp đáy hình thang +#hinh-chop-tu-giac(duong-cao: "dinh-a") // SA ⊥ đáy +#hinh-hop(duong-cheo: true) // hộp + đường chéo AC′ (nghieng: độ xiên) +#hinh-hop-chu-nhat(duong-cheo: true) // hộp chữ nhật dài (nhìn thẳng, nghieng: 0) +#hinh-lap-phuong() +#hinh-lang-tru-tam-giac() +#hinh-non() #hinh-tru() #hinh-cau() +#truc-oxyz(don-vi: true) + +// Hình chóp đặc biệt +#hinh-chop-tam-giac-deu(trung-tuyen: true) // S.ABC đều, SO ⊥ đáy tại trọng tâm +#hinh-chop-tu-giac-deu() // S.ABCD đều, 2 đường chéo + SO +#hinh-chop-tam-dien-vuong() // O.ABC: OA, OB, OC đôi một vuông góc +#hinh-chop-day-tam-giac-vuong() // ABC vuông tại B, SA ⊥ (ABC) +#hinh-chop-day-chu-nhat() // đáy chữ nhật, SA ⊥ đáy +``` + +**Vẽ thêm lên hình có sẵn** bằng `them` — nhận `ctx` và từ điển đỉnh `d` +(`d.S`, `d.A`, `d.B`, `d.C`, `d.D`, hình hộp/lăng trụ có `d.A1` = A′…): + +```typst +#hinh-chop-tu-giac(them: (ctx, d) => { + let M = trung-diem(d.S, d.C) + diem(ctx, M, ten: $M$, huong: "phai", mau: blue) + doan(ctx, d.B, M, mau: blue, dut: true) + da-giac(ctx, (d.A, M, d.B), to: rgb(30,100,200,40)) // thiết diện mờ +}) +``` + +## 5. Đồ thị hàm số (do-thi.typ) + +**Vẽ nhanh nhất** — chỉ cần công thức và màu, cửa sổ tự chọn: + +```typst +#ve-do-thi(x => x*x*x - 3*x, mau: red) +#ve-do-thi(x => calc.sin(2*x) + x/2, mau: purple, ten: $y = sin 2x + x/2$) +#ve-do-thi(x => calc.exp(x) - 2, mau: blue, xmin: -3, xmax: 2) +``` + +(Hàm cần xác định trên đoạn vẽ; hàm có tiệm cận đứng dùng `do-thi-phan-thuc`.) + +Đồ thị dựng sẵn (tự chọn cửa sổ đẹp, gióng điểm đặc biệt): + +```typst +#do-thi-bac-nhat(2, -1) // y = 2x − 1 +#do-thi-bac-hai(1, -2, -1) // parabol, gióng đỉnh +#do-thi-bac-ba(1, -3, 0, 2) // gióng 2 cực trị +#do-thi-trung-phuong(1, -2, 0) +#do-thi-phan-thuc(2, -1, 1, -1) // y=(2x−1)/(x−1), 2 tiệm cận đứt đỏ +#do-thi-mu(2) #do-thi-log(2) +#do-thi-sin() #do-thi-cos() #do-thi-tan() // nhãn π/2, π, 2π +#do-thi-can() +``` + +Mọi đồ thị dựng sẵn đều nhận `luoi-o: true` (lưới ô vuông mờ) và `vach: true` +(vạch chia + số trên hai trục; riêng sin/cos/tan chỉ có `luoi-o` vì trục hoành +đã ghi theo π). Khi bật `vach`, nên đặt `giao-ox: none, giao-oy: none` để nhãn +giao trục không chồng lên số vạch. + +Vẽ tay trong `#hinh`: `he-truc(ctx)` gộp lưới + trục + vạch chia + số một lệnh: + +```typst +#hinh(xmin: -4, xmax: 4, ymin: -3, ymax: 3, ctx => { + he-truc(ctx) // tuỳ chọn: luoi-o, vach, so, buoc-x, buoc-y, ten-x... + ve-ham(ctx, x => x*x - 2, mau: blue) +}) +``` + +Hàm bất kỳ + vẽ chồng qua `them`: + +```typst +#do-thi-ham(x => calc.abs(x*x - 2*x), xmin: -2, xmax: 4, ymin: -1, ymax: 4, + ten: $y = |x^2 - 2x|$, luoi-o: true, vach: true, + them: ctx => { + ve-ham(ctx, x => 1.5, mau: red, dut: true) // đường y = m + giong(ctx, (3, 3)) // gióng điểm về 2 trục + }) +``` + +### Nhiều đồ thị trên cùng một hệ trục + +Mỗi hàm gói bằng `ham(...)`; `giao-diem: auto` chấm đỏ giao điểm từng cặp: + +```typst +#do-thi-nhieu-ham( + ham(x => x*x - 2, mau: blue, ten: $y = x^2 - 2$), + ham(x => x, mau: red, ten: $y = x$, dut: true, tai: -2), // tai: hoành độ đặt nhãn + xmin: -3, xmax: 3, ymin: -3, ymax: 4, + giao-diem: auto, +) +``` + +### Miền nghiệm BPT / hệ BPT bậc nhất hai ẩn + +Quy ước SGK: gạch phần **không** là miền nghiệm; biên nét đứt nếu BPT ngặt. +Mỗi BPT `a·x + b·y (dau) c` khai báo bằng `bpt(a, b, c, dau: "<=")`: + +Nhãn `ten:` mặc định **NẰM NGHIÊNG theo chiều đường thẳng** (tự đọc xuôi, không +lộn ngược); đặt tại điểm `ten-tai:` (0..1 dọc đoạn nhìn thấy). `huong-ten:` chọn +**phía** đặt nhãn so với đường: `"above"` (trên) hoặc `"below"` (dưới). Muốn chữ +nằm ngang như cũ thì `nghieng-ten: false` (khi đó `huong-ten` nhận mọi hướng +`"above"/"below"/"left"/"right"/…`). Khoảng cách tới đường: `cach-ten:`. + +Kiểu gạch chéo đặt được RIÊNG cho từng BPT (giống `mau-gach`): `goc-gach:` (góc) +và `buoc-gach:` (bước) trong `bpt(...)` — `auto` = theo giá trị chung khai ở +`mien-nghiem`. + +```typst +#mien-nghiem(bpt(4, 5, -8, dau: "<"), giao-truc: auto) // 4x + 5y < −8 +#mien-nghiem( // hệ BPT + tô miền + bpt(3, -2, -9, mau: red, ten: $3x - 2y = -9$, ten-tai: 0.7, huong-ten: "above"), + bpt(-3, 5, 18, mau: green.darken(25%), ten: $-3x + 5y = 18$, huong-ten: "below", + goc-gach: 135deg), // gạch riêng đường này + to-mien: rgb(40, 90, 200, 35), + xmin: -7.5, xmax: 2.5, ymin: -1.5, ymax: 5, +) +``` + +### Diện tích hình phẳng giới hạn bởi 2 đồ thị + +`g` mặc định là trục hoành; `a, b: auto` = tự lấy giao điểm ngoài cùng +(trong phạm vi `tu..den`); miền tô tự tách tại giao điểm ở giữa: + +```typst +#dien-tich-2-ham(x => x*x, g: x => x + 2) // tự tìm giao điểm +#dien-tich-2-ham(x => calc.sin(x), a: 0.5, b: 2.6) // trên đoạn [a, b] +#dien-tich-2-ham(x => x*x - 1, g: x => -x - 1, a: -2, b: 1.5, + ten-f: $y = x^2 - 1$, ten-g: $y = -x - 1$, mau-to: rgb(200, 60, 60, 60)) +``` + +Cần vẽ tay trong `them` của đồ thị khác: `to-vung-2-ham(ctx, f, g, a, b)` +(và `to-vung(ctx, f, a, b)` với trục hoành); gạch chéo đa giác lồi: +`gach-mien(ctx, cac-dinh)`. + +### Khối tròn xoay (07/2026) + +`#khoi-tron-xoay(f, a, b)` vẽ **hai hình cạnh nhau**: trái là miền phẳng đã tô +màu (giới hạn bởi `y = f(x)`, trục hoành, `x = a`, `x = b`), phải là khối tròn +xoay sinh ra khi quay miền đó quanh `Ox` — nét khuất tự vẽ đứt. + +```typst +#khoi-tron-xoay(x => calc.sqrt(x), 0, 4, ten-ham: $y = sqrt(x)$, the-tich: true) +#khoi-tron-xoay(x => 0.35*x*x + 0.5, 0, 3, mat-cat: 2) // thiết diện tại x = 2 +#khoi-tron-xoay(x => 1.6, 0, 3, g: x => 0.7) // khối rỗng (vành khăn) +#khoi-tron-xoay(y => 0.6 + 0.5*y, 0, 3, truc: "Oy") // quay quanh Oy +#khoi-tron-xoay(f, a, b, hien: "khoi") // chỉ lấy khối 3D +#khoi-tron-xoay(f, a, b, hien: "mien") // chỉ lấy miền phẳng +``` + +Tham số: `truc` (`"Ox"` mặc định | `"Oy"` — khi đó `f` là bán kính theo `y`), +`hien` (`"ca-hai"` | `"mien"` | `"khoi"`), `g` (bán kính trong → vành khăn), +`mat-cat: c` (vẽ đĩa/vành thiết diện tại `c` + bán kính, kèm `ten-ban-kinh`, +`ten-mat-cat`), `the-tich: true` (ghi công thức `V = π∫f²dx` dưới hình), +`w` (bề rộng hình khối), `k` (độ "dẹt" của elip, mặc định 0.26), `ten-ham`, +`ten-ham-trong`, `ten-a`, `ten-b`, `ten-goc`, `nhan-giua`, `mau`, `mau-to`, +`mau-mien`, `them` / `them-mien` (vẽ chồng vào hình khối / hình miền). + +Vẽ riêng vào một khung `#hinh` có sẵn: `ve-khoi-xoay(f, a, b, ...)` và +`ve-mien-xoay(f, a, b, ...)` (thêm `ngang: false` cho trục quay thẳng đứng). + +Gạch chéo **miền bất kì** (biên cong, hợp/giao/hiệu tuỳ ý): khai báo mỗi hình +đúng MỘT lần bằng `mien-tron(O, r)` / `mien-elip(O, a, b, quay:)`, vẽ biên bằng +`ve-mien(ctx, m)`, gạch bằng `gach-vung` + phép tập hợp `giao`/`hop`/`bu` — +đổi tâm/bán kính một chỗ là vẽ và gạch tự khớp. Ví dụ Ven $(A sect B) without C$: + +```typst +#hinh(w: 4.5cm, xmin: -2, xmax: 2, ymin: -2, ymax: 2, ctx => { + let A = mien-tron((-0.5, 0.3), 0.9) + let B = mien-tron((0.5, 0.3), 0.9) + let C = mien-tron((0, -0.5), 0.5) // bán kính khác nhau thoải mái + ve-mien(ctx, A) + ve-mien(ctx, B) + ve-mien(ctx, C) + gach-vung(ctx, giao(A, B, bu(C)), mau: black, day: 0.4pt, buoc: 4.5pt) +}) +``` + +`giao`/`hop`/`bu` lồng nhau tuỳ ý: `hop(A, B)`, `giao(A, hop(B, C))`, +`bu(hop(A, B))`... Miền không phải tròn/elip thì `gach-vung` vẫn nhận hàm +`P => true/false` như cũ; helper điểm: `trong(P, m)` (nhận miền hoặc hàm), +`trong-tron(P, O, r)`, `trong-elip(P, O, a, b, quay:)`. + +Tuỳ chọn: `goc:` (hướng vạch, mặc định 45°), `buoc:` (khoảng cách vạch), +`n:` (số mẫu mỗi vạch — biên càng mịn). + +### Phép quay & tịnh tiến (ve.typ) + +Elip xoay: `elip(ctx, O, a, b, quay: 30deg)` (dương = ngược chiều kim đồng hồ, +xoay quanh tâm O); kiểm tra điểm cùng góc: `trong-elip(P, O, a, b, quay: 30deg)`. +`cung` và `cung-elip` cũng nhận `quay:` (xoay cung quanh tâm). + +Quay/tịnh tiến **cả cụm hình** — mọi hàm vẽ theo điểm (doan, da-giac, tam-giac, +duong-tron, diem, nhan, vecto...) đều tự biến đổi khi vẽ bằng ctx đã bọc: + +```typst +#hinh(w: 7cm, xmin: -1, xmax: 8, ymin: -1, ymax: 4, ctx => { + let ve-hinh = c => { + tam-giac(c, (0, 0), (2, 0), (0.6, 1.6)) + duong-tron(c, (1, 0.55), 0.4, mau: blue) + } + ve-hinh(ctx) // hình gốc + ve-hinh(ctx-quay(ctx, 35deg, tam: (0, 0))) // ảnh quay quanh O + ve-hinh(ctx-tinh-tien(ctx-quay(ctx, 35deg, tam: (0, 0)), (4.5, 0.5))) +}) // quay trước, tịnh tiến sau +``` + +Biến đổi điểm lẻ: `quay-diem(P, tam, goc)`, `tinh-tien-diem(P, v)`. + +Toạ độ CỰC kiểu TikZ — `toa-cuc(tam, bk, goc)` trả về điểm cách `tam` một +khoảng `bk`, quay góc `goc` (số trần = ĐỘ, nhận cả `30deg`; dương = ngược +kim đồng hồ). TikZ `(30:2)` ⇔ `toa-cuc((0,0), 2, 30)`. Ví dụ lục giác đều: + +```typst +#let dinh = range(6).map(k => toa-cuc((0, 0), 2.6, 60 * k)) +#duong-gap-khuc(ctx, dinh, dong: true) +``` + +Lưu ý: `gach-vung` mô tả miền theo toạ độ GỐC của khung (không qua ctx-quay); +riêng elip xoay đã có `trong-elip(..., quay:)` tương ứng. + +### Sơ đồ cây xác suất (so-do-cay.typ) + +Cây sâu tuỳ ý; nút lá có ô kết quả bên phải; màu ô tự đổi theo cấp: + +```typst +#so-do-cay( + goc: $1$, + nhanh: ( + nut($A$, xs: $1/6$, con: ( + nut($B$, xs: $1/2$, kq: $A B: 1/12$), + nut($overline(B)$, xs: $1/2$, kq: $A overline(B): 1/12$), + )), + nut($overline(A)$, xs: $5/6$, con: ( + nut($B$, xs: $1/3$, kq: $overline(A) B: 5/18$), + nut($overline(B)$, xs: $2/3$, kq: $overline(A) overline(B): 5/9$), + )), + ), +) +``` + +### Hệ trục Oxyz theo đơn vị thật (hinh-khong-gian.typ) + +`oxyz` vẽ 3 trục + vectơ đơn vị $arrow(i), arrow(j), arrow(k)$; `them` +nhận `(ctx, t3)` với `t3` đổi toạ độ $(x, y, z)$ thành điểm 2D — dùng +được với mọi hàm vẽ phẳng: + +```typst +#oxyz(x: 5, y: 8, z: 8, them: (ctx, t3) => { + giong-oxyz(ctx, t3, (5, 8, 8)) // hộp gióng nét đứt + vecto-oxyz(ctx, t3, (0, 0, 0), (5, 8, 8), mau: red) + diem-oxyz(ctx, t3, (5, 8, 8), ten: $B$, huong: "above-right") +}) +``` + +Kèm `doan-oxyz(ctx, t3, A, B)`; điểm nằm trong (Oxy) chỉ vẽ hình gióng phẳng. +Xem `thu-ve-moi.typ` để có ví dụ đầy đủ của cả 6 định nghĩa. + +Mới (07/2026): `vach: true` vạch chia đơn vị trên 3 trục, `so: true` ghi số +tại các vạch, `buoc:` bước chia, `luoi:` lưới trên mặt phẳng toạ độ — +`"xy"` | tuple `("xy", "xz", "yz")` | `true` (cả 3), màu chỉnh bằng `mau-luoi`: + +```typst +#oxyz(x: 3, y: 4, z: 3, vach: true, so: true, luoi: "xy") +``` + +## 6. Bảng biến thiên & bảng xét dấu (bang.typ) + +**BBT tự động từ hệ số** (07/2026) — chỉ nhập hệ số (positional), thuật toán +tự tìm cực trị/tiệm cận/chiều biến thiên, phủ mọi trường hợp, nhãn số đẹp +(phân số, căn, `(m ± k√n)/q`): + +```typst +#bbt-bac-hai(1, -3, 1) // y = x² − 3x + 1 +#bbt-bac-ba(1, 0, -3, 1) // y = x³ − 3x + 1: 2 cực trị / Δ'=0 / đơn điệu +#bbt-trung-phuong(1, -2, 0) // y = x⁴ − 2x²: 3 hoặc 1 cực trị +#bbt-phan-thuc(2, -1, 1, -1) // y = (2x−1)/(x−1): tự xét dấu ad − bc +#bbt-huu-ti(1, -1, 1, 1, -1) // y = (x²−x+1)/(x−1): 2 cực trị / đơn điệu +#bbt-can-bac-hai-ham-bac-hai(1, 0, -4) // y = √(x²−4): tự phân 3 TH theo a, Δ +``` + +`bbt-can-bac-hai-ham-bac-hai(a, b, c)` — BBT hàm $y = sqrt(a x^2 + b x + c)$, +đỉnh $x_0 = -b\/(2a)$, giá trị đỉnh $sqrt((4 a c - b^2)\/(4a))$ (in căn thức +chính xác). Ba trường hợp: **a > 0, Δ > 0** (2 nghiệm) → TXĐ $(-oo, x_1] union +[x_2, +oo)$, khoảng giữa gạch chéo (ngoài TXĐ), giá trị 2 nghiệm $= 0$; +**a > 0, Δ ≤ 0** (nghiệm kép / vô nghiệm) → TXĐ $RR$, cực tiểu tại đỉnh +(Δ = 0 → min $= 0$, $y'$ không xác định tại đỉnh, ghi ‖); **a < 0, Δ > 0** +(2 nghiệm) → TXĐ $[x_1, x_2]$, cực đại tại đỉnh (a < 0 mà Δ ≤ 0 → TXĐ rỗng, báo lỗi). + +Form cũ (named) vẫn chạy: `bbt-bac-hai(a: 1, xd:, yd:)`, `bbt-bac-ba(a: -1, x1: $-1$, ...)`, +`bbt-bac-ba-don-dieu(a: 1)`, `bbt-trung-phuong(a: 1, x0:, yc:, y0:)`, +`bbt-phan-thuc(x0: $1$, y0: $2$, dong-bien: false)`, `xet-dau-tam-thuc(a: 1, x1: $1$, x2: $2$)`. + +**Khảo sát & vẽ đồ thị tự động** (07/2026, `khao-sat.typ`) — chỉ nhập hệ số là +xổ ra TRỌN lời giải (tập xác định, đạo hàm, giới hạn, chiều biến thiên, cực trị +/ tiệm cận, bảng biến thiên) **kèm đồ thị**, phủ mọi trường hợp của mỗi hàm: + +```typst +#khao-sat-ve-do-thi-ham-bac-hai(1, -2, -3) // y = x² − 2x − 3 (a>0/a<0, Δ mọi dấu) +#khao-sat-ve-do-thi-ham-bac-ba(1, 0, -3, 1) // y = x³ − 3x + 1 (2 cực trị / Δ'=0 / đơn điệu) +#khao-sat-ve-do-thi-ham-trung-phuong(1, -2, 0) // y = x⁴ − 2x² (3 hoặc 1 cực trị) +#khao-sat-ve-do-thi-ham-phan-thuc(2, -1, 1, -1) // y = (2x−1)/(x−1) (đồng/nghịch biến) +#khao-sat-ve-do-thi-ham-huu-ti(1, 4, 20, 1, 2) // y = (x²+4x+20)/(x+2) (2 cực trị / đơn điệu) +``` + +Tham số chung: `tieu-de:` (auto = dòng đậm "Khảo sát… y = …"; `none` = bỏ; hoặc +nội dung tuỳ ý), `w:` bề rộng đồ thị (mặc định 7.6cm), `giua: true` canh giữa +BBT + đồ thị. Đặt thẳng trong thân câu hoặc trong `loi-giai:` của `#tl(...)`. + +Bảng tuỳ ý — quy ước: `dau` dài `2n−1` xen kẽ *[tại mốc, trên khoảng, …]*; +tại điểm gián đoạn thêm chỉ số vào `kep` và cho `gia-tri` một **cặp** (trái, phải): + +```typst +// y = x³ − 3x² + 2 +#bbt( + x: ($-oo$, $0$, $2$, $+oo$), + dau: ("", "+", "0", "-", "0", "+", ""), + gia-tri: ($-oo$, $2$, $-2$, $+oo$), + huong: ("len", "xuong", "len"), +) + +// y = (2x−1)/(x−1): tiệm cận đứng x = 1 (kẹp ‖) +#bbt( + x: ($-oo$, $1$, $+oo$), + dau: ("", "-", "||", "-", ""), + gia-tri: ($2$, ($-oo$, $+oo$), $2$), + huong: ("xuong", "xuong"), + kep: (1,), +) + +// Bảng xét dấu nhiều dòng +#bang-xet-dau( + x: ($-oo$, $1$, $3$, $+oo$), + dong: ( + ($x - 1$, ("", "-", "0", "+", "", "+", "")), + ($x - 3$, ("", "-", "", "-", "0", "+", "")), + ($f(x)$, ("", "+", "0", "-", "0", "+", "")), + ), +) +``` + +Ký hiệu dấu: `"+"`, `"-"`, `"0"`, `"||"` (kẹp), `""` (trống); `huong`: `"len" | "xuong" | "ngang"`. + +**Tự bù ô trống hai đầu** (08/2026) — dãy `dau` phải dài `2n−1` nên **hai đầu +luôn là ô trống `""`**; AI sinh bài rất hay quên đúng hai ô đó. Nay `bbt` và +`bang-xet-dau` **tự chèn thêm** (chỉ chèn ở ĐẦU/CUỐI, không bao giờ chèn vào +giữa; thiếu 1 ô thì tự đoán đầu nào thiếu theo quy ước `"+"`/`"-"` nằm trên +khoảng, `"0"`/`"||"` nằm tại mốc). Thiếu quá 2 ô hoặc thừa ô thì báo lỗi bằng +tiếng Việt nói rõ cần bao nhiêu phần tử — thay cho `array index out of bounds`. +Kèm theo: `dau` nhận cả số trần (`0` thay vì `"0"`), còn `x` và `gia-tri` nhận +chuỗi `"-oo"`/`"+oo"` và số trần (`-5/3`) rồi tự quy về nội dung toán. Nghĩa là +đoạn dưới đây chạy đúng y như bản viết đủ: + +```typst +#bbt( // thiếu 2 ô trống + "-oo" dạng chuỗi + x: ("-oo", -3, -1, "+oo"), + dau: ("+", 0, "-", 0, "+"), + gia-tri: ("-oo", 5, -5/3, "+oo"), + huong: ("len", "xuong", "len"), +) +``` + +File thử: `thu-bbt-bu-dau.typ`. + +**Mũi tên nửa ô** (07/2026) — khi hàm đơn điệu qua mốc y′ = 0 (nghiệm kép), +để 2 mũi tên cùng chiều nằm **trên 1 đường thẳng** đi qua giá trị đặt giữa ô: +`"len-duoi"` (đáy→giữa) rồi `"len-tren"` (giữa→đỉnh); nghịch biến dùng +`"xuong-tren"` (đỉnh→giữa) rồi `"xuong-duoi"` (giữa→đáy). Giá trị tại mốc kề +đầu mút "giữa" tự đặt chính giữa ô — không cần box thủ công: + +```typst +// y = x³ − 3x² + 3x: y' = 0 kép tại x = 1, vẫn đồng biến +#bbt( + x: ($-oo$, $1$, $+oo$), + dau: ("", "+", "0", "+", ""), + gia-tri: ($-oo$, $1$, $+oo$), + huong: ("len-duoi", "len-tren"), +) +``` + +`bbt-bac-ba(a, b, c, d)` (và `khao-sat-ve-do-thi-ham-bac-ba`) trường hợp +Δ′ = 0 tự dùng kiểu này. + +## 7. Câu hỏi định dạng đề thi 2025 (cau-hoi.typ) + +Bảy dạng: MC/TF/SA/TL đánh số "Câu N" liên tục chung; **HĐ** (hoạt động) +đánh "HĐ1, HĐ2...", **Luyện tập** đánh "Luyện tập 1, 2..." (chung bộ đếm +với khung `#luyen-tap`) và **Vận dụng** đánh "Vận dụng 1, 2..." — mỗi loại +bộ đếm riêng. Ngoài ra có 3 hình thức hoạt động SGK **KHÔNG đánh số**: +`#kham-pha` (Khám phá, xanh lục), `#trai-nghiem` (Trải nghiệm, xanh lá), +`#thao-luan` (Thảo luận, xanh dương) — thân giống `#hd` (dùng được `loi-giai:`, +`hinh:`, `lo-da:`, `cot-item`…). **Một công tắc** đổi giữa bản giáo viên +(hiện đáp án, tô xanh) và bản chiếu cho học sinh: `#bat-dap-an()` / `#tat-dap-an()`. +Ở chế độ **beamer**, công tắc tự BẬT sẵn (kể cả khi dùng `bai-giang` trực tiếp, +không qua `de-toan`) — đáp án được đánh dấu ở bước `lo-da` (bước cuối) của mỗi câu; +muốn ẩn thì gọi `#tat-dap-an()`. +Đánh số lại — MỖI thể loại có bộ đếm & hàm đặt lại RIÊNG (không còn đặt lại +tất cả cùng lúc): `#dat-lai-cau()` chỉ nhóm **Câu** (tn/ds/tln/tl); +`#dat-lai-cau-vd()` Ví dụ, `#dat-lai-cau-hd()` Hoạt động, `#dat-lai-cau-lt()` +Luyện tập, `#dat-lai-cau-vdtt()` Vận dụng thực tế; `#dat-lai-cau-tat-ca()` đặt +lại cả 5 nhóm cùng lúc (hành vi cũ). Không tham số hoặc `(0)` → về 1; +`(3)` → đánh tiếp từ 4 (tổng quát `n` → từ n + 1). + +```typst +// MC — 4 phương án, cot: 1 | 2 | 4 +#cau-mc([Câu hỏi...?], ($1$, $2$, $3$, $4$), dap-an: "B", cot: 4, + loi-giai: [Hướng dẫn giải...]) + +// TF — 4 ý đúng/sai +#cau-tf([Đề bài...], ([ý a], [ý b], [ý c], [ý d]), + dap-an: (true, true, false, false), loi-giai: [...]) + +// SA — trả lời ngắn (khi ẩn đáp án sẽ hiện 4 ô điền như phiếu TLĐ) +#cau-sa([Tính ...?], dap-an: $12$, loi-giai: [...]) + +// TL — tự luận; lời giải chỉ hiện khi bật đáp án +#cau-tl([Đề bài...], diem: 2, loi-giai: [Hướng dẫn...], cho-trong: 3cm) + +// HĐ — hoạt động (khởi động/khám phá): thẻ "HĐ1" cam, hình thức như TL; +// nhiều ý hỏi thì dùng #cot-item trong thân câu +#cau-hd([Cho hàm số... #cot-item([Tìm GTLN...], [Tìm GTNN...])], + loi-giai: [a) ... \ b) ...]) + +// LT — luyện tập (củng cố lý thuyết): thẻ "Luyện tập 1" vàng đậm, +// chung bộ đếm với khung #luyen-tap +#cau-lt([Tìm GTLN — GTNN của $y = sqrt(2x - x^2)$.], loi-giai: [...]) + +// VDTT — vận dụng thực tế: thẻ "Vận dụng 1" tím, hình thức như TL +#cau-vdtt([Một chiếc hộp...], loi-giai: [Gọi $x$... \ Vậy...]) +``` + +Cả 7 dạng đều có `loi-giai:`; trong slide hoạt hình dùng thêm +`lo-da:` (bước lộ đáp án) và `lo-giai:` (bước lộ lời giải, mặc định +cùng bước với đáp án). Số thứ tự câu/ví dụ/luyện tập **không đổi** +qua các bước hoạt hình. + +Khung công thức: `#cong-thuc[$S = pi r^2$]` (cùng họ với `#dinh-nghia`, `#dinh-ly`). + +## 8. Hoạt hình xuất hiện từng bước + +Nguyên lý như Beamer: slide có `so-buoc: n` được in thành `n` trang PDF — +khi trình chiếu, bấm phím chuyển trang là nội dung "hiện dần". +Chân trang đánh số theo slide (không tăng theo bước). + +```typst +#slide(tieu-de: [Ví dụ], so-buoc: 3)[ + Nội dung luôn hiển thị. + #lo(2)[Hiện từ bước 2 — khi chưa hiện KHÔNG chiếm chỗ (như \pause của LaTeX + beamer; nhờ vậy slide dài không sinh trang trắng đệm).] + #lo(2, giu-cho: true)[Ẩn nhưng giữ chỗ — chỉ dùng khi slide chắc chắn gọn 1 trang.] + #chi(2)[Chỉ hiện đúng ở bước 2 (mặc định giữ chỗ).] + #chi(3, giu-cho: false)[Chỉ hiện ở bước 3, không giữ chỗ.] + #an(3)[Hiện từ đầu rồi BIẾN MẤT ở bước 3 (hiệu ứng "thoát" của PowerPoint; + mặc định không giữ chỗ, nội dung dưới dồn lên).] + #hien-khoang(2, 4)[Chỉ hiện ở bước 2–3, biến mất từ bước 4.] + #tung-buoc([ý 1 — bước 2], [ý 2 — bước 3]) // danh sách hiện dần + #buoc(hien-dan: true, [Bước giải 1...], [Bước giải 2...]) +] +``` + +`#an(n)` là bản đối xứng của `#lo(n)`: `#lo` cho nội dung **hiện ra**, `#an` +cho nội dung **biến mất**. Ghép hai lệnh để **thay thế** phần tử tại chỗ — hiện +nội dung tạm rồi xoá đi, cho nội dung mới hiện lên cùng bước: + +```typst +#slide(tieu-de: [Thay thế], so-buoc: 3)[ + #an(3)[$x = 1$ (giá trị tạm)] // biến mất ở bước 3 + #lo(3)[$x = 2$ (giá trị đúng)] // hiện lên ở bước 3 +] +``` + +Bản in (`dethi`/`loigiai`) hiện hết như `#lo`; muốn bản in chỉ giữ trạng thái +cuối thì đặt phần biến mất trong `#chi`. + +Với câu hỏi, dùng `lo-da` để **đề hiện trước, đáp án lộ sau**: + +```typst +#slide(tieu-de: [Kiểm tra], so-buoc: 2)[ + #cau-mc([Đề...?], ($1$, $2$, $3$, $4$), dap-an: "B", lo-da: 2) +] +``` + +Bước 1 chiếu đề cho học sinh làm, bấm một cái → phương án đúng được tô xanh. +(Ở beamer công tắc đáp án đã bật sẵn; chỉ cần `#bat-dap-an()` cho bản in A4.) + +`#lo` dùng được với MỌI nội dung — ví dụ hiện dần lời giải khảo sát: + +```typst +#slide(tieu-de: [Khảo sát hàm số], so-buoc: 4)[ + #vi-du[Khảo sát $y = -x^3 + 3x$.] // bước 1: đề + #lo(2)[#loi-giai[$y' = -3x^2 + 3$]] // bước 2: đạo hàm + #lo(3)[ + #bbt( + x: ($-oo$, $-1$, $1$, $+oo$), + dau: ("", "-", "0", "+", "0", "-", ""), + gia-tri: ($+oo$, $-2$, $2$, $-oo$), + huong: ("xuong", "len", "xuong"), + ) + ] // bước 3: bảng biến thiên + #lo(4)[#figure(do-thi-bac-ba(-1, 0, 3, 0))] // bước 4: đồ thị +] +``` + +### Lời giải dài — `#sang-man` và tự ngắt màn + +Trong `loi-giai:` của 8 dạng câu hỏi, đặt `#sang-man` trên **một dòng riêng** +để đẩy phần sau đó sang một slide mới mang nhãn "Hướng dẫn giải (tiếp)". +Nhớ dấu `\` ở **cuối dòng chữ ngay trước** dấu này. Bản in A4 bỏ qua nó. + +```typst +loi-giai: [ + a) Hàm số đồng biến trên $(0; 2)$. \ + Cực đại tại $x = 2$. \ + #sang-man \ + b) Xét $f'(x) = 0 <=> x = 1$. +], +``` + +Quên chèn mà lời giải vẫn cao hơn thân slide thì thư viện **tự đo và tự +ngắt**: dòng nào làm tràn sẽ được đẩy sang màn "(tiếp)". Nhờ vậy đề bài không +còn bị in lại xen giữa các trang lời giải — mỗi bước hoạt hình vốn in slide +lại từ đầu, nên một slide tràn trang sẽ đẻ ra trang "đề" lặp. Đề dài tới mức +không dòng lời giải nào lọt thì slide đầu chỉ hiện đề. + +```typst +#tu-ngat-man(false) +``` + +## 9. Thanh điều hướng & mục lục + +- **Đầu trang mỗi slide** (beamer): dải màu đậm phía trên thanh tiêu đề luôn + hiện **tên bài học** (`tieu-de-ngan`, mặc định là `tieu-de`) bên trái và + tên mục hiện tại bên phải. +- **Chân trang mỗi slide** là thanh điều hướng: bên trái là tên các phần + (bấm để nhảy tới đầu phần; phần hiện tại in đậm), giữa là dãy **chấm tròn** + — mỗi chấm một slide của phần hiện tại, chấm đặc là slide đang xem, + bấm chấm nào nhảy tới slide đó. +- `#muc(ngan: [Phần 1])[Phần 1. Tên đầy đủ...]` — `ngan` là nhãn rút gọn + hiển thị trên thanh điều hướng. +- `#muc-luc()` (đặt sau trang bìa) — slide mục lục tự động liệt kê mọi phần + và tiêu đề mọi slide, bấm vào tiêu đề nào nhảy thẳng tới slide đó. + +Liên kết hoạt động trong mọi PDF reader (SumatraPDF, Edge, Adobe...). + +## 10. Một file nguồn — ba kiểu PDF (hồ sơ hiển thị) + +Soạn đề MỘT LẦN trong file kiểu `de-mau.typ`, xuất được 3 bản: + +| Hồ sơ | Kết quả | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `"dethi"` | Đề thi A4: MC/TF/SA/TL**ẩn** đáp án + lời giải; ví dụ vẫn hiện lời giải; phần Họ tên / Số báo danh (hoặc Lớp) / Mã đề **chỉ hiện khi khai báo** `hien-ho-ten` / `sbd` / `hien-ma-de` | +| `"loigiai"` | Bản A4 đáp án: mọi câu**hiện** lời giải, tô đáp án đúng | +| `"beamer"` | Trình chiếu:**mỗi câu một slide** — hiện đề → lời giải hiện dần theo từng dấu `\` → cuối cùng đánh dấu đáp án | + +```typst +#import "@preview/conic-toan:0.3.4": * +#let ho-so = sys.inputs.at("ho-so", default: "dethi") + +#show: de-toan.with(ho-so: ho-so, tieu-de: [ĐỀ KIỂM TRA...], + mon: [MÔN TOÁN 12], thoi-gian: "90 phút", truong: [...], ma-de: "101", + ngay: "30/12/2026", // bản dethi in "Ngày kiểm tra: ..." dưới dòng thời gian + hien-ho-ten: true, // 3 dòng thông tin thí sinh CHỈ hiện khi khai báo: + sbd: "sbd", // sbd: "sbd"=Số báo danh | "lop"=Lớp | none=ẩn + hien-ma-de: true, // hien-ho-ten / hien-ma-de: bật ô tương ứng + ti-le-chu: 1.0, // hệ số phóng cỡ chữ thân — dùng chung cho cả 3 hồ sơ + gian-dong: 1.0, // hệ số GIÃN DÒNG — dùng chung cho cả 3 hồ sơ (xem §Giãn dòng) + mau-cong-thuc: auto) // màu công thức $...$; auto = đen (thừa kế). Đặt màu để nhuộm cả bài + +// Gọi thẳng #vd/#tn/#ds/#tln/#tl/#hd/#lt/#vdtt/#phan — các hàm tự nhận +// biết chế độ, không cần khai báo tao-cau-hoi (dòng cũ vẫn chạy được). + +#phan([PHẦN I. Trắc nghiệm], ngan: [Phần I]) +#tn([Đề...?], ($1$,$2$,$3$,$4$), dap-an: "B", + loi-giai: [Bước 1... \ Bước 2... \ Chọn *B*.], + tieu-de: [Cực trị]) // tiêu đề slide, chỉ dùng ở beamer + +#hd([Hoạt động khám phá...], loi-giai: [a) ... \ b) ...], + tieu-de: [HĐ1: Nhận biết]) // câu hoạt động — thẻ "HĐ1" +#lt([Bài luyện tập...], loi-giai: [$y' = ...$ \ Vậy...], + tieu-de: [Luyện tập 1]) // câu luyện tập — thẻ "Luyện tập 1" +#vdtt([Bài toán thực tế...], loi-giai: [Gọi $x$... \ Vậy...], + tieu-de: [Vận dụng]) // câu vận dụng — thẻ "Vận dụng 1" +#kham-pha([Từ định lí côsin, hãy viết công thức tính $cos A$...]) // thẻ "Khám phá" (không số) +#trai-nghiem([Vẽ một tam giác $A B C$, đo các cạnh và góc $A$...]) // thẻ "Trải nghiệm" +#thao-luan([Liệu $sin A$ và diện tích $S$ có tính được theo các cạnh?]) // thẻ "Thảo luận" +``` + +Xuất cả 3 bản không cần sửa file: + +``` +typst compile --input ho-so=dethi de-mau.typ de-thi.pdf +typst compile --input ho-so=loigiai de-mau.typ dap-an.pdf +typst compile --input ho-so=beamer de-mau.typ trinh-chieu.pdf +``` + +Quy ước: trong `loi-giai:` của câu hỏi, **mỗi dấu `\` (xuống dòng) là một +bước xuất hiện** khi trình chiếu; ở bản A4 chúng chỉ là xuống dòng thường. + +### Giãn dòng — `gian-dong` + +Khi một dòng có phân số, căn thức hoặc chỉ số **nhiều tầng** +(`$-(11 pi)/5 = -(11 pi)/5 dot (180/pi) degree$`), tử số của dòng dưới dễ +dính — thậm chí chồng — vào dòng trên. `gian-dong` là **hệ số nhân** vào +khoảng cách dòng: `1.0` = mốc mặc định, `1.3` = giãn thêm 30%, `0.9` = thu +lại 10%. Bốn cách dùng, phạm vi từ rộng đến hẹp: + +```typst +// 1) TOÀN TÀI LIỆU — khai ở dòng #show +#show: bai-giang.with(tieu-de: [CHƯƠNG I], gian-dong: 1.25) +#show: de-toan.with(ho-so: ho-so, ma-de: "0101", gian-dong: 1.25) + +// 2) ĐỔI GIỮA BÀI — áp cho các slide + khung lời giải PHÍA SAU +#gian-dong(1.4) +... // phần này giãn 1.4 +#gian-dong(1.0) // trả về mốc mặc định + +// 3) RIÊNG MỘT CÂU — tham số gian-dong: của #vd/#tn/#ds/#tln/#tl/#hd/#lt/#vdtt +#tn([Đề...?], ($1$, True($2$), $3$, $4$), + gian-dong: 1.5, + loigiai: [$-(11 pi)/5 = -396 degree$. \ Vậy...]) + +// 4) RIÊNG MỘT KHỐI nội dung bất kì +#voi-gian-dong(1.7)[ + Đoạn này giãn 1.7; phần trước và sau không đổi. +] +``` + +Mốc `1.0` ứng với leading gốc của từng hồ sơ (trình chiếu `0.62em` · A4 bài +giảng `0.6em` · A4 đề thi `0.65em`) nên **một hệ số dùng chung được cho cả 3 +bản PDF**, và đặt `1.0` thì bố cục/số trang y như trước. + +Hệ số này giãn **cả hai** loại khoảng cách: giữa các dòng trong cùng một đoạn +(nối bằng `\`) và giữa hai đoạn (chỗ để **dòng trống**, mốc `1.2em`). Trước +08/2026 chỉ giãn loại thứ nhất, nên nội dung có dòng trống trông như +`voi-gian-dong` "chỉ ăn đoạn đầu rồi mất tác dụng". + +Khung lời giải hiện dần ở beamer (`giai-buoc`) vốn đã rộng hơn thân slide +(mốc `0.95em`); hệ số `gian-dong` nhân thêm vào mốc đó. Muốn ấn định độ dài +tuyệt đối thì dùng `giai-buoc(..., gian: 1.2em)`. + +Từ 08/2026 hệ số này còn giãn **khoảng cách giữa các hàng phương án** của +`#tn`, giữa các ý của `#ds`, giữa các item của `cot-item`, và **khoảng cách từ +đề xuống phương án** — trước đây nó không với tới những chỗ đó (chúng nằm trong +ô của `grid` hoặc là khoảng trắng cố định, không phải khoảng cách dòng). Hệ số +`1.0` vẫn cho đúng số cũ nên bài cũ không đổi bố cục. + +Lưu ý về vai trò: từ khi có `cao-that` (mục dưới), **`gian-dong` không còn là +thứ để chữa dính chữ** — chữ dính đã được lib tự lo. `gian-dong` nay chỉ để +làm cả bài thoáng hơn hoặc chặt lại theo ý người soạn. + +File thử: `thu-gian-dong.typ`. + +### Chiều cao thật của công thức trong dòng — `cao-that` + +Typst đóng khung công thức **trong dòng** theo số đo phông chữ (cap-height → +đường chân chữ) chứ không theo nét vẽ: `measure($1/2$)` và `measure($0,5$)` +cho **cùng** một chiều cao, trong khi phân số vẽ ra cao gần gấp đôi và tràn cả +trên lẫn dưới khung. Vì thế phân số hay dính vào dòng trên, và trong ô của +`grid` (phương án `#tn`, ý `#ds`, `cot-item`) thì đè hẳn sang hàng trên — +`gian-dong` không chữa được vì nó chỉ chạm khoảng cách dòng. + +Lib so nét vẽ thật với **nét chữ thường** (chữ thường cũng tràn khỏi khung mà +không ai kêu, vì `leading` đã chừa chỗ), rồi chèn một **cột chống vô hình rộng +0pt** đúng bằng phần còn thiếu. Dòng và ô tự nới **đúng chỗ cần**; chữ thường +và công thức thấp (`$x$`, `$0,5$`) không đổi một pt nào. Cơ chế **bật sẵn** +trong `bai-giang` và `de-toan`, và đúng cả khi tài liệu bật kiểu LaTeX +`#show math.equation.where(block: false): it => math.display(it)` (phân số to +đẹp — cũng chính là cách viết làm dính chữ nặng nhất). + +```typst +#cao-that(false) // tắt từ đây trở đi +#cao-that() // bật lại +#cao-that(them: 1pt) // nới thêm 1pt mỗi phía cho công thức có tràn +#kieu-cau-hoi(cao-that: false) // tắt cho cả bài, khai cùng chỗ mau/hinh + +// Tài liệu KHÔNG đi qua bai-giang/de-toan thì áp cho một khối: +#voi-cao-that[ + Giá trị $11 pi/3$ và $17 pi/4$ trên hai dòng liền nhau. \ + Dòng này không còn chạm vào dòng trên. +] +``` + +Nhờ nó mà `gian-dong` để `1.0` là đủ; chỉ dùng `gian-dong` khi muốn cả bài +thoáng hơn, chứ không phải để chữa dính chữ nữa. + +File thử: `thu-cao-that.typ`; bảng số đo đối chiếu: `thu-cao-that-chan-doan.typ`. + +### Kết thúc đề & bảng đáp án (`#het`, `#bang-dap-an`) + +Hai hàm này CHỈ dựng ở bản in A4 (`dethi`/`loigiai`); ở `beamer` tự bỏ qua +(không sinh slide thừa). Đặt ở CUỐI file, sau câu cuối cùng: + +``` +#het() // dòng "––––– HẾT –––––" + 2 dòng ghi chú (căn giữa) +#het(chu: [THE END], // tuỳ chỉnh chữ giữa + ghi-chu: [Không được dùng tài liệu.]) // ghi-chu: none = ẩn 2 dòng dưới + +#bang-dap-an() // 3 bảng đáp án: tn (1.C 2.A…), ds (Đ/S vòng tròn), + // tln (mỗi ký tự một ô, như phiếu TLN) +``` + +`#bang-dap-an` **tự thu thập đáp án MỌI câu `tn`/`ds`/`tln`** trong tài liệu +(kể cả form cũ `dap-an:` lẫn form mới `True(...)`) — không cần khai báo lại; +mỗi loại đánh số `1..n` độc lập theo thứ tự xuất hiện. Tham số: + +- `ma-de`: `auto` (mặc định) = **tự lấy mã đề khai ở `de-toan(ma-de: …)`** nên + gọi `#bang-dap-an()` là đủ, luôn đồng bộ; `none` = tiêu đề "BẢNG ĐÁP ÁN" + (không mã đề); hoặc giá trị cụ thể để ghi đè. +- `tieu-de`: `auto` (theo `ma-de`) | nội dung tuỳ ý | `none` (ẩn tiêu đề). +- `so-o-tln`: số ô mỗi đáp án trả lời ngắn (mặc định 4; tự nới nếu đáp án dài hơn). +- `ngat-trang`: `true` (mặc định) = sang trang mới trước bảng. + +Đáp án `tln` truyền dạng chuỗi (`"60,7"`), số, hoặc nội dung (`[1160]`) đều +được — hàm tự tách thành từng ký tự để điền vào ô; nếu không tách được thì in +nguyên nội dung vào một ô. Để in bảng có điều kiện, bọc trong `#if`: +`#if muon-in-dap-an { bang-dap-an(ma-de: ma-de) }`. + +### Chia cột phần câu hỏi — `#chia-2-cot`, `#chia-2-cot-lech` (08/2026) + +Hai lệnh bố cục dùng như **show-rule**: *đặt ở đâu thì áp dụng từ dòng đó trở +xuống*, đúng lối `#show: de-toan.with(...)` đã quen. + +``` +#show: de-toan.with(ho-so: ho-so, tieu-de: [ĐỀ KIỂM TRA]) + +#show: chia-2-cot // từ đây trở xuống: câu hỏi xếp 2 cột ĐỀU nhau +#show: chia-2-cot-lech // hoặc: trái = câu hỏi, phải = chỗ HS làm bài +``` + +`#chia-2-cot(so: 2, khoang: 18pt, can: true)` — chia phần câu hỏi thành hai cột +bằng nhau (đặt `so: 3` nếu muốn ba cột), chữ chảy từ cột trái sang cột phải. +Hai cột được **tự cân bằng chiều cao** (`can: false` để tắt): Typst vốn rót +đầy cột 1 tới hết chiều cao trang rồi mới sang cột 2, nên đề ngắn sẽ nằm gọn +một cột và cột 2 để trống — thư viện tự chèn dấu ngắt cột để hai bên đều nhau. + +`#chia-2-cot-lech(...)` — cột **trái** chứa câu hỏi, cột **phải** để trống có +hàng kẻ ngang mờ cho học sinh làm bài (kèm vạch dọc mờ ngăn hai cột). Số hàng +kẻ tự tính theo chiều cao thật của phần câu hỏi nên không thừa/thiếu giấy. + +| Tham số | Mặc định | Ý nghĩa | +|---|---|---| +| `rong-trai` | `70%` | **bề rộng cột trái** — nhận tỉ lệ (`60%`) hoặc độ dài (`11cm`) | +| `khoang` | `10pt` | khe giữa hai cột | +| `cao-dong` | `9mm` | khoảng cách giữa hai hàng kẻ | +| `mau` / `day` | `luma(65%)` / `0.4pt` | màu và độ dày hàng kẻ | +| `ke` | `true` | `false` = cột phải trắng trơn (không kẻ dòng) | +| `vach-ngan` | `true` | vạch dọc mờ ngăn hai cột | +| `tieu-de-phai` | `none` | vd `[Bài làm]` — in nhạt ở đầu cột phải | + +**Ngừng chia cột** bằng `#thoi-cot()`: phần sau dấu này trở lại nguyên khổ +giấy. **Bắt buộc** đặt `#thoi-cot()` trước `#het()`/`#bang-dap-an()` — hàm +`#bang-dap-an` có `#pagebreak` mà lệnh ngắt trang không chạy được bên trong +cột. + +```typst +#show: chia-2-cot-lech.with(rong-trai: 65%, tieu-de-phai: [Bài làm]) +#tl([Giải phương trình $x^2 - 5x + 6 = 0$.]) +#tl([Tính đạo hàm của hàm số $y = x^3 - 3x + 1$.]) +#thoi-cot() // trở lại nguyên khổ giấy +#het() +#bang-dap-an() +``` + +Cả hai lệnh cũng dùng được **dạng khối** cho một đoạn: `#chia-2-cot[ ... ]`, +`#chia-2-cot-lech(rong-trai: 55%)[ ... ]`. + +Ghi chú: hai lệnh chỉ tác dụng ở bản in A4 (`dethi`/`loigiai`); bản trình +chiếu `beamer` tự bỏ qua (mỗi câu vốn đã là một slide riêng). Khi bật hoán vị +(trộn đề), các câu nằm trong cột **vẫn được trộn** bình thường và bản `dethi` +với bản `loigiai` vẫn trộn giống hệt nhau. File thử: `thu-chia-cot.typ`. + +### Kẻ dòng lấp đầy trang — `#ke-het-trang` (08/2026) + +Đặt ở đâu thì kẻ dòng từ chỗ đó xuống **hết trang đó** — số dòng tự tính theo +chỗ trống còn lại, không phải đếm tay. + +``` +#ke-het-trang() // nét chấm, cách 9mm +#ke-het-trang(cao-dong: 7mm, kieu: none) // nét liền, dòng dày hơn +#ke-het-trang(chua: 3cm) // chừa 3cm cuối trang +#ke-het-trang(dai: 50%, them-trang: 1) // nửa bề ngang + kẻ trọn 1 trang nữa +``` + +| Tham số | Mặc định | Ý nghĩa | +|---|---|---| +| `cao-dong` | `9mm` | khoảng cách giữa hai dòng kẻ | +| `mau` / `day` | `luma(65%)` / `0.4pt` | màu và độ dày nét | +| `kieu` | `"dotted"` | `"dashed"`, `"dash-dotted"`, hoặc `none` = nét liền | +| `dai` | `100%` | bề dài dòng kẻ (`50%` = nửa bề ngang) | +| `chua` | `0pt` | chừa thêm khoảng trắng ở đáy trang | +| `them-trang` | `0` | kẻ thêm bấy nhiêu **trang đầy** nữa sau trang hiện tại | +| `le-tren` / `le-duoi` | `auto` | tự đọc từ lề trang; chỉ đặt tay khi lề khai kiểu lạ | + +Chỉ dựng ở bản in A4 (`dethi`/`loigiai`); bản `beamer` tự bỏ qua. File thử: +`thu-ke-het-trang.typ`. + +### Hoán vị — trộn đề (08/2026) + +Bật **một công tắc ở đầu file** là trộn được đề: thứ tự câu và thứ tự phương +án tự đảo, đáp án + bảng đáp án tự chạy theo. + +```typst +#let ho-so = sys.inputs.at("ho-so", default: "dethi") +#let hoan-vi = false // <- ĐỔI THÀNH true LÀ TRỘN (mặc định false) + +#show: de-toan.with(ho-so: ho-so, ma-de: "0101", hoan-vi: hoan-vi) +``` + +Nguyên tắc: + +- Hoán vị các **câu** trong cùng một **nhóm thể loại**: nhóm `tn` xáo với nhau, + nhóm `ds` xáo với nhau, nhóm `tln` xáo với nhau. Câu `tl` **giữ nguyên** thứ tự. +- "Nhóm" = dãy câu **cùng loại đứng liền nhau** (chỉ cách nhau bằng dòng trống). + Gặp `#phan`, một đoạn văn xen giữa hay một câu khác loại là hết nhóm ⇒ câu + **không bao giờ nhảy qua tiêu đề phần**. +- Hoán vị **4 phương án A/B/C/D** của câu `tn`. Các **ý a/b/c/d của câu `ds` + giữ nguyên**, không hoán vị (ý sau thường dựa vào ý trước). +- Chỉ chạy ở bản in A4 (`dethi` / `loigiai`); trình chiếu `beamer` không trộn. + +| Tham số của `de-toan` | Ý nghĩa | +| --- | --- | +| `hoan-vi: false` | mặc định — giữ nguyên thứ tự soạn | +| `hoan-vi: true` | trộn cả thứ tự câu lẫn phương án | +| `hoan-vi: "cau"` | chỉ trộn thứ tự câu | +| `hoan-vi: "pa"` | chỉ trộn phương án | +| `mam: auto` | **mã trộn** — `auto` băm từ `ma-de`, nên `0101` và `0102` tự cho hai thứ tự khác nhau; hoặc đặt tay `mam: 7` | + +Bộ trộn là **tất định** (LCG có mầm, không dùng ngẫu nhiên thật): cùng một mầm +luôn cho cùng một thứ tự, nên bản `dethi` và bản `loigiai` biên dịch hai lần +vẫn trộn **giống hệt nhau** — bảng đáp án luôn khớp. + +```sh +typst compile --input ho-so=dethi de-mau.typ de-0101.pdf +typst compile --input ho-so=loigiai de-mau.typ dapan-0101.pdf # cùng thứ tự +``` + +Muốn ra nhiều mã đề từ MỘT file: đọc mã đề qua `sys.inputs` rồi đổi lúc biên dịch. + +```typst +#let ma = sys.inputs.at("ma", default: "0101") +#show: de-toan.with(ho-so: ho-so, ma-de: ma, hoan-vi: true) +``` + +Hai điều **phải nhớ**: + +- Câu `tn` viết theo **form cũ** `dap-an: "B"` (đáp án là chữ cái) **không được + trộn phương án** — trộn sẽ lệch. Muốn trộn thì bọc phương án đúng bằng + `True(...)`. Lib tự kiểm: câu nào không có `True(...)` thì bỏ qua, không xáo. +- `khoa-pa: true` ở một câu `tn` ⇒ riêng câu đó giữ nguyên thứ tự phương án + (dùng cho câu kiểu "Cả A, B, C đều đúng"). + +Tài liệu nào không đi qua `de-toan` thì dùng show-rule riêng: +`#show: hoan-vi-de.with(true)` (hoặc `.with(true, mam: "0102")`). + +File thử: `thu-hoan-vi.typ`. + +### Form mới của tn/ds/tln — đáp án gắn liền nội dung (khuyến nghị) + +Đáp án đúng bọc `True(...)` (bí danh `Dung(...)`) ngay trong danh sách, +nên **hoán vị phương án không bao giờ lệch đáp án** — nền tảng cho việc +xáo trộn tạo mã đề. Form cũ (`dap-an:`, `hinh:`, `loi-giai:`, `cot:`) +vẫn dùng được song song. + +```typst +#tn( + [Nội dung đề bài], + ( [$A$], True([$B$]), [$C$], [$D$] ), // đáp án đúng bọc True(...) + loigiai: [Lời giải chi tiết], + // Tuỳ chọn: + fig: none, fig-pos: "right", fig-width: 35%, // hình: vị trí right/left/center/top/bottom + // ("center" = canh giữa, nằm giữa phần đề + // và các phương án), + // bề rộng cột hình (auto = ôm hình) + cols: 0, // 0 = tự chia cột; 1/2/4 = cố định + lines: 0, // số dòng chừa làm bài (chỉ bản in đề) + num: auto, // auto = đánh số tự động; số cụ thể = ghi đè (không tăng bộ đếm) + prefix: "Câu", // chữ trên thẻ đầu câu + boxed: false, // true = đóng khung cả câu +) + +#ds( + [Thân câu chung — nêu tình huống / bài toán], + ( + [Phát biểu a — SAI], + True([Phát biểu b — ĐÚNG]), + True([Phát biểu c — ĐÚNG]), + [Phát biểu d — SAI], + ), + loigiai: [Phân tích từng ý.], +) + +#tln( + [Nội dung đề bài], + [$6$], // đáp án đặt NGAY SAU đề + loigiai: [Lời giải.], + show-boxes: true, box-count: 4, // ô điền trên phiếu (bản in đề) +) +``` + +`loigiai:`/`fig:` cũng dùng được cho `#vd/#tl/#hd/#lt/#vdtt` (bí danh của +`loi-giai:`/`hinh:`), kèm cả `fig-pos:`/`fig-width:` — tám dạng câu nhận cùng +một bộ tham số hình. + +### Lời giải gắn từng ý của `#ds` — an toàn khi trộn đề hoán vị ý + +Khối `loigiai:` viết cứng "a) … b) … c) … d) …" sẽ **sai** nếu công cụ trộn +xáo thứ tự các ý. Đặt lời giải ngay cạnh ý bằng `giai:` thì lời giải đi theo +ý, và nhãn a) b) c) d) được đánh lại theo thứ tự **sau** hoán vị: + +```typst +#ds([Cho hàm số $y = x^3 - 3x$.], + ( + True([$y' = 3x^2 - 3$], giai: [Đạo hàm của $x^n$ là $n x^(n-1)$.]), + False([Hàm số không có cực trị], giai: [$y' = 0$ có hai nghiệm phân biệt.]), + True([Đồ thị nhận điểm uốn $I(0; 0)$ làm tâm đối xứng]), // không cần giải riêng + [Hàm số đồng biến trên $RR$], // ý sai, viết trần + ), +) +``` + +- `True(nd, giai: ...)` — ý ĐÚNG; `False(nd, giai: ...)` — ý SAI có lời giải. +- Ý sai không cần lời giải thì viết trần `[...]` như cũ; `giai:` là tuỳ chọn, + ý nào không khai thì phần lời giải bỏ qua ý đó. +- Dùng chung với `loigiai:` được: phần chung in **trước** làm dẫn nhập + (tập xác định, đạo hàm…), rồi mới tới phần theo từng ý. +- `khoa-y: true` (và `khoa-pa: true` cho `#tn`) đánh dấu câu **không được + hoán vị** — dùng khi ý sau dựa vào kết quả ý trước, hoặc có phương án kiểu + "Cả A và B đều đúng". Cờ này được ghi vào metadata `` để công cụ + trộn đọc. + +Form cũ (khối `loigiai:` chung, ý bọc `True(...)` không có `giai:`) vẫn chạy +nguyên vẹn — `giai:`/`False`/`khoa-y` chỉ là bổ sung. +File thử: `thu-ds-giai.typ`. + +### Hình ở LỜI GIẢI và trong PHƯƠNG ÁN/Ý (mọi dạng câu) + +Hình không chỉ đặt ở đề bài — cả 8 dạng (`vd/tn/ds/tln/tl/hd/lt/vdtt`) đều +nhận thêm: + +```typst +fig-giai: do-thi-ham(...), // hình kèm LỜI GIẢI (bí danh: hinh-giai) +fig-giai-pos: "right", // right | left | top | bottom +fig-giai-width: auto, // auto = ôm hình; hoặc 35%, 5cm... +``` + +Bố cục 2 cột giống hình kèm đề; ở beamer hình hiện cùng bước đầu tiên của +lời giải. Màn `#sang-man` sau muốn có hình thì chèn thẳng trong nội dung màn. + +Hình trong PHƯƠNG ÁN (tn) hoặc Ý (ds): phương án là content nên nhúng trực +tiếp, thường kèm `cols:` cố định: + +```typst +#tn([Đồ thị nào là của $y = x^2$?], ( + [#do-thi-ham(x => x*x*x, w: 3.2cm)], + True([#do-thi-ham(x => x*x, w: 3.2cm)]), + [#do-thi-ham(x => -x*x, w: 3.2cm)], + [#do-thi-ham(x => 2*x, w: 3.2cm)], +), cols: 4, loigiai: [Đồ thị hàm bậc hai $y = x^2$ có đỉnh tại gốc toạ độ, bề lõm hướng lên.]) +``` + +## 11. Giao diện câu hỏi (theme tiền tố) + +- Thẻ "Câu X" và thẻ phương án "A.", ý "a)" dùng **chung một màu và một + hình dáng**; đổi một lệnh là đồng bộ toàn bài (đặt đầu file, hoặc giữa + chừng để đổi từ vị trí đó): + +```typst +#kieu-cau-hoi(mau: rgb("#e67e22"), hinh: "luc-giac", hien-o: true) +// hinh: "bo-tron" (mặc định) | "chu-nhat" | "luc-giac" | "khong-to" +// "khong-to": không tô nền, chỉ viền + chữ màu (tiết kiệm mực); +// riêng hồ sơ DETHI: bỏ luôn viền — "Câu 1." / "A." (ý ds: "a)") +// in đậm cùng màu, dấu liền kề (kiểu LaTeX). +// hien-o: false = ẩn ô tick Đ/S (ds có o-tick) và ô điền "Trả lời" (tln) +// đồng bộ toàn bài — dùng khi HS làm thẳng vào phiếu trả lời. +// om-hinh: false = TẮT chế độ chữ ôm hình (xem ngay dưới), quay về lối +// 2 cột cũ từ vị trí này trở đi. +// eq-trong-dong: false = giữ nguyên công thức khối `$ ... $` trong phương án +// (xem ngay dưới); mặc định true = ép thành công thức trong dòng. +``` + +### Công thức trong phương án luôn nằm TRONG DÒNG (mặc định BẬT) + +Viết `$ cases(x > 0, y > 0, x + y < 3) $` (có khoảng trắng sát hai dấu `$`) +là công thức **trình bày giữa dòng**: Typst tách nó xuống dòng riêng và canh +giữa, nên hệ 3 bất phương trình trông khác hẳn hệ 2 bất phương trình viết +`$cases(...)$`. Từ 08/2026 thư viện tự ép mọi công thức khối trong **phương án +`#tn`** và **ý `#ds`** về dạng trong dòng, nên hai lối viết cho ra cùng một +bố cục — khỏi phải sửa tay từng file do AI sinh ra. + +- Chỉ đụng tới công thức toán; phương án là hình, bảng, danh sách… giữ nguyên. +- Vì công thức không còn là "khối", dấu chấm cuối phương án cũng được thêm + như các phương án chữ (xem `cham-cuoi`). +- Số cột `cot: auto` đo theo bề rộng TRONG DÒNG ⇒ hệ 3 bpt thường xếp đủ 4 cột. +- Tắt toàn bài: `#kieu-cau-hoi(eq-trong-dong: false)`; tắt một câu: + `#tn(..., trong-dong: false)` / `#ds(..., trong-dong: false)`. + +File thử: `thu-eq-trong-dong.typ`. + +### Chữ ôm hình (mặc định BẬT) + +Hình đặt bên phải/trái mà **lời giải dài hơn hình** thì cột hình sẽ trống +một khoảng lớn phía dưới. Từ 08/2026 thư viện tự chữa: đo chiều cao hình, +đặt vừa đủ các **đoạn đầu** cạnh hình, phần còn lại tràn **nguyên bề rộng** +xuống dưới hình — không phải khai báo gì thêm. + +- Chỗ cắt luôn rơi vào ranh giới đoạn văn / công thức tách dòng / bảng / + danh sách, nên không bao giờ cắt ngang một công thức. +- Công thức tách dòng quá rộng so với cột hẹp sẽ được đẩy xuống phần dưới + (tràn hết bề rộng) thay vì bị bó trong cột. +- Nội dung vốn đã thấp hơn hình ⇒ giữ nguyên lối 2 cột canh giữa như cũ. +- Tắt: `#kieu-cau-hoi(om-hinh: false)` (toàn bài hoặc một đoạn), hoặc + `voi-hinh(..., om: false)` khi gọi trực tiếp. + +File thử: `thu-om-hinh.typ`. + +- **MC tự chia cột**: mặc định `cot: auto` — đo phương án dài nhất rồi tự + chọn 4 / 2 / 1 cột; vẫn ép được `cot: 2`. +- **TF hai kiểu**: mặc định nhãn Đ/S hiện sau mỗi ý; thêm `o-tick: true` + để có 2 ô vuông Đ | S dóng thẳng hàng sát lề phải (bản đề thi: ô trống + cho học sinh tick; bản đáp án/trình chiếu: tự tick ✓ xanh/đỏ). + +### Tự thêm dấu chấm cuối phương án / ý hỏi (07/2026) + +Phương án của `#tn` và ý của `#ds` là một CÂU nên phải kết bằng dấu chấm. +Thư viện tự dò và **thêm `.` khi còn thiếu** (bật sẵn), gõ nhanh không cần +nhớ: + +```typst +#tn([...], ([$(-oo; 0)$], True([$(-oo; -2)$]), [$(-2; 0)$], [$(-2; +oo)$])) +// in ra: A (−∞; 0). B (−∞; −2). C (−2; 0). D (−2; +∞). +``` + +Quy tắc dò (duyệt ngược nội dung, tìm ký tự có nghĩa cuối cùng): + +- đã kết bằng `.` `!` `?` `…` `:` `;` → **giữ nguyên**, kể cả khi có dấu + nháy/ngoặc bọc ngoài: `"đúng rồi!"`, `(đã có chấm.)`; +- kết bằng khối — danh sách `-`/`+`, bảng, hình, `\` xuống dòng, công thức + trình bày giữa dòng `$ ... $` → **bỏ qua** (dấu chấm sẽ rơi xuống dòng); +- còn lại (chữ, số, công thức trong dòng, dấu ngoặc đóng) → **thêm `.`** + +Tắt/bật: + +```typst +#kieu-cau-hoi(cham-cuoi: false) // tắt toàn bài (từ vị trí này) +#tn([Đề bài], ([A], True([B]), [C], [D]), cham: false) // tắt riêng một câu +#ds([Đề bài], ([ý a], True([ý b])), cham: true) // ép bật riêng một câu +``` + +File thử: `thu-cham-cau.typ` (đủ 5 nhóm ca). + +### `#cot-item` — không đánh chồng nhãn a), b) (07/2026) + +`#cot-item` TỰ đánh nhãn `a)`, `b)`, `c)`… nên **không cần gõ tay**. Nhưng nếu +nội dung đã có sẵn nhãn (thường gặp khi chép từ SGK hoặc để AI sinh bài), thư +viện tự nhận ra và **bỏ nhãn tự động ở đúng item đó** — hết cảnh `a) a) Phải +quay kim phút…`. + +```typst +#cot-item([Phải quay…], [Có bao nhiêu…]) // in: a) Phải quay… b) Có bao nhiêu… +#cot-item([a) Phải quay…], [b) Có bao nhiêu…]) // in: a) Phải quay… b) Có bao nhiêu… +``` + +Nhãn được nhận: `a)` `a.` `a:` `A)` `(a)` `[b]` `1)` `1.` `(2)` `ii)` `IV.` +(chữ cái đơn, số ≤ 99, số La Mã ≤ xii; sau dấu `.` hoặc `:` phải có khoảng +trắng). Quy tắc quyết định: + +- **mọi** item đều có nhãn tay → bỏ hết nhãn tự động (kể cả khi đánh tiếp + `d) e) f)` cho phần sau); +- chỉ **một vài** item có nhãn tay → chỉ bỏ ở item mà nhãn tay đúng thứ tự của + nó, các item còn lại vẫn được đánh (tránh nhận nhầm câu mở đầu kiểu + `A. B. C thẳng hàng`); +- không nhận nhầm `0.5 lít nước`, `12:30`, `$x^2 - 3x = 0$`, `Cho tam giác…`. + +Ép đánh nhãn dù item đã có sẵn: `#cot-item(..., do-nhan-tay: false)`; +bỏ hẳn nhãn: `kieu-nhan: none`. File thử: `thu-cot-item.typ` (8 nhóm ca). + +## Mẹo + +- Font: mặc định `("Charis SIL", "Noto Serif", "Libertinus Serif", "Times New Roman")`. **Cần CÀI Charis SIL** (miễn phí: software.sil.org/charis) — chưa + cài thì tự rơi về font sau trong danh sách. + Lý do: dấu CHỒNG tiếng Việt (**Ể Ổ Ữ Ẩ Ấ Ộ**). Libertinus/Times vẽ dấu hỏi + nhỏ, nằm NGANG cạnh dấu mũ → nhìn như mất dấu. Noto Serif ổn ở chữ thường + nhưng với chữ HOA (tiêu đề đề thi viết hoa toàn bộ) vẫn thu nhỏ dấu hỏi và + đẩy lệch sang phải. Charis SIL do SIL thiết kế cho ngôn ngữ nhiều dấu chồng, + giữ dấu nguyên cỡ + chồng thẳng ở CẢ chữ hoa lẫn chữ thường. + Đổi font: `#show: bai-giang.with(phong: ("Noto Serif",))`. + Bề rộng Charis SIL ≈ Libertinus nên số trang không đổi (Noto Serif rộng hơn + ~2%, bài dài có thể trôi thêm 1 trang). +- Xuất PowerPoint không hỗ trợ; trình chiếu trực tiếp file PDF (PDF reader + chế độ full screen, hoặc trình chiếu bằng Sumatra/Okular). +- Mỗi hàm vẽ đều có `mau`, `day` (độ dày), `dut` (nét đứt) để tuỳ biến. + +## Giá trị cực trị dạng căn thức (07/2026) + +BBT và lời giải khảo sát (`bbt-bac-ba`, `bbt-trung-phuong`, `bbt-huu-ti`, +`khao-sat-ve-do-thi-ham-*`) nay hiển thị CẢ GIÁ TRỊ cực trị dạng chính xác +(căn thức/phân số) thay vì số thập phân — ví dụ `y = x³ − 4x² − x + 6` cho +`x = (4 ± √19)/3` và `y = (−2 ∓ 38√19)/27`. Tính thẳng từ hệ số (không +nhận dạng số thập phân) nên đúng với mọi mẫu số lớn: + +- `so-can-thuc(P, K, n, Q)` (do-thi.typ): in `(P + K√n)/Q` đã rút gọn + (gcd, rút thừa số chính phương √96 → 4√6, chuẩn hoá n hữu tỉ + √(2/3) → √6/3); hệ số không "đẹp" thì tự rơi về `so-toan` như cũ. +- `cuc-tri-bac-ba(a, b, c, d)` / `cuc-tri-huu-ti(a, b, c, d, e)`: trả + `(x1:, y1:, x2:, y2:)` chính xác (x1 < x2) — dùng thẳng trong bài soạn. + +**Cập nhật:** nhãn trên ĐỒ THỊ cũng chính xác như vậy. Đường gióng cực trị, +đỉnh parabol, điểm uốn, tâm đối xứng, tiệm cận và giao Ox của +`do-thi-bac-hai`, `do-thi-bac-ba`, `do-thi-trung-phuong`, `do-thi-huu-ti`, +`do-thi-phan-thuc` lấy nhãn thẳng từ hệ số (hết cảnh `10.48` — nay là +`283/27`). Muốn ghi khác thì vẫn đặt `cuc-tri: (ten-x: .., ten-y: ..)`. +Ngoài ra `so-toan` nhận thêm phân số mẫu lớn (tới 400, khớp chặt 1e-9) +nên các giá trị như `283/27`, `−13/12` hiện đúng ở mọi chỗ. + +## Trục số, conic, côtang & hình lục giác/chóp cụt (07/2026) + +### Trục số — biểu diễn đoạn/khoảng (`truc-so`) + +Vẽ một hoặc NHIỀU đoạn/khoảng/nửa khoảng trên cùng một trục. Mỗi khoảng là +một tuple `(a, b, kieu)` (hoặc thêm màu: `(a, b, kieu, mau)`): + +- `a`, `b`: số, hoặc `"-oo"` / `"+oo"` cho vô cực (a < b). +- `kieu`: chuỗi 2 kí tự đầu mút — `"[]"` đoạn, `"()"` khoảng, `"[)"` / `"(]"` + nửa khoảng. `[ ]` = lấy đầu mút, `( )` = không lấy. + +Mặc định GẠCH CHÉO phần loại bỏ (bù của hợp các khoảng) đúng kiểu SGK, kèm +ngoặc tại đầu mút. + +```typ +#truc-so((-2, 3, "[)")) // [−2; 3) +#truc-so((-2, 1, "()"), (3, "+oo", "[)")) // hai khoảng, +∞ +#truc-so(("-oo", 0, "(]"), (2, 5, "[]"), dau: "cham") // chấm đặc/rỗng +``` + +Tham số: `w`, `h` (1.5cm — chiều cao khung cố định trên trang), `min`/`max` +(auto), `ten` (nhãn trục, mặc định `$x$`), `mau`, `gach`/`mau-gach`, +`cao-gach` (2mm — chiều cao dải gạch chéo trên trang), `dau: "ngoac" | "cham"`, +`moc-phu:` (ghi thêm số), `so`, `co-chu` (13pt — cỡ chữ số tại mốc). + +### Conic: hyperbol & parabol + +- `hyperbol(a, b)` — vẽ `x²/a² − y²/b² = 1`; kèm sẵn 2 tiệm cận + `y = ±(b/a)x` (nét đứt), đỉnh `A₁A₂`, tiêu điểm `F₁F₂` (c = √(a²+b²)). + Tắt phần phụ bằng `tiem-can: false`, `dinh: false`, `tieu-diem: false`; + đổi tên qua `ten-dinh`, `ten-tieu`. +- `parabol(p)` — vẽ `y² = 2px` (p > 0 mở phải, p < 0 mở trái); kèm đỉnh O, + tiêu điểm `F(p/2; 0)`, đường chuẩn `Δ: x = −p/2` (nét đứt). Tắt qua + `tieu-diem: false`, `duong-chuan: false`. +- `duong-elip(a, b)` — vẽ `x²/a² + y²/b² = 1` (nhập 2 bán trục như hyperbol); + kèm 4 đỉnh `A₁A₂B₁B₂` và 2 tiêu điểm trên trục lớn (a ≥ b: `F(±c; 0)`; + b > a: `F(0; ±c)`, `c = √|a²−b²|`); `a = b` cho đường tròn. Tên là + `duong-elip` (không phải `elip` — `elip` là hàm vẽ elip cơ bản trong `ve.typ`). + +```typ +#duong-elip(4, 2.5) // elip nằm ngang, tiêu điểm trên Ox +#duong-elip(2, 3.2) // elip đứng, tiêu điểm trên Oy +``` + +Nhãn tên hàm nay có **nền trắng** nên đường cong không xuyên qua; vị trí nhãn +(`goc-ten: auto`) tự né đồ thị. Cơ số mũ/lôgarit hiển thị **số đẹp** (phân số, +căn): `#do-thi-mu(1/3)` → `y = (1/3)^x`, `#do-thi-log(1/3)` → `y = log_(1/3) x`. + +```typ +#hyperbol(3, 2) // x²/9 − y²/4 = 1 +#parabol(2) // y² = 4x +#parabol(-1.5) // y² = −3x (mở trái) +``` + +### Đồ thị côtang (`do-thi-cot`) + +`#do-thi-cot()` — `y = cot x`, tiệm cận đứng tại `x = kπ` (nét đứt tại ±π, +riêng x = 0 trùng trục Oy). Cùng bộ tham số như `do-thi-tan` +(`w`, `mau`, `ten`, `goc-ten`, `luoi-o`, `them`). (`do-thi-mu`, `do-thi-log` +đã có sẵn cho `y = aˣ`, `y = logₐx`.) + +### Hình lục giác đều & chóp cụt đều + +Nét khuất (đáy sau, cạnh bên/đứng tới đỉnh sau) TỰ vẽ đứt theo hình chiếu. + +- `hinh-chop-luc-giac-deu()` — chóp lục giác đều `S.ABCDEF`, có đường cao `SO`. +- `hinh-lang-tru-luc-giac-deu()` — lăng trụ lục giác đều `ABCDEF.A'B'C'D'E'F'`. +- `hinh-chop-cut-deu(n: 3 | 4 | 6)` — chóp cụt đều tam/tứ/lục giác. `R` bán + kính đáy dưới, `r` đáy trên, `cao` chiều cao; `truc: true` vẽ trục `OO'`. + Tên đỉnh tự sinh (A, B, … cho đáy dưới; phẩy cho đáy trên) hoặc truyền + `ten:` là mảng 2n nội dung. +- Bí danh gọn (cùng bộ tham số): `hinh-chop-cut-tam-giac-deu()` (ABC.A'B'C'), + `hinh-chop-cut-tu-giac-deu()` (ABCD.A'B'C'D'), + `hinh-chop-cut-luc-giac-deu()` (tên gốc, `n: 6` mặc định). + +```typ +#hinh-chop-luc-giac-deu(w: 6.5cm) +#hinh-lang-tru-luc-giac-deu(w: 6.5cm) +#hinh-chop-cut-tam-giac-deu() +#hinh-chop-cut-tu-giac-deu(truc: true) +#hinh-chop-cut-deu(n: 6) +``` + +### Bảng tần số & biểu đồ thống kê (bang-thong-ke.typ + bieu-do-thong-ke.typ) + +Dữ liệu vào của bảng và biểu đồ KHỚP NHAU: cùng `moc:` (n+1 mốc số → n nhóm) +và `tan-so:` — một nguồn số liệu dùng cho cả bảng lẫn biểu đồ. Mọi biểu đồ +đều có `them: ctx => ...` để vẽ chồng như các hình khác. + +```typ +// 4 bảng tần số bố cục NGANG đúng SGK (don-vi: hiện «tên (đơn vị)» ô góc) +#bang-tan-so(gia-tri: (0, 1, 2, 3), tan-so: (4, 3, 4, 7), ten-gia-tri: [Số con]) +#bang-ghep-nhom(moc: (150, 155, 160, 165, 170, 175), tan-so: (5, 12, 18, 9, 4), + ten-nhom: [Chiều cao], don-vi: [cm]) +// (kèm bang-tan-so-doi / bang-ghep-nhom-doi cho 2 dòng tần số) + +// HISTOGRAM — cột dính sát nhau (tần số ghép nhóm, Toán 11–12) +#bieu-do-tan-so(moc: (150, 155, 160, 165, 170, 175), tan-so: (5, 12, 18, 9, 4), + ten-x: [Chiều cao (cm)]) +#bieu-do-tan-so(moc: (150, 155, 160, 165, 170, 175), tan-so: (5, 12, 18, 9, 4), + gap-khuc: true) // + gấp khúc tần số chồng lên + +// ĐƯỜNG GẤP KHÚC TẦN SỐ đứng riêng (nối trung điểm, kéo dài về 0 hai đầu) +#da-giac-tan-so(moc: (150, 155, 160, 165, 170, 175), tan-so: (5, 12, 18, 9, 4), + keo-dai: true) + +// BIỂU ĐỒ CỘT RỜI — giá trị rời rạc: số (đặt đúng hoành độ) hoặc chữ +#bieu-do-cot(gia-tri: (0, 1, 2, 3, 4), tan-so: (4, 9, 13, 6, 2), ten-x: [Số con]) +#bieu-do-cot(gia-tri: ([Đỏ], [Xanh], [Vàng]), tan-so: (10, 7, 5)) + +// BIỂU ĐỒ HỘP (box plot) — tứ phân vị Toán 10, gióng nét đứt xuống trục số +#bieu-do-hop(du-lieu: (3, 4, 4, 5, 6, 7, 8, 9, 10), ten: [Điểm]) // tự tính +#bieu-do-hop(tom-tat: (150, 158, 163, 168, 175)) // (min, Q1, Q2, Q3, max) + +// BIỂU ĐỒ QUẠT TRÒN — chia góc theo %, bắt đầu 12 giờ, chiều kim đồng hồ +#bieu-do-quat(([Đi bộ], 25), ([Xe đạp], 30), ([Xe máy], 35), ([Khác], 10)) +``` + +Tham số chung biểu đồ cột/gấp khúc: `w`, `h`, `mau`, `vien`, `ten-x`/`ten-y`, +`so-dinh` (số trên đỉnh cột), `so-truc`, `buoc-y` (auto = bước đẹp 1/2/5·10ᵏ), +`luoi-ngang` (lưới ngang mờ), `co-chu`, `them`. Quạt: mỗi mục `(tên, số)` hoặc +`(tên, số, màu)`; `phan-tram: true` ghi % trong quạt (quạt < 8% ghi gộp bên ngoài). + +Hai hàm TRẢ GIÁ TRỊ (không vẽ, dùng thẳng trong lời giải): + +- `tu-phan-vi(du-lieu)` → `(min:, q1:, q2:, q3:, max:)` — SGK Toán 10 + (n lẻ KHÔNG kể Q₂ vào hai nửa). +- `tu-phan-vi-ghep-nhom(moc, tan-so)` → `(q1:, q2:, q3:)` — nội suy SGK Toán 11. + +### Số đặc trưng mẫu số liệu (bieu-do-thong-ke.typ) + +Tất cả TRẢ GIÁ TRỊ (không vẽ) — dùng thẳng trong lời giải, in đẹp bằng +`so-toan`/`so-dep`. KHÔNG ghép nhóm nhận mảng thô HOẶC `gia-tri + tan-so:` +(bảng tần số); GHÉP NHÓM nhận `(moc, tan-so)` như biểu đồ. + +```typ +// KHÔNG ghép nhóm — thô hoặc bảng tần số (thêm tan-so:) +#so-trung-binh((1, 2, 2, 3)) // 2 +#so-trung-binh((0, 1, 2), tan-so: (4, 3, 4)) +#mot((1, 1, 2, 3)) // MẢNG các mốt: (1,) — có thể nhiều mốt +#trung-vi(d) #tu-phan-vi(d) // tu-phan-vi -> (min:, q1:, q2:, q3:, max:) +#phuong-sai(d) #do-lech-chuan(d) // s² chia n; hieu-chinh: true -> chia n−1 +#khoang-bien-thien(d) #khoang-tu-phan-vi(d) + +// GHÉP NHÓM — trung bình/phương sai theo giá trị đại diện, còn lại nội suy SGK +#so-trung-binh-ghep-nhom(moc, tan-so) #mot-ghep-nhom(moc, tan-so) +#trung-vi-ghep-nhom(moc, tan-so) #tu-phan-vi-ghep-nhom(moc, tan-so) +#phuong-sai-ghep-nhom(moc, tan-so) #do-lech-chuan-ghep-nhom(moc, tan-so) +#khoang-bien-thien-ghep-nhom(moc, tan-so) // mốc phải nhóm cuối − mốc trái nhóm đầu CÓ dữ liệu +#khoang-tu-phan-vi-ghep-nhom(moc, tan-so) +``` + +## Nón & trụ có NÉT KHUẤT TỰ ĐỘNG — kể cả hai khối che nhau (08/2026) + +`lib/mat-cong.typ` bổ khuyết cho `da-dien.typ`: engine đa diện chỉ lo được khối +đa diện LỒI và KHÔNG xử lý được hai khối che nhau. Ở đây dùng một cơ chế duy +nhất cho cả hai việc — bắn tia từ điểm đang xét về phía người nhìn, tia gặp lòng +bất kì khối nào (kể cả chính nó) thì đoạn đó vẽ NÉT ĐỨT. + +```typ +// nón có trục là một đường sinh của trụ — đường sinh nón chui vào lòng trụ +// TỰ chuyển sang nét đứt, không phải cắt tay +#mat-cong( + mat-non(r: 2, cao: 4, mau: red), + mat-tru(tam: (0, 2, 0), r: 2, cao: 4, mau: blue), + w: 9cm, +) + +#mat-cong(mat-non(r: 2.6, cao: 4), mat-tru(r: 1.1, cao: 2)) // trụ nội tiếp nón +#mat-cong(mat-non(r: 2, cao: 3.6), hien-khuat: false) // bỏ hẳn nét đứt +``` + +Camera MẶC ĐỊNH của engine này là chiếu TRỰC GIAO +(`chieu-truc-giao(ngang: 15deg, cao: 22deg)`), KHÔNG phải `chieu-xien` như +`da-dien.typ`: khối vẽ ra NGAY NGẮN — elip đáy có trục lớn nằm ngang, trục +khối thẳng đứng, đúng lối hình sách giáo khoa, khỏi phải khai `cam:` gì cả. +Muốn lối chiếu xiên cũ thì khai rõ `cam: chieu-xien()` hoặc `cam: chieu-oxyz()`. + +Mô tả khối (TRẢ GIÁ TRỊ): `mat-non(tam:, r:, cao:, nghieng:, huong:, truc:, +mau:, to:)` và `mat-tru(...)` cùng bộ tham số — `tam` là tâm mặt đáy, `cao` đo +DỌC TRỤC; `mau`/`to` để `auto` thì lấy theo lệnh vẽ. +Tiện ích: `dinh-non(k)`, `tron-ngang(tam, r, truc:)`. + +**Trục khối đặt nghiêng được.** `nghieng` là góc giữa trục khối và `Oz`, +`huong` là hướng ngả (đo trong mặt phẳng `Oxy`); hoặc ghi đè thẳng bằng +`truc: (a, b, c)`. Mặt đáy LUÔN vuông góc với trục, nên trục nghiêng thì mặt +đáy và đường sinh nghiêng cùng — toàn thân khối nghiêng: + +```typ +#mat-cong(mat-tru(r: 1.2, cao: 5, nghieng: 55deg, huong: 200deg)) // trụ nằm nghiêng +#mat-cong(mat-non(r: 1.6, cao: 4.5, nghieng: 90deg)) // nón nằm ngang +``` + +Mọi công thức trong engine viết theo KHUNG RIÊNG `(u, v, w)` của khối (`w` là +trục, `(u, v)` là hai phương trong mặt đáy) nên trục đứng chỉ là trường hợp +riêng `nghieng: 0deg`. + +`ve-mat-cong(..khoi, cam:, mau:, day:, to:, hien-khuat:, mau-khuat:, day-khuat:, +n:, duong:, them:)` nhận `ctx`; `mat-cong(...)` tự tạo khung hình, mọi +tuỳ chọn khác chảy thẳng xuống `ve-mat-cong`. + +Đường vẽ thêm cũng được tự chia liền/đứt — mỗi mục của `duong:` là một dict: + +```typ +#mat-cong( + mat-non(r: 2, cao: 4), mat-tru(tam: (0, 2, 0), r: 2, cao: 4), + duong: ((pts: tron-ngang((0, 0, 1.6), 1.2), mau: green, day: 0.9pt),), +) +``` + +### Hệ trục Oxyz nghiêng CÙNG góc với khối — `truc:` / `ve-truc-3d` + +`oxyz` của `hinh-khong-gian.typ` tự dựng phép chiếu XIÊN riêng. Trong phép chiếu +xiên, đường tròn nằm ngang chiếu ra elip NGHIÊNG trong khi `Oz` vẫn dựng đứng — +đáy nghiêng mà trục đứng, nhìn vô lý. Muốn cả khung hình chung MỘT góc nghiêng +thì dùng camera TRỰC GIAO thật và vẽ hệ trục bằng chính camera đó: + +```typ +#let nghieng = 22deg // góc nhìn từ trên xuống — MỘT biến cho cả khung hình +#let quay = 15deg // xoay quanh trục đứng, chỉnh cho hai khối tách nhau + +#mat-cong( + mat-non(r: 2, cao: 4), mat-tru(tam: (0, 2, 0), r: 2, cao: 4), + cam: chieu-truc-giao(ngang: quay, cao: nghieng), + truc: (x: 2.6, y: 4.4, z: 4.6), // hệ trục vẽ bằng CHÍNH camera đó +) +``` + +Với `chieu-truc-giao`, elip đáy có trục lớn NẰM NGANG (bán trục bé = +`sin(nghieng)`) và trục khối THẲNG ĐỨNG — đúng như nhìn vật thật. +ĐÁNH ĐỔI cần biết: bù lại `Ox` và `Oy` đều chếch xuống, không còn `Oy` nằm +ngang như lối vẽ trục quen thuộc. Không thể có đồng thời cả hai — đó là tính +chất hình học, không phải lỗi. + +`ve-truc-3d(cam, x:, y:, z:, am:, dm:, ten:, ten-goc:, huong-ten:, huong-goc:, +mau:, day:, cach:)` dùng được với BẤT KÌ camera nào (`chieu-xien`, +`chieu-oxyz`, `chieu-truc-giao`). `ve-mat-cong` còn có `truoc:` (vẽ trước khối) +song song với `them:` (vẽ sau khối). + +Nếu vẫn muốn dùng `oxyz` sẵn có thì `k` và `goc` của `oxyz` PHẢI trùng với +`chieu-oxyz(k:, goc:)`, nếu không khối sẽ lệch khỏi hệ trục: + +```typ +#oxyz(x: 2.6, y: 4.4, z: 4.6, k: 0.7, goc: 30deg, them: (ctx, t3) => { + ve-mat-cong(ctx, mat-non(r: 2, cao: 4), mat-tru(tam: (0, 2, 0), r: 2, cao: 4), + cam: chieu-oxyz(k: 0.7, goc: 30deg)) +}) +``` + +Phép thử che khuất là GIẢI TÍCH (giao tia với mặt đáy là bậc nhất, với mặt bên +là bậc hai), KHÔNG quét mẫu — nên không bỏ sót nét khuất dù dây cung qua khối +rất ngắn. Sai số duy nhất còn lại nằm ở mắt lưới của đường được vẽ, chỉnh bằng +`n:`. + +GIỚI HẠN, đọc trước khi dùng: chưa có mặt cầu; đường sinh biên CỐ Ý không để +chính khối của nó che (tia bắn từ đúng đường biên là tiếp tuyến, xét ở đó sẽ +chập chờn làm nét biên lúc liền lúc đứt). + +## Engine đa diện tổng quát, mặt phẳng Oxyz & thiết diện (07/2026) + +`lib/da-dien.typ` — nhập DANH SÁCH ĐỈNH 3D + DANH SÁCH MẶT là vẽ được bất kì +khối đa diện LỒI: tự chiếu, tự phân biệt mặt trước / mặt sau, tự vẽ nét liền và +nét khuất, tự tô mặt theo độ sâu. Thứ tự đỉnh trong mỗi mặt KHÔNG cần theo +chiều nào (pháp tuyến được lật ra ngoài theo tâm khối). + +```typ +// khối dựng sẵn — bung bằng toán tử `..` (trả (dinh:, mat:, ten:)) +#da-dien(..khoi-chop-deu(n: 4, R: 1.9, cao: 3.6), to: blue.lighten(86%)) +#da-dien(..khoi-lang-tru-deu(n: 6, R: 1.8, cao: 3.4)) +#da-dien(..khoi-chop-cut-deu(n: 4, R: 2, r: 1.1, cao: 3)) +#da-dien(..khoi-hop-chu-nhat(dai: 4, rong: 2.6, cao: 3)) +#da-dien(..khoi-lap-phuong(a: 3)) +#da-dien(..khoi-tu-dien-deu(a: 3)) +#da-dien(..khoi-bat-dien-deu(a: 3)) +#da-dien(..khoi-hop((0,0,0), (3,0,0), (0.6,2.2,0), (0.9,0.4,2.8))) // hộp lệch +#da-dien(..khoi-chop(day, S)) // đáy = mảng điểm 3D bất kì +#da-dien(..khoi-lang-tru(day, v)) // đáy tịnh tiến theo vectơ v +#da-dien(..khoi-chop-cut(day, day-tren)) // hai đáy cùng số đỉnh + +// tự khai đỉnh + mặt (mỗi mặt là mảng CHỈ SỐ đỉnh) +#da-dien( + dinh: ((0,0,0), (3.2,0,0), (0,3,0), (0,0,3)), + mat: ((0,1,2), (0,1,3), (0,2,3), (1,2,3)), + ten: ($O$, $A$, $B$, $C$), to: blue.lighten(88%), +) +``` + +Tham số của `da-dien` / `ve-da-dien`: `ten` (mảng nhãn theo thứ tự đỉnh), +`huong` (auto = tự đặt nhãn vào KHE GÓC RỘNG NHẤT giữa các cạnh nên không đè +lên nét), `to` (màu tô các mặt trước), `to-mat` (mảng màu riêng từng mặt, +`auto` = theo `to`), `hien-khuat: false` (bỏ nét khuất, ẩn luôn đỉnh chỉ thuộc +mặt sau), `mau-khuat` / `day-khuat`, `hien-dinh`, `bk`, `w`, `le`, +`them: (ctx, p) => ...` (`p` chiếu `(x, y, z)` → điểm 2D, dùng được với MỌI hàm +vẽ phẳng). Bản `ve-da-dien(ctx, ...)` vẽ vào khung có sẵn, `da-dien(...)` tự +tạo khung. + +**Camera** — `cam:` nhận một trong: + +```typ +cam: chieu-xien(goc: 40deg, k: 0.5) // mặc định, khớp hình dựng sẵn +cam: chieu-truc-giao(ngang: 65deg, cao: 22deg) // camera Euler thật +cam: chieu-oxyz(k: 0.55, goc: 35deg) // khớp hệ trục #oxyz +cam: P => (P.at(1), P.at(2)) // hoặc hàm chiếu tuỳ ý +``` + +Hướng nhìn (để biết mặt nào là mặt trước) được SUY RA từ ma trận chiếu, nên +hàm chiếu tuỳ ý cũng phân biệt nét khuất đúng. + +**Mặt phẳng trong Oxyz** — gọi trong `oxyz(them: (ctx, t3) => ...)`; phần trục +toạ độ nằm SAU mặt phẳng TỰ vẽ đứt: + +```typ +#oxyz(x: 5, y: 6, z: 5, them: (ctx, t3) => { + // x/4 + y/5 + z/3.5 = 1, cắt ba trục tại A(4,0,0), B(0,5,0), C(0,0,3.5) + mat-phang-oxyz(ctx, t3, 4, 5, 3.5, ten-dinh: true, ten: $(P)$) + // mặt phẳng lơ lửng: tâm + hai nửa-vectơ (4 đỉnh = tam ± u ± v) + mat-phang-bh(ctx, t3, (2.2, 2.8, 2.6), (2.4, 0, 0), (0, 2.6, 0), + ten: $(alpha)$, truc: ((0, 5), (0, 6), (0, 5))) +}) +``` + +`truc:` khai phạm vi ba trục để xử lí nét đứt (`auto` của `mat-phang-oxyz` = +từ gốc tới mốc cắt trục; `none` = không xử lí). + +**Thiết diện khối lồi**: + +```typ +#let ch = khoi-chop-deu(n: 4, R: 1.9, cao: 3.6) +#da-dien-thiet-dien(..ch, mp: mp-qua-3-diem(A, M, N), td: (mau: red)) +// mặt phẳng: mp-qua-3-diem(A, B, C) · mp-qua-phap(P, n) · mp-cat-truc(a, b, c) +// mp-song-song(mp, P) +// thiet-dien(dinh, mat, mp) TRẢ VỀ mảng điểm 3D đã sắp theo VÒNG +// ve-thiet-dien(ctx, dinh:, mat:, mp:) vẽ vào khung có sẵn +``` + +Cạnh thiết diện nằm trên mặt THẤY vẽ liền, nằm trên mặt KHUẤT vẽ đứt. Mặt +phẳng đi qua đỉnh của khối vẫn xử lí đúng. + +Vectơ 3D để dựng điểm: `v3-cong`, `v3-tru`, `v3-nhan`, `v3-vo-huong`, +`v3-co-huong`, `v3-dai`, `v3-chuan`, `trung-diem-3d(A, B)`, `chia-3d(A, B, t)`, +`tam-3d(ds)`, `day-deu(n, R, z: 0)`, `phap-da-giac(ds)`, `phan-tich-khoi(dinh, mat)`. + +**Điểm phụ và đoạn phụ trong khối** — `diem:` và `duong:` (có ở cả `da-dien`, +`ve-da-dien` và `da-dien-thiet-dien`): + +```typ +#let cd = khoi-chop-deu(n: 4, R: 1.9, cao: 3.6) +#let (A, B, C, D, S) = cd.dinh +#let O = tam-3d((A, B, C, D)) // tâm đáy +#let M = trung-diem-3d(S, B) // trung điểm SB +#let N = diem-canh(cd.dinh, 2, 4, t: 0.6) // trên cạnh nối đỉnh #2 với #4 +#let H = hinh-chieu-3d(A, S, C) // chân đường vuông góc từ A xuống SC + +#da-dien( + ..cd, to: blue.lighten(92%), + diem: ((O, $O$), (M, $M$), (N, $N$), (H, $H$, auto, red)), + duong: ( + (S, O, (mau: red, vuong: A, ten: $h$, tai: 0.22, huong: "right")), + (A, H, (mau: red)), + (A, M), + ), +) +``` + +Mỗi mục của `diem:` là `P` · `(P, ten)` · `(P, ten, huong)` · `(P, ten, huong, mau)`. Điểm nằm **trên một cạnh** của khối thì nhãn TỰ đặt vuông góc với cạnh +đó (đúng lối sách); điểm khác thì nhãn toả ra xa tâm hình chiếu. + +Mỗi mục của `duong:` là `(A, B)` hoặc `(A, B, tuỳ-chọn)`, tuỳ-chọn là dict nhận +`mau`, `day`, `ten`, `tai`, `huong`, `cach`, `dut` (ép kiểu nét), `hien-khuat`, +`mau-khuat`, `day-khuat`, `vuong` (một điểm $C$ — vẽ ký hiệu góc vuông tại $B$, +giữa $BA$ và $BC$) và `r` (cỡ ký hiệu). Phần đoạn **bị khối che tự vẽ đứt**, +phần thấy vẽ liền — một đoạn có thể vừa liền vừa đứt (đường cao $SO$ thấy ở +ngoài rồi chui vào trong khối). Phép kiểm là CHÍNH XÁC, không quét mẫu: xét tia +từ điểm về phía người nhìn có cắt khối lồi hay không. + +Với `hien-khuat: false`, điểm bị khối che, ký hiệu góc vuông và nhãn nằm ở chỗ +bị che đều được ẩn theo. + +Tiện ích dựng điểm: `diem-canh(dinh, i, j, t: 0.5)` (chia cạnh nối hai đỉnh), +`hinh-chieu-3d(P, A, B)` (chân đường vuông góc hạ xuống đường thẳng $AB$), +`hinh-chieu-mp(P, mp)` (hình chiếu lên mặt phẳng), `tam-3d(ds)` (tâm của mảng +điểm — dùng lấy tâm đáy). + +GIỚI HẠN: quy tắc nét khuất chỉ đúng với khối LỒI (đủ cho toàn bộ THPT). Khối +KHÔNG lồi, hai khối che nhau, hoặc mặt cong (cầu/trụ/nón) thì dùng các hàm cũ +trong `hinh-khong-gian.typ`. File thử: `thu-da-dien.typ`. + +## Tính toán trong không gian Oxyz (`lib/oxyz-toan.typ`) (07/2026) + +Nhóm hàm **tính** (không vẽ): mọi hàm trả về **giá trị** (số, điểm, dict) hoặc +**nội dung toán đã định dạng** (nhóm `pt-*` và `hien-*`), gọi thẳng, không cần +`ctx`. + +Kiểu dữ liệu dùng chung: + +| Đối tượng | Cách viết | +| --------------- | ---------------------------------------------------- | +| điểm / vectơ | `(x, y, z)` | +| mặt phẳng | `(a: , b: , c: , d: )` ⇔ `ax + by + cz + d = 0` | +| đường thẳng | `(P: điểm đi qua, u: vectơ chỉ phương)` | +| mặt cầu | `(I: tâm, R: bán kính)` | + +Các hàm nhận mặt phẳng cũng **nhận luôn dạng `(n: , d: )`** của `da-dien.typ` +(`mp-qua-3-diem`, `mp-cat-truc`…), nên hai nhóm dùng lẫn được. + +### Vectơ + +```typst +#let A = (1, 2, -1) +#let B = (2, -1, 3) +#let C = (-4, 7, 5) + +vecto-3d(A, B) // toạ độ AB = B − A → (1, -3, 4) +do-dai-vecto(vecto-3d(A, B)) // độ dài (số thực) +tich-vo-huong(u, v) // u · v +tich-co-huong(u, v) // [u, v] +tich-hon-tap(u, v, w) // [u, v] · w (định thức cấp ba) +cos-goc-vecto(u, v) · goc-vecto(u, v) // số thực · kiểu angle +cung-phuong(u, v) · vuong-goc(u, v) · dong-phang(u, v, w) // true/false +``` + +Các phép cộng/trừ/nhân số có sẵn từ `da-dien.typ`: `v3-cong`, `v3-tru`, +`v3-nhan`, `v3-chuan`, `v3-dai`. + +### Điểm — khoảng cách, điểm đặc biệt của tam giác + +```typst +khoang-cach-3d(A, B) // = do-dai-doan-3d(A, B) +trung-diem-3d(A, B) · chia-3d(A, B, t) +trong-tam-3d(A, B, C) // nhận số điểm tuỳ ý +truc-tam-3d(A, B, C) // hệ thức Euler H = A + B + C − 2O +tam-ngoai-tiep-3d(A, B, C) · ban-kinh-ngoai-tiep-3d(A, B, C) +tam-noi-tiep-3d(A, B, C) · ban-kinh-noi-tiep-3d(A, B, C) +tam-bang-tiep-3d(A, B, C) // bàng tiếp TRONG GÓC A +``` + +### Diện tích — thể tích + +```typst +dien-tich-tam-giac-3d(A, B, C) dien-tich-hbh-3d(A, B, C) +the-tich-tu-dien(A, B, C, D) the-tich-hinh-hop(A, B, C, D) +bon-diem-dong-phang(A, B, C, D) +``` + +### Mặt phẳng + +```typst +mat-phang-qua-phap(A, (2, -3, 1)) // qua 1 điểm + 1 vectơ pháp tuyến +mat-phang-qua-3-diem(A, B, C) +mat-phang-doan-chan(p, q, r) // x/p + y/q + z/r = 1 +mat-phang-trung-truc(A, B) +mat-phang-song-song(mp, A) // qua A, song song mp +mat-phang-vuong-goc-duong(A, d) // qua A, vuông góc đường thẳng d +mat-phang-qua-2-phuong(A, u, v) + +#pt-mat-phang(mat-phang-qua-3-diem(A, B, C)) // $19x + 13y + 5z − 40 = 0$ +``` + +Hệ số **tự rút gọn về nguyên tố cùng nhau**, hệ số khác 0 đầu tiên mang dấu +dương. Đổi tên ẩn: `pt-mat-phang(P, an: ("X", "Y", "Z"))`. + +```typst +phap-tuyen-mp(P) the-vao-mp(A, P) // ax₀+by₀+cz₀+d +khoang-cach-diem-mp(A, P) goc-2-mp(P, Q) +hinh-chieu-len-mp(A, P) doi-xung-qua-mp(A, P) +vi-tri-2-mp(P, Q) // "trùng nhau" | "song song" | "cắt nhau" +``` + +### Đường thẳng + +```typst +#let d1 = duong-thang-qua-2-diem(A, B) +#let d2 = duong-thang-qua-vtcp((1, 2, 3), (2, 0, -1)) +duong-thang-vuong-goc-mp(A, P) + +#pt-tham-so(d1) // hệ cases(x = …, y = …, z = …), đổi tham số: t: "k" +#pt-chinh-tac(d1) // thành phần chỉ phương bằng 0 tự tách ra sau dấu phẩy +``` + +```typst +khoang-cach-diem-duong(A, d) khoang-cach-2-duong(d1, d2) +hinh-chieu-len-duong(A, d) doi-xung-qua-duong(A, d) +goc-2-duong(d1, d2) goc-duong-mp(d, P) +vi-tri-2-duong(d1, d2) // "trùng nhau" | "song song" | "cắt nhau" | "chéo nhau" +giao-2-duong(d1, d2) giao-duong-mp(d, P) giao-2-mp(P, Q) // none nếu không có +vtcp-dep(u) // rút gọn vectơ chỉ phương về nguyên, dấu đầu dương +``` + +### Mặt cầu + +```typst +#let S = mat-cau((1, -2, 3), 5) +mat-cau-duong-kinh(A, B) mat-cau-tam-tiep-xuc-mp(I, P) +mat-cau-qua-4-diem(A, B, C, D) // đồng phẳng → none + +#pt-mat-cau(S) // $(x−1)² + (y+2)² + (z−3)² = 25$ +#pt-mat-cau-khai-trien(S) // $x² + y² + z² − 2x + 4y − 6z − 11 = 0$ + +vi-tri-mp-mat-cau(P, S) // "cắt nhau" | "tiếp xúc" | "không cắt" +duong-tron-giao(P, S) // (I: tâm, R: bán kính) đường tròn giao tuyến +``` + +### Hiển thị — chính xác hay gần đúng + +```typst +#hien-diem(A, ten: "A") // $A(1; 2; −1)$ +#hien-vecto(u, ten: "AB") // $arrow(A B) = (1; −3; 4)$ +#hien-so(v) // nguyên / phân số CHÍNH XÁC, nếu không thì thập phân +#hien-gan-dung(v, chu-so: 2) // giá trị gần đúng +#hien-goc(goc-2-mp(P, Q)) // 77.7° +#hien-can(4030) // căn thức đã rút thừa số chính phương +``` + +Các đại lượng chứa căn có bản **chính xác** riêng (không làm tròn): +`hien-do-dai(u)`, `hien-khoang-cach(A, B)`, `hien-cos-goc(u, v)`, +`hien-khoang-cach-diem-mp(A, P)`, `hien-khoang-cach-diem-duong(A, d)`, +`hien-khoang-cach-2-duong(d1, d2)`, `hien-dien-tich-tam-giac(A, B, C)`, +`hien-dien-tich-hbh(A, B, C)`, `hien-the-tich-tu-dien(A, B, C, D)`. + +> `hien-so` chỉ nhận dạng số nguyên và phân số nên **luôn đúng**; muốn ra căn +> thức thì dùng đúng hàm `hien-*` ở trên. (`so-toan` của `do-thi.typ` có đoán +> thêm dạng căn nên với số vô tỉ bất kì có thể ra một dạng "đẹp" mà sai.) + +### Tính xong thì vẽ + +```typst +#oxyz(w: 8cm, x: 4.4, y: 5.4, z: 4.4, them: (ctx, t3) => { + let Q = mat-phang-qua-phap((1, 1.2, 1.4), (1, 1, 1)) + let (T, u, v) = khung-mp(Q, tam: (1, 1.2, 1.4), r: 2.5) // khung để vẽ + mat-phang-bh(ctx, t3, T, u, v, ten: $(alpha)$) + let H = hinh-chieu-len-mp((0, 0, 0), Q) + doan-oxyz(ctx, t3, (0, 0, 0), H, mau: red, dut: true) +}) +``` + +`khung-mp(mp, tam: auto, r: 2)` trả `(tâm, u, v)` để đưa thẳng vào +`mat-phang-bh`; `mp-de-ve(mp)` đổi mặt phẳng sang dạng `(n: , d: )` của +`da-dien.typ`. Mặt phẳng dạng đoạn chắn vẽ bằng `mat-phang-oxyz(ctx, t3, p, q, r)`. + +File thử: `thu-oxyz.typ`. diff --git a/packages/preview/conic-toan/0.3.4/assets/exam-page.png b/packages/preview/conic-toan/0.3.4/assets/exam-page.png new file mode 100644 index 0000000000..b350b94585 Binary files /dev/null and b/packages/preview/conic-toan/0.3.4/assets/exam-page.png differ diff --git a/packages/preview/conic-toan/0.3.4/assets/feasible-region.png b/packages/preview/conic-toan/0.3.4/assets/feasible-region.png new file mode 100644 index 0000000000..358dfc4121 Binary files /dev/null and b/packages/preview/conic-toan/0.3.4/assets/feasible-region.png differ diff --git a/packages/preview/conic-toan/0.3.4/assets/graphs.png b/packages/preview/conic-toan/0.3.4/assets/graphs.png new file mode 100644 index 0000000000..29c4e98d5b Binary files /dev/null and b/packages/preview/conic-toan/0.3.4/assets/graphs.png differ diff --git a/packages/preview/conic-toan/0.3.4/assets/plane-geometry.png b/packages/preview/conic-toan/0.3.4/assets/plane-geometry.png new file mode 100644 index 0000000000..7a0711bde7 Binary files /dev/null and b/packages/preview/conic-toan/0.3.4/assets/plane-geometry.png differ diff --git a/packages/preview/conic-toan/0.3.4/assets/slide.png b/packages/preview/conic-toan/0.3.4/assets/slide.png new file mode 100644 index 0000000000..d33ed099d2 Binary files /dev/null and b/packages/preview/conic-toan/0.3.4/assets/slide.png differ diff --git a/packages/preview/conic-toan/0.3.4/assets/solid-geometry.png b/packages/preview/conic-toan/0.3.4/assets/solid-geometry.png new file mode 100644 index 0000000000..3e455b2fa4 Binary files /dev/null and b/packages/preview/conic-toan/0.3.4/assets/solid-geometry.png differ diff --git a/packages/preview/conic-toan/0.3.4/assets/solid-of-revolution.png b/packages/preview/conic-toan/0.3.4/assets/solid-of-revolution.png new file mode 100644 index 0000000000..131c8962f8 Binary files /dev/null and b/packages/preview/conic-toan/0.3.4/assets/solid-of-revolution.png differ diff --git a/packages/preview/conic-toan/0.3.4/assets/statistics.png b/packages/preview/conic-toan/0.3.4/assets/statistics.png new file mode 100644 index 0000000000..9df2479964 Binary files /dev/null and b/packages/preview/conic-toan/0.3.4/assets/statistics.png differ diff --git a/packages/preview/conic-toan/0.3.4/assets/variation-tables.png b/packages/preview/conic-toan/0.3.4/assets/variation-tables.png new file mode 100644 index 0000000000..037f3c6c39 Binary files /dev/null and b/packages/preview/conic-toan/0.3.4/assets/variation-tables.png differ diff --git a/packages/preview/conic-toan/0.3.4/baigiang.typ b/packages/preview/conic-toan/0.3.4/baigiang.typ new file mode 100644 index 0000000000..ac8fe89fd9 --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/baigiang.typ @@ -0,0 +1,569 @@ +// ===================================================================== +// baigiang.typ — NHẬP MỘT FILE DUY NHẤT NÀY LÀ DÙNG ĐƯỢC TẤT CẢ +// #import "baigiang.typ": * +// ===================================================================== +#import "lib/ve.typ": * +#import "lib/hinh-phang.typ": * +#import "lib/hinh-khong-gian.typ": * +#import "lib/da-dien.typ": * +#import "lib/mat-cong.typ": * +#import "lib/oxyz-toan.typ": * +#import "lib/do-thi.typ": * +#import "lib/so-do-cay.typ": * +#import "lib/bang.typ": * +#import "lib/khao-sat.typ": * +#import "lib/bang-thong-ke.typ": * +#import "lib/bieu-do-thong-ke.typ": * +#import "lib/tron-xoay.typ": * +#import "lib/slide.typ": * +#import "lib/cau-hoi.typ": * +#import "lib/che-do.typ": * + +// ===================================================================== +// ve-voi(ctx) — BỘ HÀM VẼ ĐÃ GẮN SẴN ctx (đỡ phải gõ ctx mỗi lệnh) +// Khai báo MỘT dòng ở đầu khối `them:` rồi gọi hàm như bình thường, +// bỏ tham số ctx đầu tiên: +// +// them: ctx => { +// let (doan, diem, nhan, ve-ham) = ve-voi(ctx) // lấy hàm nào kê tên hàm đó +// doan(M, N, mau: blue, dut: true) +// ve-ham(g, mau: red) +// diem(A, mau: red, ten: $A$) +// } +// +// (Typst KHÔNG cho gọi `v.doan(M, N)` sau `let v = ve-voi(ctx)` — báo +// "cannot directly call dictionary keys as functions"; muốn giữ một biến thì +// phải bọc ngoặc `(v.doan)(M, N)`. Vậy nên cách kê tên ở trên là gọn nhất.) +// +// Cách gọi cũ `doan(ctx, M, N)` vẫn dùng được bình thường (không đổi gì). +// LƯU Ý: các hàm KHÔNG cần ctx (trung-diem, chia, hinh-chieu, giao-ham, +// ham-qua-2-diem, so-toan, mien-tron, giao/hop/bu...) cứ gọi thẳng như cũ; +// đổi khung nhìn (ctx-quay, ctx-tinh-tien) thì lấy bộ hàm mới: +// let (doan, diem) = ve-voi(ctx-quay(ctx, 30deg)) +// ===================================================================== +#let ve-voi(ctx) = ( + // ve.typ — nguyên thuỷ + toa: (..a) => toa(ctx, ..a), + toa-pt: (..a) => toa-pt(ctx, ..a), + toa-nguoc: (..a) => toa-nguoc(ctx, ..a), + goc-truc: (..a) => goc-truc(ctx, ..a), + dau-mui-ten: (..a) => dau-mui-ten(ctx, ..a), + doan: (..a) => doan(ctx, ..a), + cac-doan: (..a) => cac-doan(ctx, ..a), + nhan: (..a) => nhan(ctx, ..a), + nhan-goc: (..a) => nhan-goc(ctx, ..a), + diem: (..a) => diem(ctx, ..a), + cac-diem: (..a) => cac-diem(ctx, ..a), + mui-ten: (..a) => mui-ten(ctx, ..a), + mui-ten-2-dau: (..a) => mui-ten-2-dau(ctx, ..a), + vecto: (..a) => vecto(ctx, ..a), + duong-cong: (..a) => duong-cong(ctx, ..a), + duong-gap-khuc: (..a) => duong-gap-khuc(ctx, ..a), + da-giac: (..a) => da-giac(ctx, ..a), + duong-tron: (..a) => duong-tron(ctx, ..a), + elip: (..a) => elip(ctx, ..a), + cung: (..a) => cung(ctx, ..a), + cung-elip: (..a) => cung-elip(ctx, ..a), + xoan-oc: (..a) => xoan-oc(ctx, ..a), + goc-luong-giac: (..a) => goc-luong-giac(ctx, ..a), + goc: (..a) => goc(ctx, ..a), + goc-vuong: (..a) => goc-vuong(ctx, ..a), + ve-goc: (..a) => ve-goc(ctx, ..a), // lối TikZ: đỉnh ở GIỮA + ve-goc-vuong: (..a) => ve-goc-vuong(ctx, ..a), // lối TikZ: đỉnh ở GIỮA + danh-dau: (..a) => danh-dau(ctx, ..a), + ve-ham: (..a) => ve-ham(ctx, ..a), + to-vung: (..a) => to-vung(ctx, ..a), + to-vung-2-ham: (..a) => to-vung-2-ham(ctx, ..a), + gach-mien: (..a) => gach-mien(ctx, ..a), + gach-vung: (..a) => gach-vung(ctx, ..a), + ve-mien: (..a) => ve-mien(ctx, ..a), + duong-luon: (..a) => duong-luon(ctx, ..a), + nhan-cong: (..a) => nhan-cong(ctx, ..a), + // do-thi.typ — hệ trục & gióng + truc: (..a) => truc(ctx, ..a), + luoi: (..a) => luoi(ctx, ..a), + vach-chia: (..a) => vach-chia(ctx, ..a), + he-truc: (..a) => he-truc(ctx, ..a), + giong: (..a) => giong(ctx, ..a), + tiep-tuyen: (..a) => tiep-tuyen(ctx, ..a), + nhan-pi: (..a) => nhan-pi(ctx, ..a), + // hinh-phang.typ + da-giac-ten: (..a) => da-giac-ten(ctx, ..a), + tam-giac: (..a) => tam-giac(ctx, ..a), + duong-cao: (..a) => duong-cao(ctx, ..a), + trung-tuyen: (..a) => trung-tuyen(ctx, ..a), + phan-giac: (..a) => phan-giac(ctx, ..a), + trung-truc: (..a) => trung-truc(ctx, ..a), + tam-giac-deu: (..a) => tam-giac-deu(ctx, ..a), + tam-giac-vuong: (..a) => tam-giac-vuong(ctx, ..a), + tam-giac-can: (..a) => tam-giac-can(ctx, ..a), + tam-giac-vuong-can: (..a) => tam-giac-vuong-can(ctx, ..a), + tu-giac: (..a) => tu-giac(ctx, ..a), + hinh-binh-hanh: (..a) => hinh-binh-hanh(ctx, ..a), + hinh-chu-nhat: (..a) => hinh-chu-nhat(ctx, ..a), + hinh-thang: (..a) => hinh-thang(ctx, ..a), + duong-tron-ngoai-tiep: (..a) => duong-tron-ngoai-tiep(ctx, ..a), + duong-tron-noi-tiep: (..a) => duong-tron-noi-tiep(ctx, ..a), + duong-tron-bang-tiep: (..a) => duong-tron-bang-tiep(ctx, ..a), + ve-truc-tam: (..a) => ve-truc-tam(ctx, ..a), + tiep-tuyen-tu-diem: (..a) => tiep-tuyen-tu-diem(ctx, ..a), + tiep-tuyen-tai-diem: (..a) => tiep-tuyen-tai-diem(ctx, ..a), + // tron-xoay.typ + ve-khoi-xoay: (..a) => ve-khoi-xoay(ctx, ..a), + ve-mien-xoay: (..a) => ve-mien-xoay(ctx, ..a), + // hinh-khong-gian.typ — Oxyz + diem-oxyz: (..a) => diem-oxyz(ctx, ..a), + doan-oxyz: (..a) => doan-oxyz(ctx, ..a), + vecto-oxyz: (..a) => vecto-oxyz(ctx, ..a), + giong-oxyz: (..a) => giong-oxyz(ctx, ..a), + // da-dien.typ — engine đa diện tổng quát, mặt phẳng, thiết diện + // (da-dien / da-dien-thiet-dien TỰ tạo khung nên không kê ở đây) + ve-da-dien: (..a) => ve-da-dien(ctx, ..a), + mat-phang: (..a) => mat-phang(ctx, ..a), + mat-phang-oxyz: (..a) => mat-phang-oxyz(ctx, ..a), + mat-phang-bh: (..a) => mat-phang-bh(ctx, ..a), + ve-thiet-dien: (..a) => ve-thiet-dien(ctx, ..a), + // mat-cong.typ — nón/trụ có nét khuất tự động (mat-cong tự tạo khung, không kê) + ve-mat-cong: (..a) => ve-mat-cong(ctx, ..a), + ve-truc-3d: (..a) => ve-truc-3d(ctx, ..a), +) + + +// ===================================================================== +// ctx NGẦM — gọi hàm vẽ KHÔNG cần truyền ctx (07/2026) +// Trong thân #hinh(...) hoặc trong `them: ctx => ...` của mọi hình/đồ thị +// dựng sẵn, cứ gọi thẳng: +// +// them: ctx => { +// doan(M, N, mau: blue, dut: true) +// ve-ham(g, mau: red) +// diem(A, mau: red, ten: $A$) +// } +// +// Cách CŨ `doan(ctx, M, N)` vẫn chạy y nguyên (đối số đầu là ctx thì gọi +// thẳng) — bài soạn cũ không phải sửa gì. +// NGOẠI LỆ vẫn phải truyền ctx: các hàm TRẢ VỀ GIÁ TRỊ, không vẽ — +// toa, toa-pt, toa-nguoc, goc-truc, ctx-quay, ctx-tinh-tien. +// (Bên trong lib các hàm vẫn gọi nhau theo lối cũ nên không ảnh hưởng.) +// ===================================================================== +// ve.typ — nguyên thuỷ vẽ +/// `doan(A, B, mau: black, day: 1pt, dut: false, ten: none, tai: 0.5, huong: auto, cach: 6pt, ten-quay: false, mau-ten: auto)` +/// +/// Đoạn thẳng AB, kèm NHÃN CHÚ THÍCH đặt trên đoạn (tuỳ chọn): +/// ten : nội dung nhãn (vd $a$, [3 cm]) — none = không ghi +/// tai : vị trí nhãn theo TỈ LỆ từ A đến B (0 = tại A, 1 = tại B, .5 = giữa) +/// huong : auto = tự đặt VUÔNG GÓC phía trên đoạn (đoạn dọc thì ra trái); +/// hoặc tên hướng ("above", "below", "left", ...), hoặc vectơ (dx, dy) +/// cach : khoảng cách từ đoạn tới nhãn +#let doan = _voi-ctx(doan) +/// `cac-doan(..noi-dung, mau: black, day: 1pt, dut: false, dong: false, to: none, mui-ten: false, ten: none, tai: 0.5, huong: auto, cach: 6pt, ten-quay: false, mau-ten: auto)` +/// +/// Vẽ NHIỀU nét trong một lệnh: điểm liền nhau thành một đường gấp khúc, +/// mỗi MẢNG điểm là một nét riêng — `cac-doan((A, B), (C, D), (M, N))`. +/// Nét cần kiểu riêng thì bọc `duong(...)`. +#let cac-doan = _voi-ctx(cac-doan) +/// `nhan(P, noi-dung, huong: "above", cach: 6pt, mau: black, quay: 0deg)` +/// +/// Nhãn văn bản đặt cạnh điểm P. +/// huong: "above", "below", "left", "right", "above-left", "above-right", +/// "below-left", "below-right", "center" +/// (tên tiếng Việt cũ "tren", "duoi", "trai", "phai", ... vẫn dùng được) +/// quay: góc xoay chữ (vd 90deg, -45deg, hoặc goc-truc(...)). Mặc định 0deg. +/// Đặt chữ/công thức tại điểm P. `huong` là phía đặt chữ so với P +#let nhan = _voi-ctx(nhan) +/// `nhan-goc(..muc, mau: black, ban-kinh: 6pt)` +/// +/// Đặt nhãn cho nhiều điểm, VỊ TRÍ nhãn xác định bằng GÓC LƯỢNG GIÁC (thay cho +/// tên hướng "above"/"below"…). Mỗi mục là một tuple: +/// (P, nội-dung, goc) — bán kính & màu lấy mặc định +/// (P, nội-dung, goc, ban-kinh) — ban-kinh là ĐỘ DÀI trang (vd 8pt) +/// (P, nội-dung, goc, ban-kinh, mau) — kèm màu riêng +/// `goc` là góc lượng giác (số trần = ĐỘ, dương = ngược kim đồng hồ; nhãn đặt +#let nhan-goc = _voi-ctx(nhan-goc) +/// `diem(P, ten: none, huong: "tren", bk: 2pt, mau: black, cach: 6pt, mau-ten: black)` +/// +/// Điểm (chấm tròn) + nhãn tuỳ chọn. +/// cach: khoảng cách nhãn tới điểm · mau-ten: màu chữ (auto = màu chấm). +/// Chấm một điểm, kèm tên: `diem(A, ten: $A$, huong: "below-left")`. +/// Nhiều điểm một lệnh thì dùng `cac-diem`. +#let diem = _voi-ctx(diem) +/// `cac-diem(..noi-dung, mau: black, bk: 2pt, huong: "above", cach: 6pt, mau-ten: black)` +/// +/// Mỗi đối số vị trí là một điểm, viết theo 1 trong 4 dạng: +/// A -> chỉ chấm, không nhãn +/// (A, $A$) -> chấm + nhãn, hướng lấy theo `huong` chung +/// (A, $A$, "left") -> + hướng riêng +/// (A, $A$, "left", red) -> + màu riêng (cả chấm lẫn chữ nếu mau-ten: auto) +/// Tuỳ chọn chung: mau · bk (bán kính chấm) · huong · cach (nhãn cách chấm) +#let cac-diem = _voi-ctx(cac-diem) +/// `mui-ten(A, B, mau: black, day: 1pt, kich: 7pt, dut: false)` +/// +/// Mũi tên A -> B. +#let mui-ten = _voi-ctx(mui-ten) +/// `mui-ten-2-dau(A, B, mau: black, day: 1pt, kich: 7pt, dut: false, ten: none, huong: auto, cach: 5pt, ten-quay: false, mau-ten: auto, trong: auto, dem: 3pt, nen: white, vach: false, dai-vach: 9pt, le: 0pt)` +/// +/// không đủ dài thì tự đặt chữ ra ngoài (phía trên đoạn) +/// huong : chỗ đặt chữ khi KHÔNG nằm giữa thân +/// (auto = vuông góc phía trên đoạn, như `doan`) +/// ten-quay : true = chữ NẰM DỌC theo mũi tên (luôn đọc xuôi) +/// vach : true = kẻ vạch chặn vuông góc ở hai đầu (kiểu ghi kích thước) +/// le : lùi hai đầu mũi tên vào trong, để hở khỏi vật đang đo +#let mui-ten-2-dau = _voi-ctx(mui-ten-2-dau) +/// `vecto(A, B, ten: none, huong: "tren", mau: black, day: 1.1pt, dut: false)` +/// +/// Vectơ A -> B kèm tên đặt ở trung điểm. +/// dut: true = thân nét đứt (vectơ nằm trên cạnh khuất, kiểu hình SGK); +/// đầu mũi tên luôn nét liền. +#let vecto = _voi-ctx(vecto) +/// `duong-cong(cac-diem, mau: black, day: 1pt, dut: false, dong: false)` +/// +/// Đường gấp khúc qua dãy điểm. dut: nét đứt (vẽ cách đoạn -> đều trên đường cong). +#let duong-cong = _voi-ctx(duong-cong) +/// `duong-gap-khuc(cac-diem, mau: black, day: 1pt, dut: false, dong: false)` +/// +/// Đường gấp khúc nối lần lượt A - B - C - ... KHÁC duong-cong ở chỗ: +/// dut: true là nét đứt THẬT trên TỪNG đoạn (hợp polyline ít đoạn; +/// duong-cong đứt kiểu "bỏ đoạn xen kẽ" chỉ hợp đường nhiều mẫu). +/// dong: true nối điểm cuối về điểm đầu. +#let duong-gap-khuc = _voi-ctx(duong-gap-khuc) +/// `da-giac(cac-diem, mau: black, day: 1pt, dut: false, to: none)` +/// +/// Đa giác kín: viền + tô màu tuỳ chọn (to: màu hoặc none). +/// Đa giác khép kín. Đỉnh truyền vào dưới dạng MỘT MẢNG: +/// `da-giac((A, B, C))` — không phải `da-giac(A, B, C)`. `to:` để tô màu. +#let da-giac = _voi-ctx(da-giac) +/// `duong-tron(O, r, mau: black, day: 1pt, dut: false, to: none)` +/// +/// Đường tròn tâm O bán kính r (theo đơn vị trục x; nếu 2 trục cùng tỉ lệ +/// thì là đường tròn thật, khác tỉ lệ sẽ thành elip tương ứng). +#let duong-tron = _voi-ctx(duong-tron) +/// `elip(O, a, b, mau: black, day: 1pt, dut: false, to: none, quay: 0deg, n: 72)` +/// +/// Elip tâm O, bán trục a (ngang), b (dọc); quay: góc xoay quanh tâm +/// (dương = ngược chiều kim đồng hồ), n: số mẫu khi vẽ elip xoay. +#let elip = _voi-ctx(elip) +/// `cung(O, r, tu: 0deg, den: 180deg, mau: black, day: 1pt, dut: false, n: 48, quay: 0deg)` +/// +/// Cung tròn tâm O bán kính r từ góc `tu` đến `den` (kiểu angle, vd 30deg). +#let cung = _voi-ctx(cung) +/// `cung-elip(O, a, b, tu: 0deg, den: 180deg, mau: black, day: 1pt, dut: false, n: 48, quay: 0deg)` +/// +/// Cung elip (dùng cho hình không gian: đáy nón, trụ...). +#let cung-elip = _voi-ctx(cung-elip) +/// `xoan-oc(O, tu: 0deg, den: 360deg, r: 0.12, r-cuoi: auto, buoc: 0.16, mau: black, day: 1pt, dut: false, mui-ten: true, kich: 7pt, n: auto, ten: none, huong: "above", cach: 6pt, mau-ten: auto)` +/// +/// Xoắn ốc Archimedes tâm O: bán kính TĂNG ĐỀU theo góc, quét từ `tu` đến `den`. +/// Góc quét |den - tu| ĐƯỢC PHÉP lớn hơn 360° (nhiều vòng); +/// den > tu = ngược chiều kim đồng hồ (chiều dương), den < tu = cùng chiều kim đồng hồ. +/// r : bán kính tại góc `tu` +/// r-cuoi : bán kính tại góc `den` (auto = r + buoc · số vòng quét) +/// buoc : khoảng cách giữa hai vòng liên tiếp (dùng khi r-cuoi: auto) +#let xoan-oc = _voi-ctx(xoan-oc) +/// `goc-luong-giac(O, A, M, chieu: "duong", vong: 0, r: auto, r-cuoi: auto, buoc: auto, mau: blue, day: 0.9pt, dut: false, kich: 6pt, ten: none, so-do: false, huong: "above", cach: 6pt, mau-ten: auto, n: auto)` +/// +/// Góc lượng giác (OA, OM): xoắn ốc từ tia OA quay tới tia OM (kiểu SGK 11). +/// chieu : "duong" = ngược chiều kim đồng hồ (mặc định) · "am" = cùng chiều +/// vong : số vòng quay THÊM (0, 1, 2, ...) trước khi dừng ở tia OM +/// so-do : true = tự ghi số đo (vd -430°) cạnh mũi tên; `ten` được ưu tiên +/// Ví dụ hình "AOM = 70°, (OA, OM) = -430°": goc-luong-giac(O, A, M, chieu: "am", vong: 1) +#let goc-luong-giac = _voi-ctx(goc-luong-giac) +/// `goc(O, A, B, r: 0.45, ten: none, so-do: false, so-cung: 1, vach: 0, vach-danh-dau: auto, dai-vach: 6pt, to: none, mau: black, day: 0.8pt, cach-nhan: 1.9)` +/// +/// Góc (thường, không vuông) giữa hai tia O->A và O->B: +/// cung nhỏ + nhãn + số cung (1..3) + tô quạt + tự ghi số đo. +/// to : màu tô hình quạt (nên trong suốt, vd rgb(255, 170, 0, 70)) +/// so-do : true = tự ghi số đo góc (vd 60°) khi không đặt `ten` +/// vach : 1..3 vạch CẮT NGANG cung (ký hiệu hai góc bằng nhau, kiểu SGK). +/// Khác `so-cung` (vẽ nhiều cung đồng tâm) — `vach` chỉ vẽ MỘT cung +#let goc = _voi-ctx(goc) +/// `goc-vuong(O, A, B, r: 0.32, mau: black, day: 0.8pt)` +/// +/// Ký hiệu góc vuông tại O (giữa hai tia O->A, O->B). +/// Ký hiệu góc vuông TẠI O (giữa hai tia O→A, O→B). ĐỈNH là đối số ĐẦU. +/// Quen lối TikZ (đỉnh ở giữa) thì dùng `ve-goc-vuong(A, O, B)`. +#let goc-vuong = _voi-ctx(goc-vuong) +/// `ve-goc(A, O, B, ..tuy-chon)` +/// +/// `goc`/`goc-vuong` ở trên đặt ĐỈNH góc làm đối số ĐẦU: goc-vuong(O, A, B). +/// TikZ lại viết đỉnh ở GIỮA (pic angle = A--O--B), nên hai hàm dưới đây nhận +/// thứ tự quen thuộc đó — đỉnh là đối số THỨ HAI: +/// ve-goc(A, O, B, ...) <=> goc(O, A, B, ...) +/// ve-goc-vuong(A, O, B, ...) <=> goc-vuong(O, A, B, ...) +/// Mọi tuỳ chọn (r, ten, so-do, so-cung, vach, to, mau, day, cach-nhan…) giữ +#let ve-goc = _voi-ctx(ve-goc) +/// `ve-goc-vuong(A, O, B, ..tuy-chon)` +/// +/// Ký hiệu góc vuông TẠI O theo lối TikZ: ĐỈNH nằm GIỮA — `ve-goc-vuong(A, O, B)`. +/// Cùng bộ tuỳ chọn với `goc-vuong` (r, mau, day). +#let ve-goc-vuong = _voi-ctx(ve-goc-vuong) +/// `danh-dau(A, B, so: 1, dai: 6pt, mau: black, day: 1pt, nghieng: 0deg, cheo: false)` +/// +/// so : số vạch song song (1/2/3…) tại trung điểm đoạn. +/// nghieng: góc NGHIÊNG của vạch so với pháp tuyến đoạn (0deg = vuông góc như cũ); +/// đặt ~20–30deg cho kiểu vạch chéo "/" "//" "///". +/// cheo : true = mỗi mốc vẽ dấu CHÉO NHAU "✕" (hai vạch ±góc); khi đó `nghieng` +/// = nửa góc mở của dấu ✕ (mặc định 30deg nếu để 0deg). +#let danh-dau = _voi-ctx(danh-dau) +/// `ve-ham(f, tu: auto, den: auto, n: 150, mau: blue, day: 1.3pt, dut: false)` +/// +/// Vẽ đồ thị hàm f trên [tu, den]; tự tách nhánh khi ra ngoài cửa sổ y +/// (dùng được cho hàm có tiệm cận đứng như 1/x, tan x). +#let ve-ham = _voi-ctx(ve-ham) +/// `to-vung(f, a, b, mau: rgb(30, 100, 200, 60), n: 80)` +/// +/// Tô miền giữa đồ thị f và trục hoành trên [a, b] (mau nên có độ trong suốt). +#let to-vung = _voi-ctx(to-vung) +/// `to-vung-2-ham(f, g, a, b, mau: rgb(30, 100, 200, 60), n: 80)` +/// +/// Tô miền giữa HAI đồ thị f và g trên [a, b] (mau nên có độ trong suốt). +#let to-vung-2-ham = _voi-ctx(to-vung-2-ham) +/// `gach-mien(cac-dinh, goc: 45deg, buoc: 6.5pt, mau: red, day: 0.55pt)` +/// +/// Gạch chéo đa giác lồi theo toạ độ TOÁN. +#let gach-mien = _voi-ctx(gach-mien) +/// `gach-vung(kiem, goc: 45deg, buoc: 6.5pt, mau: red, day: 0.55pt, n: 180)` +/// +/// Gạch chéo MIỀN BẤT KÌ (biên cong tuỳ ý). +/// kiem: miền đặt tên/tổ hợp giao-hop-bu (khuyên dùng) HOẶC hàm P => true/false: +/// gach-vung(ctx, giao(A, B, bu(C))) +/// gach-vung(ctx, P => trong(P, A) and P.at(1) > 0) +/// goc: hướng vạch; buoc: khoảng cách 2 vạch; n: số mẫu mỗi vạch (biên càng mịn). +#let gach-vung = _voi-ctx(gach-vung) +/// `ve-mien(m, mau: black, day: 1pt, dut: false, to: none)` +/// +/// Vẽ biên miền (đường tròn/elip tương ứng với khai báo). +#let ve-mien = _voi-ctx(ve-mien) +/// `duong-luon(..noi-dung, mau: black, day: 1pt, dut: false, dong: false, to: none, mui-ten: false, kich: 7pt, ten: none, tai: 0.5, huong: auto, cach: 6pt, ten-quay: false, mau-ten: auto, n: 16)` +/// +/// Vẽ đường cong uốn lượn qua các điểm neo, điểm điều khiển kiểu `controls` TikZ. +/// duong-luon(A, B, C, D) // trơn tự động qua 4 neo +/// duong-luon(A, dieu-khien(c1, c2), B) // Bézier bậc ba một đoạn +/// duong-luon(A, dieu-khien(c1,c2), B, C, dong: true, to: blue.lighten(85%)) +/// Tuỳ chọn: mau · day · dut · dong (khép kín) · to (tô) · mui-ten · kich · +/// ten/tai/huong/cach/ten-quay/mau-ten (nhãn đặt theo TỈ LỆ độ dài, như cac-doan) · +#let duong-luon = _voi-ctx(duong-luon) +/// `nhan-cong(duong, chu, tu: 0, can: "trai", khoang: 0pt, co: auto, mau: black, phia: "tren", cach: 2pt, dao: false)` +/// +/// Đặt từng ký tự của `chu` DỌC THEO đường `duong` (mảng điểm toạ độ toán, vd +/// lấy từ diem-luon / diem-cung / lay-mau), tự xoay tiếp tuyến, canh theo độ dài cung. +/// duong: mảng điểm (>= 2). chu: str (khuyên dùng) hoặc content văn bản thuần. +/// tu : vị trí BẮT ĐẦU theo tỉ lệ độ dài đường (0 = đầu, 1 = cuối, .5 = giữa) +/// can : "trai" (bắt đầu tại `tu`) · "giua" (canh giữa chữ quanh `tu`) · "phai" +/// khoang: giãn cách thêm giữa các ký tự (length) +#let nhan-cong = _voi-ctx(nhan-cong) +/// `dau-mui-ten(A, B, mau: black, kich: 7pt)` +/// +/// Đầu mũi tên tại B, hướng A->B. +#let dau-mui-ten = _voi-ctx(dau-mui-ten) + +// do-thi.typ — hệ trục, lưới, vạch chia, đường gióng +/// `truc(ten-x: $x$, ten-y: $y$, ten-goc: $O$, mau: black, day: 0.9pt)` +/// +#let truc = _voi-ctx(truc) +/// `luoi(buoc: 1, mau: luma(87%), day: 0.5pt)` +/// +/// Lưới ô vuông mờ. +#let luoi = _voi-ctx(luoi) +/// `vach-chia(buoc-x: 1, buoc-y: 1, so: true, mau: black)` +/// +/// Vạch chia + số trên hai trục (bỏ qua 0). +#let vach-chia = _voi-ctx(vach-chia) +/// `he-truc(ten-x: $x$, ten-y: $y$, ten-goc: $O$, luoi-o: true, buoc-luoi: auto, vach: true, so: true, buoc-x: 1, buoc-y: 1, mau: black, day: 0.9pt, mau-luoi: luma(87%))` +/// +/// Hệ trục TRỌN GÓI: lưới + trục + vạch chia + số — một lệnh thay ba. +/// #hinh(xmin: -4, xmax: 4, ymin: -3, ymax: 3, ctx => { he-truc(ctx) ... }) +/// luoi-o: lưới ô vuông mờ · vach: vạch chia đơn vị · so: ghi số trên vạch +#let he-truc = _voi-ctx(he-truc) +/// `giong(P, ten-x: auto, ten-y: auto, huong-x: "below", huong-y: "left", mau: gray.darken(20%), mau-diem: red, diem-to: true)` +/// +/// Đường gióng từ điểm P về 2 trục (nét đứt) + nhãn toạ độ trên trục. +/// ten-x / ten-y: auto = tự ghi số; none = không ghi; hoặc nội dung tuỳ ý. +#let giong = _voi-ctx(giong) +/// `tiep-tuyen(f, x0, mau: red, day: 1.2pt, dut: false, dai: auto, cham: true, mau-cham: auto, ten-diem: none, huong-diem: "above-left", ten: none, tai: 0.9, huong-ten: auto, cach: 6pt, ten-quay: false, giong: false, h: auto)` +/// +/// TIẾP TUYẾN của đồ thị y = f(x) tại điểm có hoành độ x0 (hoặc DÃY hoành độ). +/// Hệ số góc lấy bằng đạo hàm số nên dùng được cho MỌI hàm f viết bằng closure. +/// tiep-tuyen(f, 1) // tiếp tuyến tại x = 1 +/// tiep-tuyen(f, (-1, 2), mau: green) // nhiều tiếp điểm +/// tiep-tuyen(f, 1, ten: auto) // tự ghi y = kx + m +/// tiep-tuyen(f, 1, ten: $Delta$, ten-diem: $M$) // đặt tên tuỳ ý +#let tiep-tuyen = _voi-ctx(tiep-tuyen) +/// `nhan-pi()` +/// +/// Ghi nhãn các bội của π trên trục hoành. +#let nhan-pi = _voi-ctx(nhan-pi) + +// hinh-phang.typ — tam giác, tứ giác, đường tròn đặc biệt +/// `da-giac-ten(dinh, ten: none, mau: black, day: 1.1pt, to: none, cham: true)` +/// +/// Vẽ đa giác + nhãn đỉnh tự động hướng ra ngoài. +/// ten: mảng nội dung nhãn (cùng độ dài số đỉnh) hoặc none. +#let da-giac-ten = _voi-ctx(da-giac-ten) +/// `tam-giac(A, B, C, ten: ($A$, $B$, $C$), mau: black, day: 1.1pt, to: none, cham: true)` +/// +#let tam-giac = _voi-ctx(tam-giac) +/// `duong-cao(P, A, B, ten-chan: none, mau: red, day: 1pt, dut: false, vuong: true)` +/// +/// Đường cao hạ từ P xuống đường thẳng AB (kèm ký hiệu vuông góc). +/// ten-chan: nhãn chân đường cao (vd $H$). +#let duong-cao = _voi-ctx(duong-cao) +/// `trung-tuyen(P, A, B, ten-chan: none, mau: blue, day: 1pt, dut: false, so-vach: 1)` +/// +/// Trung tuyến từ P đến trung điểm AB (kèm 2 vạch đánh dấu bằng nhau). +#let trung-tuyen = _voi-ctx(trung-tuyen) +/// `phan-giac(O, A, B, ten-chan: none, mau: green.darken(20%), day: 1pt, dut: false, r-cung: 0.5, vach: 0, so-cung: 1, dai-vach: 6pt, lech: auto, ten-goc: none, cach-nhan: 1.9)` +/// +/// Phân giác trong góc O của tam giác OAB (kèm 2 cung đánh dấu góc bằng nhau). +/// vach : số VẠCH gạch ngang cung (0/1/2/3) — ký hiệu hai góc bằng nhau +/// so-cung : số CUNG đồng tâm vẽ ở mỗi góc (1/2/3) +/// lech : hệ số bán kính cung thứ hai so với cung thứ nhất +/// (auto = 1.18 khi KHÔNG đánh dấu, = 1 khi có vạch/nhiều cung — +/// vì hai góc bằng nhau thì phải vẽ cùng bán kính mới đúng quy ước) +#let phan-giac = _voi-ctx(phan-giac) +/// `trung-truc(A, B, dai: 1.5, mau: purple, day: 1pt, dut: true, so-vach: 1)` +/// +/// Đường trung trực của đoạn AB (đoạn vuông góc tại trung điểm, dài 2*dai). +#let trung-truc = _voi-ctx(trung-truc) +/// `tam-giac-deu(A, canh, ten: ($A$, $B$, $C$), mau: black, day: 1.1pt, to: none, vach: 1)` +/// +/// Tam giác đều cạnh `canh`, đỉnh A ở dưới-trái, đáy nằm ngang. +/// vach: số vạch đánh dấu cạnh bằng nhau (0 = không vẽ). +#let tam-giac-deu = _voi-ctx(tam-giac-deu) +/// `tam-giac-vuong(A, a, b, ten: ($A$, $B$, $C$), mau: black, day: 1.1pt, to: none)` +/// +/// Tam giác vuông tại A: cạnh AB ngang dài a, cạnh AC đứng dài b. +#let tam-giac-vuong = _voi-ctx(tam-giac-vuong) +/// `tam-giac-can(A, canh-day, chieu-cao, ten: ($A$, $B$, $C$), mau: black, day: 1.1pt, to: none, vach: 1)` +/// +/// Tam giác cân tại C: đáy AB ngang dài `canh-day`, chiều cao `chieu-cao`. +#let tam-giac-can = _voi-ctx(tam-giac-can) +/// `tam-giac-vuong-can(A, canh, ten: ($A$, $B$, $C$), mau: black, day: 1.1pt, to: none, vach: 1)` +/// +/// Tam giác vuông cân tại A, hai cạnh góc vuông dài `canh`. +#let tam-giac-vuong-can = _voi-ctx(tam-giac-vuong-can) +/// `tu-giac(A, B, C, D, ten: ($A$, $B$, $C$, $D$), mau: black, day: 1.1pt, to: none)` +/// +/// Tứ giác bất kỳ ABCD. +#let tu-giac = _voi-ctx(tu-giac) +/// `hinh-binh-hanh(A, B, C, ten: ($A$, $B$, $C$, $D$), mau: black, day: 1.1pt, to: none)` +/// +/// Hình bình hành biết 3 đỉnh liên tiếp A, B, C (D tự tính = A + C - B). +#let hinh-binh-hanh = _voi-ctx(hinh-binh-hanh) +/// `hinh-chu-nhat(A, r, c, ten: ($A$, $B$, $C$, $D$), mau: black, day: 1.1pt, to: none, goc-vg: true)` +/// +/// Hình chữ nhật: góc dưới-trái A, chiều rộng r, chiều cao c. +#let hinh-chu-nhat = _voi-ctx(hinh-chu-nhat) +/// `hinh-thang(A, a, b, c, lech: 0.6, ten: ($A$, $B$, $C$, $D$), mau: black, day: 1.1pt, to: none)` +/// +/// Hình thang: đáy lớn AB (dưới, dài a), đáy nhỏ DC (trên, dài b), +/// cao c, lech: độ lệch ngang của D so với A. +#let hinh-thang = _voi-ctx(hinh-thang) +/// `duong-tron-ngoai-tiep(..vi-tri, ten-tam: $O$, huong-tam: auto, mau: blue, day: 1pt, dut: false, to: none, ban-kinh: false, dinh-r: auto, ten-r: none, canh: false, mau-canh: black)` +/// +/// Đường tròn ngoại tiếp TAM GIÁC hoặc ĐA GIÁC. +/// duong-tron-ngoai-tiep(A, B, C) — 3 đỉnh rời (lối cũ) +/// duong-tron-ngoai-tiep((A, B, C, D, E)) — MỘT MẢNG đỉnh, đa giác tuỳ ý +/// Từ 4 đỉnh trở lên, tâm/bán kính khớp theo bình phương bé nhất (đa giác nội +/// tiếp được cho ra đúng đường tròn của nó — xem `tron-qua-diem`). +/// ten-tam : nhãn tâm (none = không ghi) +#let duong-tron-ngoai-tiep = _voi-ctx(duong-tron-ngoai-tiep) +/// `duong-tron-noi-tiep(A, B, C, ten-tam: $I$, mau: orange.darken(10%), day: 1pt, dut: false, ban-kinh: false)` +/// +/// Đường tròn nội tiếp tam giác ABC. +#let duong-tron-noi-tiep = _voi-ctx(duong-tron-noi-tiep) +/// `duong-tron-bang-tiep(A, B, C, ten-tam: $J$, ten-tiep: none, mau: green.darken(25%), day: 1pt, keo-dai: true, tiep-diem: true, ban-kinh: false)` +/// +/// Đường tròn bàng tiếp TRONG GÓC A của tam giác ABC (tiếp xúc cạnh BC và +/// phần kéo dài của AB, AC). Muốn góc B thì gọi duong-tron-bang-tiep(ctx, B, C, A). +/// ten-tam : nhãn tâm (mặc định $J$) +/// keo-dai : vẽ nét đứt kéo dài hai cạnh AB, AC tới tiếp điểm +/// tiep-diem : chấm 3 tiếp điểm +/// ban-kinh : vẽ bán kính tới cạnh BC (kèm ký hiệu vuông góc) +#let duong-tron-bang-tiep = _voi-ctx(duong-tron-bang-tiep) +/// `ve-truc-tam(A, B, C, ten: $H$, ten-chan: ($H_A$, $H_B$, $H_C$), ten-dinh: ($A$, $B$, $C$), canh: true, mau: black, mau-cao: red, day: 1.1pt, dut: false, vuong: true)` +/// +/// Ba đường cao của tam giác ABC + trực tâm H. +/// ten : nhãn trực tâm (none = không ghi) +/// ten-chan : nhãn 3 chân đường cao theo thứ tự hạ từ A, B, C +/// (none = không ghi; mặc định $H_A$, $H_B$, $H_C$) +/// vuong : vẽ ký hiệu góc vuông tại chân đường cao +/// canh : true = vẽ luôn tam giác ABC (kèm nhãn đỉnh) +#let ve-truc-tam = _voi-ctx(ve-truc-tam) +/// `tiep-tuyen-tu-diem(O, r, M, ten-tam: $O$, ten-diem: $M$, ten-tiep: ($T_1$, $T_2$), mau: black, mau-tt: red, day: 1pt)` +/// +/// Hai tiếp tuyến kẻ từ điểm M ngoài đường tròn (O; r). +#let tiep-tuyen-tu-diem = _voi-ctx(tiep-tuyen-tu-diem) +/// `tiep-tuyen-tai-diem(O, r, M, dai: auto, mau: black, day: 1pt, dut: false)` +/// +/// Dựng tiếp tuyến của đường tròn (O; r) TẠI điểm M (M nằm trên đường tròn): +/// đoạn thẳng qua M, VUÔNG GÓC với bán kính OM. +/// dai : nửa độ dài đoạn tiếp tuyến (auto = 1.4·r); đơn vị toạ độ toán. +/// mau / day / dut : kiểu nét của đoạn tiếp tuyến. +/// M không cần chính xác trên đường tròn — hướng tiếp tuyến lấy theo OM thực tế. +/// Tiếp tuyến của (O; r) TẠI điểm M: đoạn qua M vuông góc OM. +#let tiep-tuyen-tai-diem = _voi-ctx(tiep-tuyen-tai-diem) + +// tron-xoay.typ — khối tròn xoay (khoi-tron-xoay tự tạo khung, không kê ở đây) +/// `ve-khoi-xoay(f, a, b, g: none, ngang: true, k: 0.26, mau: blue, day: 1.2pt, mau-to: rgb(70, 130, 200, 60), duong-sinh: true, truc-mo: true, mat-cat: none, mau-mat-cat: rgb(235, 130, 40, 120), ten-ban-kinh: auto, ten-mat-cat: auto, n: 120)` +/// +#let ve-khoi-xoay = _voi-ctx(ve-khoi-xoay) +/// `ve-mien-xoay(f, a, b, g: none, ngang: true, mau: blue, day: 1.3pt, mau-to: rgb(70, 130, 200, 90), ten-ham: auto, ten-ham-trong: none, ten-a: auto, ten-b: auto, giong-ab: true, n: 120)` +/// +#let ve-mien-xoay = _voi-ctx(ve-mien-xoay) + +// hinh-khong-gian.typ — nhóm Oxyz +/// `diem-oxyz(t3, P, ten: none, huong: "above", bk: 2pt, mau: black)` +/// +/// Điểm 3D: chấm + nhãn. +#let diem-oxyz = _voi-ctx(diem-oxyz) +/// `doan-oxyz(t3, A, B, mau: black, day: 1pt, dut: false)` +/// +/// Đoạn / vectơ nối hai điểm 3D. +#let doan-oxyz = _voi-ctx(doan-oxyz) +/// `vecto-oxyz(t3, A, B, ten: none, huong: "tren", mau: black, day: 1.1pt, dut: false)` +/// +#let vecto-oxyz = _voi-ctx(vecto-oxyz) +/// `giong-oxyz(t3, P, mau: blue, day: 0.8pt)` +/// +/// Hộp gióng nét đứt từ gốc O đến điểm P = (a, b, c): vẽ các cạnh của +/// hình hộp chữ nhật [0,a]×[0,b]×[0,c] (trừ 3 cạnh nằm trên trục). +#let giong-oxyz = _voi-ctx(giong-oxyz) + +// da-dien.typ — khối đa diện tổng quát, mặt phẳng, thiết diện +// (da-dien, da-dien-thiet-dien tự tạo khung; các hàm TRẢ GIÁ TRỊ như +// chieu-*, khoi-*, mp-*, thiet-dien, v3-*, phan-tich-khoi KHÔNG bọc) +/// `ve-da-dien(dinh: (), mat: (), cam: auto, ten: none, huong: auto, cach: 6pt, mau-ten: black, hien-dinh: true, bk: 1.6pt, mau: black, day: 1.1pt, mau-khuat: auto, day-khuat: auto, hien-khuat: true, to: none, to-mat: none, diem: (), duong: (), mau-diem: auto, bk-diem: 1.8pt, them: none)` +/// +#let ve-da-dien = _voi-ctx(ve-da-dien) +/// `mat-phang(p, dinh3, to: auto, mau: blue.darken(15%), day: 1pt, dut: false, ten: none, ten-tai: 0, huong: "above-left", cach: 5pt, che: (), mau-che: black, day-che: 0.9pt, nhin: auto, them: none)` +/// +#let mat-phang = _voi-ctx(mat-phang) +/// `mat-phang-oxyz(t3, a, b, c, to: auto, mau: blue.darken(15%), day: 1pt, dut: false, ten: none, ten-tai: 0, huong: "above-left", cach: 5pt, ten-dinh: false, ten-abc: ($A$, $B$, $C$), hien-dinh: true, bk: 1.8pt, truc: auto, mau-che: black, day-che: 0.9pt, them: none)` +/// +#let mat-phang-oxyz = _voi-ctx(mat-phang-oxyz) +/// `mat-phang-bh(t3, tam, u, v, to: auto, mau: blue.darken(15%), day: 1pt, dut: false, ten: none, ten-tai: 1, huong: "above-right", cach: 5pt, truc: none, mau-che: black, day-che: 0.9pt, them: none)` +/// +#let mat-phang-bh = _voi-ctx(mat-phang-bh) +/// `ve-thiet-dien(dinh: (), mat: (), mp: none, cam: auto, to: auto, mau: red.darken(10%), day: 1.1pt, hien-khuat: true, mau-khuat: auto, day-khuat: auto, ten: none, huong: auto, cach: 6pt, hien-dinh: true, bk: 1.8pt, mau-ten: auto, them: none)` +/// +/// Vẽ thiết diện TRÊN một khối: tô miền + viền LIỀN ở phần nằm trên mặt +/// THẤY, ĐỨT ở phần nằm trên mặt KHUẤT (nhận ctx ⇒ kê vào ve-voi). +/// ve-thiet-dien(ctx, dinh: d, mat: m, mp: mp-qua-3-diem(P, Q, R)) +#let ve-thiet-dien = _voi-ctx(ve-thiet-dien) + +// --------------------------------------------------------------------- +// mat-cong.typ — nón / trụ có NÉT KHUẤT TỰ ĐỘNG (kể cả hai khối che nhau). +// `mat-non`, `mat-tru`, `dinh-non` TRẢ GIÁ TRỊ ⇒ KHÔNG kê ở đây. +// `mat-cong` tự tạo khung hình ⇒ cũng KHÔNG kê. +// --------------------------------------------------------------------- +/// `ve-mat-cong(..khoi, cam: auto, mau: black, day: 1.1pt, to: none, hien-khuat: true, mau-khuat: auto, day-khuat: auto, n: 48, duong: (), truc: none, truoc: none, them: none)` +/// +/// Vẽ một hoặc nhiều khối nón/trụ trục đứng, TỰ chia nét liền/đứt bằng cách +/// bắn tia về phía người nhìn — lo cả tự khuất lẫn hai khối che nhau. +/// ve-mat-cong(mat-non(r: 2, cao: 4), mat-tru(tam: (0, 2, 0), r: 2, cao: 4)) +#let ve-mat-cong = _voi-ctx(ve-mat-cong) +/// `ve-truc-3d(cam, x: 3, y: 4, z: 4, am: 0.4, dm: 0.55, ten: ($x$, $y$, $z$), ten-goc: $O$, huong-ten: ("below-left", "below-right", "left"), huong-goc: "below-right", mau: black, day: 0.9pt, cach: 5pt)` +/// +/// Vẽ hệ trục Oxyz bằng CHÍNH camera của khối ⇒ cả khung hình chung một góc +/// nghiêng (khác `oxyz`, vốn tự dựng phép chiếu xiên riêng). +#let ve-truc-3d = _voi-ctx(ve-truc-3d) diff --git a/packages/preview/conic-toan/0.3.4/lib/bang-thong-ke.typ b/packages/preview/conic-toan/0.3.4/lib/bang-thong-ke.typ new file mode 100644 index 0000000000..43ee9fac99 --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/bang-thong-ke.typ @@ -0,0 +1,156 @@ +// ===================================================================== +// bang-thong-ke.typ — BẢNG TẦN SỐ (THỐNG KÊ) +// +// #import "bang-thong-ke.typ": * +// +// 4 loại bảng (bố cục NGANG — giá trị chạy theo cột, đúng kiểu SGK): +// KHÔNG ghép nhóm, ĐƠN -> bang-tan-so +// KHÔNG ghép nhóm, ĐÔI -> bang-tan-so-doi +// CÓ ghép nhóm, ĐƠN -> bang-ghep-nhom +// CÓ ghép nhóm, ĐÔI -> bang-ghep-nhom-doi +// +// "Biến đơn vị của giá trị dấu hiệu": tham số don-vi. Khi có, nhãn ô +// góc trên-trái hiện thành «tên (đơn vị)», vd: Số tiền (nghìn đồng). +// +// Ghi chú chung: +// gia-tri / tan-so : mảng nội dung hoặc số. +// moc : mảng mốc để TỰ tạo các khoảng ghép nhóm (n mốc -> n-1 khoảng). +// nhom : thay cho moc nếu muốn tự truyền sẵn nội dung từng khoảng. +// ===================================================================== + +// Nhãn ô góc: gắn thêm (đơn vị) nếu có. +#let _nhan(ten, don-vi) = if don-vi == none { ten } else { [#ten] + [ (] + [#don-vi] + [)] } + +// Một khoảng nửa mở/đóng, vd [35; 40). In-line, hợp gu SGK. +#let khoang(a, b, dong-trai: true, dong-phai: false) = { + let tl = if dong-trai { "[" } else { "(" } + let tr = if dong-phai { "]" } else { ")" } + [#tl] + [#a] + [; ] + [#b] + [#tr] +} + +// n mốc -> mảng n-1 khoảng [m0;m1), [m1;m2), ... +#let khoang-tu-moc(moc, dong-trai: true, dong-phai: false) = range(moc.len() - 1).map( + i => khoang(moc.at(i), moc.at(i + 1), dong-trai: dong-trai, dong-phai: dong-phai), +) + +// ------- LÕI DỰNG BẢNG ------- +// nhan-gia-tri : nội dung ô góc trên-trái (nhãn hàng giá trị). +// gia-tri : mảng nhãn các cột giá trị. +// hang : mảng ((nhãn-hàng, mảng-số), ...) cho các dòng tần số. +#let _bang( + nhan-gia-tri, + gia-tri, + hang, + co-chu: 11pt, + mau-vien: black, + day-vien: 0.7pt, + mau-tieu-de: none, // tô nền cột nhãn (none = không tô) + rong-nhan: auto, // bề rộng cột nhãn bên trái +) = { + let n = gia-tri.len() + set text(size: co-chu) + let o(x) = align(center + horizon, [#x]) + let nhan(x) = align(left + horizon, strong([#x])) + + let cells = () + // hàng tiêu đề: nhãn giá trị + các giá trị + cells.push(if mau-tieu-de == none { nhan(nhan-gia-tri) } else { + table.cell(fill: mau-tieu-de, nhan(nhan-gia-tri)) + }) + for v in gia-tri { cells.push(o(v)) } + // các hàng tần số + for hg in hang { + let ten = hg.at(0) + let mang = hg.at(1) + cells.push(if mau-tieu-de == none { nhan(ten) } else { + table.cell(fill: mau-tieu-de, nhan(ten)) + }) + for t in mang { cells.push(o(t)) } + } + + table( + columns: (rong-nhan,) + (auto,) * n, + inset: (x: 8pt, y: 6pt), + align: center + horizon, + stroke: day-vien + mau-vien, + ..cells, + ) +} + +// ===================== KHÔNG GHÉP NHÓM ===================== + +// ĐƠN — một dòng tần số. +#let bang-tan-so( + gia-tri: (), + tan-so: (), + ten-gia-tri: [Giá trị], + ten-tan-so: [Tần số], + don-vi: none, + ..tuy-chon, +) = _bang( + _nhan(ten-gia-tri, don-vi), + gia-tri, + ((ten-tan-so, tan-so),), + ..tuy-chon, +) + +// ĐÔI — hai dòng tần số (vd Nam / Nữ). +#let bang-tan-so-doi( + gia-tri: (), + tan-so-1: (), + tan-so-2: (), + ten-gia-tri: [Giá trị], + ten-1: [Nhóm 1], + ten-2: [Nhóm 2], + don-vi: none, + ..tuy-chon, +) = _bang( + _nhan(ten-gia-tri, don-vi), + gia-tri, + ((ten-1, tan-so-1), (ten-2, tan-so-2)), + ..tuy-chon, +) + +// ======================= CÓ GHÉP NHÓM ======================= + +// ĐƠN — một dòng tần số. Truyền moc (mốc) hoặc nhom (khoảng sẵn). +#let bang-ghep-nhom( + moc: none, + nhom: none, + tan-so: (), + ten-nhom: [Nhóm], + ten-tan-so: [Tần số], + don-vi: none, + dong-phai: false, // khoảng cuối đóng phải? + ..tuy-chon, +) = { + let cac-khoang = if nhom != none { nhom } else { khoang-tu-moc(moc, dong-phai: dong-phai) } + _bang( + _nhan(ten-nhom, don-vi), + cac-khoang, + ((ten-tan-so, tan-so),), + ..tuy-chon, + ) +} + +// ĐÔI — hai dòng tần số trên cùng bộ khoảng. +#let bang-ghep-nhom-doi( + moc: none, + nhom: none, + tan-so-1: (), + tan-so-2: (), + ten-nhom: [Nhóm], + ten-1: [Nhóm 1], + ten-2: [Nhóm 2], + don-vi: none, + dong-phai: false, + ..tuy-chon, +) = { + let cac-khoang = if nhom != none { nhom } else { khoang-tu-moc(moc, dong-phai: dong-phai) } + _bang( + _nhan(ten-nhom, don-vi), + cac-khoang, + ((ten-1, tan-so-1), (ten-2, tan-so-2)), + ..tuy-chon, + ) +} diff --git a/packages/preview/conic-toan/0.3.4/lib/bang.typ b/packages/preview/conic-toan/0.3.4/lib/bang.typ new file mode 100644 index 0000000000..a377a54e67 --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/bang.typ @@ -0,0 +1,905 @@ +// ===================================================================== +// bang.typ — BẢNG BIẾN THIÊN & BẢNG XÉT DẤU +// +// #bbt( +// x: (n mốc, vd ($-oo$, $1$, $+oo$)), +// dau: 2n-1 phần tử xen kẽ [tại x0, khoảng, tại x1, ...] +// dùng chuỗi "+", "-", "0", "||", "" hoặc nội dung tuỳ ý +// (none => ẩn dòng f'), +// gia-tri: n giá trị của f tại các mốc; tại điểm gián đoạn dùng cặp +// ($-oo$, $+oo$) — giá trị 2 bên kẹp, +// huong: n-1 chiều mũi tên: "len" | "xuong" | "ngang"; +// NỬA Ô (07/2026, để 2 mũi tên cùng chiều nằm TRÊN 1 ĐƯỜNG qua +// giá trị đặt GIỮA ô): "len-duoi" (đáy→giữa) + "len-tren" +// (giữa→đỉnh); "xuong-tren" (đỉnh→giữa) + "xuong-duoi" (giữa→đáy). +// Giá trị tại mốc kề đầu mút "giữa" tự đặt chính giữa ô, +// kep: chỉ số các mốc có kẹp ‖ (tiệm cận đứng), vd (1,), +// gach: chỉ số các KHOẢNG hàm KHÔNG XÁC ĐỊNH -> gạch chéo, vd (1,) +// (khoảng k nằm giữa mốc k và k+1; huong tại khoảng đó ghi "ngang"; +// dau tại mốc kề bên ghi "||" -> kẹp ‖ vẽ CAO KÍN dòng dấu ngay +// giữa cột mốc, dải gạch phủ lấn tới tận kẹp), +// ) +// +// #bang-xet-dau(x: (...), dong: ((ten, mang-dau-2n-1), ...), gach: (...)) +// ===================================================================== +#import "ve.typ": doan-pt, da-giac-pt +#import "do-thi.typ": so-toan, so-can-thuc, cuc-tri-bac-ba, cuc-tri-huu-ti + +// Cặp vạch đứng ‖ cao `cao`. +#let kep-vach(cao, day: 0.7pt) = stack( + dir: ltr, spacing: 1.9pt, + line(angle: 90deg, length: cao, stroke: day), + line(angle: 90deg, length: cao, stroke: day), +) + +// Khối gạch chéo 45° (miền không xác định) — phần tử KHỐI, kích thước +// tuyệt đối nên đặt trong ô/grid là khít, không dính metric dòng chữ. +#let o-gach(rong, cao, buoc: 5.5pt, day: 0.5pt, mau: luma(45%)) = block( + width: rong, height: cao, clip: true, + { + let w = rong.pt() + let h = cao.pt() + let t = -h + while t < w { + doan-pt((t, h), (t + h, 0), mau: mau, day: day) + t = t + buoc.pt() + } + }, +) + +// Đổi ký hiệu dấu -> nội dung. +// Nhận cả SỐ (AI hay gõ `0` thay vì `"0"`) — quy về chuỗi rồi xử lý như cũ. +#let ky-hieu-dau(s) = { + if type(s) == int or type(s) == float { + if s == 0 { $0$ } else { $#s$ } + } else if type(s) == str { + if s == "+" { $+$ } else if s == "-" { $-$ } else if s == "0" { $0$ } + else if s == "||" { kep-vach(1.1em) } else if s == "" { none } else { s } + } else { s } +} + +// ---------- TỰ BÙ Ô TRỐNG HAI ĐẦU DÃY DẤU ---------- +// Dãy `dau` (bbt) / mảng dấu mỗi dòng (bang-xet-dau) phải có ĐÚNG 2n−1 phần +// tử, xen kẽ [tại mốc, trên khoảng, tại mốc, ...] nên HAI ĐẦU luôn là ô +// TRỐNG "". AI sinh bài rất hay quên đúng hai ô đó (viết ("+","0","-") thay +// vì ("","+","0","-","")) — trước đây lỗi ra "array index out of bounds", +// VS Code không báo trước, GV phải tự thêm tay. +// +// `_diem-day-dau` chấm điểm một dãy theo quy ước vị trí: "+"/"-" phải nằm ở +// chỉ số LẺ (trên khoảng), "0"/"||" ở chỉ số CHẴN (tại mốc). +#let _diem-day-dau(day) = { + let d = 0 + for (j, s) in day.enumerate() { + let so = type(s) == int or type(s) == float + let k = if so { str(s) } else if type(s) == str { s } else { "?" } + if k == "+" or k == "-" { d += (if calc.odd(j) { 1 } else { -1 }) } + if k == "0" or k == "||" { d += (if calc.even(j) { 1 } else { -1 }) } + } + d +} + +// Chuẩn hoá dãy dấu về đúng `m` = 2n−1 phần tử. CHỈ chèn "" vào ĐẦU/CUỐI, +// không bao giờ chèn vào giữa; chọn cách chèn khớp quy ước nhất, hoà thì ưu +// tiên chèn CÂN hai đầu. Thiếu quá 2 phần tử hoặc thừa -> báo lỗi rõ ràng. +#let _chuan-day-dau(day, m, ten: "dau") = { + if day == none { return none } + if type(day) != array { + panic("`" + ten + "` phải là MẢNG " + str(m) + " phần tử (= 2 × số mốc − 1).") + } + let l = day.len() + if l == m { return day } + let ns = " Dãy dấu xen kẽ [tại mốc, trên khoảng, tại mốc, ...], hai đầu là ô trống \"\"." + if l > m { + panic("`" + ten + "` có " + str(l) + " phần tử nhưng bảng cần đúng " + + str(m) + " (= 2 × " + str(int((m + 1) / 2)) + " mốc − 1): thừa " + + str(l - m) + " phần tử." + ns) + } + if m - l > 2 { + panic("`" + ten + "` chỉ có " + str(l) + " phần tử, bảng cần " + str(m) + + " (= 2 × " + str(int((m + 1) / 2)) + " mốc − 1): thiếu " + str(m - l) + + " phần tử nên không tự bù được." + ns) + } + let thieu = m - l + let tot = none + let diem-tot = -10000 + for k in range(thieu + 1) { + let thu = range(k).map(i => "") + day + range(thieu - k).map(i => "") + // ưu tiên điểm khớp quy ước; hoà thì ưu tiên chèn cân hai đầu + let uu = _diem-day-dau(thu) * 10 - calc.abs(k - (thieu - k)) + if uu > diem-tot { diem-tot = uu; tot = thu } + } + tot +} + +// Đổi độ dài -> số pt (nhận sẵn số thì giữ nguyên). +#let _pt-so(v) = if type(v) == length { v.pt() } else { v } + +// Mũi tên biến thiên trong ô (rong × cao). +// `y1`, `y2` (tuỳ chọn): TUNG ĐỘ đầu/cuối tính từ MÉP TRÊN ô, y hướng XUỐNG. +// Dùng để đặt đầu mút mũi tên đúng vào TIM chữ số ở hai mốc kề (lối tikz-tab) +// — `auto` thì rơi về mốc cũ (sát mép trên/dưới ô). +#let mui-ten-bbt(rong, cao, huong: "len", mau: black, day: 1pt, + y1: auto, y2: auto) = box( + width: rong, height: cao, + { + let w = rong.pt() + let h = cao.pt() + // (y đầu, y cuối) theo toạ độ trang (y hướng xuống); các hướng "-duoi"/ + // "-tren" là mũi tên NỬA Ô: đầu mút tại h/2 khớp giá trị đặt giữa ô, + // để 2 mũi tên cùng chiều ở 2 khoảng kề nhau nằm trên 1 đường thẳng. + let dau-cuoi = ( + "len": (h - 1.5, 1.5), + "len-duoi": (h - 1.5, h / 2), + "len-tren": (h / 2, 1.5), + "xuong": (1.5, h - 1.5), + "xuong-tren": (1.5, h / 2), + "xuong-duoi": (h / 2, h - 1.5), + "ngang": (h / 2, h / 2), + ).at(huong) + let ya = if y1 == auto { dau-cuoi.at(0) } else { _pt-so(y1) } + let yb = if y2 == auto { dau-cuoi.at(1) } else { _pt-so(y2) } + let (p, q) = ((1.5, ya), (w - 1.5, yb)) + doan-pt(p, q, mau: mau, day: day) + let dx = q.at(0) - p.at(0) + let dy = q.at(1) - p.at(1) + let l = calc.sqrt(dx * dx + dy * dy) + let ux = dx / l + let uy = dy / l + let px = -uy + let py = ux + let k = 6.5 + da-giac-pt( + ( + q, + (q.at(0) - k * ux + 0.38 * k * px, q.at(1) - k * uy + 0.38 * k * py), + (q.at(0) - 0.72 * k * ux, q.at(1) - 0.72 * k * uy), + (q.at(0) - k * ux - 0.38 * k * px, q.at(1) - k * uy - 0.38 * k * py), + ), + to: mau, + ) + }, +) + +// Ô giá trị: `vi` là bool (true = đỉnh, false = đáy — form cũ) hoặc +// alignment dọc (top/horizon/bottom). +#let o-gia-tri(v, vi, cao) = { + let doc = if type(vi) == bool { if vi { top } else { bottom } } else { vi } + box(height: cao, align(doc + center, v)) +} + +// Ô có kẹp ‖: giá trị trái + kẹp + giá trị phải. +// `cao` = chiều cao BĂNG nội dung (nơi đặt giá trị, khớp với mũi tên). +// `cao-kin` = chiều cao TOÀN Ô: vạch ‖ kẻ KÍN từ mép trên tới mép dưới ô +// (lối tikz-tab), còn giá trị vẫn nằm trong băng nội dung nhờ đệm `dem`. +// ⚠️ PHẢI dùng `box` (không phải `block`) để bọc đệm dọc: `block` là phần tử +// KHỐI, bề rộng mặc định NUỐT TRỌN chỗ trống của cột `auto` -> bảng phình ra +// hết bề ngang trang, các cột mốc bị bóp lại làm số có căn thức XÔ XUỐNG DÒNG. +#let o-kep(trai, phai, tren-trai, tren-phai, cao, cao-kin: auto, dem: 0pt) = { + let hk = if cao-kin == auto { cao } else { cao-kin } + grid( + columns: 3, + column-gutter: 2.6pt, + rows: hk, + box(height: hk, inset: (y: dem), o-gia-tri(trai, tren-trai, cao)), + kep-vach(hk), + box(height: hk, inset: (y: dem), o-gia-tri(phai, tren-phai, cao)), + ) +} + +// ---------- CHUẨN HOÁ MỐC & GIÁ TRỊ ---------- +// AI sinh bài hay gõ x: ("-oo", -3, -1, "+oo") và gia-tri: ("-oo", 5, -5/3, +// "+oo") — tức CHUỖI "-oo" (in ra chữ "-oo" chứ không ra −∞) và SỐ trần +// (Typst không nhận số ở vị trí nội dung). Hàm này quy về nội dung toán: +// chuỗi vô cực -> $-oo$/$+oo$, số nguyên -> $#v$, số thập phân -> so-toan +// (ra phân số đẹp: -5/3). Nội dung có sẵn ($...$) giữ NGUYÊN. +#let _chuan-o(v) = { + if type(v) == array { return v.map(_chuan-o) } + if type(v) == int { return $#v$ } + if type(v) == float { return so-toan(v) } + if type(v) != str { return v } + let t = v.trim() + if t in ("-oo", "−oo", "-inf", "-infty", "-∞", "−∞") { $-oo$ } + else if t in ("+oo", "oo", "+inf", "inf", "+infty", "infty", "+∞", "∞") { $+oo$ } + else { v } +} + +// ---------- BẢNG BIẾN THIÊN ---------- +#let bbt( + x: (), + dau: none, + gia-tri: (), + huong: (), + kep: (), + gach: (), // khoảng không xác định -> gạch chéo + ten-x: $x$, + ten-fp: $f'(x)$, + ten-f: $f(x)$, + rong-cot: 2.5cm, // bề rộng mỗi khoảng + rong-so-gach: 0.9cm, // bề rộng cột MỐC kề vùng gạch (nhỏ -> số sát mũi tên) + cao-bt: 2.3cm, // chiều cao dòng biến thiên + mau-mui-ten: blue.darken(20%), + co-chu: 11pt, +) = context { + let n = x.len() + let m = 2 * n - 1 + // "-oo"/số trần -> nội dung toán (xem _chuan-o) + let x = x.map(_chuan-o) + let gia-tri = gia-tri.map(_chuan-o) + // tự bù 2 ô trống hai đầu nếu dãy dấu bị thiếu (xem _chuan-day-dau) + let dau = _chuan-day-dau(dau, m) + // báo lỗi RÕ thay cho "array index out of bounds" khi mảng bị hụt + if gia-tri.len() < n { + panic("`gia-tri` có " + str(gia-tri.len()) + " phần tử nhưng `x` có " + + str(n) + " mốc — cần ĐÚNG " + str(n) + " giá trị (mỗi mốc một giá trị).") + } + if huong.len() < n - 1 { + panic("`huong` có " + str(huong.len()) + " phần tử nhưng `x` có " + str(n) + + " mốc — cần ĐÚNG " + str(n - 1) + " chiều mũi tên (\"len\"/\"xuong\"/\"ngang\").") + } + let inset = 5pt + // đệm NGANG nhỏ hơn đệm dọc: số nằm gần mũi tên hơn (lối tikz-tab, nơi + // mũi tên nối thẳng từ số này sang số kia) + let inset-x = 2.5pt + let cao-o = cao-bt - 2 * inset + let co-dau = dau != none + // ĐO chiều cao thật của từng giá trị -> đặt đầu mút mũi tên đúng vào TIM + // chữ số (mũi tên thẳng hàng với ký tự, lối tikz-tab). + let cao-cua(v) = measure(text(size: co-chu, v)).height + let h-tri = gia-tri.map(v => if type(v) == array { v.map(cao-cua) } else { cao-cua(v) }) + // dòng dấu cao CỐ ĐỊNH để kẹp ‖ và dải gạch phủ kín từ mép trên tới mép dưới + let cao-hang-dau = co-chu * 1.3 + 2 * inset + // ô gạch kín (tự đo đúng kích thước ô qua layout) + let o-gach-kin = table.cell(inset: 0pt, layout(sz => o-gach(sz.width, sz.height))) + // dải gạch phủ nửa ô mốc, cao `cao` + let nua-gach(cao) = layout(sz => o-gach(sz.width, cao)) + // các cột: nhãn + xen kẽ (mốc, khoảng) + // mốc kề vùng gạch -> bề rộng cố định nhỏ để nửa ô hẹp (số gần mũi tên) + let cols = (auto,) + range(m).map(j => { + if calc.odd(j) { rong-cot } + else { + let i = int(j / 2) + if ((i - 1) in gach) or (i in gach) { rong-so-gach } else { auto } + } + }) + + // ----- dòng x ----- + let hang-x = (align(horizon + center, ten-x),) + for j in range(m) { + hang-x.push( + if calc.even(j) { align(horizon + center, x.at(int(j / 2))) } else { none } + ) + } + + // ----- dòng dấu f' ----- + // Ô dấu tại MỐC: "||" -> cặp vạch ‖ kẻ KÍN từ mép trên tới mép dưới ô + // (bỏ đệm dọc của ô), như tikz-tab; dấu khác vẫn căn giữa như cũ. + let o-dau-moc(s, cao) = if s == "||" { + table.cell(inset: (x: inset-x, y: 0pt), kep-vach(cao)) + } else { align(horizon + center, ky-hieu-dau(s)) } + let hang-dau = () + if co-dau { + hang-dau.push(align(horizon + center, ten-fp)) + for j in range(m) { + if calc.odd(j) { + // khoảng: gạch kín ô hoặc ký hiệu dấu + hang-dau.push( + if int((j - 1) / 2) in gach { o-gach-kin } + else { align(horizon + center, ky-hieu-dau(dau.at(j))) } + ) + } else { + // mốc: nếu kề vùng gạch thì kẹp ‖ cao kín dòng đặt GIỮA cột + // (thẳng hàng với số ở dòng x), gạch phủ nửa ô phía vùng gạch + let i = int(j / 2) + let trai = (i - 1) in gach + let phai = i in gach + hang-dau.push( + if trai or phai { + let giua = if dau.at(j) == "||" { kep-vach(cao-hang-dau) } else { + block(height: 100%, inset: (x: 2.5pt), align(horizon + center, ky-hieu-dau(dau.at(j)))) + } + table.cell(inset: 0pt, grid( + columns: (1fr, auto, 1fr), + rows: cao-hang-dau, + if trai { nua-gach(cao-hang-dau) } else { none }, + giua, + if phai { nua-gach(cao-hang-dau) } else { none }, + )) + } else { o-dau-moc(dau.at(j), cao-hang-dau) } + ) + } + } + } + + // ----- dòng biến thiên ----- + // vị trí dọc của giá trị tại mốc i: theo ĐẦU MÚT mũi tên chạm vào mốc + // (mũi tên kết thúc/bắt đầu ở giữa ô -> giá trị đặt chính giữa "horizon"). + // Đầu mút mũi tên bên PHẢI mốc (nơi mũi tên huong.at(i) XUẤT PHÁT): + let mut-phai = ( + "len": bottom, "len-duoi": bottom, "len-tren": horizon, + "xuong": top, "xuong-tren": top, "xuong-duoi": horizon, + ) + // Đầu mút mũi tên bên TRÁI mốc (nơi mũi tên huong.at(i-1) KẾT THÚC): + let mut-trai = ( + "len": top, "len-tren": top, "len-duoi": horizon, + "xuong": bottom, "xuong-duoi": bottom, "xuong-tren": horizon, + ) + // Số bám theo mũi tên KHÔNG "ngang" (nét bằng/vùng gạch không định chiều): + // ưu tiên mũi tên trái, nếu trái là "ngang" thì theo mũi tên phải (và ngược + // lại) — nhờ vậy mốc kề vùng gạch, số đặt đúng đầu mút mũi tên thật. + let vi-tri-tai(i) = { + let ben-trai = if i > 0 { huong.at(i - 1) } else { none } + let ben-phai = if i < n - 1 { huong.at(i) } else { none } + if ben-trai != none and ben-trai != "ngang" { + mut-trai.at(ben-trai, default: bottom) + } else if ben-phai != none and ben-phai != "ngang" { + mut-phai.at(ben-phai, default: bottom) + } else { horizon } + } + // Chiều cao ĐO ĐƯỢC của giá trị tại mốc i, nhìn từ phía `ben` (mốc có kẹp ‖ + // mang HAI giá trị: trái = giá trị bên trái vạch, phải = bên phải). + let cao-tai(i, ben) = { + let h = h-tri.at(i) + if type(h) == array { if ben == "trai" { h.at(0) } else { h.at(1) } } else { h } + } + // TUNG ĐỘ (tính từ mép trên BĂNG nội dung cao `cao-o`, y hướng XUỐNG) của + // đầu mút mũi tên tại mốc i, nhìn từ phía `ben`. + // * Nếu giá trị THẬT SỰ nằm ở đầu mút đó -> ngắm đúng TIM chữ số (đo bằng + // `measure`) => mũi tên thẳng hàng với ký tự, lối tikz-tab. + // * Nếu không (vd hai mũi tên cùng "len" liên tiếp: số nằm ở ĐỈNH ô bên + // trái, mũi tên sau phải xuất phát từ ĐÁY ô) -> giữ sát mép như cũ. + let y-tai(i, ben) = { + let d = if ben == "trai" { + mut-trai.at(huong.at(i - 1), default: bottom) + } else { + mut-phai.at(huong.at(i), default: bottom) + } + let co-so = (i in kep) or (d == vi-tri-tai(i)) + let h = cao-tai(i, ben) + if d == top { + if co-so { h / 2 } else { 1.5pt } + } else if d == bottom { + if co-so { cao-o - h / 2 } else { cao-o - 1.5pt } + } else { cao-o / 2 } + } + let hang-f = (align(horizon + center, ten-f),) + for j in range(m) { + if calc.even(j) { + let i = int(j / 2) + let v = gia-tri.at(i) + let trai = (i - 1) in gach + let phai = i in gach + if i in kep { + let (tr, ph) = if type(v) == array { (v.at(0), v.at(1)) } else { (v, v) } + // ‖ kẻ KÍN toàn ô (cao-bt), giá trị vẫn nằm trong băng nội dung + hang-f.push(table.cell(inset: (x: inset-x, y: 0pt), o-kep( + tr, ph, + if i > 0 { mut-trai.at(huong.at(i - 1), default: bottom) } else { bottom }, + if i < n - 1 { mut-phai.at(huong.at(i), default: bottom) } else { bottom }, + cao-o, cao-kin: cao-bt, dem: inset, + ))) + } else if trai or phai { + // mốc kề vùng gạch: gạch phủ nửa ô tới đúng tim cột (thẳng với ‖ ở trên), + // giá trị dạt SÁT về phía mép gạch (trai -> căn trái, phai -> căn phải) + let doc = vi-tri-tai(i) + let ngang = if trai and phai { center } else if trai { left } else { right } + // inset nhỏ ở phía mép gạch, nhỏ luôn phía mũi tên để số gần cả hai + // (tên KHÁC `inset-x` ở đầu hàm để khỏi che nhầm) + let dem-x = if trai and phai { (x: 1pt) } + else if trai { (left: 2pt, right: 1pt) } + else { (left: 1pt, right: 2pt) } + let o-gia = block(width: 100%, height: 100%, inset: (..dem-x, y: inset), align(doc + ngang, v)) + hang-f.push(table.cell(inset: 0pt, + if trai and phai { + grid(columns: (1fr, auto, 1fr), rows: cao-bt, + nua-gach(cao-bt), o-gia, nua-gach(cao-bt)) + } else if trai { + grid(columns: (1fr, 1fr), rows: cao-bt, nua-gach(cao-bt), o-gia) + } else { + grid(columns: (1fr, 1fr), rows: cao-bt, o-gia, nua-gach(cao-bt)) + } + )) + } else { + hang-f.push(o-gia-tri(v, vi-tri-tai(i), cao-o)) + } + } else { + let k = int((j - 1) / 2) + hang-f.push( + if k in gach { o-gach-kin } + else { + // đầu mút ngắm đúng TIM giá trị hai mốc kề; "ngang" giữ nằm ngang. + // Mũi tên CHIẾM TRỌN bề ngang cột khoảng (ô inset ngang = 0) để nối + // liền hai số hai bên như tikz-tab — trước đây hụt 2·(5+3)pt mỗi bên + // nên nhìn như mũi tên "rời" khỏi số. + let ngang-hoa = huong.at(k) == "ngang" + table.cell(inset: (x: 0pt, y: inset), mui-ten-bbt( + rong-cot, cao-o, + huong: huong.at(k), mau: mau-mui-ten, + y1: if ngang-hoa { auto } else { y-tai(k, "phai") }, + y2: if ngang-hoa { auto } else { y-tai(k + 1, "trai") }, + )) + } + ) + } + } + + let so-hang = if co-dau { 3 } else { 2 } + set text(size: co-chu) + table( + columns: cols, + rows: if co-dau { (auto, cao-hang-dau, cao-bt) } else { (auto, cao-bt) }, + inset: (x: inset-x, y: inset), + align: center + horizon, + stroke: (x, y) => ( + left: if x <= 1 { 0.7pt } else { none }, + right: if x == m { 0.7pt } else { none }, + top: 0.7pt, + bottom: if y == so-hang - 1 { 0.7pt } else { none }, + ), + ..hang-x, + ..hang-dau, + ..hang-f, + ) +} + +// ---------- BẢNG XÉT DẤU ---------- +// dong: mảng các cặp (ten, mang-dau) — mỗi mảng dấu dài 2n-1. +#let bang-xet-dau( + x: (), + dong: (), + gach: (), // khoảng không xác định -> gạch chéo + ten-x: $x$, + rong-cot: 2.2cm, + co-chu: 11pt, +) = { + let n = x.len() + let m = 2 * n - 1 + let x = x.map(_chuan-o) + let cols = (auto,) + range(m).map(j => if calc.odd(j) { rong-cot } else { auto }) + // hàng cao cố định để kẹp ‖ và dải gạch phủ kín + let cao-hang = co-chu * 1.3 + 14pt + let o-gach-kin = table.cell(inset: 0pt, layout(sz => o-gach(sz.width, sz.height))) + let nua-gach = layout(sz => o-gach(sz.width, cao-hang)) + + let cells = (align(horizon + center, ten-x),) + for j in range(m) { + cells.push(if calc.even(j) { align(horizon + center, x.at(int(j / 2))) } else { none }) + } + for (ten, mang) in dong { + // tự bù 2 ô trống hai đầu cho từng dòng dấu + let mang = _chuan-day-dau(mang, m, ten: "mảng dấu của dòng") + cells.push(align(horizon + center, ten)) + for j in range(m) { + if calc.odd(j) { + cells.push( + if int((j - 1) / 2) in gach { o-gach-kin } + else { align(horizon + center, ky-hieu-dau(mang.at(j))) } + ) + } else { + let i = int(j / 2) + let trai = (i - 1) in gach + let phai = i in gach + cells.push( + if trai or phai { + let giua = if mang.at(j) == "||" { kep-vach(cao-hang) } else { + block(height: 100%, inset: (x: 2.5pt), align(horizon + center, ky-hieu-dau(mang.at(j)))) + } + table.cell(inset: 0pt, grid( + columns: (1fr, auto, 1fr), + rows: cao-hang, + if trai { nua-gach } else { none }, + giua, + if phai { nua-gach } else { none }, + )) + } else if mang.at(j) == "||" { + // ‖ kẻ KÍN từ mép trên tới mép dưới ô (bỏ đệm dọc) + table.cell(inset: (x: 5pt, y: 0pt), kep-vach(cao-hang)) + } else { align(horizon + center, ky-hieu-dau(mang.at(j))) } + ) + } + } + } + + set text(size: co-chu) + table( + columns: cols, + rows: cao-hang, + inset: (x: 5pt, y: 7pt), + align: center + horizon, + stroke: (x, y) => ( + left: if x <= 1 { 0.7pt } else { none }, + right: if x == m { 0.7pt } else { none }, + top: 0.7pt, + bottom: if y == dong.len() { 0.7pt } else { none }, + ), + ..cells, + ) +} + +// ---------- CÁC BẢNG DỰNG SẴN THƯỜNG GẶP ---------- +// +// FORM MỚI (07/2026) — nhập HỆ SỐ (positional), thuật toán tự tìm cực trị/ +// tiệm cận/chiều biến thiên, phủ MỌI trường hợp, nhãn đẹp qua `so-toan`: +// #bbt-bac-hai(1, -2, 3) y = x² − 2x + 3 +// #bbt-bac-ba(1, 0, -3, 1) y = x³ − 3x + 1 (2 cực trị/y'=0 kép/đơn điệu) +// #bbt-trung-phuong(1, -2, 0) y = x⁴ − 2x² (3 hoặc 1 cực trị) +// #bbt-phan-thuc(2, -1, 1, -1) y = (2x−1)/(x−1) (tự xét dấu ad−bc) +// #bbt-huu-ti(1, -1, 1, 1, -1) y = (x²−x+1)/(x−1) (2 cực trị/đơn điệu) +// FORM CŨ (named: a:, x1:, y1:, x0:, yc:, dong-bien:, ...) vẫn chạy song song. + +// eps so sánh 0 (hệ số thường là số "đẹp") +#let _eps-bbt = 0.000001 + +// BBT hàm bậc hai y = ax² + bx + c. +// Form mới: bbt-bac-hai(a, b, c). Form cũ: bbt-bac-hai(a: 1, xd: .., yd: ..). +#let bbt-bac-hai(..he-so, a: 1, xd: $(-b)/(2a)$, yd: $(-Delta)/(4a)$, ten-f: $y$, ten-fp: $y'$) = { + let hs = he-so.pos() + let (a, xd, yd) = if hs.len() == 3 { + let (a, b, c) = hs + assert(calc.abs(a) > _eps-bbt, message: "bbt-bac-hai: cần a ≠ 0") + (a, so-toan(-b / (2 * a)), so-toan(c - b * b / (4 * a))) + } else { (a, xd, yd) } + if a > 0 { + bbt( + x: ($-oo$, xd, $+oo$), + dau: ("", "-", "0", "+", ""), + gia-tri: ($+oo$, yd, $+oo$), + huong: ("xuong", "len"), + ten-f: ten-f, ten-fp: ten-fp, + ) + } else { + bbt( + x: ($-oo$, xd, $+oo$), + dau: ("", "+", "0", "-", ""), + gia-tri: ($-oo$, yd, $-oo$), + huong: ("len", "xuong"), + ten-f: ten-f, ten-fp: ten-fp, + ) + } +} + +// BBT hàm y = √(ax² + bx + c) — CHỈ CÓ form hệ số. Đặt g(x) = ax² + bx + c, +// Δ = b² − 4ac, đỉnh x₀ = −b/(2a), giá trị đỉnh y₀ = √((4ac − b²)/(4a)). +// Tự phân 3 trường hợp: +// a > 0, Δ > 0 (g có 2 nghiệm): TXĐ = (−∞, x₁] ∪ [x₂, +∞); nghịch biến rồi +// (khoảng giữa gạch chéo — ngoài TXĐ) đồng biến; giá trị 2 nghiệm = 0. +// a > 0, Δ ≤ 0 (nghiệm kép / vô nghiệm): TXĐ = ℝ; giảm rồi tăng, cực tiểu tại +// đỉnh. Δ < 0 → cực tiểu y₀ > 0, y' = 0 tại đỉnh; Δ = 0 → y = √a·|x − x₀|, +// cực tiểu 0, y' KHÔNG xác định tại đỉnh (ghi ‖). +// a < 0, Δ > 0 (g có 2 nghiệm): TXĐ = [x₁, x₂]; đồng biến rồi nghịch biến, +// cực đại y₀ tại đỉnh, giá trị 2 nghiệm = 0. (a < 0 mà Δ ≤ 0 → TXĐ rỗng.) +#let bbt-can-bac-hai-ham-bac-hai(a, b, c, ten-f: $y$, ten-fp: $y'$) = { + assert(calc.abs(a) > _eps-bbt, message: "bbt-can-bac-hai-ham-bac-hai: cần a ≠ 0") + let delta = b * b - 4 * a * c + let x-dinh = so-toan(-b / (2 * a)) + // giá trị tại đỉnh: y₀ = √((4ac − b²)/(4a)) (đúng cho cả a≷0 khi TXĐ ≠ ∅) + let y-dinh = so-can-thuc(0, 1, (4 * a * c - b * b) / (4 * a), 1) + if a > 0 { + if delta > _eps-bbt { + // 2 nghiệm: gạch khoảng giữa (ngoài TXĐ), x₁ < x₂ + let x1 = so-can-thuc(-b, -1, delta, 2 * a) + let x2 = so-can-thuc(-b, 1, delta, 2 * a) + bbt( + x: ($-oo$, x1, x2, $+oo$), + dau: ("", "-", "||", "", "||", "+", ""), + gia-tri: ($+oo$, $0$, $0$, $+oo$), + huong: ("xuong", "ngang", "len"), + gach: (1,), + ten-f: ten-f, ten-fp: ten-fp, + ) + } else if delta < -_eps-bbt { + // vô nghiệm: TXĐ = ℝ, cực tiểu y₀ tại đỉnh (y' = 0) + bbt( + x: ($-oo$, x-dinh, $+oo$), + dau: ("", "-", "0", "+", ""), + gia-tri: ($+oo$, y-dinh, $+oo$), + huong: ("xuong", "len"), + ten-f: ten-f, ten-fp: ten-fp, rong-cot: 3.2cm, + ) + } else { + // nghiệm kép: y = √a·|x − x₀|, cực tiểu 0, y' không xác định tại đỉnh (‖) + bbt( + x: ($-oo$, x-dinh, $+oo$), + dau: ("", "-", "||", "+", ""), + gia-tri: ($+oo$, $0$, $+oo$), + huong: ("xuong", "len"), + ten-f: ten-f, ten-fp: ten-fp, rong-cot: 3.2cm, + ) + } + } else { + // a < 0: TXĐ = [x₁, x₂] chỉ khi Δ > 0 + assert(delta > _eps-bbt, message: "bbt-can-bac-hai-ham-bac-hai: a < 0 thì cần Δ > 0 (nếu không TXĐ rỗng)") + let x1 = so-can-thuc(-b, 1, delta, 2 * a) + let x2 = so-can-thuc(-b, -1, delta, 2 * a) + bbt( + x: (x1, x-dinh, x2), + dau: ("", "+", "0", "-", ""), + gia-tri: ($0$, y-dinh, $0$), + huong: ("len", "xuong"), + ten-f: ten-f, ten-fp: ten-fp, rong-cot: 3.2cm, + ) + } +} + +// BBT hàm bậc ba. +// Form mới: bbt-bac-ba(a, b, c, d) — tự phân 3 trường hợp theo Δ' = b² − 3ac: +// Δ' > 0: 2 cực trị | Δ' = 0: y' = 0 kép (vẫn đơn điệu, có mốc) | Δ' < 0: đơn điệu. +// Form cũ: bbt-bac-ba(a: 1, x1:, y1:, x2:, y2:) — luôn coi là có 2 cực trị. +#let bbt-bac-ba( + ..he-so, + a: 1, + x1: $x_1$, y1: $y_(c d)$, + x2: $x_2$, y2: $y_(c t)$, + ten-f: $y$, ten-fp: $y'$, +) = { + let hs = he-so.pos() + if hs.len() == 4 { + let (a, b, c, d) = hs + assert(calc.abs(a) > _eps-bbt, message: "bbt-bac-ba: cần a ≠ 0") + let f = x => ((a * x + b) * x + c) * x + d + let delta = b * b - 3 * a * c + if delta > _eps-bbt { + // 2 cực trị — giá trị CHÍNH XÁC dạng căn thức (cuc-tri-bac-ba) + let ct = cuc-tri-bac-ba(a, b, c, d) + return bbt-bac-ba( + a: a, + x1: ct.x1, y1: ct.y1, + x2: ct.x2, y2: ct.y2, + ten-f: ten-f, ten-fp: ten-fp, + ) + } + if delta < -_eps-bbt { + // y' vô nghiệm: đơn điệu, không mốc + return if a > 0 { + bbt( + x: ($-oo$, $+oo$), dau: ("", "+", ""), + gia-tri: ($-oo$, $+oo$), huong: ("len",), + ten-f: ten-f, ten-fp: ten-fp, rong-cot: 5cm, + ) + } else { + bbt( + x: ($-oo$, $+oo$), dau: ("", "-", ""), + gia-tri: ($+oo$, $-oo$), huong: ("xuong",), + ten-f: ten-f, ten-fp: ten-fp, rong-cot: 5cm, + ) + } + } + // Δ' = 0: nghiệm kép x₀ = −b/(3a), vẫn đơn điệu nhưng ghi mốc y' = 0. + // Hai mũi tên NỬA Ô (đáy→giữa rồi giữa→đỉnh) nằm trên CÙNG 1 đường thẳng + // đi qua giá trị mốc đặt chính giữa ô. + let t = -b / (3 * a) + let y-moc = so-can-thuc(2 * b * b * b - 9 * a * b * c + 27 * a * a * d, 0, 0, 27 * a * a) + let (dau0, huong2, dau-vc) = if a > 0 { + ("+", ("len-duoi", "len-tren"), ($-oo$, $+oo$)) + } else { + ("-", ("xuong-tren", "xuong-duoi"), ($+oo$, $-oo$)) + } + return bbt( + x: ($-oo$, so-toan(t), $+oo$), + dau: ("", dau0, "0", dau0, ""), + gia-tri: (dau-vc.at(0), y-moc, dau-vc.at(1)), + huong: huong2, + ten-f: ten-f, ten-fp: ten-fp, rong-cot: 3.2cm, + ) + } + // ----- form cũ: 2 cực trị nhập tay ----- + if a > 0 { + bbt( + x: ($-oo$, x1, x2, $+oo$), + dau: ("", "+", "0", "-", "0", "+", ""), + gia-tri: ($-oo$, y1, y2, $+oo$), + huong: ("len", "xuong", "len"), + ten-f: ten-f, ten-fp: ten-fp, + ) + } else { + bbt( + x: ($-oo$, x1, x2, $+oo$), + dau: ("", "-", "0", "+", "0", "-", ""), + gia-tri: ($+oo$, y1, y2, $-oo$), + huong: ("xuong", "len", "xuong"), + ten-f: ten-f, ten-fp: ten-fp, + ) + } +} + +// BBT hàm bậc ba đơn điệu (y' không đổi dấu). +#let bbt-bac-ba-don-dieu(a: 1, ten-f: $y$, ten-fp: $y'$) = { + if a > 0 { + bbt( + x: ($-oo$, $+oo$), + dau: ("", "+", ""), + gia-tri: ($-oo$, $+oo$), + huong: ("len",), + ten-f: ten-f, ten-fp: ten-fp, + rong-cot: 5cm, + ) + } else { + bbt( + x: ($-oo$, $+oo$), + dau: ("", "-", ""), + gia-tri: ($+oo$, $-oo$), + huong: ("xuong",), + ten-f: ten-f, ten-fp: ten-fp, + rong-cot: 5cm, + ) + } +} + +// BBT hàm trùng phương y = ax⁴ + bx² + c. +// Form mới: bbt-trung-phuong(a, b, c) — tự phân 2 trường hợp: +// a·b < 0: 3 cực trị tại ±√(−b/2a) và 0 | còn lại: 1 cực trị tại 0. +// Form cũ: bbt-trung-phuong(a: 1, x0:, yc:, y0:) — luôn coi là 3 cực trị. +#let bbt-trung-phuong( + ..he-so, + a: 1, + x0: $x_0$, // = √(-b/2a) + yc: $y_1$, // giá trị tại ±x0 + y0: $y_0$, // giá trị tại 0 + ten-f: $y$, ten-fp: $y'$, +) = { + let hs = he-so.pos() + if hs.len() == 3 { + let (a, b, c) = hs + assert(calc.abs(a) > _eps-bbt, message: "bbt-trung-phuong: cần a ≠ 0") + if a * b < -_eps-bbt { + // 3 cực trị — x0 và giá trị cực trị CHÍNH XÁC (kể cả căn thức/phân số) + return bbt-trung-phuong( + a: a, + x0: so-can-thuc(0, 1, -b / (2 * a), 1), + yc: so-can-thuc(4 * a * c - b * b, 0, 0, 4 * a), + y0: so-toan(c), + ten-f: ten-f, ten-fp: ten-fp, + ) + } + // 1 cực trị tại x = 0 (b = 0 hoặc a·b > 0) + return if a > 0 { + bbt( + x: ($-oo$, $0$, $+oo$), + dau: ("", "-", "0", "+", ""), + gia-tri: ($+oo$, so-toan(c), $+oo$), + huong: ("xuong", "len"), + ten-f: ten-f, ten-fp: ten-fp, rong-cot: 3.2cm, + ) + } else { + bbt( + x: ($-oo$, $0$, $+oo$), + dau: ("", "+", "0", "-", ""), + gia-tri: ($-oo$, so-toan(c), $-oo$), + huong: ("len", "xuong"), + ten-f: ten-f, ten-fp: ten-fp, rong-cot: 3.2cm, + ) + } + } + // ----- form cũ: 3 cực trị nhập tay ----- + if a > 0 { + bbt( + x: ($-oo$, $-$ + x0, $0$, x0, $+oo$), + dau: ("", "-", "0", "+", "0", "-", "0", "+", ""), + gia-tri: ($+oo$, yc, y0, yc, $+oo$), + huong: ("xuong", "len", "xuong", "len"), + ten-f: ten-f, ten-fp: ten-fp, + rong-cot: 2.1cm, + ) + } else { + bbt( + x: ($-oo$, $-$ + x0, $0$, x0, $+oo$), + dau: ("", "+", "0", "-", "0", "+", "0", "-", ""), + gia-tri: ($-oo$, yc, y0, yc, $-oo$), + huong: ("len", "xuong", "len", "xuong"), + ten-f: ten-f, ten-fp: ten-fp, + rong-cot: 2.1cm, + ) + } +} + +// BBT hàm phân thức y = (ax+b)/(cx+d). +// Form mới: bbt-phan-thuc(a, b, c, d) — tự tính x₀ = −d/c, y₀ = a/c, +// chiều biến thiên theo dấu ad − bc. +// Form cũ: bbt-phan-thuc(x0:, y0:, dong-bien:). +#let bbt-phan-thuc( + ..he-so, + x0: $x_0$, y0: $y_0$, + dong-bien: true, // y' > 0 trên từng khoảng? + ten-f: $y$, ten-fp: $y'$, +) = { + let hs = he-so.pos() + let (x0, y0, dong-bien) = if hs.len() == 4 { + let (a, b, c, d) = hs + assert(calc.abs(c) > _eps-bbt, message: "bbt-phan-thuc: cần c ≠ 0") + let D = a * d - b * c + assert(calc.abs(D) > _eps-bbt, message: "bbt-phan-thuc: ad − bc = 0 (hàm hằng, không có BBT)") + (so-toan(-d / c), so-toan(a / c), D > 0) + } else { (x0, y0, dong-bien) } + if dong-bien { + bbt( + x: ($-oo$, x0, $+oo$), + dau: ("", "+", "||", "+", ""), + gia-tri: (y0, ($+oo$, $-oo$), y0), + huong: ("len", "len"), + kep: (1,), + ten-f: ten-f, ten-fp: ten-fp, + rong-cot: 3.2cm, + ) + } else { + bbt( + x: ($-oo$, x0, $+oo$), + dau: ("", "-", "||", "-", ""), + gia-tri: (y0, ($-oo$, $+oo$), y0), + huong: ("xuong", "xuong"), + kep: (1,), + ten-f: ten-f, ten-fp: ten-fp, + rong-cot: 3.2cm, + ) + } +} + +// BBT hàm hữu tỉ y = (ax² + bx + c)/(dx + e) — CHỈ CÓ form hệ số. +// Viết y = px + q + r/(dx + e) với p = a/d, q = (bd − ae)/d², r = c − qe. +// Tự phân 3 trường hợp: p·r·d > 0: 2 cực trị hai bên tiệm cận đứng | +// p·r·d < 0: đơn điệu (2 khoảng) | r = 0: suy biến (đường thẳng khuyết điểm). +#let bbt-huu-ti(a, b, c, d, e, ten-f: $y$, ten-fp: $y'$) = { + assert(calc.abs(a) > _eps-bbt and calc.abs(d) > _eps-bbt, message: "bbt-huu-ti: cần a ≠ 0 và d ≠ 0") + let x0 = -e / d + let p = a / d + let q = (b * d - a * e) / (d * d) + let r = c - q * e + let f = x => (a * x * x + b * x + c) / (d * x + e) + if p * r * d > _eps-bbt { + // 2 cực trị — hoành độ & giá trị CHÍNH XÁC dạng căn thức (cuc-tri-huu-ti) + let ct = cuc-tri-huu-ti(a, b, c, d, e) + return if p > 0 { + bbt( + x: ($-oo$, ct.x1, so-toan(x0), ct.x2, $+oo$), + dau: ("", "+", "0", "-", "||", "-", "0", "+", ""), + gia-tri: ($-oo$, ct.y1, ($-oo$, $+oo$), ct.y2, $+oo$), + huong: ("len", "xuong", "xuong", "len"), + kep: (2,), + ten-f: ten-f, ten-fp: ten-fp, rong-cot: 2.1cm, + ) + } else { + bbt( + x: ($-oo$, ct.x1, so-toan(x0), ct.x2, $+oo$), + dau: ("", "-", "0", "+", "||", "+", "0", "-", ""), + gia-tri: ($+oo$, ct.y1, ($+oo$, $-oo$), ct.y2, $-oo$), + huong: ("xuong", "len", "len", "xuong"), + kep: (2,), + ten-f: ten-f, ten-fp: ten-fp, rong-cot: 2.1cm, + ) + } + } + // đơn điệu trên từng khoảng. Giới hạn tại x₀: trái = −sign(rd)·∞, + // phải = +sign(rd)·∞; riêng r = 0 (suy biến): 2 bên hữu hạn = px₀ + q. + let (trai, phai) = if calc.abs(r) < _eps-bbt { + let l = so-toan(p * x0 + q) + (l, l) + } else if r * d > 0 { ($-oo$, $+oo$) } else { ($+oo$, $-oo$) } + if p > 0 { + bbt( + x: ($-oo$, so-toan(x0), $+oo$), + dau: ("", "+", "||", "+", ""), + gia-tri: ($-oo$, (trai, phai), $+oo$), + huong: ("len", "len"), + kep: (1,), + ten-f: ten-f, ten-fp: ten-fp, rong-cot: 3.2cm, + ) + } else { + bbt( + x: ($-oo$, so-toan(x0), $+oo$), + dau: ("", "-", "||", "-", ""), + gia-tri: ($+oo$, (trai, phai), $-oo$), + huong: ("xuong", "xuong"), + kep: (1,), + ten-f: ten-f, ten-fp: ten-fp, rong-cot: 3.2cm, + ) + } +} + +// Bảng xét dấu tam thức bậc hai có 2 nghiệm (dấu theo a). +// (form hệ số: xem các bbt-* phía trên) +#let xet-dau-tam-thuc(a: 1, x1: $x_1$, x2: $x_2$, ten: $f(x)$) = { + let (ngoai, trong) = if a > 0 { ("+", "-") } else { ("-", "+") } + bang-xet-dau( + x: ($-oo$, x1, x2, $+oo$), + dong: ((ten, ("", ngoai, "0", trong, "0", ngoai, "")),), + ) +} +// (đồng bộ mount 07/2026) diff --git a/packages/preview/conic-toan/0.3.4/lib/bieu-do-thong-ke.typ b/packages/preview/conic-toan/0.3.4/lib/bieu-do-thong-ke.typ new file mode 100644 index 0000000000..35200c5bbb --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/bieu-do-thong-ke.typ @@ -0,0 +1,542 @@ +// ===================================================================== +// bieu-do-thong-ke.typ — BIỂU ĐỒ THỐNG KÊ (CT GDPT 2018) +// +// #import "bieu-do-thong-ke.typ": * +// +// 5 biểu đồ dựng sẵn (KHÔNG nhận ctx — tự dựng khung như truc-so; +// vẽ chồng qua `them: ctx => ...` như mọi hình khác): +// bieu-do-tan-so : HISTOGRAM tần số ghép nhóm — cột DÍNH SÁT nhau +// (kèm `gap-khuc: true` vẽ chồng đường gấp khúc tần số) +// da-giac-tan-so : đường gấp khúc tần số (ogive) đứng riêng +// bieu-do-cot : biểu đồ cột RỜI cho giá trị rời rạc (số hoặc chữ) +// bieu-do-hop : biểu đồ hộp (box plot) — tứ phân vị Toán 10 +// bieu-do-quat : biểu đồ quạt tròn (pie chart) — chia góc theo % +// +// 2 hàm TRẢ GIÁ TRỊ (không vẽ): +// tu-phan-vi(du-lieu) -> (min:, q1:, q2:, q3:, max:) [SGK 10] +// tu-phan-vi-ghep-nhom(moc, tan-so) -> (q1:, q2:, q3:) [SGK 11] +// +// Dữ liệu vào KHỚP với bang-thong-ke.typ: cùng `moc:` (n+1 mốc -> n nhóm) +// và `tan-so:` — một nguồn số liệu dùng được cả bảng lẫn biểu đồ. +// ===================================================================== +#import "ve.typ": * +#import "do-thi.typ": so-dep, so-toan + +// Bảng màu mặc định cho biểu đồ quạt/cột. +#let _mau-bd = ( + rgb("#4e79a7"), rgb("#f28e2b"), rgb("#e15759"), rgb("#76b7b4"), + rgb("#59a14f"), rgb("#edc949"), rgb("#b07aa1"), rgb("#9c755f"), +) + +// Bước chia "đẹp" 1/2/5·10^k cho trục tung (~muc vạch). +#let _buoc-dep(vmax, muc: 5) = { + let raw = calc.max(vmax, 1e-9) / muc + let k = calc.floor(calc.log(raw)) + let p = calc.pow(10.0, k) + let r = raw / p + let m = if r <= 1 { 1 } else if r <= 2 { 2 } else if r <= 5 { 5 } else { 10 } + m * p +} + +// Chỗ trống phải nới thêm bên PHẢI khung để đặt TÊN TRỤC HOÀNH cạnh mũi tên. +// Trước đây nhãn đặt "above-left" ngay tại đầu mũi tên nên nằm ĐÈ LÊN biểu đồ +// (trục vẽ xuyên qua chữ). Nay nhãn lui hẳn sang phải đầu mũi tên, và khung +// được nới đúng bằng bề rộng chữ để nhãn không tràn ra ngoài hình. +// TRẢ VỀ số ĐƠN VỊ TOẠ ĐỘ cần cộng thêm vào xmax. PHẢI gọi trong `context`. +#let _cho-ten-x(ten-x, w, base, co-chu: 10pt, cach: 5pt) = { + if ten-x == none { return 0 } + let s = measure(text(size: co-chu, ten-x)) + let k = calc.min((s.width + cach) / w, 0.55) + base * k / (1 - k) +} + +// Trục Ox + Oy (mũi tên), nhãn tên trục. +#let _truc-bd(ctx, x0, xb, ytop, ten-x, ten-y) = { + doan(ctx, (x0, 0), (xb, 0), day: 0.9pt) + dau-mui-ten(ctx, (x0, 0), (xb, 0)) + doan(ctx, (x0, 0), (x0, ytop), day: 0.9pt) + dau-mui-ten(ctx, (x0, 0), (x0, ytop)) + if ten-x != none { nhan(ctx, (xb, 0), ten-x, huong: "right", cach: 5pt) } + if ten-y != none { nhan(ctx, (x0, ytop), ten-y, huong: "right", cach: 7pt) } +} + +// Vạch chia + số trên trục tung (tại k·buoc) và các đường lưới ngang mờ. +#let _vach-tung(ctx, x0, xb, buoc, vmax, so: true, luoi: false) = { + let dx = 2.6pt / ctx.sx + let k = 1 + while k * buoc <= vmax * 1.02 + buoc * 0.0001 { + let y = k * buoc + if luoi { doan(ctx, (x0, y), (xb, y), mau: luma(86%), day: 0.5pt) } + doan(ctx, (x0 - dx, y), (x0 + dx, y), day: 0.8pt) + if so { nhan(ctx, (x0, y), [#so-dep(y)], huong: "left", cach: 5pt) } + k += 1 + } +} + +// Vạch chia + số trên trục hoành tại các hoành độ xs. +#let _vach-hoanh(ctx, xs, so: true) = { + let dy = 2.6pt / ctx.sy + for x in xs { + doan(ctx, (x, -dy), (x, dy), day: 0.8pt) + if so { nhan(ctx, (x, 0), [#so-dep(x)], huong: "below", cach: 5pt) } + } +} + +// ===================================================================== +// HISTOGRAM — biểu đồ tần số ghép nhóm, cột dính sát nhau. +// moc : n+1 mốc SỐ (như bang-ghep-nhom) -> n cột +// tan-so : n tần số (hoặc tần số tương đối — đổi ten-y cho khớp) +// gap-khuc: true -> vẽ chồng đường gấp khúc tần số (nối trung điểm đỉnh cột, +// kéo dài về 0 ở hai nhóm ảo ngoài cùng) +// ===================================================================== +#let bieu-do-tan-so( + moc: (), + tan-so: (), + w: 9cm, h: 5.5cm, + mau: rgb(78, 121, 167, 160), // màu tô cột + vien: rgb("#2f5d8a"), // màu viền cột + ten-x: none, // nhãn trục hoành (vd [Chiều cao (cm)]) + ten-y: [Tần số], + so-dinh: true, // ghi tần số trên đỉnh mỗi cột + so-truc: true, // số trên 2 trục + buoc-y: auto, // bước chia trục tung (auto = 1/2/5·10^k) + luoi-ngang: false, // lưới ngang mờ theo vạch trục tung + gap-khuc: false, + mau-gap-khuc: red, + co-chu: 10pt, + them: none, +) = { + let n = tan-so.len() + let m0 = moc.first() + let mn = moc.last() + let fmax = calc.max(..tan-so) + let by = if buoc-y == auto { _buoc-dep(fmax) } else { buoc-y } + let b1 = moc.at(1) - m0 + let bn = mn - moc.at(n - 1) + let x0 = m0 - b1 * 0.55 // trục tung đặt hụt sang trái nửa nhóm + let xb = mn + bn * 0.7 + let ytop = calc.max(fmax * 1.18, by) + let xa = x0 - (xb - x0) * 0.02 + context { + hinh( + w: w, h: h, + xmin: xa, xmax: xb + _cho-ten-x(ten-x, w, xb - xa, co-chu: co-chu), + ymin: -ytop * 0.16, ymax: ytop * 1.06, + co-chu: co-chu, + ctx => { + if luoi-ngang { _vach-tung(ctx, x0, xb, by, fmax, so: false, luoi: true) } + // các cột dính sát nhau + for i in range(n) { + let (a, b, f) = (moc.at(i), moc.at(i + 1), tan-so.at(i)) + da-giac(ctx, ((a, 0), (b, 0), (b, f), (a, f)), to: mau, mau: vien, day: 0.9pt) + if so-dinh and f > 0 { + nhan(ctx, ((a + b) / 2, f), [#so-dep(f)], huong: "above", cach: 4pt) + } + } + // đường gấp khúc tần số chồng lên (kéo về 0 ở 2 nhóm ảo ngoài cùng) + if gap-khuc { + let pts = ((m0 - b1 / 2, 0),) + for i in range(n) { + pts.push(((moc.at(i) + moc.at(i + 1)) / 2, tan-so.at(i))) + } + pts.push((mn + bn / 2, 0)) + duong-gap-khuc(ctx, pts, mau: mau-gap-khuc, day: 1.1pt) + for p in pts { diem(ctx, p, mau: mau-gap-khuc, bk: 1.8pt) } + } + _truc-bd(ctx, x0, xb, ytop, ten-x, ten-y) + _vach-tung(ctx, x0, xb, by, fmax, so: so-truc) + _vach-hoanh(ctx, moc, so: so-truc) + if them != none { them(ctx) } + }, + ) + } +} + +// ===================================================================== +// ĐƯỜNG GẤP KHÚC TẦN SỐ (ogive) đứng riêng — nối trung điểm các nhóm. +// keo-dai: true -> nối thêm về 0 tại trung điểm 2 nhóm ảo ngoài cùng (SGK) +// ===================================================================== +#let da-giac-tan-so( + moc: (), + tan-so: (), + w: 9cm, h: 5.5cm, + mau: rgb("#1d4ed8"), + ten-x: none, + ten-y: [Tần số], + so-dinh: true, + so-truc: true, + buoc-y: auto, + luoi-ngang: false, + keo-dai: true, + co-chu: 10pt, + them: none, +) = { + let n = tan-so.len() + let m0 = moc.first() + let mn = moc.last() + let fmax = calc.max(..tan-so) + let by = if buoc-y == auto { _buoc-dep(fmax) } else { buoc-y } + let b1 = moc.at(1) - m0 + let bn = mn - moc.at(n - 1) + let x0 = m0 - b1 * 0.55 + let xb = mn + bn * 0.7 + let ytop = calc.max(fmax * 1.18, by) + let xa = x0 - (xb - x0) * 0.02 + context { + hinh( + w: w, h: h, + xmin: xa, xmax: xb + _cho-ten-x(ten-x, w, xb - xa, co-chu: co-chu), + ymin: -ytop * 0.16, ymax: ytop * 1.06, + co-chu: co-chu, + ctx => { + if luoi-ngang { _vach-tung(ctx, x0, xb, by, fmax, so: false, luoi: true) } + let giua = range(n).map(i => ((moc.at(i) + moc.at(i + 1)) / 2, tan-so.at(i))) + let pts = if keo-dai { + ((m0 - b1 / 2, 0),) + giua + ((mn + bn / 2, 0),) + } else { giua } + duong-gap-khuc(ctx, pts, mau: mau, day: 1.1pt) + for p in pts { diem(ctx, p, mau: mau, bk: 1.9pt) } + if so-dinh { + for (i, p) in giua.enumerate() { + if tan-so.at(i) > 0 { nhan(ctx, p, [#so-dep(p.at(1))], huong: "above", cach: 5pt, mau: mau) } + } + } + _truc-bd(ctx, x0, xb, ytop, ten-x, ten-y) + _vach-tung(ctx, x0, xb, by, fmax, so: so-truc) + _vach-hoanh(ctx, moc, so: so-truc) + if them != none { them(ctx) } + }, + ) + } +} + +// ===================================================================== +// BIỂU ĐỒ CỘT RỜI — giá trị rời rạc (số HOẶC nhãn chữ), cột cách nhau. +// gia-tri: mảng số (đặt cột đúng hoành độ) hoặc mảng nội dung/chuỗi +// (cột đặt đều tại 1, 2, 3, ...) +// rong : bề rộng cột theo tỉ lệ khoảng cách nhỏ nhất giữa 2 cột +// ===================================================================== +#let bieu-do-cot( + gia-tri: (), + tan-so: (), + w: 9cm, h: 5.5cm, + mau: rgb(78, 121, 167, 160), + vien: rgb("#2f5d8a"), + ten-x: none, + ten-y: [Tần số], + rong: 0.5, + so-dinh: true, + so-truc: true, + buoc-y: auto, + luoi-ngang: false, + co-chu: 10pt, + them: none, +) = { + let n = tan-so.len() + let bang-so = gia-tri.all(v => type(v) == int or type(v) == float) + let xs = if bang-so { gia-tri } else { range(1, n + 1) } + // khoảng cách nhỏ nhất giữa 2 cột kề nhau + let kc = if n >= 2 { + calc.min(..range(n - 1).map(i => xs.at(i + 1) - xs.at(i))) + } else { 1 } + let nua = kc * rong / 2 + let fmax = calc.max(..tan-so) + let by = if buoc-y == auto { _buoc-dep(fmax) } else { buoc-y } + let x0 = xs.first() - kc * 0.8 + let xb = xs.last() + kc * 0.8 + let ytop = calc.max(fmax * 1.18, by) + let xa = x0 - (xb - x0) * 0.02 + context { + hinh( + w: w, h: h, + xmin: xa, xmax: xb + _cho-ten-x(ten-x, w, xb - xa, co-chu: co-chu), + ymin: -ytop * 0.16, ymax: ytop * 1.06, + co-chu: co-chu, + ctx => { + if luoi-ngang { _vach-tung(ctx, x0, xb, by, fmax, so: false, luoi: true) } + for i in range(n) { + let (x, f) = (xs.at(i), tan-so.at(i)) + da-giac(ctx, ((x - nua, 0), (x + nua, 0), (x + nua, f), (x - nua, f)), + to: mau, mau: vien, day: 0.9pt) + if so-dinh and f > 0 { nhan(ctx, (x, f), [#so-dep(f)], huong: "above", cach: 4pt) } + } + _truc-bd(ctx, x0, xb, ytop, ten-x, ten-y) + _vach-tung(ctx, x0, xb, by, fmax, so: so-truc) + // nhãn chân cột: số -> so-dep, chữ -> in nguyên + let dy = 2.6pt / ctx.sy + for (i, x) in xs.enumerate() { + doan(ctx, (x, -dy), (x, dy), day: 0.8pt) + if so-truc { + let nd = if bang-so { [#so-dep(x)] } else { [#gia-tri.at(i)] } + nhan(ctx, (x, 0), nd, huong: "below", cach: 5pt) + } + } + if them != none { them(ctx) } + }, + ) + } +} + +// ===================================================================== +// TỨ PHÂN VỊ (số liệu KHÔNG ghép nhóm — SGK Toán 10): +// sắp xếp tăng; Q2 = trung vị; Q1/Q3 = trung vị nửa dưới/nửa trên +// (n lẻ thì KHÔNG kể Q2 vào hai nửa). +// ===================================================================== +#let _trung-vi(d) = { + let n = d.len() + if calc.rem(n, 2) == 1 { d.at(int((n - 1) / 2)) } + else { (d.at(int(n / 2) - 1) + d.at(int(n / 2))) / 2 } +} + +#let tu-phan-vi(du-lieu) = { + let d = du-lieu.sorted() + let n = d.len() + let nd = int(calc.floor(n / 2)) + ( + min: d.first(), + q1: _trung-vi(d.slice(0, nd)), + q2: _trung-vi(d), + q3: _trung-vi(d.slice(n - nd)), + max: d.last(), + ) +} + +// Tứ phân vị của mẫu GHÉP NHÓM (SGK Toán 11): nội suy trong nhóm chứa Qk, +// Qk = L + (k·n/4 − cf)/f · (R − L). +#let tu-phan-vi-ghep-nhom(moc, tan-so) = { + let n = tan-so.sum() + let q(k) = { + let muc = k * n / 4 + let cf = 0 + for i in range(tan-so.len()) { + let f = tan-so.at(i) + if f > 0 and cf + f >= muc { + return moc.at(i) + (muc - cf) / f * (moc.at(i + 1) - moc.at(i)) + } + cf += f + } + moc.last() + } + (q1: q(1), q2: q(2), q3: q(3)) +} + +// ===================================================================== +// BIỂU ĐỒ HỘP (box plot) — nằm ngang trên trục số, gióng đứt xuống trục. +// du-lieu: mảng số thô (tự tính 5 số tóm tắt theo SGK 10), HOẶC +// tom-tat: (min, q1, q2, q3, max) — mảng 5 số hay dict cùng khoá. +// ===================================================================== +#let bieu-do-hop( + du-lieu: none, + tom-tat: none, + w: 10cm, h: 3.9cm, + mau: rgb(78, 121, 167, 90), + vien: rgb("#2f5d8a"), + ten: $x$, + so: true, // ghi 5 giá trị dưới trục + co-chu: 10pt, + them: none, +) = { + let t = if du-lieu != none { tu-phan-vi(du-lieu) } + else if type(tom-tat) == array { + (min: tom-tat.at(0), q1: tom-tat.at(1), q2: tom-tat.at(2), + q3: tom-tat.at(3), max: tom-tat.at(4)) + } else { tom-tat } + let pad = calc.max((t.max - t.min) * 0.14, 0.5) + let xa = t.min - pad + let xb = t.max + pad * 1.4 + // hộp ở trên (tâm yc), trục số ở dưới (y = ytr) + let yc = 0.42 + let nb = 0.4 // nửa chiều cao hộp + let nr = 0.2 // nửa chiều cao vạch râu min/max + let ytr = -0.78 // cao độ trục số + context { + hinh( + w: w, h: h, + xmin: xa, xmax: xb + _cho-ten-x(ten, w, xb - xa, co-chu: co-chu), + ymin: -1.62, ymax: 1.05, co-chu: co-chu, + ctx => { + // gióng nét đứt 5 đặc trưng xuống trục + for x in (t.min, t.q1, t.q2, t.q3, t.max) { + doan(ctx, (x, yc - nb), (x, ytr), mau: luma(55%), day: 0.6pt, dut: true) + } + // râu + 2 vạch đầu mút + doan(ctx, (t.min, yc), (t.q1, yc), day: 1pt) + doan(ctx, (t.q3, yc), (t.max, yc), day: 1pt) + doan(ctx, (t.min, yc - nr), (t.min, yc + nr), mau: vien, day: 1.2pt) + doan(ctx, (t.max, yc - nr), (t.max, yc + nr), mau: vien, day: 1.2pt) + // hộp Q1–Q3 + trung vị đậm + da-giac(ctx, ((t.q1, yc - nb), (t.q3, yc - nb), (t.q3, yc + nb), (t.q1, yc + nb)), + to: mau, mau: vien, day: 1.1pt) + doan(ctx, (t.q2, yc - nb), (t.q2, yc + nb), mau: vien, day: 2pt) + // trục số + vạch + 5 số + doan(ctx, (xa, ytr), (xb, ytr), day: 0.9pt) + dau-mui-ten(ctx, (xa, ytr), (xb, ytr)) + nhan(ctx, (xb, ytr), ten, huong: "right", cach: 5pt) + let dv = 2.6pt / ctx.sy + for x in (t.min, t.q1, t.q2, t.q3, t.max) { + doan(ctx, (x, ytr - dv), (x, ytr + dv), day: 0.9pt) + if so { nhan(ctx, (x, ytr), so-toan(x), huong: "below", cach: 6pt) } + } + if them != none { them(ctx) } + }) + } +} + +// ===================================================================== +// BIỂU ĐỒ QUẠT TRÒN (pie chart) — chia góc theo tỉ lệ %. +// Mỗi mục positional: (tên, số-liệu) hoặc (tên, số-liệu, màu). +// Bắt đầu từ 12 giờ, quay theo chiều kim đồng hồ (kiểu SGK). +// phan-tram: true -> ghi % trong quạt (quạt bé thì ghi kèm tên ngoài) +// ===================================================================== +#let bieu-do-quat( + ..muc, + w: 7.5cm, + phan-tram: true, + vien: white, // màu đường phân cách các quạt + co-chu: 10pt, + them: none, +) = { + let ms = muc.pos() + let tong = ms.map(m => m.at(1)).sum() + hinh( + w: w, xmin: -1.8, xmax: 1.8, ymin: -1.45, ymax: 1.45, co-chu: co-chu, + ctx => { + let g = 90deg // bắt đầu 12 giờ + for (i, m) in ms.enumerate() { + let phan = m.at(1) / tong + let g2 = g - phan * 360deg // chiều kim đồng hồ + let mq = if m.len() >= 3 { m.at(2) } else { _mau-bd.at(calc.rem(i, _mau-bd.len())) } + let pts = ((0, 0),) + diem-cung((0, 0), 1, 1, g2, g, n: calc.max(int(phan * 72), 2)) + da-giac(ctx, pts, to: mq, mau: vien, day: 1.2pt) + // nhãn tại tia phân giác của quạt + let gm = g - phan * 180deg + let (hx, hy) = (calc.cos(gm), calc.sin(gm)) + let pc = [#so-dep(calc.round(phan * 100, digits: 1))%] + let huong = if hx > 0.35 { "right" } else if hx < -0.35 { "left" } + else if hy > 0 { "above" } else { "below" } + if phan >= 0.08 { + // % trắng đậm trong quạt, tên ngoài + if phan-tram { + nhan(ctx, (hx * 0.62, hy * 0.62), text(fill: white, weight: "bold", pc), + huong: "center") + } + nhan(ctx, (hx * 1.06, hy * 1.06), [#m.at(0)], huong: huong, cach: 4pt) + } else { + // quạt bé: tên + % gộp chung bên ngoài + let nd = if phan-tram { [#m.at(0) (#pc)] } else { [#m.at(0)] } + nhan(ctx, (hx * 1.06, hy * 1.06), nd, huong: huong, cach: 4pt) + } + g = g2 + } + if them != none { them(ctx) } + }, + ) +} + +// ===================================================================== +// SỐ ĐẶC TRƯNG MẪU SỐ LIỆU (TRẢ GIÁ TRỊ, không vẽ — dùng thẳng trong lời giải) +// KHÔNG ghép nhóm: nhận mảng thô, hoặc gia-tri + tan-so: (bảng tần số). +// GHÉP NHÓM (moc, tan-so): trung bình/phương sai theo GIÁ TRỊ ĐẠI DIỆN +// (trung điểm nhóm); mốt/trung vị/tứ phân vị NỘI SUY theo SGK 11–12. +// ===================================================================== + +// Bung bảng tần số (gia-tri + tan-so) thành mảng thô; tan-so: none -> giữ nguyên. +#let _bung(du-lieu, tan-so) = { + if tan-so == none { return du-lieu } + let d = () + for (i, v) in du-lieu.enumerate() { + for _ in range(tan-so.at(i)) { d.push(v) } + } + d +} + +#let so-trung-binh(du-lieu, tan-so: none) = { + if tan-so == none { du-lieu.sum() / du-lieu.len() } + else { + range(du-lieu.len()).map(i => du-lieu.at(i) * tan-so.at(i)).sum() / tan-so.sum() + } +} + +// Mốt: TRẢ VỀ MẢNG các giá trị có tần số lớn nhất (mẫu có thể nhiều mốt). +#let mot(du-lieu, tan-so: none) = { + let (gt, ts) = if tan-so != none { (du-lieu, tan-so) } else { + let dem = (:) + for v in du-lieu { + let k = repr(v) + if k in dem { dem.insert(k, (v, dem.at(k).at(1) + 1)) } + else { dem.insert(k, (v, 1)) } + } + (dem.values().map(c => c.at(0)), dem.values().map(c => c.at(1))) + } + let fmax = calc.max(..ts) + range(gt.len()).filter(i => ts.at(i) == fmax).map(i => gt.at(i)) +} + +#let trung-vi(du-lieu, tan-so: none) = _trung-vi(_bung(du-lieu, tan-so).sorted()) + +// tu-phan-vi nay nhận thêm tan-so: (bảng tần số) — bọc lại bản gốc phía trên. +#let _tpv-tho = tu-phan-vi +#let tu-phan-vi(du-lieu, tan-so: none) = _tpv-tho(_bung(du-lieu, tan-so)) + +#let khoang-bien-thien(du-lieu, tan-so: none) = { + let d = _bung(du-lieu, tan-so) + calc.max(..d) - calc.min(..d) +} + +#let khoang-tu-phan-vi(du-lieu, tan-so: none) = { + let q = tu-phan-vi(du-lieu, tan-so: tan-so) + q.q3 - q.q1 +} + +// Phương sai s² (SGK: chia n); hieu-chinh: true -> s'² chia (n − 1). +#let phuong-sai(du-lieu, tan-so: none, hieu-chinh: false) = { + let d = _bung(du-lieu, tan-so) + let n = d.len() + let m = d.sum() / n + d.map(v => (v - m) * (v - m)).sum() / (if hieu-chinh { n - 1 } else { n }) +} + +#let do-lech-chuan(du-lieu, tan-so: none, hieu-chinh: false) = calc.sqrt( + phuong-sai(du-lieu, tan-so: tan-so, hieu-chinh: hieu-chinh), +) + +// -------------------- GHÉP NHÓM (moc: n+1 mốc, tan-so: n) -------------------- + +// Giá trị đại diện = trung điểm từng nhóm. +#let _dai-dien(moc) = range(moc.len() - 1).map(i => (moc.at(i) + moc.at(i + 1)) / 2) + +#let so-trung-binh-ghep-nhom(moc, tan-so) = so-trung-binh(_dai-dien(moc), tan-so: tan-so) + +// Mốt (SGK 11): nhóm chứa mốt = nhóm tần số LỚN NHẤT (nhiều nhóm bằng nhau +// thì lấy nhóm đầu); Mo = L + (f₁−f₀)/((f₁−f₀)+(f₁−f₂))·h, f ngoài biên = 0. +#let mot-ghep-nhom(moc, tan-so) = { + let fmax = calc.max(..tan-so) + let j = tan-so.position(f => f == fmax) + let f0 = if j > 0 { tan-so.at(j - 1) } else { 0 } + let f2 = if j < tan-so.len() - 1 { tan-so.at(j + 1) } else { 0 } + let h = moc.at(j + 1) - moc.at(j) + let ms = (fmax - f0) + (fmax - f2) + if ms == 0 { moc.at(j) + h / 2 } else { moc.at(j) + (fmax - f0) / ms * h } +} + +#let trung-vi-ghep-nhom(moc, tan-so) = tu-phan-vi-ghep-nhom(moc, tan-so).q2 + +#let phuong-sai-ghep-nhom(moc, tan-so, hieu-chinh: false) = phuong-sai( + _dai-dien(moc), tan-so: tan-so, hieu-chinh: hieu-chinh, +) + +#let do-lech-chuan-ghep-nhom(moc, tan-so, hieu-chinh: false) = calc.sqrt( + phuong-sai-ghep-nhom(moc, tan-so, hieu-chinh: hieu-chinh), +) + +// R = mốc phải nhóm CUỐI có dữ liệu − mốc trái nhóm ĐẦU có dữ liệu. +#let khoang-bien-thien-ghep-nhom(moc, tan-so) = { + let i = tan-so.position(f => f > 0) + let j = tan-so.len() - 1 - tan-so.rev().position(f => f > 0) + moc.at(j + 1) - moc.at(i) +} + +#let khoang-tu-phan-vi-ghep-nhom(moc, tan-so) = { + let q = tu-phan-vi-ghep-nhom(moc, tan-so) + q.q3 - q.q1 +} diff --git a/packages/preview/conic-toan/0.3.4/lib/cau-hoi.typ b/packages/preview/conic-toan/0.3.4/lib/cau-hoi.typ new file mode 100644 index 0000000000..14da8b09ec --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/cau-hoi.typ @@ -0,0 +1,1076 @@ +// ===================================================================== +// cau-hoi.typ — CÂU HỎI THEO ĐỊNH DẠNG ĐỀ THI 2025 +// #cau-mc(...) Trắc nghiệm nhiều phương án (tự chia cột theo độ dài) +// #cau-tf(...) Đúng — Sai 4 ý (tuỳ chọn ô tick sát lề phải) +// #cau-sa(...) Trả lời ngắn +// #cau-tl(...) Tự luận +// #cau-hd(...) Hoạt động (thẻ "HĐN", bộ đếm riêng, hình thức như TL) +// #cau-lt(...) Luyện tập (thẻ "Luyện tập N", chung bộ đếm với khung +// #luyen-tap của slide.typ) +// #cau-vdtt(...) Vận dụng thực tế (thẻ "Vận dụng N", bộ đếm riêng) +// +// Số câu tự đánh liên tục cho 4 dạng MC/TF/SA/TL; HĐ, Luyện tập, Vận dụng +// mỗi loại có bộ đếm riêng (không đổi qua bước hoạt hình). +// CÔNG TẮC ĐÁP ÁN: #bat-dap-an() / #tat-dap-an() +// KIỂU TIỀN TỐ (thẻ "Câu X", "A.", "a)"): #kieu-cau-hoi(mau: ..., hinh: ...) +// hinh: "bo-tron" (mặc định) | "chu-nhat" | "luc-giac" | "khong-to" +// ("khong-to" = không tô nền, chỉ viền + chữ màu — in không bị đen; +// riêng hồ sơ dethi: bỏ cả viền — "Câu 1." / "A." đậm cùng màu) +// hien-o: false = ẩn ô tick Đ/S (ds) + ô điền "Trả lời" (tln) toàn bài. +// -> đổi MỘT LẦN, đồng bộ toàn bài. +// CHỪA CHỖ TL (bản in): #trong-tl(cao: 5cm) / #khong-trong-tl() +// ===================================================================== +#import "slide.typ": _buoc-ht, _bd-cau, _bd-vd, _bd-hd, _bd-lt, _bd-vdtt, _bd-tat-ca, _so-moi, _ho-so, _he-so-gian, _cao-that + +#let _hien-da = state("ch-hien-da", false) + +// Điều kiện hiện đáp án: công tắc bật + (nếu có lo-da) đã đến bước lo-da. +#let _da-hien(lo-da) = _hien-da.get() and (lo-da == none or _buoc-ht.get() >= lo-da) + +#let bat-dap-an() = _hien-da.update(true) +#let tat-dap-an() = _hien-da.update(false) +// Đặt lại số thứ tự — MỖI thể loại có bộ đếm & hàm đặt lại RIÊNG: +// #dat-lai-cau() -> chỉ nhóm "Câu" (tn/ds/tln/tl); KHÔNG đụng vd/hd/lt/vdtt +// #dat-lai-cau-vd() -> Ví dụ #dat-lai-cau-hd() -> Hoạt động +// #dat-lai-cau-lt() -> Luyện tập #dat-lai-cau-vdtt() -> Vận dụng thực tế +// #dat-lai-cau-tat-ca()-> đặt lại CẢ 5 nhóm cùng lúc (hành vi cũ của dat-lai-cau) +// Không tham số hoặc `(0)` -> đánh lại từ 1; `(3)` -> đánh tiếp từ 4; ... +#let _dat-lai-bd(bd, n) = { + bd.goc.update(n) + bd.cnt.update(0) +} +#let dat-lai-cau(..so) = _dat-lai-bd(_bd-cau, so.pos().at(0, default: 0)) +#let dat-lai-cau-vd(..so) = _dat-lai-bd(_bd-vd, so.pos().at(0, default: 0)) +#let dat-lai-cau-hd(..so) = _dat-lai-bd(_bd-hd, so.pos().at(0, default: 0)) +#let dat-lai-cau-lt(..so) = _dat-lai-bd(_bd-lt, so.pos().at(0, default: 0)) +#let dat-lai-cau-vdtt(..so) = _dat-lai-bd(_bd-vdtt, so.pos().at(0, default: 0)) +#let dat-lai-cau-tat-ca(..so) = { + let n = so.pos().at(0, default: 0) + for bd in _bd-tat-ca { _dat-lai-bd(bd, n) } +} + +// ---------- Chừa chỗ trống sau câu TL/HĐ/LT/VDTT (chỉ ở BẢN IN, khi ẩn đáp án) ---------- +// Đổi ĐỒNG BỘ toàn bài, hai tuỳ chọn: +// #trong-tl(cao: 5cm) -> CHỪA chỗ làm bài sau mỗi câu: +// • câu có đặt cho-trong: dùng đúng cho-trong đó; +// • câu KHÔNG đặt cho-trong: chừa mặc định `cao`. +// #khong-trong-tl() -> KHÔNG chừa chỗ (bỏ qua cả cho-trong) — in gọn. +// Mặc định (bat: true, cao: 0pt): giữ nguyên hành vi cũ — chỉ chừa đúng +// theo cho-trong của từng câu. Ở beamer/loigiai (hiện đáp án) tuỳ chọn này +// không tác dụng vì chỗ đó dành cho lời giải. +#let _trong-tl = state("ch-trong-tl", (bat: true, cao: 0pt)) +#let trong-tl(cao: 5cm) = _trong-tl.update((bat: true, cao: cao)) +#let khong-trong-tl() = _trong-tl.update(k => (bat: false, cao: k.cao)) + +#let _xanh = rgb("#0f4c81") +#let _luc = rgb("#1e8449") +#let _do = rgb("#a93226") + +// ---------- ĐÁNH DẤU ĐÁP ÁN ĐÚNG NGAY TRONG DANH SÁCH ---------- +// Bọc phương án/phát biểu ĐÚNG bằng True(...) — đáp án đi kèm nội dung nên +// hoán vị/xáo trộn thoải mái, không lệch: +// #tn([...], ([$A$], True([$B$]), [$C$], [$D$])) -> đáp án B +// #ds([...], ([a sai], True([b đúng]), True([c]), [d])) -> (S, Đ, Đ, S) +// (bí danh tiếng Việt: Dung) +// +// LỜI GIẢI GẮN LIỀN TỪNG Ý (#ds) — cần khi công cụ trộn HOÁN VỊ các ý: +// khối lời giải viết cứng "a) … b) …" sẽ sai sau khi xáo, nên đặt lời giải +// ngay cạnh ý bằng `giai:`; lib tự đánh lại nhãn theo thứ tự SAU hoán vị. +// #ds([Cho $y = x^3 - 3x$.], ( +// True([$y' = 3x^2 - 3$], giai: [Đạo hàm của $x^n$ là $n x^(n-1)$.]), +// False([Hàm số không có cực trị], giai: [$y' = 0$ có hai nghiệm phân biệt.]), +// [Ý sai, không cần giải riêng — viết trần như cũ], +// )) +// `loi-giai:` chung vẫn dùng được — in TRƯỚC phần giải theo ý (phần dẫn nhập). +#let True(nd, giai: none) = (bg-dung: true, nd: nd, giai: giai) +#let Dung = True +#let False(nd, giai: none) = (bg-dung: false, nd: nd, giai: giai) +#let _la-y(x) = type(x) == dictionary and "nd" in x +#let _la-true(x) = type(x) == dictionary and x.at("bg-dung", default: false) +#let _bo-true(x) = if _la-y(x) { x.nd } else { x } +#let _giai-y(x) = if _la-y(x) { x.at("giai", default: none) } else { none } + +// Tách danh sách có True(...)/False(...): trả (ds: nội dung thuần, +// dung: mảng bool, giai: mảng lời giải từng ý — none nếu không khai). +#let _tach-true(ds) = ( + ds: ds.map(_bo-true), + dung: ds.map(_la-true), + giai: ds.map(_giai-y), +) + +// ---------- TỰ THÊM DẤU CHẤM CUỐI PHƯƠNG ÁN / PHÁT BIỂU ---------- +// Phương án của #tn và ý của #ds là một CÂU, đúng chính tả phải kết bằng "." +// Bộ dò dưới đây duyệt NGƯỢC cây nội dung tìm ký tự có nghĩa cuối cùng: +// • đã có . ! ? … : ; (hoặc dấu đóng ngoặc kép/ngoặc sau chúng) -> giữ nguyên +// • kết thúc bằng khối (danh sách, bảng, hình, xuống dòng...) -> BỎ QUA +// • còn lại (chữ, số, công thức toán, dấu ngoặc) -> thêm "." +// Tắt toàn bài: #kieu-cau-hoi(cham-cuoi: false); tắt một câu: `cham: false`. +#let _dau-ket = (".", "!", "?", "…", ":", ";", "。", "!", "?") +// Dấu "vỏ" bọc ngoài dấu kết: "Anh ấy đi!" / (đúng.) — nhìn xuyên qua. +#let _dau-vo = ("\"", "'", "”", "’", "»", ")", "]", "}", "›") + +// Các phần tử KHỐI: đứng cuối thì không thêm dấu chấm (chấm sẽ rơi xuống dòng). +// Lưu ý: trong markup, `- a` sinh thẳng `list.item` (chưa gom thành `list`). +#let _la-khoi(f) = ( + f == list or f == enum or f == terms or f == table or f == grid + or f == list.item or f == enum.item or f == terms.item + or f == figure or f == image or f == block or f == place + or f == parbreak or f == linebreak or f == line or f == v or f == pagebreak +) +// Phần tử "khoảng trắng" của markup (bỏ qua khi dò ngược từ cuối). +#let _f-space = [ ].func() + +// Đoạn văn bản có nghĩa CUỐI CÙNG (none = không tìm được: hình, công thức...). +#let _van-cuoi(c) = { + if c == none { return none } + let t = type(c) + if t == int or t == float { return str(c) } + if t == str { + let s = c.trim() + return if s == "" { none } else { s } + } + if t == array { + for x in c.rev() { + let r = _van-cuoi(x) + if r != none { return r } + } + return none + } + if t != content { return none } + if c.has("text") { return _van-cuoi(c.text) } + if c.has("children") { return _van-cuoi(c.children) } + if c.has("body") { return _van-cuoi(c.body) } + none +} + +// Phân loại phần tử có nghĩa CUỐI CÙNG: +// 1 = khối (danh sách/bảng/hình/ngắt dòng/công thức giữa dòng) -> KHÔNG chấm +// 0 = nội dung thường (chữ, số, công thức trong dòng) -> chấm được +// -1 = không có gì nhìn thấy (khoảng trắng, metadata, cập nhật trạng thái, +// phần tử bọc rỗng) -> đi tiếp sang phần tử trước đó +#let _loai-cuoi(c) = { + if c == none { return -1 } + let t = type(c) + if t == str { return if c.trim() == "" { -1 } else { 0 } } + if t == int or t == float { return 0 } + if t != content { return 0 } + let f = c.func() + if _la-khoi(f) { return 1 } + if f == _f-space or f == metadata { return -1 } + // công thức: TRÌNH BÀY GIỮA DÒNG ($ ... $) là khối; trong dòng luôn "thường" + if f == math.equation { + if c.at("block", default: false) { return 1 } + return if _loai-cuoi(c.body) == 1 { 1 } else { 0 } + } + if c.has("text") { return if type(c.text) == str and c.text.trim() == "" { -1 } else { 0 } } + if c.has("children") { + for x in c.children.rev() { + let r = _loai-cuoi(x) + if r >= 0 { return r } + } + return -1 + } + if c.has("body") { return _loai-cuoi(c.body) } + if c.has("child") { return _loai-cuoi(c.child) } // phần tử styled + -1 +} + +// Thêm "." nếu thiếu (giữ nguyên nội dung gốc trong mọi trường hợp còn lại). +#let cham-cau(c) = { + if c == none or c == [] { return c } + if _loai-cuoi(c) != 0 { return c } // kết bằng khối/hình hoặc rỗng -> bỏ qua + let s = _van-cuoi(c) + if s == none { return [#c#"."] } // chỉ có công thức (vd $1/2$) -> vẫn chấm + // bóc các dấu "vỏ" ngoài cùng: Đúng!" / (sai.) -> nhìn thấy ! và . + let cl = s.clusters() + let i = cl.len() - 1 + while i >= 0 and _dau-vo.contains(cl.at(i)) { i = i - 1 } + if i >= 0 and _dau-ket.contains(cl.at(i)) { return c } + [#c#"."] +} + +// ---------- THU THẬP ĐÁP ÁN CHO BẢNG ĐÁP ÁN ---------- +// Mỗi câu tn/ds/tln phát một metadata vô hình (nhãn ). Hàm bang-dap-an +// (che-do.typ) query theo document-order rồi dựng 3 bảng. loai ∈ "tn"/"ds"/"tln": +// tn -> da = chữ cái đáp án đúng ("A".."F") hoặc none. +// ds -> da = mảng bool 4 ý (true = Đ, false = S). +// tln -> da = đáp án (chuỗi/số/nội dung). +// khoa: true -> câu này KHÔNG được hoán vị phương án/ý (công cụ trộn đọc cờ này). +#let _ghi-da(loai, da, khoa: false) = [#metadata((loai: loai, da: da, khoa: khoa)) ] + +// Chữ cái đáp án đúng của câu tn (ưu tiên True(...) trong danh sách; nếu không +// có thì lấy dap-an dạng chữ "B" cũ). +#let _chu-da-tn(phuong-an, dap-an) = { + let vi = phuong-an.map(_la-true).position(d => d) + if vi != none { ("A", "B", "C", "D", "E", "F").at(vi) } else { dap-an } +} + +// ---------- Kiểu tiền tố (thẻ) — đồng bộ màu & hình dáng ---------- +// hien-o: hiện ô tick Đ/S (câu ds có o-tick) và ô điền "Trả lời" (câu tln); +// false = ẨN đồng bộ toàn bài (đề gọn, HS làm thẳng vào phiếu). +// om-hinh: chữ ÔM hình (phần dư tràn hết bề rộng dưới hình) — xem voi-hinh. +// eq-dong: ép công thức KHỐI ($ ... $ có khoảng trắng) trong phương án #tn / +// ý #ds thành công thức TRONG DÒNG — xem `_ep-trong-dong`. +#let _kieu = state("ch-kieu", (mau: rgb("#0f4c81"), hinh: "bo-tron", hien-o: true, cham: true, om-hinh: true, eq-dong: true)) + +// Đổi kiểu ở bất kỳ đâu trong tài liệu: +// #kieu-cau-hoi(mau: rgb("#e67e22"), hinh: "luc-giac", hien-o: false) +// #kieu-cau-hoi(om-hinh: false) // tắt chế độ chữ ôm hình từ đây trở đi +// #kieu-cau-hoi(eq-trong-dong: false) // giữ nguyên $ ... $ khối như cũ +// #kieu-cau-hoi(cao-that: false) // tắt cột chống chiều cao công thức +// cao-that: công thức TRONG DÒNG có khai đúng chiều cao NÉT VẼ hay không +// (xem `_chong-net` của slide.typ). Mặc định BẬT — đây là thứ chống dính chữ +// ở phân số/căn thức mà `gian-dong` không với tới được. +#let kieu-cau-hoi(mau: auto, hinh: auto, hien-o: auto, cham-cuoi: auto, om-hinh: auto, eq-trong-dong: auto, cao-that: auto) = { + _kieu.update(k => ( + mau: if mau == auto { k.mau } else { mau }, + hinh: if hinh == auto { k.hinh } else { hinh }, + hien-o: if hien-o == auto { k.at("hien-o", default: true) } else { hien-o }, + cham: if cham-cuoi == auto { k.at("cham", default: true) } else { cham-cuoi }, + om-hinh: if om-hinh == auto { k.at("om-hinh", default: true) } else { om-hinh }, + eq-dong: if eq-trong-dong == auto { k.at("eq-dong", default: true) } else { eq-trong-dong }, + )) + // Cơ chế cao-that nằm ở state RIÊNG (slide.typ) vì bai-giang/de-toan phải + // đọc được nó mà slide.typ thì không import được cau-hoi.typ. + if cao-that != auto { + _cao-that.update(c => ( + bat: cao-that, + nguong: c.at("nguong", default: 1pt), + them: c.at("them", default: 0pt), + chia: c.at("chia", default: 0.5), + hien: c.at("hien", default: false), + )) + } +} + +// Ô tick/ô điền có được hiện không (đọc trong context). +#let _hien-o() = _kieu.get().at("hien-o", default: true) + +// Tự thêm dấu chấm cuối phương án/ý hỏi không? (đọc trong context) +// cham: auto = theo cài đặt toàn bài; true/false = ép riêng câu này. +#let _cham-ds(ds, cham) = { + let bat = if cham == auto { _kieu.get().at("cham", default: true) } else { cham } + if bat { ds.map(cham-cau) } else { ds } +} + +// Thẻ tiền tố: nền màu chủ đạo, chữ trắng; 4 chế độ. +// "bo-tron" | "chu-nhat" | "luc-giac" : TÔ nền màu, chữ trắng. +// "khong-to" : KHÔNG tô nền — viền mảnh + chữ màu chủ đạo, đậm; +// in ấn không bị mảng đen (tiết kiệm mực). +// RIÊNG hồ sơ dethi: bỏ luôn viền — chỉ chữ ĐẬM cùng màu +// kèm dấu liền kề ("Câu 1.", "A." — ý ds: "a)") kiểu LaTeX; +// dấu tuỳ chỉnh qua `duoi` (mặc định "."). +#let _the(nd, mau: auto, duoi: ".") = context { + let k = _kieu.get() + let m = if mau == auto { k.mau } else { mau } + if k.hinh == "khong-to" and _ho-so.get() == "sachdethi" { + text(fill: m, weight: "bold")[#nd#duoi] + } else if k.hinh == "khong-to" { + box( + stroke: 0.8pt + m, + radius: 3.5pt, + inset: (x: 6pt, y: 2.5pt), + baseline: 1%, + text(fill: m, weight: "bold", size: 0.8em, nd), + ) + } else { + let nd2 = text(fill: white, weight: "bold", size: 0.8em, nd) + if k.hinh == "luc-giac" { + let s = measure(nd2) + let w = s.width + 15pt + let h = s.height + 5.5pt + box(width: w, height: h, baseline: 1%, { + place(polygon( + fill: m, + (h / 2, 0pt), (w - h / 2, 0pt), (w, h / 2), + (w - h / 2, h), (h / 2, h), (0pt, h / 2), + )) + place(center + horizon, nd2) + }) + } else { + box( + fill: m, + radius: if k.hinh == "chu-nhat" { 0pt } else { 3.5pt }, + inset: (x: 6pt, y: 2.5pt), + baseline: 1%, + nd2, + ) + } + } +} + +// "Câu N (x điểm)" — đầu mỗi câu hỏi. +// prefix: chữ trên thẻ ("Câu", "Bài", ...); num: auto = đếm tự động, +// hoặc số/chữ chỉ định (khi chỉ định, bộ đếm KHÔNG tăng). +#let _dau-cau(diem: none, prefix: "Câu", num: auto) = { + let so = if num == auto { _so-moi(_bd-cau) } else { [#num] } + _the([#prefix #so]) + if diem != none { + context text(weight: "bold", fill: _kieu.get().mau, size: 0.86em)[ (#diem điểm)] + } + h(6pt) +} + +// Chừa `lines` dòng trống làm bài (chỉ khi ẨN đáp án — bản in đề thi). +#let _chua-dong(lines, lo-da) = context { + if lines > 0 and not _da-hien(lo-da) { v(lines * 1.55em) } +} + +// Đóng khung cả câu khi boxed: true. +// Khoảng cách GIỮA HAI CÂU cũng nhân hệ số gian-dong: nếu không, đặt +// gian-dong: 2 thì các dòng TRONG câu giãn gấp đôi mà "Câu 2" vẫn dán sát ngay +// dưới ý cuối của câu trước — nhìn như các câu dính chùm vào nhau. +// Hệ số 1.0 cho đúng 10pt như cũ ⇒ bài cũ không đổi bố cục. +#let _khung-cau(boxed, than) = context block( + width: 100%, above: 10pt * _he-so-gian(), below: 10pt * _he-so-gian(), + stroke: if boxed { 0.7pt + luma(45%) } else { none }, + inset: if boxed { 8pt } else { 0pt }, + radius: if boxed { 4pt } else { 0pt }, + than, +) + +// (_khoi-giai/_hien-giai chuyển xuống SAU voi-hinh — nay hỗ trợ hình +// kèm LỜI GIẢI thông qua voi-hinh.) + +// ---------- ÔM HÌNH: cắt nội dung theo ĐOẠN để chữ ôm sát hình ---------- +// Bài toán: hình hẹp + lời giải DÀI ⇒ cột hình trống một khoảng lớn phía +// dưới, lãng phí giấy. Cách chữa: đặt các ĐOẠN ĐẦU cạnh hình cho vừa đúng +// chiều cao hình, phần còn lại in NGUYÊN BỀ RỘNG bên dưới hình. + +// Tách nội dung thành mảng "khối" (đoạn văn, công thức tách dòng, danh +// sách, bảng…). Dấu ngắt đoạn KHÔNG giữ lại — ghép lại bằng parbreak(). +// Nội dung ghép từ nhiều mảnh có thể LỒNG sequence trong sequence — trải +// phẳng (CHỈ trải sequence, không đụng grid/table/list… dù chúng cũng có +// trường "children"). +#let _f-seq = [].func() +#let _la-seq(c) = { + if type(c) != content { return false } + let f = c.func() + if f == _f-seq { return true } + // Dự phòng (phòng khi `[].func()` không nhận ra): có trường "children" + // mà KHÔNG phải các phần tử gom nhóm thật sự. + c.has("children") and not (f == grid or f == table or f == list or f == enum + or f == terms or f == stack or f == figure) +} +#let _phang-seq(c) = { + if _la-seq(c) { return c.children.map(_phang-seq).flatten() } + (c,) +} + +// Mỗi khối là dict `(nd: nội dung, sep: dấu ngắt đi sau nó)`. PHẢI giữ dấu +// ngắt GỐC: bản A4 đi qua `_ghep` (che-do.typ) nên mọi dấu ngắt ĐOẠN đã bị +// đổi thành ngắt DÒNG — ghép lại bằng parbreak sẽ giãn rộng ra, đổi bố cục +// mọi bài cũ. Vì vậy cũng phải CẮT ở cả linebreak, không riêng parbreak. +#let _tach-doan(c) = { + if c == none { return () } + if type(c) != content { return ((nd: c, sep: none),) } + let ds = _phang-seq(c) + let ra = () + let cum = () + for x in ds { + let f = if type(x) == content { x.func() } else { none } + if f == parbreak or f == linebreak { + if cum.len() > 0 { + ra.push((nd: cum.join(), sep: x)) + cum = () + } else if ra.len() > 0 { + // Dấu ngắt đi ngay sau một khối (vd sau công thức tách dòng): gắn + // vào khối trước, ĐỪNG bỏ đi kẻo mất một lần xuống dòng. + let u = ra.pop() + ra.push((nd: u.nd, sep: if u.sep == none { x } else { u.sep + x })) + } + } else { + cum.push(x) + if (f == block or f == grid or f == table or f == list or f == enum + or f == terms or f == figure + or (f == math.equation and x.at("block", default: false))) { + ra.push((nd: cum.join(), sep: none)) + cum = () + } + } + } + if cum.len() > 0 { ra.push((nd: cum.join(), sep: none)) } + ra +} + +// Ghép các khối lại NGUYÊN như cũ (kèm dấu ngắt gốc), bỏ dấu ngắt cuối cùng. +#let _noi-khoi(ds) = { + let ra = none + for (i, u) in ds.enumerate() { + ra = if ra == none { u.nd } else { ra + u.nd } + if i + 1 < ds.len() and u.sep != none { ra = ra + u.sep } + } + if ra == none { [] } else { ra } +} + +// Phần tử KHÔNG xuống dòng được (công thức tách dòng, ảnh, bảng): bề rộng +// tự nhiên phải lọt cột hẹp, nếu không sẽ tràn ra ngoài lề. +#let _khoi-cung(x) = { + if type(x) != content { return false } + let f = x.func() + if f == math.equation { return x.at("block", default: false) } + (f == image or f == table or f == grid or f == figure) +} + +// Khối có phần nào QUÁ RỘNG so với cột hẹp không? (gọi trong context) +#let _qua-rong(x, hep) = { + if type(x) != content { return false } + if _khoi-cung(x) { return measure(x).width > hep } + if x.has("children") { return x.children.any(y => _qua-rong(y, hep)) } + false +} + +// Số khối ĐẦU đặt vừa cạnh hình (cột rộng `hep`, cao tối đa `cao`). +// Gọi trong context (dùng measure). +#let _dem-khoi-om(khoi, hep, cao) = { + let k = 0 + while k < khoi.len() { + if _qua-rong(khoi.at(k).nd, hep) { break } + if measure(block(width: hep, _noi-khoi(khoi.slice(0, k + 1)))).height > cao { break } + k = k + 1 + } + k +} + +// Chế độ ôm có bật không: om = auto → theo kieu-cau-hoi(om-hinh:). +#let _om-bat(om) = if om == auto { _kieu.get().at("om-hinh", default: true) } else { om } + +// ---------- BỐ CỤC ĐỀ + HÌNH ---------- +// Quy tắc chung cho MỌI dạng câu (dùng qua tham số hinh: của +// cau-mc/cau-tf/cau-sa/cau-tl/... hoặc gọi trực tiếp trong thân câu): +// • hình HẸP (bề rộng ≤ ti-le × bề rộng khung): hình canh giữa CỘT PHẢI — +// cột phải ôm đúng bề rộng hình, cột trái (đề + phương án/ý hỏi) nhận +// phần còn lại; hai cột canh giữa theo chiều dọc nên độ cao cân đối. +// • hình RỘNG (BBT, bảng thống kê, hình khổ lớn): tự tách ra DÒNG RIÊNG +// canh giữa — đặt sau phần đề `de`, trước phần `duoi` (phương án/ý hỏi). +// LƯU Ý: truyền hình TRỰC TIẾP (hinh: ve-do-thi(...)), KHÔNG bọc align/block +// quanh hình — bọc thêm sẽ đo sai bề rộng. +// vi-tri ("right" | "left" | "center" | "top" | "bottom"): vị trí hình so với đề; +// • "right"/"left" — hình nằm cột phải/cột trái, chữ ôm quanh hình; +// • "center" — hình canh GIỮA trên DÒNG RIÊNG, nằm giữa phần đề và +// phần phương án/ý hỏi (dùng cho hình khổ lớn: BBT, +// bảng thống kê, đồ thị rộng); +// • "top" — hình lên TRÊN cả đề bài; +// • "bottom" — hình xuống DƯỚI cả phương án. +// be-rong: auto = ôm đúng bề rộng hình (hình rộng tự xuống dòng riêng), +// hoặc chỉ định (35%, 5cm...) = cột hình chiếm đúng bề rộng đó. +// Với "center"/"top"/"bottom" thì be-rong KHÔNG có tác dụng (hình +// đã chiếm trọn bề ngang). +// om: chữ ÔM hình — auto = theo kieu-cau-hoi(om-hinh:), mặc định BẬT; +// false = giữ lối 2 cột cũ (cột hình có thể trống nhiều phía dưới). +#let voi-hinh(de, hinh, duoi: none, ti-le: 0.46, khoang: 14pt, vi-tri: "right", be-rong: auto, om: auto) = { + assert(vi-tri in ("right", "left", "center", "top", "bottom"), + message: "vi-tri (fig-pos) chỉ nhận \"right\" | \"left\" | \"center\" | \"top\" | \"bottom\", nhận được: " + repr(vi-tri)) + let rieng = block(width: 100%, above: 8pt, below: 8pt, align(center, hinh)) + if hinh == none { + de + duoi + } else if vi-tri == "top" { + rieng + de + duoi + } else if vi-tri == "center" { + de + rieng + duoi + } else if vi-tri == "bottom" { + de + duoi + rieng + } else { + let than = { de; duoi } + let hai-cot(o-hinh) = if vi-tri == "left" { + grid( + columns: (if be-rong == auto { auto } else { be-rong }, 1fr), + column-gutter: khoang, + align: (center + horizon, left + horizon), + o-hinh, than, + ) + } else { + grid( + columns: (1fr, if be-rong == auto { auto } else { be-rong }), + column-gutter: khoang, + align: (left + horizon, center + horizon), + than, o-hinh, + ) + } + // Bố cục ÔM (gọi TRONG context): none = không cắt được hoặc không cần + // cắt (nội dung vốn đã thấp hơn hình) ⇒ quay về lối 2 cột cũ. + let bo-cuc-om(w-hinh, kich) = { + let hep = kich.width - w-hinh - khoang + if hep < 3cm { return none } + let khoi = _tach-doan(than) + if khoi.len() < 2 { return none } + let k = _dem-khoi-om(khoi, hep, measure(hinh).height) + if k == 0 or k == khoi.len() { return none } + let tren = _noi-khoi(khoi.slice(0, k)) + block(above: 0pt, below: 0pt, if vi-tri == "left" { + grid( + columns: (w-hinh, 1fr), column-gutter: khoang, + align: (center + top, left + top), + hinh, tren, + ) + } else { + grid( + columns: (1fr, w-hinh), column-gutter: khoang, + align: (left + top, center + top), + tren, hinh, + ) + }) + parbreak() + _noi-khoi(khoi.slice(k)) + } + if be-rong != auto { + context layout(kich => { + let w = if type(be-rong) == ratio { kich.width * be-rong } else if type(be-rong) == length { be-rong } else { none } + let r = if _om-bat(om) and w != none { bo-cuc-om(w, kich) } else { none } + if r == none { hai-cot(align(center + horizon, hinh)) } else { r } + }) + } else { + context layout(kich => { + let w = measure(hinh).width + if w <= ti-le * kich.width { + let r = if _om-bat(om) { bo-cuc-om(w, kich) } else { none } + if r == none { hai-cot(hinh) } else { r } + } else { + de + rieng + duoi + } + }) + } + } +} + +// Khối "Hướng dẫn giải" dùng chung cho cả 4 dạng. +// hinh: hình kèm LỜI GIẢI — bố cục 2 cột/hàng riêng như voi-hinh +// (vi-tri: "right"|"left"|"center"|"top"|"bottom"; be-rong: auto hoặc chỉ định). +#let _khoi-giai(lg, hinh: none, vi-tri: "right", be-rong: auto) = block( + width: 100%, inset: (left: 11pt, top: 4pt, bottom: 4pt), + stroke: (left: 2.5pt + _luc), above: 8pt, + voi-hinh({ + align(center, text(fill: _luc, weight: "bold", size: 0.84em)[Hướng dẫn giải]) + lg + }, hinh, vi-tri: vi-tri, be-rong: be-rong), +) + +// Hiện lời giải theo công tắc + bước `buoc`; trước bước đó KHÔNG chiếm chỗ +// (tránh phần ẩn tràn trang sinh trang trắng đệm khi trình chiếu). +#let _hien-giai(loi-giai, buoc, hinh: none, vi-tri: "right", be-rong: auto) = context { + if _hien-da.get() and loi-giai != none { + if buoc == none or _buoc-ht.get() >= buoc { + _khoi-giai(loi-giai, hinh: hinh, vi-tri: vi-tri, be-rong: be-rong) + } + } +} + +// ---------- MC: trắc nghiệm nhiều phương án ---------- +// Một phương án: thẻ chữ cái + nội dung; phương án đúng tô xanh lá. +// ---------- ÉP CÔNG THỨC KHỐI THÀNH TRONG DÒNG ---------- +// Người soạn (và nhất là AI sinh bài) rất hay gõ `$ cases(x > 0, y > 0, x + y < 3) $` +// CÓ khoảng trắng sát hai dấu $ ⇒ Typst hiểu là công thức TRÌNH BÀY GIỮA DÒNG +// (block) ⇒ phương án bị xuống dòng và canh giữa, cột phình cao, hệ 3 bpt trông +// khác hẳn hệ 2 bpt viết `$cases(...)$`. Hàm này duyệt nội dung phương án/ý và +// đổi MỌI equation `block: true` → `block: false`, cho ra đúng dáng trong dòng. +// AN TOÀN: chỉ đụng tới `math.equation`; nội dung không có công thức khối thì +// trả về NGUYÊN VẸN (so sánh `==` giữ nguyên đối tượng cũ). KHÔNG đệ quy vào +// block/grid/table/figure… (phương án là hình, bảng… giữ nguyên bố cục cũ). +#let _ep-trong-dong(c) = { + if type(c) == array { return c.map(_ep-trong-dong) } + if type(c) != content { return c } + let f = c.func() + if f == math.equation { + if c.at("block", default: false) { return math.equation(block: false, c.body) } + return c + } + if _la-seq(c) { + let ds = c.children.map(_ep-trong-dong) + return if ds.len() == 0 { c } else { ds.join() } + } + c +} + +// Có ép công thức khối thành trong dòng không? (đọc trong context) +// trong-dong: auto = theo cài đặt toàn bài; true/false = ép riêng câu này. +#let _ep-ds(ds, trong-dong) = { + let bat = if trong-dong == auto { _kieu.get().at("eq-dong", default: true) } else { trong-dong } + if bat { ds.map(_ep-trong-dong) } else { ds } +} + +#let _o-pa(chu, noi-dung, dung) = { + if dung { + box(fill: _luc.lighten(87%), radius: 3.5pt, inset: (x: 5pt, y: 3.5pt))[ + #_the(chu, mau: _luc) #noi-dung + ] + } else { + [#_the(chu) #noi-dung] + } +} + +// cot: auto = tự chọn 4 / 2 / 1 cột theo độ dài phương án dài nhất, +// hoặc số cột cố định (1, 2, 4). +// lo-da: bước lộ đáp án; loi-giai + lo-giai: lời giải và bước lộ lời giải. +// Phương án đúng: bọc True(...) trong danh sách (ưu tiên) hoặc dap-an: "B" (cũ). +#let cau-mc( + cau, phuong-an, dap-an: none, cot: auto, diem: none, hinh: none, + lo-da: none, loi-giai: none, lo-giai: auto, cham: auto, khoa-pa: false, + trong-dong: auto, + fig-pos: "right", fig-width: auto, lines: 0, num: auto, prefix: "Câu", boxed: false, + fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto, +) = { + _ghi-da("tn", _chu-da-tn(phuong-an, dap-an), khoa: khoa-pa) + _khung-cau(boxed, { + let hinh-giai = if hinh-giai != none { hinh-giai } else { fig-giai } + let t = _tach-true(phuong-an) + let pa = t.ds + let vi-dung = t.dung.position(d => d) + let da = if vi-dung != none { ("A", "B", "C", "D", "E", "F").at(vi-dung) } else { dap-an } + voi-hinh({ _dau-cau(diem: diem, prefix: prefix, num: num); cau }, hinh, + vi-tri: fig-pos, be-rong: fig-width, duoi: { + // khoảng cách ĐỀ → phương án cũng theo hệ số gian-dong (1.0 = 6pt như cũ) + context v(6pt * _he-so-gian()) + context { + let hien = _da-hien(lo-da) and da != none + // ÉP TRONG DÒNG TRƯỚC rồi mới chấm câu: sau khi ép, `$ ... $` không còn + // là "khối" nên `cham-cau` thêm được dấu chấm — đúng như hệ 2 bpt. + let pa = _cham-ds(_ep-ds(pa, trong-dong), cham) + let cells = pa.enumerate().map(p => { + let chu = ("A", "B", "C", "D", "E", "F").at(p.at(0)) + _o-pa(chu, p.at(1), hien and chu == da) + }) + layout(kich => { + let so-cot = if cot == auto { + // đo phương án dài nhất, cộng bề rộng thẻ + khoảng đệm + let rong = calc.max(..pa.map(x => measure([#x]).width)) + 42pt + if 4 * rong <= kich.width { 4 } else if 2 * rong <= kich.width { 2 } else { 1 } + } else { cot } + grid( + columns: (1fr,) * so-cot, + // gian-dong với tới CẢ khoảng cách giữa các hàng phương án + // (hệ số 1.0 = 9pt như cũ ⇒ bài cũ không đổi bố cục). + row-gutter: 9pt * _he-so-gian(), column-gutter: 12pt, + ..cells, + ) + }) + } + }) + _chua-dong(lines, lo-da) + _hien-giai(loi-giai, if lo-giai == auto { lo-da } else { lo-giai }, + hinh: hinh-giai, vi-tri: fig-giai-pos, be-rong: fig-giai-width) + }, + ) +} + +// ---------- DÒ NHÃN THỨ TỰ GÕ TAY (chống lặp "a) a) …") ---------- +// Người soạn (hoặc AI sinh bài) hay tự gõ sẵn "a)", "1.", "(b)"… vào đầu item, +// trong khi cot-item vốn TỰ đánh nhãn ⇒ ra "a) a) Phải quay…". Bộ dò dưới đây +// đọc CHUỖI ĐẦU của từng item; item nào đã có nhãn thì cot-item bỏ nhãn tự động. +// NHẬN: a) a. a: A) (a) [b] 1) 1. (2) ii) IV. +// KHÔNG nhận: "0,5 lít" · "0.5 lít" (sau dấu là chữ số) · "Ta có" · "$x = 1$" +#let _dau-nhan = (")", ".", ":") +#let _chu-cai = "abcdefghijklmnopqrstuvwxyz" +#let _chu-so = "0123456789" +#let _so-la-ma = ("i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix", "x", + "xi", "xii") + +// Chuỗi văn bản ĐẦU của một nội dung (chỉ cần vài ký tự đủ soi nhãn). +// Gặp "vật cản" (toán, hình, bảng, danh sách) thì trả "$" để chắc chắn không khớp. +#let _dau-van(c, gh: 10) = { + if c == none { return "" } + let t = type(c) + if t == int or t == float { return str(c) } + if t == str { return c } + if t == array { + let s = "" + for x in c { + s = s + _dau-van(x, gh: gh) + if s.trim(at: start).clusters().len() >= gh { break } + } + return s + } + if t != content { return "" } + let f = c.func() + if f == _f-space or f == linebreak or f == parbreak { return " " } + if f == metadata { return "" } + if _la-khoi(f) or f == math.equation { return "$" } + if c.has("text") { return if type(c.text) == str { c.text } else { "$" } } + if c.has("children") { return _dau-van(c.children, gh: gh) } + if c.has("body") { return _dau-van(c.body, gh: gh) } + if c.has("child") { return _dau-van(c.child, gh: gh) } // phần tử styled + "" +} + +// Thứ tự của nhãn gõ tay ở đầu item: 1 cho a)/1)/(a), 2 cho b)/2)… ; +// none = không thấy nhãn nào. +#let _thu-nhan-tay(c) = { + let cl = _dau-van(c).trim(at: start).clusters() + if cl.len() == 0 { return none } + let mo = (cl.at(0) == "(" or cl.at(0) == "[") + let i = if mo { 1 } else { 0 } + // thân nhãn: TOÀN chữ cái hoặc TOÀN chữ số, tối đa 4 ký tự + let than = "" + let loai = none + while i < cl.len() and than.clusters().len() < 4 { + let l = lower(cl.at(i)) + let la-so = (_chu-so.contains(l) and loai != "chu") + let la-chu = (_chu-cai.contains(l) and loai != "so") + if la-so { loai = "so" } else if la-chu { loai = "chu" } else { break } + than = than + l + i = i + 1 + } + if loai == none or i >= cl.len() { return none } + // dấu ngăn: trong ngoặc thì phải đóng ngoặc, ngoài ngoặc thì ) . : + let d = cl.at(i) + let hop-le = if mo { d == ")" or d == "]" } else { _dau-nhan.contains(d) } + if not hop-le { return none } + // Sau dấu CHẤM / HAI CHẤM bắt buộc là khoảng trắng hoặc hết chuỗi, kẻo nhận + // nhầm "0.5 lít" hay "12:30". Sau dấu ĐÓNG NGOẶC thì "a)Nội dung" vẫn tính. + i = i + 1 + let can-cach = (d == "." or d == ":") + if can-cach and i < cl.len() and cl.at(i).trim() != "" { return none } + if loai == "so" { + if than.starts-with("0") { return none } // "01." không phải nhãn + let k = int(than) + return if k >= 1 and k <= 99 { k } else { none } + } + if than.clusters().len() == 1 { + let k = _chu-cai.clusters().position(x => x == than) + return if k == none { none } else { k + 1 } + } + let k = _so-la-ma.position(x => x == than) + if k == none { none } else { k + 1 } +} + +// Quyết định BỎ nhãn tự động cho từng item: +// • MỌI item đều đã có nhãn tay -> bỏ hết (kể cả khi đánh tiếp d), e), f)). +// • chỉ VÀI item có nhãn tay -> chỉ bỏ ở item mà nhãn tay ĐÚNG thứ tự của nó +// (tránh nhận nhầm câu mở đầu bằng "A. B. C thẳng hàng"). +#let _bo-nhan-tu-dong(items) = { + let n = items.len() + let thu = items.map(_thu-nhan-tay) + let co = thu.filter(x => x != none).len() + if co == n and n > 0 { range(n).map(i => true) } else { + range(n).map(i => thu.at(i) == i + 1) + } +} + +// ---------- CHIA CỘT CÁC ITEM (dùng trong thân câu, nhất là TL) ---------- +// Tự đánh nhãn a) b) c)... và xếp các item thành nhiều cột. +// so-cot: auto => ĐO item dài nhất rồi chọn số cột lớn nhất còn vừa bề rộng +// (thử lần lượt trong `muc-cot`); hoặc số cột cố định. +// theo-cot: true => xếp DỌC theo cột (a,b,c | d,e,f); false => theo hàng. +// kieu-nhan: mẫu numbering ("a)", "1)", ...); none => không đánh nhãn. +// do-nhan-tay: true (mặc định) => item nào ĐÃ gõ sẵn "a)", "1.", "(b)"… thì +// KHÔNG đánh nhãn tự động nữa (chống lặp "a) a) …"); false => luôn đánh. +// +// LƯU Ý (beamer): các `\` NẰM TRONG một item chỉ là xuống dòng thường — +// KHÔNG tạo bước hoạt hình, vì chúng nằm trong lưới nên bộ tách bước ở cấp +// cao nhất của loi-giai không thấy. Cả item (thậm chí cả khối cot-item) hiện +// MỘT LƯỢT trong một bước. Muốn hiện dần từng cột theo bước thì bọc từng item +// bằng lo(n)[...], ví dụ: cot-item(so-cot: 2, lo(2)[...], lo(3)[...]). +// Cần hiện dần từng dòng như bình thường thì viết thẳng với `\`, đừng cho vào +// cot-item. +#let cot-item( + ..noi-dung, + so-cot: auto, + muc-cot: (4, 3, 2, 1), // các mức cột sẽ thử khi so-cot = auto (ưu tiên lớn) + dem: 42pt, // đệm mỗi cột (nhãn + khoảng cách), như cau-mc + theo-cot: true, + kieu-nhan: "a)", + do-nhan-tay: true, // dò nhãn a)/1)… gõ sẵn trong item -> không đánh chồng + cach-cot: 14pt, + cach-hang: auto, // auto = 8pt × hệ số gian-dong (1.0 ⇒ 8pt như cũ) +) = { + let items = noi-dung.pos() + let n = items.len() + if n == 0 { return } + let bo = if do-nhan-tay { _bo-nhan-tu-dong(items) } else { range(n).map(i => false) } + let nhan(i) = if kieu-nhan == none or bo.at(i) { [] } else { + [#numbering(kieu-nhan, i + 1) ] + } + let o(i) = box(width: 100%, [#nhan(i)#items.at(i)]) + context layout(kich => { + let so = if so-cot == auto { + // đo item dài nhất (kèm nhãn) + đệm để tính số cột vừa khung + let rong = calc.max(..range(n).map(i => measure([#nhan(i)#items.at(i)]).width)) + dem + let vua = muc-cot.filter(k => k * rong <= kich.width) + if vua.len() > 0 { calc.max(..vua) } else { 1 } + } else { so-cot } + let so = calc.min(so, n) + let so-hang = calc.ceil(n / so) + let cells = () + for r in range(so-hang) { + for c in range(so) { + let idx = if theo-cot { c * so-hang + r } else { r * so + c } + cells.push(if idx < n { o(idx) } else { [] }) + } + } + grid( + columns: (1fr,) * so, + column-gutter: cach-cot, + row-gutter: if cach-hang == auto { 8pt * _he-so-gian() } else { cach-hang }, + align: left + top, + ..cells, + ) + }) +} + +// ---------- TF: đúng — sai ---------- +// o-tick: true => mỗi phát biểu có 2 ô Đ/S dóng thẳng sát lề phải +// (khi hiện đáp án sẽ tick ✓ vào ô tương ứng); false => nhãn Đ/S sau câu. +// Phát biểu ĐÚNG: bọc True(...) trong danh sách (ưu tiên) hoặc +// dap-an: (false, true, ...) như cũ. +// giai: của True/False -> lời giải riêng từng ý, tự đánh nhãn theo thứ tự hiện +// tại nên hoán vị không lệch. khoa-y: true -> báo công cụ trộn ĐỪNG xáo các ý +// (dùng cho câu mà ý sau dựa vào kết quả ý trước). +#let cau-tf( + cau, cac-y, dap-an: none, diem: none, hinh: none, + lo-da: none, loi-giai: none, lo-giai: auto, o-tick: false, cham: auto, + khoa-y: false, trong-dong: auto, + fig-pos: "right", fig-width: auto, lines: 0, num: auto, prefix: "Câu", boxed: false, + fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto, +) = { + _ghi-da("ds", if _tach-true(cac-y).dung.any(d => d) { _tach-true(cac-y).dung } else { dap-an }, + khoa: khoa-y) + _khung-cau(boxed, { + let hinh-giai = if hinh-giai != none { hinh-giai } else { fig-giai } + let t = _tach-true(cac-y) + let cac-y = t.ds + let dap-an = if t.dung.any(d => d) { t.dung } else { dap-an } + // Lời giải riêng từng ý -> ghép thành khối "a) … b) …" theo thứ tự HIỆN TẠI + let loi-giai = if t.giai.any(g => g != none) { + let nhan-y = ("a", "b", "c", "d", "e", "f") + { + if loi-giai != none { loi-giai; v(3pt) } + for (j, g) in t.giai.enumerate() { + if g != none { + block(above: 5pt, below: 5pt, { + _the(nhan-y.at(j), duoi: ")"); [ ]; g + }) + } + } + } + } else { loi-giai } + voi-hinh({ _dau-cau(diem: diem, prefix: prefix, num: num); cau }, hinh, + vi-tri: fig-pos, be-rong: fig-width, duoi: { + context v(4pt * _he-so-gian()) + context { + let hien = _da-hien(lo-da) and dap-an != none + let cac-y = _cham-ds(_ep-ds(cac-y, trong-dong), cham) + let nhan-y = ("a", "b", "c", "d", "e") + let k-mau = _kieu.get().mau + if o-tick and _hien-o() { + let o-vuong(tick, m) = box( + width: 15pt, height: 15pt, stroke: 0.8pt + luma(45%), radius: 2pt, + fill: if tick { m.lighten(80%) } else { white }, + align(center + horizon, + if tick { text(fill: m, weight: "bold", size: 9pt)[✓] }), + ) + grid( + columns: (1fr, 26pt, 26pt), + align: (left + horizon, center + horizon, center + horizon), + row-gutter: 8pt * _he-so-gian(), column-gutter: 6pt, + [], text(weight: "bold", fill: k-mau, size: 0.82em)[Đ], + text(weight: "bold", fill: k-mau, size: 0.82em)[S], + ..cac-y.enumerate().map(p => ( + [#_the(nhan-y.at(p.at(0)), duoi: ")") #p.at(1)], + o-vuong(hien and dap-an.at(p.at(0)), _luc), + o-vuong(hien and not dap-an.at(p.at(0)), _do), + )).flatten(), + ) + } else { + let gy = 6pt * _he-so-gian() // gian-dong với tới khoảng cách giữa các ý + for j in range(cac-y.len()) { + block(above: gy, below: gy, { + _the(nhan-y.at(j), duoi: ")") + [ ] + cac-y.at(j) + if hien { + h(7pt) + let dung = dap-an.at(j) + let m = if dung { _luc } else { _do } + box(fill: m.lighten(86%), radius: 3pt, inset: (x: 6pt, y: 1.5pt), + text(size: 0.78em, weight: "bold", fill: m, if dung { "Đ" } else { "S" })) + } + }) + } + } + } + }) + _chua-dong(lines, lo-da) + _hien-giai(loi-giai, if lo-giai == auto { lo-da } else { lo-giai }, + hinh: hinh-giai, vi-tri: fig-giai-pos, be-rong: fig-giai-width) + }, + ) +} + +// ---------- SA: trả lời ngắn ---------- +// Khi ẩn đáp án: hiện các ô vuông để điền (như phiếu trả lời). +// Đáp án đặt NGAY SAU đề (positional thứ 2, ưu tiên) hoặc dap-an: (cũ): +// #cau-sa([Đề...], [$6$], loi-giai: [...]) +// show-boxes/box-count: hiện/số ô điền (bí danh cũ o-dien vẫn chạy). +#let cau-sa( + cau, ..tra-loi, dap-an: none, diem: none, o-dien: 4, hinh: none, + lo-da: none, loi-giai: none, lo-giai: auto, + fig-pos: "right", fig-width: auto, show-boxes: true, box-count: auto, + lines: 0, num: auto, prefix: "Câu", boxed: false, + fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto, +) = { + _ghi-da("tln", if tra-loi.pos().len() > 0 { tra-loi.pos().first() } else { dap-an }) + _khung-cau(boxed, { + let hinh-giai = if hinh-giai != none { hinh-giai } else { fig-giai } + let dap-an = if tra-loi.pos().len() > 0 { tra-loi.pos().first() } else { dap-an } + let so-o = if box-count == auto { o-dien } else { box-count } + voi-hinh({ _dau-cau(diem: diem, prefix: prefix, num: num); cau }, hinh, + vi-tri: fig-pos, be-rong: fig-width, duoi: { + context v(6pt * _he-so-gian()) + context { + if _da-hien(lo-da) and dap-an != none { + text(weight: "bold", fill: _luc)[Đáp án: ] + text(fill: _luc, dap-an) + } else if show-boxes and _hien-o() { + [Trả lời: ] + box(baseline: 32%, stack( + dir: ltr, spacing: 4pt, + ..range(so-o).map(k => rect(width: 21pt, height: 21pt, stroke: 0.8pt + _xanh, radius: 2pt)), + )) + } + } + }) + _chua-dong(lines, lo-da) + _hien-giai(loi-giai, if lo-giai == auto { lo-da } else { lo-giai }, + hinh: hinh-giai, vi-tri: fig-giai-pos, be-rong: fig-giai-width) + }, + ) +} + +// ---------- Thân chung cho các dạng "kiểu tự luận" (TL / HĐ / VDTT) ---------- +// dau: thẻ đầu câu; loi-giai: chỉ hiện khi bật đáp án; +// cho-trong: chừa chỗ trống khi in đề. +#let _than-tl(dau, cau, loi-giai, cho-trong, lo-da, hinh: none, + fig-pos: "right", fig-width: auto, + hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto) = context block( + width: 100%, above: 10pt * _he-so-gian(), below: 10pt * _he-so-gian(), + { + voi-hinh({ dau; cau }, hinh, vi-tri: fig-pos, be-rong: fig-width) + context { + if _da-hien(lo-da) and loi-giai != none { + _khoi-giai(loi-giai, hinh: hinh-giai, vi-tri: fig-giai-pos, be-rong: fig-giai-width) + } else { + let t = _trong-tl.get() + if t.bat { + let cao = if cho-trong > 0pt { cho-trong } else { t.cao } + if cao > 0pt { v(cao) } + } + } + } + }, +) + +// Thẻ đầu câu có nhãn + bộ đếm + màu RIÊNG (HĐ, Vận dụng). +#let _dau-rieng(nhan, bd, mau, diem) = { + _the([#nhan#_so-moi(bd)], mau: mau) + if diem != none { + text(weight: "bold", fill: mau, size: 0.86em)[ (#diem điểm)] + } + h(6pt) +} + +// Thẻ đầu câu KHÔNG đánh số + màu RIÊNG (Khám phá, Trải nghiệm, Thảo luận — +// dạng hoạt động một-lần của SGK, nhãn không mang số thứ tự). +#let _dau-nhan(nhan, mau, diem) = { + _the([#nhan], mau: mau) + if diem != none { + text(weight: "bold", fill: mau, size: 0.86em)[ (#diem điểm)] + } + h(6pt) +} + +// ---------- TL: tự luận ---------- +#let cau-tl(cau, loi-giai: none, diem: none, cho-trong: 0pt, hinh: none, lo-da: none, + fig: none, fig-pos: "right", fig-width: auto, + fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto) = _than-tl( + _dau-cau(diem: diem), cau, loi-giai, cho-trong, lo-da, + hinh: if hinh != none { hinh } else { fig }, + fig-pos: fig-pos, fig-width: fig-width, + hinh-giai: if hinh-giai != none { hinh-giai } else { fig-giai }, + fig-giai-pos: fig-giai-pos, fig-giai-width: fig-giai-width, +) + +// ---------- HĐ: hoạt động (khởi động / khám phá kiến thức) ---------- +// Thẻ "HĐ1", "HĐ2"... — bộ đếm riêng, hình thức như TL (dùng được cot-item +// trong thân câu khi có nhiều ý hỏi). +#let _cam-hd = rgb("#d35400") +#let cau-hd(cau, loi-giai: none, diem: none, cho-trong: 0pt, hinh: none, lo-da: none, + fig: none, fig-pos: "right", fig-width: auto, + fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto) = _than-tl( + _dau-rieng([HĐ], _bd-hd, _cam-hd, diem), cau, loi-giai, cho-trong, lo-da, + hinh: if hinh != none { hinh } else { fig }, + fig-pos: fig-pos, fig-width: fig-width, + hinh-giai: if hinh-giai != none { hinh-giai } else { fig-giai }, + fig-giai-pos: fig-giai-pos, fig-giai-width: fig-giai-width, +) + +// ---------- LT: luyện tập (củng cố lý thuyết vừa học) ---------- +// Thẻ "Luyện tập 1", "Luyện tập 2"... — DÙNG CHUNG bộ đếm với khung +// #luyen-tap (slide.typ) nên số thứ tự liên tục dù trộn hai kiểu. +#let _vang-lt = rgb("#b9770e") +#let cau-lt(cau, loi-giai: none, diem: none, cho-trong: 0pt, hinh: none, lo-da: none, + fig: none, fig-pos: "right", fig-width: auto, + fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto) = _than-tl( + _dau-rieng([Luyện tập~], _bd-lt, _vang-lt, diem), cau, loi-giai, cho-trong, lo-da, + hinh: if hinh != none { hinh } else { fig }, + fig-pos: fig-pos, fig-width: fig-width, + hinh-giai: if hinh-giai != none { hinh-giai } else { fig-giai }, + fig-giai-pos: fig-giai-pos, fig-giai-width: fig-giai-width, +) + +// ---------- VDTT: vận dụng thực tế ---------- +// Thẻ "Vận dụng 1", "Vận dụng 2"... — bộ đếm riêng, hình thức như TL. +#let _tim-vdtt = rgb("#8e44ad") +#let cau-vdtt(cau, loi-giai: none, diem: none, cho-trong: 0pt, hinh: none, lo-da: none, + fig: none, fig-pos: "right", fig-width: auto, + fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto) = _than-tl( + _dau-rieng([Vận dụng~], _bd-vdtt, _tim-vdtt, diem), cau, loi-giai, cho-trong, lo-da, + hinh: if hinh != none { hinh } else { fig }, + fig-pos: fig-pos, fig-width: fig-width, + hinh-giai: if hinh-giai != none { hinh-giai } else { fig-giai }, + fig-giai-pos: fig-giai-pos, fig-giai-width: fig-giai-width, +) + +// ---------- 3 HÌNH THỨC HOẠT ĐỘNG SGK — KHÔNG đánh số (Kết nối tri thức) ---- +// Nhãn "Khám phá" / "Trải nghiệm" / "Thảo luận" như thẻ HĐ nhưng KHÔNG mang số +// thứ tự (hoạt động một-lần); hình thức thân như TL (dùng được cot-item, hình, +// lời giải, lo-da…). Mỗi loại một màu riêng. +#let _luc-kham-pha = rgb("#16a085") // xanh lục (khám phá) +#let cau-kham-pha(cau, loi-giai: none, diem: none, cho-trong: 0pt, hinh: none, lo-da: none, + fig: none, fig-pos: "right", fig-width: auto, + fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto) = _than-tl( + _dau-nhan([Khám phá], _luc-kham-pha, diem), cau, loi-giai, cho-trong, lo-da, + hinh: if hinh != none { hinh } else { fig }, + fig-pos: fig-pos, fig-width: fig-width, + hinh-giai: if hinh-giai != none { hinh-giai } else { fig-giai }, + fig-giai-pos: fig-giai-pos, fig-giai-width: fig-giai-width, +) + +#let _luc-trai-nghiem = rgb("#27ae60") // xanh lá (trải nghiệm) +#let cau-trai-nghiem(cau, loi-giai: none, diem: none, cho-trong: 0pt, hinh: none, lo-da: none, + fig: none, fig-pos: "right", fig-width: auto, + fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto) = _than-tl( + _dau-nhan([Trải nghiệm], _luc-trai-nghiem, diem), cau, loi-giai, cho-trong, lo-da, + hinh: if hinh != none { hinh } else { fig }, + fig-pos: fig-pos, fig-width: fig-width, + hinh-giai: if hinh-giai != none { hinh-giai } else { fig-giai }, + fig-giai-pos: fig-giai-pos, fig-giai-width: fig-giai-width, +) + +#let _lam-thao-luan = rgb("#2980b9") // xanh dương (thảo luận) +#let cau-thao-luan(cau, loi-giai: none, diem: none, cho-trong: 0pt, hinh: none, lo-da: none, + fig: none, fig-pos: "right", fig-width: auto, + fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto) = _than-tl( + _dau-nhan([Thảo luận], _lam-thao-luan, diem), cau, loi-giai, cho-trong, lo-da, + hinh: if hinh != none { hinh } else { fig }, + fig-pos: fig-pos, fig-width: fig-width, + hinh-giai: if hinh-giai != none { hinh-giai } else { fig-giai }, + fig-giai-pos: fig-giai-pos, fig-giai-width: fig-giai-width, +) diff --git a/packages/preview/conic-toan/0.3.4/lib/che-do.typ b/packages/preview/conic-toan/0.3.4/lib/che-do.typ new file mode 100644 index 0000000000..4d207cd50a --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/che-do.typ @@ -0,0 +1,1394 @@ +// ===================================================================== +// che-do.typ — MỘT FILE NGUỒN, BA KIỂU PDF (hồ sơ hiển thị) +// +// "dethi" : dạng đề thi/sách A4 — MC, TF, SA, TL ẩn đáp án + lời giải; +// riêng VÍ DỤ vẫn hiện lời giải. +// "loigiai" : dạng A4 — tất cả hiện lời giải, đánh dấu đáp án. +// "beamer" : trình chiếu — MỖI CÂU MỘT SLIDE: hiện đề → lời giải hiện +// dần theo từng dấu \ (xuống dòng) → cuối cùng đánh dấu đáp án. +// (Tên cũ "de-thi", "loi-giai" vẫn được chấp nhận.) +// +// Cách dùng (xem de-mau.typ): +// #let ho-so = sys.inputs.at("ho-so", default: "dethi") +// #show: de-toan.with(ho-so: ho-so, tieu-de: [...], ...) +// rồi gọi THẲNG #vd/#tn/#ds/#tln/#tl/#hd/#lt/#vdtt/#phan — các hàm tự +// nhận biết hồ sơ hiển thị, KHÔNG cần khai báo gì thêm. +// (tn = trắc nghiệm, ds = đúng sai, tln = trả lời ngắn, tl = tự luận, +// hd = hoạt động, lt = luyện tập, vdtt = vận dụng thực tế; +// tên cũ mc/tf/sa và dòng tao-cau-hoi cũ vẫn dùng được) +// ===================================================================== +#import "slide.typ": slide, muc, bai-giang, lo, vi-du, loi-giai, _ho-so, _la-sach, _buoc-ht, gian-dong, _gd, _dat-gian, _he-so-gian, _gian-ht, _bd-cau, cao-that, _cao-that, _chong-net, voi-cao-that, _vung-than +#import "cau-hoi.typ": cau-mc, cau-tf, cau-sa, cau-tl, cau-hd, cau-lt, cau-vdtt, cau-kham-pha, cau-trai-nghiem, cau-thao-luan, bat-dap-an, tat-dap-an, voi-hinh, True, Dung, _la-y + +// Gộp tham số kiểu mới / kiểu cũ: ưu tiên giá trị kiểu mới nếu được đặt. +#let _uu-tien(moi, cu) = if moi != none { moi } else { cu } + +#let _xanh = rgb("#0f4c81") +#let _luc = rgb("#1e8449") +#let _do = rgb("#a93226") +// giữ tham chiếu đến môi trường lời giải (vì tham số cùng tên sẽ che nó) +#let _mt-loi-giai = loi-giai + +// Mã đề khai ở de-toan -> lưu vào state để #bang-dap-an tự lấy (khỏi nhập tay). +#let _ma-de = state("bg-ma-de", none) + +// ===================================================================== +// HOÁN VỊ (TRỘN ĐỀ) — công tắc đặt Ở ĐẦU FILE ĐỀ +// --------------------------------------------------------------------- +// Cách dùng (khung một file ngân hàng / một đề thi): +// #let ho-so = sys.inputs.at("ho-so", default: "dethi") +// #let hoan-vi = false // <- ĐỔI THÀNH true LÀ TRỘN +// #show: de-toan.with(ho-so: ho-so, ma-de: "0101", hoan-vi: hoan-vi) +// +// NGUYÊN TẮC (chốt 08/2026): +// • Hoán vị các CÂU trong cùng MỘT NHÓM THỂ LOẠI: nhóm tn xáo với nhau, +// nhóm ds xáo với nhau, nhóm tln xáo với nhau. Câu tl GIỮ NGUYÊN thứ tự. +// "Nhóm" = dãy câu CÙNG LOẠI đứng liền nhau (chỉ cách nhau bằng dòng +// trống). Gặp #phan, một đoạn văn xen giữa, một câu tl... là hết nhóm và +// sang nhóm mới ⇒ câu KHÔNG BAO GIỜ nhảy qua tiêu đề phần. +// • Hoán vị 4 PHƯƠNG ÁN A/B/C/D của câu tn. Các Ý a/b/c/d của câu ds GIỮ +// NGUYÊN, KHÔNG hoán vị (ý sau thường dựa vào ý trước). +// • Chỉ chạy ở bản in A4 (dethi/loigiai); trình chiếu beamer không trộn. +// • dethi và loigiai cùng mầm ⇒ RA CÙNG MỘT THỨ TỰ, bảng đáp án luôn khớp. +// +// hoan-vi: false (mặc định) | true (trộn cả câu lẫn phương án) +// | "cau" (chỉ trộn thứ tự câu) | "pa" (chỉ trộn phương án) +// mam: MÃ TRỘN — auto (mặc định) = băm từ `ma-de`, nên hai mã đề 0101 / +// 0102 tự cho hai thứ tự khác nhau; đặt tay `mam: 7` để ghi đè. +// +// GIỚI HẠN đã biết: +// • Câu tn viết theo FORM CŨ `dap-an: "B"` (chữ cái) KHÔNG được trộn phương +// án — trộn sẽ lệch đáp án. Muốn trộn thì bọc phương án đúng bằng True(...). +// • `khoa-pa: true` ở một câu tn ⇒ riêng câu đó giữ nguyên thứ tự phương án. +// ===================================================================== + +// ---------- Bộ sinh số giả ngẫu nhiên TẤT ĐỊNH (LCG 32 bit) ---------- +// Typst không có random. Dùng LCG để CÙNG một mầm luôn cho CÙNG một kết quả — +// nhờ vậy bản dethi và bản loigiai (hai lần biên dịch khác nhau) trộn giống hệt. +#let _hv-m = 2147483648 +#let _hv-tiep(s) = calc.rem(1103515245 * s + 12345, _hv-m) + +// Chuẩn hoá mầm: số / chuỗi / nội dung / none -> số nguyên không âm (băm djb2). +#let _hv-mam(x) = { + if x == none or x == auto { return 20250101 } + if type(x) == int { return calc.rem(calc.abs(x), _hv-m) } + let s = if type(x) == str { x } else { repr(x) } + let h = 5381 + for c in s.clusters() { + h = calc.rem(h * 33 + str.to-unicode(c), _hv-m) + } + h +} + +// Xáo một mảng theo mầm — RÚT ngẫu nhiên không hoàn lại (khỏi gán theo chỉ số). +#let _hv-xao(mang, mam) = { + let con = mang + let kq = () + let s = _hv-tiep(_hv-mam(mam) + 1013904223) + while con.len() > 0 { + s = _hv-tiep(s) + let j = calc.rem(calc.quo(s, 65536), con.len()) + kq.push(con.at(j)) + con = con.slice(0, j) + con.slice(j + 1) + } + kq +} + +// ---------- Nhãn nhận diện câu (đánh dấu chỗ được phép xáo) ---------- +#let _hv-nhan = (tn: , ds: , tln: ) +#let _gan-tn(c) = [#c ] +#let _gan-ds(c) = [#c ] +#let _gan-tln(c) = [#c ] + +// Phần tử "trắng" giữa hai câu (dòng trống, xuống dòng, khoảng trắng, +// metadata) — bỏ qua khi gom nhóm và LUÔN giữ nguyên vị trí lúc ghép lại. +#let _hv-f-space = [ ].func() +#let _hv-trong(c) = { + if type(c) == str { return c.trim() == "" } + if type(c) != content { return false } + let f = c.func() + f == _hv-f-space or f == parbreak or f == linebreak or f == metadata +} + +// Loại của một phần tử: "tn" | "ds" | "tln" | none. +// Nếu markup bọc câu trong một sequence MỘT phần tử thì bóc thêm một lớp. +#let _hv-loai(c) = { + if type(c) != content { return none } + let l = c.at("label", default: none) + if l == _hv-nhan.tn { return "tn" } + if l == _hv-nhan.ds { return "ds" } + if l == _hv-nhan.tln { return "tln" } + if l == none and c.has("children") { + let ct = c.children.filter(x => not _hv-trong(x)) + if ct.len() == 1 { return _hv-loai(ct.first()) } + } + none +} + +// ---------- Xáo THỨ TỰ CÂU trong thân tài liệu ---------- +// Duyệt các phần tử cấp cao nhất, gom từng DÃY LIỀN NHAU cùng loại rồi hoán vị +// các phần tử của dãy đó TẠI CHỖ (vị trí nào là câu tn thì sau khi trộn vẫn là +// câu tn). Phần tử không nhận diện được giữ nguyên vị trí ⇒ nếu vì lý do nào đó +// không nhận ra câu, tài liệu vẫn dựng đúng y như khi tắt trộn. +#let _hv-xao-than(than, mam) = { + if type(than) != content or not than.has("children") { return than } + let cs = than.children + let n = cs.len() + let kq = () + let nhom = 0 + let i = 0 + while i < n { + let loai = _hv-loai(cs.at(i)) + if loai == none { + kq.push(cs.at(i)) + i = i + 1 + } else { + // gom dãy: các câu CÙNG loại, chỉ được cách nhau bằng phần tử trắng + let vt = () + let cuoi = i + let j = i + while j < n { + let lj = _hv-loai(cs.at(j)) + if lj == loai { + vt.push(j) + cuoi = j + j = j + 1 + } else if _hv-trong(cs.at(j)) { + j = j + 1 + } else { + break + } + } + nhom = nhom + 1 + let cac = vt.map(k => cs.at(k)) + let moi = if cac.len() > 1 { _hv-xao(cac, _hv-mam(mam) + nhom * 7919) } else { cac } + let d = 0 + for k in range(i, cuoi + 1) { + if vt.contains(k) { + kq.push(moi.at(d)) + d = d + 1 + } else { + kq.push(cs.at(k)) + } + } + i = cuoi + 1 + } + } + if kq.len() == 0 { than } else { kq.join() } +} + +// Trạng thái để #tn biết có xáo phương án hay không (và xáo theo mầm nào). +// bat: xáo PHƯƠNG ÁN · cau: xáo THỨ TỰ CÂU · mam: mầm chung. +// Khoá `cau` để các lệnh bố cục (#chia-2-cot, #chia-2-cot-lech) tự xáo lại +// phần thân NẰM TRONG cột — vì thân đó đã bị bọc thành MỘT phần tử nên vòng +// duyệt ở #de-toan không nhìn thấy các câu bên trong. +#let _hv = state("bg-hoan-vi", (bat: false, cau: false, mam: 0)) + +// Tách công tắc hoan-vi thành hai cờ: xáo CÂU và xáo PHƯƠNG ÁN. +#let _hv-che(h) = ( + cau: h == true or h == "cau" or h == "ca-hai", + pa: h == true or h == "pa" or h == "ca-hai", +) + +// Dùng như một show-rule RIÊNG khi tài liệu không đi qua #de-toan: +// #show: hoan-vi-de.with(true) +// #show: hoan-vi-de.with(true, mam: "0102") +// (Đi qua #de-toan thì chỉ cần tham số hoan-vi: — khỏi gọi hàm này.) +#let hoan-vi-de(bat, mam: auto, body) = { + let che = _hv-che(bat) + let m = _hv-mam(mam) + _hv.update((bat: che.pa, cau: che.cau, mam: m)) + if che.cau { _hv-xao-than(body, m) } else { body } +} + +// ---------- Tách lời giải thành từng dòng ---------- +// Nhận nội dung có dấu \ (hoặc mảng nội dung), trả về mảng các dòng. +#let tach-dong(nd) = { + if nd == none { return () } + if type(nd) == array { return nd } + if type(nd) == content and nd.has("children") { + let phan = () + let dong = () + for c in nd.children { + if c.func() == linebreak or c.func() == parbreak { + phan.push(dong.join()) + dong = () + } else { + dong.push(c) + } + } + phan.push(dong.join()) + return phan.filter(p => p != none) + } + (nd,) +} + +// ---------- Ngắt lời giải dài sang slide mới (chỉ tác dụng ở beamer) ---------- +// Đặt #sang-man trên MỘT DÒNG RIÊNG trong lời giải: +// loi-giai: [dòng 1 \ dòng 2 \ #sang-man \ dòng 3 \ ...] +// => "dòng 3" trở đi chuyển sang slide kế tiếp (nhãn "Hướng dẫn giải (tiếp)"), +// tránh lời giải tràn trang làm sinh trang đệm trắng khi hiện từng bước. +// Ở bản A4 ("dethi"/"loigiai"), dấu này được bỏ qua. +#let sang-man = metadata("bg-sang-man") + +#let _co-sang-man(d) = { + if type(d) != content { return false } + if d.func() == metadata { return d.value == "bg-sang-man" } + if d.has("children") { d.children.any(c => _co-sang-man(c)) } else { false } +} + +#let _khoang-trang = [ ].func() + +// Dòng "rỗng" — chỉ gồm khoảng trắng / dấu ngắt / metadata (không in ra gì). +#let _dong-trong(d) = { + if d == none { return true } + if type(d) != content { return false } + let f = d.func() + if f == _khoang-trang or f == linebreak or f == parbreak or f == metadata { + return true + } + if f == text { return d.text.trim() == "" } + if d.has("children") { return d.children.all(c => _dong-trong(c)) } + false +} + +// Cắt MỘT dòng tại dấu #sang-man đầu tiên -> (trước, sau, có-cắt-được). +// ⚠️ Lỗi cũ (09/08/2026): `tach-man` coi CẢ DÒNG chứa dấu là dấu ngắt rồi +// VỨT ĐI. Người soạn viết `... hết ý.` xuống dòng rồi `#sang-man \` (không có +// dấu \ ở cuối dòng chữ) thì cả câu chữ đó BIẾN MẤT khỏi trình chiếu. Nay cắt +// đôi tại dấu, giữ nguyên phần chữ hai bên. +// Chỉ cắt ở lớp con TRỰC TIẾP để không bóc mất lớp bọc (strong, emph...); +// dấu nằm sâu hơn thì trả `false` và dòng được giữ nguyên (dấu vô hình, chỉ +// mất tác dụng ngắt màn) — thà không ngắt còn hơn mất chữ. +#let _cat-sang-man(d) = { + if type(d) != content { return (d, none, false) } + if d.func() == metadata { + return if d.value == "bg-sang-man" { (none, none, true) } else { (d, none, false) } + } + if not d.has("children") { return (d, none, false) } + let truoc = () + let sau = () + let co = false + for c in d.children { + if co { sau.push(c) } + else if (type(c) == content and c.func() == metadata + and c.value == "bg-sang-man") { co = true } + else { truoc.push(c) } + } + if not co { (d, none, false) } else { (truoc.join(), sau.join(), true) } +} + +// Tách lời giải thành các màn; mỗi màn là một mảng dòng. +#let tach-man(nd) = { + let man = () + let ht = () + for d in tach-dong(nd) { + let con = d + let dau = true // lần lặp đầu: `con` còn là NGUYÊN dòng ban đầu + while con != none { + let (truoc, sau, co) = _cat-sang-man(con) + // mảnh cắt ra chỉ toàn khoảng trắng thì bỏ; dòng nguyên thì giữ y như cũ + if (dau and not co) or not _dong-trong(truoc) { ht.push(truoc) } + if not co { con = none } else { + if ht.len() > 0 { man.push(ht) } + ht = () + con = sau + } + dau = false + } + } + if ht.len() > 0 { man.push(ht) } + if man.len() == 0 { ((),) } else { man } +} + +// Ghép lại thành một khối (dùng cho bản A4) — bỏ các dấu #sang-man nhưng +// GIỮ nguyên chữ nằm cùng dòng với dấu (đi chung một đường với tach-man). +#let _ghep(nd) = { + if nd == none { return none } + let dong = () + for mk in tach-man(nd) { dong += mk } + if dong.len() == 0 { return none } + dong.join(linebreak()) +} + +// ---------- Lời giải hiện dần từng dòng (dùng trong slide) ---------- +// Khung hiện từ bước `tu`; dòng thứ i hiện ở bước tu + i. +// gian: giãn dòng trong lời giải — ĐỘ DÀI tuyệt đối (mốc 0.95em, ~150% so với +// thân slide 0.62em) để phân số, căn thức chồng tầng không dính vào nhau. +// `auto` (mặc định) = 0.95em × hệ số giãn dòng hiện hành. +// gian-dong: HỆ SỐ giãn dòng riêng cho khung này (auto = lấy hệ số toàn tài +// liệu do #bai-giang/#de-toan/#gian-dong đặt). +// do: true -> dựng SẴN mọi bước (không qua #lo) để ĐO chiều cao lúc cuối bài; +// dùng cho bộ tự ngắt màn, không phải để hiển thị. +#let giai-buoc(nd, tu: 2, nhan: [Hướng dẫn giải. ], gian: auto, gian-dong: auto, + do: false) = { + let dong = tach-dong(nd) + if dong.len() == 0 { return } + let khung = block( + width: 100%, inset: (left: 11pt, top: 5pt, bottom: 5pt), + stroke: (left: 2.5pt + _luc), above: 10pt, + context { + let k = if gian-dong == auto { _he-so-gian() } else { gian-dong } + let g = if gian == auto { 0.95em * k } else { gian } + set par(leading: g, spacing: _gd.get().at("doan", default: 1.2em) * k) + align(center, text(fill: _luc, weight: "bold", size: 0.84em, nhan)) + dong.at(0) + for i in range(1, dong.len()) { + let b = block(above: g, dong.at(i)) + if do { b } else { lo(tu + i, b) } + } + }, + ) + if do { khung } else { lo(tu, khung) } +} + +// ---------- Ghi đè giãn dòng cho MỘT câu ---------- +// gd == auto -> giữ nguyên; ngược lại nhân hệ số gd vào leading NỀN của hồ sơ. +#let _voi-gian(gd, nd) = if gd == auto { nd } else { + context { + let g = _gd.get() + // giãn CẢ khoảng cách dòng lẫn khoảng cách đoạn (dòng trống trong nội dung) + set par(leading: g.nen * gd, spacing: g.at("doan", default: 1.2em) * gd) + nd + } +} + +// ---------- Thiết lập tài liệu theo hồ sơ ---------- +// Chuẩn hoá tên hồ sơ: "de-thi" -> "dethi", "LoiGiai" -> "loigiai"... +#let _chuan-ho-so(hs) = lower(hs).replace("-", "").replace("_", "") + +#let de-toan( + ho-so: "dethi", + tieu-de: [ĐỀ KIỂM TRA], + tieu-de-ngan: none, // tên bài rút gọn ở header mọi slide (chỉ dùng ở beamer) + nen: "trang", // nền slide beamer: "trang" | "kem" | "xanh-nhat" + // | "luc-nhat" | "xam" | màu tuỳ ý (bản A4 luôn trắng) + mon: none, // vd [MÔN TOÁN 12] + thoi-gian: none, // vd "90 phút" + truong: none, // vd [SỞ GD&ĐT ... \ TRƯỜNG THPT ...] + ma-de: none, + // ----- 3 thông tin header đề thi: CHỈ hiện khi được khai báo ----- + hien-ho-ten: false, // true -> dòng "Họ và tên thí sinh: ..." + sbd: none, // hiện dòng số báo danh / lớp: + // none | false -> ẩn + // true | "sbd" -> "Số báo danh: ..." + // "lop" -> "Lớp: ..." + hien-ma-de: false, // true -> ô "Mã đề ..." (dùng giá trị ma-de nếu có) + thong-tin-hs: true, // true: đề thi thật — có "(Đề thi có N trang)" (bản dethi) + // và "(Đề thi có N trang)"; false: tài liệu bài học + ti-le-chu: 1.0, // HỆ SỐ PHÓNG CỠ CHỮ THÂN NỘI DUNG (toàn file) — + // 1.0 giữ nguyên; >1 to hơn, <1 nhỏ đi. Áp dụng cho + // cả 3 hồ sơ (beamer + A4 dethi/loigiai). + gian-dong: 1.0, // HỆ SỐ GIÃN DÒNG (toàn file) — 1.0 = mốc mặc định + // (A4 đề thi 0.65em, trình chiếu 0.62em); 1.25 = giãn + // thêm 25%; 0.9 = thu lại 10%. Tăng khi phân số/căn + // thức nhiều tầng làm hai dòng dính nhau. Đổi giữa + // bài: #gian-dong(1.4); riêng một câu: tham số + // gian-dong: của #vd/#tn/#ds/#tln/#tl/#hd/#lt/#vdtt. + mau-cong-thuc: auto, // MÀU MỌI CÔNG THỨC trong $...$ (toàn file) — auto = thừa + // kế màu chữ (đen ở thân, trắng ở tiêu đề); đặt màu cụ + // thể để nhuộm tất cả. Dùng chung cho cả 3 hồ sơ. + hoan-vi: false, // TRỘN ĐỀ — xem khối "HOÁN VỊ" ở đầu file này: + // false (mặc định) = giữ nguyên thứ tự soạn; + // true = trộn thứ tự CÂU (theo nhóm tn/ds/tln, câu + // tl giữ nguyên) VÀ trộn phương án A/B/C/D + // của câu tn (ý a/b/c/d của ds giữ nguyên); + // "cau" = chỉ trộn thứ tự câu; + // "pa" = chỉ trộn phương án. + // Chỉ tác dụng ở bản in A4 (dethi/loigiai). + mam: auto, // MÃ TRỘN: auto = băm từ `ma-de` (mã đề khác ⇒ thứ tự + // khác); hoặc số/chuỗi tuỳ ý để ghi đè. + phu-de: none, gv: none, ngay: none, + body, +) = { + let hs = _chuan-ho-so(ho-so) + _ma-de.update(ma-de) // lưu mã đề để #bang-dap-an tự đồng bộ + // ----- Hoán vị (trộn đề): chỉ ở bản in A4, beamer giữ nguyên bài giảng ----- + let _che = _hv-che(hoan-vi) + let _mam = _hv-mam(if mam == auto { ma-de } else { mam }) + _hv.update((bat: _che.pa and hs != "beamer", cau: _che.cau and hs != "beamer", mam: _mam)) + let body = if _che.cau and hs != "beamer" { _hv-xao-than(body, _mam) } else { body } + if hs == "beamer" { + bai-giang(tieu-de: tieu-de, tieu-de-ngan: tieu-de-ngan, nen: nen, + ti-le-chu: ti-le-chu, gian-dong: gian-dong, mau-cong-thuc: mau-cong-thuc, + phu-de: phu-de, gv: gv, ngay: ngay, { + bat-dap-an() + body + }) + } else { + // Ghi hồ sơ vào trạng thái để các hàm câu hỏi (vd/tn/ds/...) tự nhận + // biết đang ở bản A4; mọi #lo(n) trên bản in đều hiện đủ. + _ho-so.update("sach" + hs) + _buoc-ht.update(1000000) + set page( + paper: "a4", + margin: (x: 1.8cm, top: 1.6cm, bottom: 2.2cm), + footer: context align(center, text(size: 9pt, fill: luma(35%), { + [Trang #counter(page).display() / #counter(page).final().first()] + if ma-de != none { [ — Mã đề #ma-de] } + })), + ) + set text(size: 11pt * ti-le-chu, lang: "vi") + show math.equation: it => { + if mau-cong-thuc == auto { it } else { + set text(fill: mau-cong-thuc) + it + } + } + // 0.65em = leading mặc định của Typst -> gian-dong: 1.0 giữ y nguyên bố cục. + _dat-gian(0.65em, gian-dong) + set par(justify: true, leading: 0.65em * gian-dong, spacing: 1.2em * gian-dong) + // Công thức TRONG DÒNG khai đúng chiều cao nét vẽ (phân số, căn, chỉ số + // chồng tầng) ⇒ dòng VÀ ô lưới phương án tự nới đúng chỗ cần, khỏi phải + // đôn `gian-dong` lên 3 rồi làm trống hoác chỗ khác. Tắt: #cao-that(false). + show math.equation.where(block: false): _chong-net + if hs == "loigiai" { bat-dap-an() } else { tat-dap-an() } + // ----- đầu đề thi ----- + grid( + columns: (1fr, 1.25fr), column-gutter: 14pt, + align(center, { + if truong != none { + text(weight: "bold", size: 10pt, truong) + v(3pt) + } + // Dòng "(Đề thi có N trang)" chỉ dành cho đề thi thật — + // tài liệu kiểu bài học đặt thong-tin-hs: false là bỏ. + if thong-tin-hs { + text(size: 9pt, style: "italic")[(Đề thi có #context [#counter(page).final().first()] trang)] + } + }), + align(center, { + text(weight: "bold", size: 12.5pt, fill: _xanh, tieu-de) + if mon != none { + linebreak() + text(weight: "bold", size: 11pt, mon) + } + if thoi-gian != none { + linebreak() + text(size: 9pt, style: "italic")[Thời gian làm bài: #thoi-gian] + } + if ngay != none and hs == "dethi" { + linebreak() + text(size: 9pt, style: "italic")[Ngày kiểm tra: #ngay] + } + if hs == "loigiai" { + linebreak() + text(weight: "bold", size: 10.5pt, fill: _do)[ĐÁP ÁN VÀ LỜI GIẢI CHI TIẾT] + } + }), + ) + // ----- 3 thông tin tuỳ chọn: chỉ hiện mục nào được khai báo ----- + let hien-sbd = sbd != none and sbd != false + let co-trai = hien-ho-ten or hien-sbd + if hs == "dethi" and (co-trai or hien-ma-de) { + v(10pt) + let cot-trai = { + let dong = () + if hien-ho-ten { + dong.push([Họ và tên thí sinh: #box(width: 1fr, repeat[.])]) + } + if hien-sbd { + let nhan-sbd = if sbd == "lop" [Lớp] else [Số báo danh] + dong.push([#nhan-sbd: #box(width: 1fr, repeat[.])]) + } + dong.join(v(4pt)) + } + let o-ma-de = rect(inset: 8pt, stroke: 0.8pt, + text(weight: "bold")[Mã đề #if ma-de == none [......] else [#ma-de]]) + if co-trai and hien-ma-de { + grid(columns: (1fr, auto), column-gutter: 12pt, + cot-trai, align(horizon, o-ma-de)) + } else if co-trai { + cot-trai + } else { + align(right, o-ma-de) + } + } + v(8pt) + line(length: 100%, stroke: 0.7pt) + v(4pt) + body + } +} + +// ---------- Bộ hàm câu hỏi theo hồ sơ ---------- +// Trả về (vd, tn, ds, tln, tl, phan) có CÙNG cách gọi ở cả 3 hồ sơ +// (kèm bí danh cũ mc/tf/sa để tương thích ngược). +// Trong lời giải, mỗi dấu \ là một bước xuất hiện (chỉ tác dụng ở beamer). +// tieu-de: tiêu đề slide của câu (chỉ dùng ở beamer). +// Các slide "(tiếp)" cho những màn lời giải sau màn đầu. +// nhan-dau: nhãn của màn ĐẦU TIÊN khi màn 0 rỗng (đề dài quá nên lời giải bị +// đẩy hết sang đây) — lúc đó chưa có gì để "tiếp", ghi nhãn thường. +#let _man-tiep(man, tieu-de, nhan: [Hướng dẫn giải (tiếp). ], + nhan-dau: [Hướng dẫn giải. ], gd: auto) = { + for (i, mk) in man.slice(1).enumerate() { + let nh = if i == 0 and man.at(0).len() == 0 { nhan-dau } else { nhan } + slide(tieu-de: tieu-de, so-buoc: calc.max(1, mk.len()))[ + #_voi-gian(gd, giai-buoc(mk, tu: 1, nhan: nh, gian-dong: gd)) + ] + } +} + +// ---------- 8 DẠNG CÂU DÙNG TRỰC TIẾP Ở MỌI CHẾ ĐỘ ---------- +// Các hàm #vd/#tn/#ds/#tln/#tl/#hd/#lt/#vdtt/#phan tự đọc hồ sơ hiển thị +// (do bai-giang/de-toan ghi vào trạng thái) rồi chọn cách dựng: +// beamer -> mỗi câu một slide, lời giải hiện dần theo từng dấu \; +// A4 -> khối liên tục (dethi ẩn/loigiai hiện đáp án). +// KHÔNG cần khai báo #let (...) = tao-cau-hoi(...) nữa — import là dùng. +// Trình tự beamer: bước 1 hiện đề — bước 2.. lời giải từng dòng — đánh dấu +// đáp án ở BƯỚC CUỐI màn đầu; các màn #sang-man thành slide "(tiếp)". + +// Hình kèm LỜI GIẢI (mọi dạng câu): fig-giai (bí danh hinh-giai) + +// fig-giai-pos/fig-giai-width — bố cục 2 cột như hình kèm đề; ở beamer +// hình hiện từ bước đầu tiên của lời giải, các màn #sang-man sau muốn có +// hình thì chèn trực tiếp trong nội dung màn đó. +#let _giai-kem-hinh(nd, hg, vi-tri, be-rong, do: false) = voi-hinh( + nd, + if hg == none { none } else if do { hg } else { lo(2, hg) }, + vi-tri: vi-tri, be-rong: be-rong, +) + +// ---------- TỰ NGẮT MÀN KHI LỜI GIẢI TRÀN TRANG (chỉ beamer) ---------- +// Bệnh cũ: lời giải cao hơn thân slide thì Typst đẩy phần thừa sang TRANG SAU, +// mà mỗi bước hoạt hình lại in slide LẠI TỪ ĐẦU ⇒ bấm chuyển bước thì thấy +// xen kẽ "đề bài + mấy dòng đầu" — "phần tràn" — "đề bài + mấy dòng đầu" … +// tức ĐỀ BÀI HIỆN LẶP LẠI giữa lời giải. Nay đo trước: dòng nào làm tràn thì +// tự đẩy sang một màn "(tiếp)" y như đặt #sang-man bằng tay. +// Tắt (giữ nếp cũ) cho cả tài liệu: #tu-ngat-man(false). +#let _tu-man = state("bg-tu-man", true) +#let tu-ngat-man(bat) = _tu-man.update(bat) + +// Cắt một màn thành nhiều màn vừa trang. `dung(dòng, có-đề)` dựng thử nội dung +// slide (đã hiện hết mọi bước) để đo. Tìm nhị phân số dòng lớn nhất còn vừa. +#let _cat-vua(dong, co-de, dung, vung) = { + if dong.len() == 0 { return (dong,) } + let vua(n) = measure(block(width: vung.rong, dung(dong.slice(0, n), co-de))).height <= vung.cao + if vua(dong.len()) { return (dong,) } + // Đề bài dài, chiếm gần hết slide: slide đầu CHỈ hiện đề, lời giải sang màn + // "(tiếp)" — thà một slide đề trơn còn hơn đề bị in lại giữa lời giải. + if co-de and not vua(1) { return ((),) + _cat-vua(dong, false, dung, vung) } + let a = 1 // màn tiếp luôn giữ ít nhất 1 dòng ⇒ chắc chắn tiến + let b = dong.len() // đã biết là KHÔNG vừa + while b - a > 1 { + let m = int((a + b) / 2) + if vua(m) { a = m } else { b = m } + } + (dong.slice(0, a),) + _cat-vua(dong.slice(a), false, dung, vung) +} + +#let _cat-man-vua(man, dung) = { + if not _tu-man.get() { return man } + let vung = _vung-than() + let kq = () + for (i, mk) in man.enumerate() { + if mk.len() == 0 { kq.push(mk) } else { kq += _cat-vua(mk, i == 0, dung, vung) } + } + kq +} + +#let vd(noi-dung, loi-giai: none, loigiai: none, diem: none, hinh: none, fig: none, + fig-pos: "right", fig-width: auto, + fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto, + gian-dong: auto, tieu-de: [Ví dụ]) = context { + let loi-giai = _uu-tien(loigiai, loi-giai) + let hinh = _uu-tien(fig, hinh) + let hg = _uu-tien(fig-giai, hinh-giai) + let gd = gian-dong + if _la-sach(_ho-so.get()) { + // Ví dụ: LUÔN hiện lời giải, kể cả trong hồ sơ "dethi". + _voi-gian(gd, { + vi-du(voi-hinh(noi-dung, hinh, vi-tri: fig-pos, be-rong: fig-width)) + if loi-giai != none { + _mt-loi-giai(voi-hinh(_ghep(loi-giai), hg, vi-tri: fig-giai-pos, be-rong: fig-giai-width)) + } + }) + } else { + let dung(dg, co-de) = if co-de { + _voi-gian(gd, [ + #vi-du(voi-hinh(noi-dung, hinh, vi-tri: fig-pos, be-rong: fig-width)) + #_giai-kem-hinh(giai-buoc(dg, nhan: [Lời giải. ], gian-dong: gd, do: true), hg, fig-giai-pos, fig-giai-width, do: true) + ]) + } else { + _voi-gian(gd, giai-buoc(dg, tu: 1, nhan: [Lời giải (tiếp). ], + gian-dong: gd, do: true)) + } + let man = _cat-man-vua(tach-man(loi-giai), dung) + // màn đầu rỗng (đề dài, lời giải đã bị đẩy hết sang màn "(tiếp)") thì + // slide này chỉ cần MỘT bước — đừng in lại y hệt lần nữa. + slide(tieu-de: tieu-de, so-buoc: if man.at(0).len() > 0 { 1 + man.at(0).len() } + else if man.len() > 1 { 1 } else { 2 })[ + #_voi-gian(gd, [ + #vi-du(voi-hinh(noi-dung, hinh, vi-tri: fig-pos, be-rong: fig-width)) + #_giai-kem-hinh(giai-buoc(man.at(0), nhan: [Lời giải. ], gian-dong: gd), hg, fig-giai-pos, fig-giai-width) + ]) + ] + _man-tiep(man, tieu-de, nhan: [Lời giải (tiếp). ], nhan-dau: [Lời giải. ], gd: gd) + } +} + +// FORM MỚI (khuyến nghị — dễ hoán vị đáp án, xáo trộn tạo mã đề): +// #tn( +// [Nội dung đề bài], +// ( [$A$], True([$B$]), [$C$], [$D$] ), // đáp án đúng bọc True(...) +// loigiai: [Lời giải chi tiết], +// fig: none, fig-pos: "right", fig-width: 35%, // hình kèm đề +// // fig-pos: "right"|"left"|"center"|"top"|"bottom" +// // ("center" = canh giữa, nằm giữa phần đề và các phương án) +// cols: 0, // 0 = tự chọn số cột; 1/2/4 = cố định +// lines: 0, // số dòng chừa làm bài (bản in đề) +// num: auto, prefix: "Câu", boxed: false, +// ) +// Form cũ (dap-an: "B", hinh:, cot:, loi-giai:) vẫn dùng được. +#let tn( + cau, phuong-an, dap-an: none, cot: auto, cols: 0, diem: none, + hinh: none, fig: none, fig-pos: "right", fig-width: auto, + loi-giai: none, loigiai: none, lines: 0, num: auto, prefix: "Câu", + boxed: false, fig-giai: none, hinh-giai: none, cham: auto, khoa-pa: false, + fig-giai-pos: "right", fig-giai-width: auto, gian-dong: auto, + trong-dong: auto, + tieu-de: [Trắc nghiệm], +) = _gan-tn(context { + let lg = _uu-tien(loigiai, loi-giai) + let hinh = _uu-tien(fig, hinh) + let hg = _uu-tien(fig-giai, hinh-giai) + let gd = gian-dong + let cot = if cols != 0 { cols } else { cot } + // ----- HOÁN VỊ PHƯƠNG ÁN A/B/C/D ----- + // Chỉ xáo khi phương án đúng được bọc True(...) (form mới) — form cũ + // `dap-an: "B"` gắn đáp án vào CHỮ CÁI nên xáo sẽ lệch. `khoa-pa: true` + // giữ nguyên riêng câu này. Mầm riêng từng câu = mầm chung + số thứ tự câu + // ⇒ mỗi câu một hoán vị khác nhau, mà vẫn tất định. + let _hvt = _hv.get() + let phuong-an = if (_hvt.bat and not khoa-pa and phuong-an.len() > 1 + and phuong-an.any(_la-y)) { + let _so = _bd-cau.goc.get() + _bd-cau.cnt.get().first() + _hv-xao(phuong-an, _hvt.mam + (_so + 1) * 104729) + } else { phuong-an } + let goi(..them) = cau-mc( + cau, phuong-an, dap-an: dap-an, cot: cot, diem: diem, hinh: hinh, cham: cham, + khoa-pa: khoa-pa, trong-dong: trong-dong, + fig-pos: fig-pos, fig-width: fig-width, lines: lines, + num: num, prefix: prefix, boxed: boxed, + hinh-giai: hg, fig-giai-pos: fig-giai-pos, fig-giai-width: fig-giai-width, ..them, + ) + if _la-sach(_ho-so.get()) { + _voi-gian(gd, goi(loi-giai: _ghep(lg))) + } else { + let dung(dg, co-de) = if co-de { + _voi-gian(gd, [ + #goi(lo-da: 1) + #_giai-kem-hinh(giai-buoc(dg, gian-dong: gd, do: true), hg, fig-giai-pos, fig-giai-width, do: true) + ]) + } else { + _voi-gian(gd, giai-buoc(dg, tu: 1, nhan: [Hướng dẫn giải (tiếp). ], + gian-dong: gd, do: true)) + } + let man = _cat-man-vua(tach-man(lg), dung) + let buoc-da = 2 + man.at(0).len() + slide(tieu-de: tieu-de, so-buoc: buoc-da)[ + #_voi-gian(gd, [ + #goi(lo-da: buoc-da) + #_giai-kem-hinh(giai-buoc(man.at(0), gian-dong: gd), hg, fig-giai-pos, fig-giai-width) + ]) + ] + _man-tiep(man, tieu-de, gd: gd) + } +}) + +// FORM MỚI: +// #ds( +// [Thân câu chung — nêu tình huống / bài toán], +// ( +// [Phát biểu a — SAI], +// True([Phát biểu b — ĐÚNG]), // ý đúng bọc True(...) +// True([Phát biểu c — ĐÚNG]), +// [Phát biểu d — SAI], +// ), +// loigiai: [Phân tích từng ý.], +// fig: none, fig-pos: "right", fig-width: 30%, +// lines: 0, num: auto, prefix: "Câu", +// ) +// Form cũ (dap-an: (false, true, true, false)) vẫn dùng được. +// Lời giải riêng từng ý (an toàn khi trộn hoán vị ý) — đặt `giai:` trong +// True(...)/Sai(...); `khoa-y: true` báo công cụ trộn đừng xáo các ý của câu. +#let ds( + cau, cac-y, dap-an: none, diem: none, + hinh: none, fig: none, fig-pos: "right", fig-width: auto, + loi-giai: none, loigiai: none, o-tick: false, lines: 0, num: auto, cham: auto, + khoa-y: false, trong-dong: auto, + prefix: "Câu", boxed: false, fig-giai: none, hinh-giai: none, + fig-giai-pos: "right", fig-giai-width: auto, gian-dong: auto, + tieu-de: [Đúng — Sai], +) = _gan-ds(context { + let lg = _uu-tien(loigiai, loi-giai) + let hinh = _uu-tien(fig, hinh) + let hg = _uu-tien(fig-giai, hinh-giai) + let gd = gian-dong + let goi(..them) = cau-tf( + cau, cac-y, dap-an: dap-an, diem: diem, hinh: hinh, o-tick: o-tick, cham: cham, + khoa-y: khoa-y, trong-dong: trong-dong, + fig-pos: fig-pos, fig-width: fig-width, lines: lines, + num: num, prefix: prefix, boxed: boxed, + hinh-giai: hg, fig-giai-pos: fig-giai-pos, fig-giai-width: fig-giai-width, ..them, + ) + if _la-sach(_ho-so.get()) { + _voi-gian(gd, goi(loi-giai: _ghep(lg))) + } else { + let dung(dg, co-de) = if co-de { + _voi-gian(gd, [ + #goi(lo-da: 1) + #_giai-kem-hinh(giai-buoc(dg, gian-dong: gd, do: true), hg, fig-giai-pos, fig-giai-width, do: true) + ]) + } else { + _voi-gian(gd, giai-buoc(dg, tu: 1, nhan: [Hướng dẫn giải (tiếp). ], + gian-dong: gd, do: true)) + } + let man = _cat-man-vua(tach-man(lg), dung) + let buoc-da = 2 + man.at(0).len() + slide(tieu-de: tieu-de, so-buoc: buoc-da)[ + #_voi-gian(gd, [ + #goi(lo-da: buoc-da) + #_giai-kem-hinh(giai-buoc(man.at(0), gian-dong: gd), hg, fig-giai-pos, fig-giai-width) + ]) + ] + _man-tiep(man, tieu-de, gd: gd) + } +}) + +// FORM MỚI — đáp án đặt NGAY SAU đề bài (positional thứ 2): +// #tln( +// [Nội dung đề bài], +// [$6$], // đáp án +// loigiai: [Lời giải.], +// fig: none, fig-pos: "right", fig-width: 30%, +// show-boxes: true, box-count: 4, // ô điền trên phiếu (bản in đề) +// lines: 0, num: auto, prefix: "Câu", +// ) +// Form cũ (dap-an: [$6$]) vẫn dùng được. +#let tln( + cau, ..tra-loi, dap-an: none, diem: none, + hinh: none, fig: none, fig-pos: "right", fig-width: auto, + loi-giai: none, loigiai: none, show-boxes: true, box-count: 4, + lines: 0, num: auto, prefix: "Câu", boxed: false, + fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto, + gian-dong: auto, tieu-de: [Trả lời ngắn], +) = _gan-tln(context { + let lg = _uu-tien(loigiai, loi-giai) + let hinh = _uu-tien(fig, hinh) + let hg = _uu-tien(fig-giai, hinh-giai) + let gd = gian-dong + let dap-an = if tra-loi.pos().len() > 0 { tra-loi.pos().first() } else { dap-an } + let goi(..them) = cau-sa( + cau, dap-an: dap-an, diem: diem, hinh: hinh, + fig-pos: fig-pos, fig-width: fig-width, + show-boxes: show-boxes, box-count: box-count, lines: lines, + num: num, prefix: prefix, boxed: boxed, + hinh-giai: hg, fig-giai-pos: fig-giai-pos, fig-giai-width: fig-giai-width, ..them, + ) + if _la-sach(_ho-so.get()) { + _voi-gian(gd, goi(loi-giai: _ghep(lg))) + } else { + let dung(dg, co-de) = if co-de { + _voi-gian(gd, [ + #goi(lo-da: 1) + #_giai-kem-hinh(giai-buoc(dg, gian-dong: gd, do: true), hg, fig-giai-pos, fig-giai-width, do: true) + ]) + } else { + _voi-gian(gd, giai-buoc(dg, tu: 1, nhan: [Hướng dẫn giải (tiếp). ], + gian-dong: gd, do: true)) + } + let man = _cat-man-vua(tach-man(lg), dung) + let buoc-da = 2 + man.at(0).len() + slide(tieu-de: tieu-de, so-buoc: buoc-da)[ + #_voi-gian(gd, [ + #goi(lo-da: buoc-da) + #_giai-kem-hinh(giai-buoc(man.at(0), gian-dong: gd), hg, fig-giai-pos, fig-giai-width) + ]) + ] + _man-tiep(man, tieu-de, gd: gd) + } +}) + +// Bộ dựng chung cho 4 dạng "kiểu tự luận" (TL/HĐ/LT/VDTT). +#let _dang-tl(ham-cau, cau, loi-giai, diem, cho-trong, hinh, tieu-de, + h-pos: "right", h-width: auto, + hg: none, hg-pos: "right", hg-width: auto, gd: auto) = context { + if _la-sach(_ho-so.get()) { + _voi-gian(gd, ham-cau(cau, loi-giai: _ghep(loi-giai), diem: diem, + cho-trong: cho-trong, hinh: hinh, fig-pos: h-pos, fig-width: h-width, + hinh-giai: hg, fig-giai-pos: hg-pos, fig-giai-width: hg-width)) + } else { + let dung(dg, co-de) = if co-de { + _voi-gian(gd, [ + #ham-cau(cau, diem: diem, hinh: hinh, fig-pos: h-pos, fig-width: h-width) + #_giai-kem-hinh(giai-buoc(dg, gian-dong: gd, do: true), hg, hg-pos, hg-width, do: true) + ]) + } else { + _voi-gian(gd, giai-buoc(dg, tu: 1, nhan: [Hướng dẫn giải (tiếp). ], + gian-dong: gd, do: true)) + } + let man = _cat-man-vua(tach-man(loi-giai), dung) + // màn đầu rỗng (đề dài, lời giải đã bị đẩy hết sang màn "(tiếp)") thì + // slide này chỉ cần MỘT bước — đừng in lại y hệt lần nữa. + slide(tieu-de: tieu-de, so-buoc: if man.at(0).len() > 0 { 1 + man.at(0).len() } + else if man.len() > 1 { 1 } else { 2 })[ + #_voi-gian(gd, [ + #ham-cau(cau, diem: diem, hinh: hinh, fig-pos: h-pos, fig-width: h-width) + #_giai-kem-hinh(giai-buoc(man.at(0), gian-dong: gd), hg, hg-pos, hg-width) + ]) + ] + _man-tiep(man, tieu-de, gd: gd) + } +} + +// (loigiai:/fig:/fig-giai: là bí danh kiểu mới của loi-giai:/hinh:/hinh-giai:) +#let tl(cau, loi-giai: none, loigiai: none, diem: none, cho-trong: 0pt, hinh: none, fig: none, fig-pos: "right", fig-width: auto, fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto, gian-dong: auto, tieu-de: [Tự luận]) = _dang-tl(cau-tl, cau, _uu-tien(loigiai, loi-giai), diem, cho-trong, _uu-tien(fig, hinh), tieu-de, h-pos: fig-pos, h-width: fig-width, hg: _uu-tien(fig-giai, hinh-giai), hg-pos: fig-giai-pos, hg-width: fig-giai-width, gd: gian-dong) +#let hd(cau, loi-giai: none, loigiai: none, diem: none, cho-trong: 0pt, hinh: none, fig: none, fig-pos: "right", fig-width: auto, fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto, gian-dong: auto, tieu-de: [Hoạt động]) = _dang-tl(cau-hd, cau, _uu-tien(loigiai, loi-giai), diem, cho-trong, _uu-tien(fig, hinh), tieu-de, h-pos: fig-pos, h-width: fig-width, hg: _uu-tien(fig-giai, hinh-giai), hg-pos: fig-giai-pos, hg-width: fig-giai-width, gd: gian-dong) +#let lt(cau, loi-giai: none, loigiai: none, diem: none, cho-trong: 0pt, hinh: none, fig: none, fig-pos: "right", fig-width: auto, fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto, gian-dong: auto, tieu-de: [Luyện tập]) = _dang-tl(cau-lt, cau, _uu-tien(loigiai, loi-giai), diem, cho-trong, _uu-tien(fig, hinh), tieu-de, h-pos: fig-pos, h-width: fig-width, hg: _uu-tien(fig-giai, hinh-giai), hg-pos: fig-giai-pos, hg-width: fig-giai-width, gd: gian-dong) +#let vdtt(cau, loi-giai: none, loigiai: none, diem: none, cho-trong: 0pt, hinh: none, fig: none, fig-pos: "right", fig-width: auto, fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto, gian-dong: auto, tieu-de: [Vận dụng]) = _dang-tl(cau-vdtt, cau, _uu-tien(loigiai, loi-giai), diem, cho-trong, _uu-tien(fig, hinh), tieu-de, h-pos: fig-pos, h-width: fig-width, hg: _uu-tien(fig-giai, hinh-giai), hg-pos: fig-giai-pos, hg-width: fig-giai-width, gd: gian-dong) +// 3 hình thức hoạt động SGK — KHÔNG đánh số (Khám phá / Trải nghiệm / Thảo luận) +#let kham-pha(cau, loi-giai: none, loigiai: none, diem: none, cho-trong: 0pt, hinh: none, fig: none, fig-pos: "right", fig-width: auto, fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto, gian-dong: auto, tieu-de: [Khám phá]) = _dang-tl(cau-kham-pha, cau, _uu-tien(loigiai, loi-giai), diem, cho-trong, _uu-tien(fig, hinh), tieu-de, h-pos: fig-pos, h-width: fig-width, hg: _uu-tien(fig-giai, hinh-giai), hg-pos: fig-giai-pos, hg-width: fig-giai-width, gd: gian-dong) +#let trai-nghiem(cau, loi-giai: none, loigiai: none, diem: none, cho-trong: 0pt, hinh: none, fig: none, fig-pos: "right", fig-width: auto, fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto, gian-dong: auto, tieu-de: [Trải nghiệm]) = _dang-tl(cau-trai-nghiem, cau, _uu-tien(loigiai, loi-giai), diem, cho-trong, _uu-tien(fig, hinh), tieu-de, h-pos: fig-pos, h-width: fig-width, hg: _uu-tien(fig-giai, hinh-giai), hg-pos: fig-giai-pos, hg-width: fig-giai-width, gd: gian-dong) +#let thao-luan(cau, loi-giai: none, loigiai: none, diem: none, cho-trong: 0pt, hinh: none, fig: none, fig-pos: "right", fig-width: auto, fig-giai: none, hinh-giai: none, fig-giai-pos: "right", fig-giai-width: auto, gian-dong: auto, tieu-de: [Thảo luận]) = _dang-tl(cau-thao-luan, cau, _uu-tien(loigiai, loi-giai), diem, cho-trong, _uu-tien(fig, hinh), tieu-de, h-pos: fig-pos, h-width: fig-width, hg: _uu-tien(fig-giai, hinh-giai), hg-pos: fig-giai-pos, hg-width: fig-giai-width, gd: gian-dong) + +#let phan(ten, ngan: none) = context { + if _la-sach(_ho-so.get()) { + // Heading cấp 1 (để vào MỤC LỤC #outline + bookmark PDF), nhưng hiển thị + // y hệt khối màu cũ qua show-rule cục bộ (không ảnh hưởng heading khác). + show heading.where(level: 1): it => block( + width: 100%, fill: rgb("#eaf2f8"), inset: (x: 10pt, y: 7pt), + stroke: (left: 3pt + _xanh), above: 14pt, below: 10pt, + text(weight: "bold", fill: _xanh, it.body), + ) + heading(level: 1, ten) + } else { + muc(ten, ngan: ngan) + } +} + +// ===================================================================== +// BỐ CỤC PHẦN CÂU HỎI — #chia-2-cot · #chia-2-cot-lech (08/2026) +// --------------------------------------------------------------------- +// Hai lệnh này dùng như SHOW-RULE: ĐẶT Ở ĐÂU THÌ ÁP DỤNG TỪ DÒNG ĐÓ TRỞ +// XUỐNG (đúng lối #show: de-toan.with(...) đã quen): +// +// #show: de-toan.with(ho-so: ho-so, tieu-de: [...]) +// #show: chia-2-cot // từ đây trở xuống: 2 cột đều nhau +// #tn(...) #tn(...) ... +// +// #show: chia-2-cot-lech // trái = câu hỏi, phải = chỗ làm bài +// #show: chia-2-cot-lech.with(rong-trai: 60%) // đổi bề rộng cột trái +// +// Muốn NGỪNG chia cột (bắt buộc trước #bang-dap-an, vì lệnh đó có +// #pagebreak mà pagebreak không chạy được bên trong cột) thì đặt +// #thoi-cot() +// — phần sau dấu này trở lại nguyên khổ giấy. +// +// Cũng dùng được dạng KHỐI cho một đoạn: #chia-2-cot[ ... ]. +// +// Chỉ tác dụng ở bản in A4 (dethi/loigiai); trình chiếu beamer bỏ qua +// (mỗi câu vốn đã là một slide riêng). +// ===================================================================== + +// ---------- Dấu kết thúc vùng chia cột ---------- +#let thoi-cot() = metadata("bg-thoi-cot") + +#let _la-thoi-cot(c) = (type(c) == content and c.func() == metadata + and c.value == "bg-thoi-cot") + +// Tách thân thành (phần NẰM TRONG cột, phần SAU #thoi-cot()). +// Không có dấu #thoi-cot() ⇒ (toàn bộ thân, none). +#let _tach-thoi-cot(than) = { + if type(than) != content or not than.has("children") { return (than, none) } + let cs = than.children + let i = cs.position(_la-thoi-cot) + if i == none { return (than, none) } + let truoc = cs.slice(0, i) + let sau = cs.slice(i + 1) + ( + if truoc.len() == 0 { none } else { truoc.join() }, + if sau.len() == 0 { none } else { sau.join() }, + ) +} + +// Phần thân nằm TRONG cột đã bị bọc thành MỘT phần tử nên vòng duyệt hoán vị +// ở #de-toan không nhìn thấy các câu bên trong ⇒ xáo lại ngay tại đây. +// (Mầm vẫn là mầm chung ⇒ bản dethi và bản loigiai trộn giống hệt nhau.) +#let _cot-than(nd) = { + let hv = _hv.get() + if nd != none and hv.at("cau", default: false) { _hv-xao-than(nd, hv.mam) } else { nd } +} + +// ---------- Chỗ trống còn lại của trang & chiều cao trọn một cột ---------- +// Trả về (chỗ còn lại của TRANG HIỆN TẠI, chiều cao MỘT CỘT đầy trang). +// Phải gọi trong `context` (dùng here().position()). +#let _cao-vung() = { + let ch = page.height + if type(ch) != length { return (none, none) } + let m = page.margin + // ⚠️ `page.margin` trả về lề đã CHUẨN HOÁ thành `relative` (vd `0% + 45.35pt`) + // chứ KHÔNG phải `length` như lúc khai — cứ đòi đúng `length` là hỏng hết. + let _dai(v) = { + if type(v) == length { v } + else if type(v) == relative { ch * v.ratio + v.length } + else if type(v) == ratio { ch * v } + else { none } + } + let _le(khoa, mac-dinh) = { + if type(m) == dictionary { _dai(m.at(khoa, default: m.at("y", default: mac-dinh))) + } else if m == auto { 2.5cm } else { _dai(m) } + } + let lt = _le("top", 1.6cm) + let ld = _le("bottom", 2.2cm) + if lt == none or ld == none { return (none, none) } + let cot = ch - lt - ld + if cot <= 0pt { return (none, none) } + (calc.max(0pt, ch - here().position().y - ld), cot) +} + +// Thân gõ bằng `#for` / `#include` thường gói TẤT CẢ câu hỏi vào MỘT phần tử +// (dãy con) ⇒ không có mối nào để chèn #colbreak(). Trải phẳng các dãy con lồng +// nhau để lấy đúng từng câu; ghép lại thì y hệt thân cũ. +#let _f-day = ([a] + [b]).func() +#let _cot-la(c) = { + if type(c) == content and c.func() == _f-day and c.has("children") { + c.children.map(_cot-la).flatten() + } else { (c,) } +} + +// ---------- Tự CÂN BẰNG chiều cao các cột ---------- +// Typst KHÔNG cân bằng `columns`: cột 1 được rót đầy tới HẾT chiều cao rồi mới +// tràn sang cột 2 ⇒ thân ngắn (chưa đầy một trang) nằm gọn trong cột 1, cột 2 +// bỏ trắng — nhìn y như lệnh không chạy. Thân DÀI thì bệnh này rơi xuống TRANG +// CUỐI: cột trái chạy hết trang, cột phải trắng trơn — tốn hẳn nửa tờ giấy. +// +// Cách chữa: các trang PHÍA TRƯỚC cứ để Typst rót tự nhiên — rót tự nhiên vốn +// đã chặt nhất, nó chẻ được cả một câu dài làm đôi, mình tính tay chỉ thua — +// chỉ chia đều phần nằm trên TRANG CUỐI bằng #colbreak(). +// +// ⚠️ HAI LỐI ĐÃ THỬ VÀ BỎ, ĐỪNG QUAY LẠI: +// 1. Xếp tay cho ĐẦY từng cột (kiểu multicol của LaTeX). Mình không chẻ được +// một câu làm đôi như Typst nên cột nào cũng hụt một khoảng; bản `loigiai` +// của de-mau-da-dang PHÌNH THÊM một trang (11 → 12). +// 2. Gài `metadata` trước mỗi câu rồi `query` lại số trang cho biết chắc câu +// nào rơi xuống trang cuối. Đúng thì có đúng, nhưng mỗi vùng chia cột lại +// bắt Typst dựng thêm một lượt; đề có 3 vùng là vượt hạn 5 lượt, hiện +// "document did not converge" và dòng "(Đề thi có N trang)" ở đầu đề kẹt +// lại số cũ. Đã đối chiếu ảnh từng trang: lối ĐO dưới đây cho bố cục GIỐNG +// HỆT lối query trên toàn bộ file thử, nên không đáng đánh đổi. +// +// Phép đo sai thì sai theo hướng "đoán trang cuối bắt đầu MUỘN hơn thực tế" +// (dòng chảy thật còn có khe hở ở chỗ khối không chẻ được), tức cột trái đầy +// hơn cột phải — vẫn đẹp chán, không bao giờ đội thêm trang. +// cao-dau : chỗ trống còn lại của trang hiện tại (thân ngắn nằm gọn ở đây) +// cao-cot : chiều cao một cột đầy trang +#let _can-cot(nd, so, wcol, cao-dau, cao-cot) = { + if so < 2 or type(nd) != content or not nd.has("children") { return nd } + if type(cao-cot) != length or cao-cot <= 0pt { return nd } + // Thân gõ bằng #for gói tất cả vào MỘT phần tử ⇒ phải trải phẳng mới có mối + // để chèn #colbreak(). + let cs = nd.children.map(_cot-la).flatten() + if cs.len() < 2 { return nd } + + // ----- đo chiều cao từng câu ở bề rộng MỘT CỘT (phần tử trắng = 0) ----- + let cao = cs.map(c => if _hv-trong(c) { 0pt } else { + measure(block(width: wcol, c)).height }) + let tong-le = cao.fold(0pt, (a, b) => a + b) + if tong-le <= 0pt { return nd } + // Các phép đo RỜI không tính khoảng cách giữa hai khối ⇒ tổng của chúng nhỏ + // hơn chiều cao thật. Đo thêm cả thân rồi rải phần chênh lệch đều cho mọi câu. + let tong-that = measure(block(width: wcol, nd)).height + let he-so = if tong-that > tong-le { tong-that / tong-le } else { 1.0 } + let cao = cao.map(h => h * he-so) + let tong = tong-le * he-so + + let an = 5mm // chừa chống tràn + let hd = if type(cao-dau) == length and cao-dau > cao-cot * 0.25 { + calc.min(cao-dau, cao-cot) } else { cao-cot } + + // Chiều cao cộng dồn TRƯỚC mỗi câu (tich.at(i)); chốt cuối = tổng cả thân. + let tich = () + let cong = 0pt + for h in cao { + tich.push(cong) + cong = cong + h + } + tich.push(cong) + + // ----- câu MỞ ĐẦU trang cuối ----- + // Trang hiện tại chứa `so` cột cao `hd`, mỗi trang sau `so` cột cao `cao-cot`. + let mo = 0 + if tong > (hd - an) * so { + let suc1 = (hd - an) * so + let sucn = (cao-cot - an) * so + let so-trang-sau = calc.max(0, calc.ceil((tong - suc1) / sucn) - 1) + let h0 = suc1 + sucn * so-trang-sau + let k = range(cs.len()).position(i => tich.at(i) >= h0) + mo = if k == none { 0 } else { k } + } + + // ----- chia ĐỀU phần nằm trên trang cuối cho `so` cột ----- + let du = tich.at(cs.len()) - tich.at(mo) + if du <= 0pt { return nd } + let muc = du / so + // Trang cuối vốn đã gần đầy thì ĐỪNG đụng vào: chia ra chẳng tiết kiệm được + // bao nhiêu, mà cột chót chỉ cần phình hơn phép đo một chút là tràn sang + // trang mới — mất không một trang giấy. Chỉ can thiệp khi còn trống hẳn 15%. + if muc > (cao-cot - an) * 0.85 { return nd } + + // Chỉ được cắt ở ĐẦU một câu THẬT (câu có chiều cao). Trang cuối không đủ + // `so` câu thì chia kiểu gì cũng có cột rỗng ⇒ để nguyên. + let that = range(mo, cs.len()).filter(i => cao.at(i) > 0pt) + if that.len() < so { return nd } + // Chọn mối GẦN mốc k·muc nhất — chứ KHÔNG phải mối đầu tiên vượt mốc: trang + // cuối thường chỉ còn vài câu, cắt kiểu "vượt mốc mới cắt" là hụt luôn + // (ba câu 100/120/228 thì chẳng mối nào vượt 224 ⇒ không cắt được chỗ nào). + let moi = that.slice(1) + let cat = () + let tu = mo + 1 + for k in range(1, so) { + let dich = tich.at(mo) + muc * k + let tot = none + let lech = none + for i in moi { + if i < tu { continue } + let d = tich.at(i) - dich + let ad = if d < 0pt { -d } else { d } + if lech == none or ad < lech { lech = ad; tot = i } + } + if tot != none { cat.push(tot); tu = tot + 1 } + } + if cat.len() == 0 { return nd } + // Cắt xong mà có cột vẫn quá sức chứa thì bỏ, để Typst tự rót. + let bien = (mo,) + cat + (cs.len(),) + let cao-moi = range(bien.len() - 1).map(k => + tich.at(bien.at(k + 1)) - tich.at(bien.at(k))) + if cao-moi.fold(0pt, calc.max) > cao-cot - an { return nd } + + // ----- ghép lại, chèn #colbreak() ở các mối đã chọn ----- + let kq = () + for (i, c) in cs.enumerate() { + if cat.contains(i) { kq.push(colbreak()) } + kq.push(c) + } + kq.join() +} + +// ---------- #chia-2-cot — chia phần câu hỏi thành 2 cột đều nhau ---------- +// so : số cột (mặc định 2; đặt 3 nếu muốn ba cột) +// khoang : khe giữa hai cột +// can : true (mặc định) = tự cân chiều cao hai cột; false = để Typst rót +// đầy cột 1 trước (lối gốc của Typst) +#let chia-2-cot(so: 2, khoang: 18pt, can: true, body) = context { + if not _la-sach(_ho-so.get()) { body } else { + let (nd0, sau) = _tach-thoi-cot(body) + if nd0 != none { + let nd = _cot-than(nd0) + let (cao-dau, cao-cot) = _cao-vung() + layout(kich => { + let wcol = (kich.width - khoang * (so - 1)) / so + let hc = if cao-cot == none { kich.height } else { cao-cot } + let than = if can { _can-cot(nd, so, wcol, cao-dau, hc) } else { nd } + columns(so, gutter: khoang, than) + }) + } + if sau != none { sau } + } +} + +// ---------- #chia-2-cot-lech — cột trái câu hỏi, cột phải cho HS làm bài ---- +// Cột phải để trống, kẻ hàng ngang MỜ (kiểu vở kẻ ngang) và có vạch dọc mờ +// ngăn giữa hai cột. Số hàng kẻ tính theo chiều cao thật của phần câu hỏi +// (đo bằng measure) nên không thừa/thiếu giấy. +// rong-trai : BỀ RỘNG CỘT TRÁI — mặc định 70% khổ chữ; nhận cả tỉ lệ +// (60%) lẫn độ dài tuyệt đối (11cm). +// khoang : khe giữa hai cột +// cao-dong : khoảng cách giữa hai hàng kẻ (mặc định 9mm) +// mau, day : màu và độ dày hàng kẻ +// ke : false = cột phải để trắng trơn (không kẻ dòng) +// vach-ngan : true (mặc định) = vạch dọc mờ ngăn hai cột +// tieu-de-phai: vd [Bài làm] — in nhạt ở đầu cột phải; none = không in +#let chia-2-cot-lech( + rong-trai: 70%, + khoang: 10pt, + cao-dong: 9mm, + mau: luma(65%), + day: 0.4pt, + ke: true, + vach-ngan: true, + tieu-de-phai: none, + body, +) = context { + if not _la-sach(_ho-so.get()) { body } else { + let (nd0, sau) = _tach-thoi-cot(body) + let nd = _cot-than(nd0) + if nd != none { + layout(kich => { + let wc = kich.width + let w0 = if type(rong-trai) == ratio { wc * rong-trai + } else if type(rong-trai) == relative { wc * rong-trai.ratio + rong-trai.length + } else { rong-trai } + // chốt chặn: cột trái không hẹp quá 2cm, không nuốt hết cột phải + let wt = calc.max(2cm, calc.min(w0, wc - khoang - 1.5cm)) + let wp = wc - wt - khoang + if wp <= 0pt { + // không đủ chỗ cho cột phải ⇒ giữ nguyên lối một cột (an toàn) + nd + } else { + let cao = measure(block(width: wt, nd)).height + let so-dong = calc.min(4000, calc.max(1, int(calc.floor(cao / cao-dong)))) + let dong-ke = block( + width: 100%, height: cao-dong, above: 0pt, below: 0pt, breakable: false, + place(bottom + left, line(length: 100%, stroke: day + mau)), + ) + grid( + columns: (wt, wp), column-gutter: khoang, + block(width: 100%, nd), + block( + width: 100%, + stroke: if vach-ngan { (left: day + mau) } else { none }, + inset: (left: if vach-ngan { 7pt } else { 0pt }), + above: 0pt, below: 0pt, + { + if tieu-de-phai != none { + block(above: 0pt, below: 3pt, + text(size: 0.85em, style: "italic", fill: mau.darken(25%), tieu-de-phai)) + } + if ke { for _ in range(so-dong) { dong-ke } } + }, + ), + ) + } + }) + } + if sau != none { sau } + } +} + +// ===================================================================== +// #ke-het-trang — KẺ DÒNG LẤP ĐẦY CHỖ TRỐNG CÒN LẠI CỦA TRANG (08/2026) +// --------------------------------------------------------------------- +// Đặt ở đâu thì kẻ dòng từ chỗ đó xuống HẾT TRANG ĐÓ — số dòng tự tính, không +// phải đếm tay: +// #ke-het-trang() // nét chấm, cách 9mm +// #ke-het-trang(cao-dong: 7mm, kieu: none) // nét liền, dày dòng hơn +// #ke-het-trang(chua: 2cm) // chừa 2cm cuối trang +// #ke-het-trang(them-trang: 1) // kẻ thêm TRỌN 1 trang nữa +// +// Cách làm: `here().position().y` cho biết đang ở đâu trên trang, trừ khỏi +// `page.height` và lề dưới là ra chỗ còn lại, chia cho `cao-dong` ra số dòng. +// Vị trí của chính lệnh này KHÔNG bị đẩy đi bởi các dòng kẻ nó sinh ra (chúng +// nằm SAU nó) nên phép đo ổn định, không lặp vô hạn. +// +// Chỉ dựng ở bản in A4 (dethi/loigiai); beamer bỏ qua như #het/#bang-dap-an. +// ===================================================================== +#let ke-het-trang( + cao-dong: 9mm, // khoảng cách giữa hai dòng kẻ + mau: luma(65%), // màu nét + day: 0.4pt, // độ dày nét + kieu: "dotted", // "dotted" | "dashed" | "dash-dotted" | none (nét liền) + dai: 100%, // bề dài dòng kẻ (50% = nửa bề ngang) + chua: 0pt, // chừa thêm chỗ trống ở đáy trang + le-tren: auto, // ghi đè lề trang nếu lề khai kiểu lạ + le-duoi: auto, + them-trang: 0, // kẻ thêm bao nhiêu TRANG ĐẦY nữa sau trang hiện tại +) = context { + if not _la-sach(_ho-so.get()) { } else if type(page.height) != length { } else { + // ----- lấy lề trên / lề dưới từ thiết lập trang ----- + let m = page.margin + let _le(khoa, mac-dinh) = { + if type(m) == dictionary { m.at(khoa, default: m.at("y", default: mac-dinh)) + } else if m == auto { 2.5cm } else { m } + } + let lt = if le-tren != auto { le-tren } else { _le("top", 1.6cm) } + let ld = if le-duoi != auto { le-duoi } else { _le("bottom", 2.2cm) } + + // MỘT dòng kẻ = MỘT khối cao ĐÚNG `cao-dong`, nét vẽ sát đáy khối. + // ⚠️ ĐỪNG viết `v(cao-dong); line(...)`: `line` tự tạo một ĐOẠN VĂN nên + // Typst cộng thêm khoảng cách đoạn trên/dưới ⇒ mỗi dòng chiếm gần GẤP ĐÔI + // `cao-dong`, số dòng tính ra thừa và tràn sang trang sau. + let net = (paint: mau, thickness: day, dash: kieu) + let mot-dong = block( + width: 100%, height: cao-dong, above: 0pt, below: 0pt, breakable: false, + place(bottom + left, line(length: dai, stroke: net)), + ) + + // ----- phần còn lại của TRANG HIỆN TẠI ----- + // trừ thêm 1mm chống tràn (khoảng cách đoạn của khối đứng ngay trên lệnh + // có thể đẩy vị trí xuống một chút so với phép đo) + let con = page.height - here().position().y - ld - chua - 1mm + for _ in range(calc.max(0, int(calc.floor(con / cao-dong)))) { mot-dong } + + // ----- các trang ĐẦY tiếp theo (nếu có) ----- + if them-trang > 0 { + let cao-trang = page.height - lt - ld - chua + let n-day = calc.max(0, int(calc.floor(cao-trang / cao-dong))) + for _ in range(them-trang) { + pagebreak() + for _ in range(n-day) { mot-dong } + } + } + } +} + +// ===================================================================== +// KẾT THÚC ĐỀ + BẢNG ĐÁP ÁN (chỉ dựng ở bản in A4 dethi/loigiai) +// ===================================================================== + +// ---------- #het — dòng kết thúc đề ---------- +// Dựng khối "––––– HẾT –––––" + ghi chú (căn giữa), chỉ hiện ở bản in A4; +// ở beamer bỏ qua (không sinh slide thừa). +// #het() -> mặc định +// #het(ghi-chu: [Không được dùng tài liệu.]) +// #het(chu: [THE END], ghi-chu: none) +#let het( + chu: [HẾT], + ghi-chu: [Thí sinh không được sử dụng tài liệu.\ Giám thị coi thi không giải thích gì thêm.], +) = context { + if _la-sach(_ho-so.get()) { + v(14pt) + align(center, { + text(weight: "bold", size: 1.05em)[#("–" * 5) #h(4pt) #chu #h(4pt) #("–" * 5)] + if ghi-chu != none { + linebreak() + v(3pt) + text(style: "italic", size: 0.95em, ghi-chu) + } + }) + } +} + +// ---------- Cố gắng lấy CHUỖI từ đáp án (cho ô điền tln) ---------- +// str/số -> chuỗi; content -> đọc .text/.children/.body đệ quy; không tách +// được -> none (khi đó bảng đáp án in nguyên nội dung vào một ô). +#let _da-chuoi(c) = { + if c == none { return "" } + if type(c) == str { return c } + if type(c) == int or type(c) == float { return str(c) } + if type(c) != content { return none } + if c.has("text") { return c.text } + if c.has("children") { + let cac = c.children.map(_da-chuoi) + if cac.any(p => p == none) { return none } + return cac.join("") + } + if c.has("body") { return _da-chuoi(c.body) } + none +} + +// ---------- #bang-dap-an — 3 bảng đáp án tn / ds / tln ---------- +// Tự thu thập đáp án MỌI câu tn/ds/tln trong tài liệu (qua metadata ) +// rồi dựng bảng theo mẫu đề 2025. Mỗi loại đánh số 1..n độc lập. +// #bang-dap-an(ma-de: "0101") +// Tham số: +// ma-de : auto = LẤY TỰ ĐỘNG từ mã đề khai ở de-toan (đồng bộ, khỏi nhập +// tay); none = không in mã đề; hoặc giá trị cụ thể để ghi đè. +// tieu-de : auto = tự dựng theo ma-de; hoặc nội dung tuỳ ý; none = ẩn. +// so-o-tln : số ô mỗi đáp án trả lời ngắn (mặc định 4; tự nới nếu dài hơn). +// ngat-trang: true = sang trang mới trước bảng. +#let bang-dap-an( + ma-de: auto, + tieu-de: auto, + so-o-tln: 4, + ngat-trang: true, +) = context { + if not _la-sach(_ho-so.get()) { } else { + let ma-de = if ma-de == auto { _ma-de.get() } else { ma-de } + let items = query().map(m => m.value) + let ds-tn = items.filter(x => x.loai == "tn") + let ds-ds = items.filter(x => x.loai == "ds") + let ds-tln = items.filter(x => x.loai == "tln") + if ds-tn.len() + ds-ds.len() + ds-tln.len() > 0 { + if ngat-trang { pagebreak(weak: true) } + + // ----- Tiêu đề ----- + let td = if tieu-de == auto { + if ma-de != none [BẢNG ĐÁP ÁN MÃ ĐỀ #ma-de] else [BẢNG ĐÁP ÁN] + } else { tieu-de } + if td != none { + align(center, text(weight: "bold", size: 13pt, fill: _xanh, upper(td))) + v(8pt) + } + + let tieu-muc(nd) = block(above: 12pt, below: 7pt, + text(weight: "bold", size: 1.05em, nd)) + + // ===== 1) Trắc nghiệm nhiều lựa chọn ===== + if ds-tn.len() > 0 { + tieu-muc[Bảng đáp án các câu trắc nghiệm nhiều lựa chọn] + let o-tn(i, x) = box(stroke: 0.7pt + _xanh, radius: 2.5pt, + inset: (x: 6pt, y: 3.5pt), baseline: 30%)[ + #text(fill: _xanh, weight: "bold")[#(i + 1).]~#text(weight: "bold")[#x.da] + ] + let cot = calc.min(ds-tn.len(), 12) + grid(columns: (auto,) * cot, column-gutter: 6pt, row-gutter: 7pt, + align: center + horizon, + ..ds-tn.enumerate().map(p => o-tn(p.at(0), p.at(1)))) + } + + // ===== 2) Đúng — Sai ===== + if ds-ds.len() > 0 { + tieu-muc[Bảng đáp án các câu trắc nghiệm đúng sai] + let vong(dung) = if dung { + circle(radius: 7pt, fill: _xanh, stroke: none, + align(center + horizon, text(fill: white, weight: "bold", size: 8pt)[Đ])) + } else { + circle(radius: 7pt, fill: white, stroke: 0.9pt + luma(45%), + align(center + horizon, text(weight: "bold", size: 8pt)[S])) + } + let o-ds(i, x) = { + let bs = if type(x.da) == array { x.da } else { (false, false, false, false) } + box(stroke: 0.7pt + luma(45%), radius: 2.5pt, inset: (x: 8pt, y: 6pt), + baseline: 30%)[ + #text(fill: _do, weight: "bold")[Câu #(i + 1).]~~#h(2pt)#box(baseline: 35%, + stack(dir: ltr, spacing: 3pt, ..bs.map(vong))) + ] + } + grid(columns: (auto,) * calc.min(ds-ds.len(), 4), + column-gutter: 8pt, row-gutter: 8pt, align: left + horizon, + ..ds-ds.enumerate().map(p => o-ds(p.at(0), p.at(1)))) + } + + // ===== 3) Trả lời ngắn ===== + if ds-tln.len() > 0 { + tieu-muc[Bảng đáp án các câu trắc nghiệm trả lời ngắn] + let o-ky-tu(ky) = box(width: 17pt, height: 19pt, stroke: 0.7pt + luma(40%), + radius: 1.5pt, inset: 0pt, align(center + horizon, + if ky != none { text(size: 0.9em, ky) })) + let o-tln(i, x) = { + let s = _da-chuoi(x.da) + let noi = if s != none { + let kts = s.clusters() + let n = calc.max(so-o-tln, kts.len()) + box(baseline: 35%, stack(dir: ltr, spacing: 3pt, + ..range(n).map(k => o-ky-tu(if k < kts.len() { kts.at(k) } else { none })))) + } else { + // Không tách được thành ký tự -> in nguyên nội dung vào một ô rộng. + box(stroke: 0.7pt + luma(40%), radius: 1.5pt, inset: (x: 5pt, y: 3pt), + baseline: 30%, x.da) + } + box(stroke: 0.7pt + luma(45%), radius: 2.5pt, inset: (x: 8pt, y: 6pt), + baseline: 30%)[ + #text(fill: _do, weight: "bold")[Câu #(i + 1).]~~#h(2pt)#noi + ] + } + grid(columns: (auto,) * calc.min(ds-tln.len(), 3), + column-gutter: 8pt, row-gutter: 8pt, align: left + horizon, + ..ds-tln.enumerate().map(p => o-tln(p.at(0), p.at(1)))) + } + } + } +} + +// Bí danh tên cũ (tương thích ngược): mc = tn, tf = ds, sa = tln. +#let mc = tn +#let tf = ds +#let sa = tln + +// TƯƠNG THÍCH NGƯỢC: các file cũ vẫn gọi được +// #let (vd, tn, ds, tln, tl, hd, lt, vdtt, phan) = tao-cau-hoi(ho-so) +// (tham số ho-so nay không còn cần thiết — hồ sơ đọc từ trạng thái). +#let tao-cau-hoi(..thua) = ( + vd: vd, tn: tn, ds: ds, tln: tln, tl: tl, hd: hd, lt: lt, vdtt: vdtt, + phan: phan, mc: tn, tf: ds, sa: tln, +) diff --git a/packages/preview/conic-toan/0.3.4/lib/da-dien.typ b/packages/preview/conic-toan/0.3.4/lib/da-dien.typ new file mode 100644 index 0000000000..bb815b53cd --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/da-dien.typ @@ -0,0 +1,876 @@ +// ===================================================================== +// da-dien.typ — ENGINE ĐA DIỆN TỔNG QUÁT (07/2026) +// +// Nhập DANH SÁCH ĐỈNH 3D + DANH SÁCH MẶT (mảng chỉ số đỉnh) là vẽ được +// BẤT KÌ khối đa diện LỒI: tự chiếu, tự xác định mặt trước/mặt sau +// (back-face culling), tự vẽ nét liền / nét khuất, tự tô mặt theo độ sâu. +// +// #da-dien(..khoi-chop-deu(n: 4, R: 2, cao: 3.6)) +// #da-dien(dinh: ((0,0,0), (3,0,0), (0,3,0), (0,0,3)), +// mat: ((0,1,2), (0,1,3), (0,2,3), (1,2,3)), +// ten: ($O$, $A$, $B$, $C$)) +// +// QUY TẮC nét khuất: cạnh LIỀN nếu thuộc ít nhất một mặt hướng về người +// nhìn, ĐỨT nếu chỉ thuộc các mặt hướng ra sau. Quy tắc này CHÍNH XÁC với +// khối LỒI (chóp, lăng trụ, hộp, chóp cụt, tứ diện, bát diện… — tức toàn +// bộ hình học THPT). Khối KHÔNG lồi hoặc hai khối che nhau thì cần z-buffer, +// KHÔNG dùng được engine này. +// +// Thứ tự đỉnh trong mỗi mặt KHÔNG cần theo chiều nào: pháp tuyến được lật +// ra ngoài theo tâm khối. +// ===================================================================== +#import "ve.typ": * + +// Bí danh nội bộ: `ve-da-dien` có THAM SỐ tên `diem`/`duong`, mà tham số CHE +// mất hàm cùng tên của ve.typ. Giữ tham chiếu tới hàm vẽ ở đây để dùng bên +// trong thân hàm. (Đừng bỏ hai dòng này.) +#let _ve-diem = diem +#let _ve-duong = duong + +// ===================================================================== +// VECTƠ 3D — mọi hàm ở khối này TRẢ VỀ GIÁ TRỊ (không kê vào ve-voi) +// ===================================================================== +#let v3-cong(A, B) = (A.at(0) + B.at(0), A.at(1) + B.at(1), A.at(2) + B.at(2)) +#let v3-tru(A, B) = (A.at(0) - B.at(0), A.at(1) - B.at(1), A.at(2) - B.at(2)) +#let v3-nhan(k, A) = (k * A.at(0), k * A.at(1), k * A.at(2)) +// Tích vô hướng (dot) và tích có hướng (cross). +#let v3-vo-huong(A, B) = A.at(0) * B.at(0) + A.at(1) * B.at(1) + A.at(2) * B.at(2) +#let v3-co-huong(A, B) = ( + A.at(1) * B.at(2) - A.at(2) * B.at(1), + A.at(2) * B.at(0) - A.at(0) * B.at(2), + A.at(0) * B.at(1) - A.at(1) * B.at(0), +) +#let v3-dai(A) = calc.sqrt(calc.max(0.0, v3-vo-huong(A, A))) +#let v3-chuan(A) = { + let d = v3-dai(A) + if d < 1e-12 { A } else { v3-nhan(1 / d, A) } +} +// Trung điểm, điểm chia theo tỉ lệ t (0 = A, 1 = B), tâm (trung bình) của mảng. +#let trung-diem-3d(A, B) = v3-nhan(0.5, v3-cong(A, B)) +#let chia-3d(A, B, t) = v3-cong(A, v3-nhan(t, v3-tru(B, A))) +#let tam-3d(ds) = v3-nhan(1 / ds.len(), ds.fold((0.0, 0.0, 0.0), v3-cong)) + +// Pháp tuyến của đa giác phẳng (mảng điểm 3D) — công thức Newell, bền với +// đa giác gần suy biến và không cần biết chiều quay. +#let phap-da-giac(ds) = { + let n = ds.len() + let s = (0.0, 0.0, 0.0) + for i in range(n) { + let A = ds.at(i) + let B = ds.at(calc.rem(i + 1, n)) + s = v3-cong(s, ( + (A.at(1) - B.at(1)) * (A.at(2) + B.at(2)), + (A.at(2) - B.at(2)) * (A.at(0) + B.at(0)), + (A.at(0) - B.at(0)) * (A.at(1) + B.at(1)), + )) + } + v3-chuan(s) +} + +// ===================================================================== +// CAMERA — phép chiếu song song + HƯỚNG NHÌN tự suy ra +// Phép chiếu p là hàm tuyến tính (x, y, z) -> (u, v) toạ độ toán của khung. +// Hai hàng của ma trận chiếu là hai trục màn hình (phải, lên); tích có hướng +// của chúng chính là hướng TỪ hình VỀ PHÍA người nhìn ⇒ suy được mặt trước. +// ===================================================================== +#let _nhin(p) = { + let o = p((0, 0, 0)) + let c1 = p((1, 0, 0)) + let c2 = p((0, 1, 0)) + let c3 = p((0, 0, 1)) + let r1 = (c1.at(0) - o.at(0), c2.at(0) - o.at(0), c3.at(0) - o.at(0)) + let r2 = (c1.at(1) - o.at(1), c2.at(1) - o.at(1), c3.at(1) - o.at(1)) + v3-chuan(v3-co-huong(r1, r2)) +} + +// Bọc một phép chiếu bất kì thành camera: (p: hàm chiếu, nhin: hướng nhìn). +#let chieu-3d(p) = (p: p, nhin: _nhin(p)) + +// Chiếu XIÊN mặc định (y = chiều sâu ra sau, z lên trên, x sang phải) — +// KHỚP với phép chiếu của các hình chóp/lăng trụ dựng sẵn trong lib. +#let chieu-xien(goc: 40deg, k: 0.5) = chieu-3d(P => ( + P.at(0) + k * P.at(1) * calc.cos(goc), + P.at(2) + k * P.at(1) * calc.sin(goc), +)) + +// Chiếu KHỚP hệ trục #oxyz (Oy sang phải, Oz lên, Ox chéo xuống-trái về +// phía người nhìn). Dùng khi vẽ khối trong `oxyz(them: (ctx, t3) => ...)` +// — nhớ để k/goc trùng với oxyz. +#let chieu-oxyz(k: 0.55, goc: 35deg) = chieu-3d(P => ( + P.at(1) - k * P.at(0) * calc.cos(goc), + P.at(2) - k * P.at(0) * calc.sin(goc), +)) + +// Camera TRỰC GIAO thật theo góc Euler: quay quanh Oz góc `ngang`, nâng +// cao `cao`. Oz luôn dựng đứng trên trang. `cao` lớn = nhìn từ trên xuống. +#let chieu-truc-giao(ngang: 65deg, cao: 22deg, ti-le: 1) = { + let e = ( + calc.cos(cao) * calc.cos(ngang), + calc.cos(cao) * calc.sin(ngang), + calc.sin(cao), + ) + let r = (-calc.sin(ngang), calc.cos(ngang), 0) + let u = v3-co-huong(e, r) + ( + p: P => (ti-le * v3-vo-huong(P, r), ti-le * v3-vo-huong(P, u)), + nhin: e, + ) +} + +// auto -> chiếu xiên mặc định · hàm -> tự suy hướng nhìn · dict -> dùng luôn. +#let _cam(c) = { + if c == auto { chieu-xien() } + else if type(c) == function { chieu-3d(c) } + else { c } +} + +// ===================================================================== +// PHÂN TÍCH KHỐI — TRẢ GIÁ TRỊ (không kê vào ve-voi) +// .cam camera đã chuẩn hoá +// .d2 mảng đỉnh đã chiếu (toạ độ toán 2D) +// .sau độ sâu từng đỉnh (LỚN hơn = gần người nhìn hơn) +// .mat mảng thông tin mặt: (chi-so, phap (ra ngoài), tam, sau, truoc) +// .canh mảng cạnh duy nhất: (i, j, hien) — hien = false thì vẽ ĐỨT +// ===================================================================== +#let _canh-vong(m) = { + let n = m.len() + range(n).map(i => (m.at(i), m.at(calc.rem(i + 1, n)))) +} +#let _khoa(i, j) = if i < j { str(i) + "-" + str(j) } else { str(j) + "-" + str(i) } + +#let phan-tich-khoi(dinh, mat, cam: auto) = { + let cm = _cam(cam) + let e = cm.nhin + let C = tam-3d(dinh) + let d2 = dinh.map(cm.p) + let ttm = () + for m in mat { + let n = phap-da-giac(m.map(i => dinh.at(i))) + let T = tam-3d(m.map(i => dinh.at(i))) + // lật pháp tuyến RA NGOÀI khối (khối lồi: ra xa tâm khối) + if v3-vo-huong(n, v3-tru(T, C)) < 0 { n = v3-nhan(-1, n) } + ttm.push(( + chi-so: m, + phap: n, + tam: T, + sau: v3-vo-huong(T, e), + truoc: v3-vo-huong(n, e) > 1e-9, + )) + } + // cạnh: hiện nếu thuộc ÍT NHẤT MỘT mặt trước; mặt hở (cạnh 1 mặt) luôn hiện + let bang = (:) + for f in ttm { + for (i, j) in _canh-vong(f.chi-so) { + let k = _khoa(i, j) + let cu = bang.at(k, default: (i: calc.min(i, j), j: calc.max(i, j), hien: false, so: 0)) + bang.insert(k, (i: cu.i, j: cu.j, hien: cu.hien or f.truoc, so: cu.so + 1)) + } + } + let canh = bang.values().map(c => ( + i: c.i, + j: c.j, + hien: if c.so == 1 { true } else { c.hien }, + )) + (cam: cm, d2: d2, sau: dinh.map(P => v3-vo-huong(P, e)), mat: ttm, canh: canh) +} + +// Cửa sổ bao các điểm 2D + lề (nội bộ). +#let _cs2(pts, mg: 0.7) = { + let xs = pts.map(p => p.at(0)) + let ys = pts.map(p => p.at(1)) + ( + xmin: calc.min(..xs) - mg, xmax: calc.max(..xs) + mg, + ymin: calc.min(..ys) - mg, ymax: calc.max(..ys) + mg, + ) +} + +// Hướng đặt nhãn cho đỉnh Q: PHÂN GIÁC của KHE GÓC RỘNG NHẤT giữa các cạnh +// đi ra từ Q (ds-ke = các đỉnh kề, toạ độ toán 2D) — nhãn không đè lên cạnh +// nào. Tính trên TOẠ ĐỘ TRANG nên đúng cả khi hai trục khác tỉ lệ. +// Trả vectơ (dx, dy) trên trang (y hướng XUỐNG) — đúng dạng `huong` của `nhan`. +#let _huong-khe(ctx, Q, ds-ke) = { + let q = toa-pt(ctx, Q) + let gs = () + for R in ds-ke { + let r = toa-pt(ctx, R) + let dx = r.at(0) - q.at(0) + let dy = r.at(1) - q.at(1) + if calc.sqrt(dx * dx + dy * dy) > 1e-6 { gs.push(calc.atan2(dx, dy).deg()) } + } + if gs.len() == 0 { return (0, -1) } + let gs = gs.sorted() + let n = gs.len() + let khe = -1.0 + let giua = 0.0 + for i in range(n) { + let a = gs.at(i) + let b = if i + 1 < n { gs.at(i + 1) } else { gs.at(0) + 360 } + if b - a > khe { + khe = b - a + giua = (a + b) / 2 + } + } + let g = giua * 1deg + (calc.cos(g), calc.sin(g)) +} + +// Hướng nhãn TOẢ RA XA tâm hình chiếu (cho điểm KHÔNG nằm trên cạnh nào). +#let _huong-xa(Q, C2) = { + let dx = Q.at(0) - C2.at(0) + let dy = Q.at(1) - C2.at(1) + let l = calc.sqrt(dx * dx + dy * dy) + if l < 1e-9 { (0, -1) } else { (dx / l, -dy / l) } +} + +// --------------------------------------------------------------------- +// CHE BỞI CHÍNH KHỐI — kiểm CHÍNH XÁC (không quét mẫu, không cần z-buffer): +// tia đi từ P về phía người nhìn có cắt khối LỒI hay không. Giao đoạn tham +// số t của tia với mọi nửa không gian của khối rồi xem còn t > 0 nào không. +// Nhất quán với quy tắc nét khuất của cạnh: điểm trên một mặt TRƯỚC thì +// KHÔNG bị che; điểm trong khối hoặc trên mặt SAU thì bị che. +// --------------------------------------------------------------------- +#let _bi-khoi-che(P, e, mat-tt) = { + let lo = -1e9 + let hi = 1e9 + for f in mat-tt { + let df = v3-vo-huong(f.phap, e) + let f0 = v3-vo-huong(f.phap, P) - v3-vo-huong(f.phap, f.tam) + if calc.abs(df) < 1e-12 { + if f0 > 1e-9 { return false } // ở ngoài một mặt song song tia nhìn + } else if df > 0 { + hi = calc.min(hi, -f0 / df) + } else { + lo = calc.max(lo, -f0 / df) + } + } + calc.max(lo, 1e-6) <= hi - 1e-9 +} + +// Vẽ ĐOẠN PHỤ trong/quanh khối, TỰ phân loại nét: phần bị khối che vẽ ĐỨT, +// phần thấy vẽ LIỀN (một đoạn có thể vừa liền vừa đứt — vd đường cao SO +// xuất phát từ đỉnh S thấy được rồi chui vào trong khối). +#let _doan-tu-dong( + ctx, p, e, mat-tt, A, B, + n: 160, mau: black, day: 1pt, + hien-khuat: true, mau-khuat: auto, day-khuat: auto, dut: auto, +) = { + if dut != auto { + doan(ctx, p(A), p(B), mau: mau, day: day, dut: dut) + return + } + let moc = () + let tt = none + let dau = 0.0 + for st in range(n) { + let che = _bi-khoi-che(chia-3d(A, B, (st + 0.5) / n), e, mat-tt) + if tt == none { + tt = che + } else if che != tt { + moc.push((dau, st / n, tt)) + tt = che + dau = st / n + } + } + moc.push((dau, 1.0, tt)) + for (u, v, che) in moc { + if che and not hien-khuat { continue } + doan( + ctx, p(chia-3d(A, B, u)), p(chia-3d(A, B, v)), + mau: if not che or mau-khuat == auto { mau } else { mau-khuat }, + day: if not che or day-khuat == auto { day } else { day-khuat }, + dut: che, + ) + } +} + +// Cạnh của khối CHỨA điểm P (nếu có) — trả phần tử của `.canh`, hoặc none. +#let _canh-chua(dinh, canh, P, eps: 1e-6) = { + for c in canh { + let A = dinh.at(c.i) + let B = dinh.at(c.j) + let AB = v3-tru(B, A) + let L2 = v3-vo-huong(AB, AB) + if L2 < 1e-12 { continue } + let t = v3-vo-huong(v3-tru(P, A), AB) / L2 + if t < -eps or t > 1 + eps { continue } + let H = chia-3d(A, B, calc.max(0.0, calc.min(1.0, t))) + if v3-dai(v3-tru(P, H)) < eps { return c } + } + none +} + +// Nhận dạng một điểm 3D trần (mảng 3 số) — phân biệt với mục (P, ten, ...). +#let _la-diem3(x) = ( + type(x) == array and x.len() == 3 and type(x.at(0)) != array +) + +// ---------- Tiện ích dựng điểm (TRẢ GIÁ TRỊ) ---------- +// Điểm chia cạnh nối đỉnh thứ i và thứ j của khối (t = 0 tại i, 1 tại j). +#let diem-canh(dinh, i, j, t: 0.5) = chia-3d(dinh.at(i), dinh.at(j), t) +// Hình chiếu vuông góc của P lên ĐƯỜNG THẲNG AB. +#let hinh-chieu-3d(P, A, B) = { + let AB = v3-tru(B, A) + let L2 = v3-vo-huong(AB, AB) + if L2 < 1e-12 { A } else { + v3-cong(A, v3-nhan(v3-vo-huong(v3-tru(P, A), AB) / L2, AB)) + } +} +// Hình chiếu vuông góc của P lên MẶT PHẲNG mp = (n: pháp đơn vị, d:). +#let hinh-chieu-mp(P, mp) = v3-tru(P, v3-nhan(v3-vo-huong(mp.n, P) - mp.d, mp.n)) + +// ===================================================================== +// VẼ KHỐI ĐA DIỆN trong khung có sẵn (nhận ctx ⇒ KÊ vào ve-voi) +// dinh mảng điểm 3D · mat mảng mặt (mảng chỉ số đỉnh) +// ten mảng nhãn theo thứ tự đỉnh (none = không ghi) +// huong auto = toả ra từ tâm hình chiếu · hoặc mảng hướng từng đỉnh +// to màu tô CÁC MẶT TRƯỚC (vẽ xa -> gần) · to-mat mảng màu riêng +// diem ĐIỂM PHỤ (trung điểm cạnh, chân đường cao…): mỗi mục là +// P · (P, ten) · (P, ten, huong) · (P, ten, huong, mau). +// Điểm nằm TRÊN CẠNH: nhãn tự đặt VUÔNG GÓC với cạnh đó; điểm bị +// khối che thì `hien-khuat: false` sẽ ẩn luôn. +// duong ĐOẠN PHỤ (đường cao, hình chiếu…): mỗi mục là (A, B) hoặc +// (A, B, tuỳ-chọn) với tuỳ-chọn là dict nhận mau/day/ten/tai/huong/ +// cach/dut/hien-khuat/mau-khuat/day-khuat/vuong/r. Phần đoạn bị khối +// che TỰ vẽ đứt. `vuong: C` = ký hiệu góc vuông tại B, giữa BA và BC. +// them (ctx, p) => ... với p là hàm chiếu (x, y, z) -> điểm 2D +// ===================================================================== +#let ve-da-dien( + ctx, + dinh: (), mat: (), + cam: auto, + ten: none, huong: auto, cach: 6pt, mau-ten: black, + hien-dinh: true, bk: 1.6pt, + mau: black, day: 1.1pt, + mau-khuat: auto, day-khuat: auto, hien-khuat: true, + to: none, to-mat: none, + diem: (), duong: (), mau-diem: auto, bk-diem: 1.8pt, + them: none, +) = { + let pt = phan-tich-khoi(dinh, mat, cam: cam) + let d2 = pt.d2 + // ---- tô mặt trước, xa vẽ trước (painter's algorithm) + if to != none or to-mat != none { + let thu-tu = range(pt.mat.len()).sorted(key: i => pt.mat.at(i).sau) + for i in thu-tu { + let f = pt.mat.at(i) + if not f.truoc { continue } + let c = if to-mat == none { to } else { + let v = to-mat.at(i, default: auto) + if v == auto { to } else { v } + } + if c != none { + da-giac-pt(f.chi-so.map(k => toa-pt(ctx, d2.at(k))), to: c) + } + } + } + // ---- cạnh khuất (vẽ trước) rồi cạnh liền + for lan in (false, true) { + for c in pt.canh { + if c.hien != lan { continue } + if not c.hien and not hien-khuat { continue } + doan( + ctx, d2.at(c.i), d2.at(c.j), + mau: if c.hien or mau-khuat == auto { mau } else { mau-khuat }, + day: if c.hien or day-khuat == auto { day } else { day-khuat }, + dut: not c.hien, + ) + } + } + // ---- đoạn phụ (đường cao, hình chiếu…): tự liền/đứt theo khối + let p = pt.cam.p + let e = pt.cam.nhin + for m in duong { + let A = m.at(0) + let B = m.at(1) + let tc = if m.len() > 2 { m.at(2) } else { (:) } + let mc = tc.at("mau", default: mau) + let hk = tc.at("hien-khuat", default: hien-khuat) + _doan-tu-dong( + ctx, p, e, pt.mat, A, B, + mau: mc, day: tc.at("day", default: day), + hien-khuat: hk, + mau-khuat: tc.at("mau-khuat", default: mau-khuat), + day-khuat: tc.at("day-khuat", default: day-khuat), + dut: tc.at("dut", default: auto), + ) + // ẩn nét khuất thì bỏ luôn ký hiệu góc vuông / nhãn ở chỗ bị khối che + if "vuong" in tc and (hk or not _bi-khoi-che(B, e, pt.mat)) { + goc-vuong( + ctx, p(B), p(A), p(tc.at("vuong")), + r: tc.at("r", default: 0.3), mau: mc, day: 0.8pt, + ) + } + if tc.at("ten", default: none) != none { + let M = chia-3d(A, B, tc.at("tai", default: 0.5)) + let hg = tc.at("huong", default: auto) + if hk or not _bi-khoi-che(M, e, pt.mat) { + nhan( + ctx, p(M), tc.at("ten"), + huong: if hg == auto { _phap-tuyen(ctx, p(A), p(B)) } else { hg }, + cach: tc.at("cach", default: cach), mau: mc, + ) + } + } + } + // ---- đỉnh + nhãn + // đỉnh kề (chỉ tính theo các cạnh ĐƯỢC VẼ) để đặt nhãn vào khe trống + let ke = (:) + for c in pt.canh { + if not c.hien and not hien-khuat { continue } + ke.insert(str(c.i), ke.at(str(c.i), default: ()) + (c.j,)) + ke.insert(str(c.j), ke.at(str(c.j), default: ()) + (c.i,)) + } + for i in range(dinh.len()) { + // ẩn nét khuất thì ẩn luôn đỉnh chỉ thuộc các mặt sau + if not hien-khuat and not pt.mat.any(f => f.truoc and i in f.chi-so) { continue } + let t = if ten == none { none } else { ten.at(i, default: none) } + let hg = if huong == auto { + _huong-khe(ctx, d2.at(i), ke.at(str(i), default: ()).map(j => d2.at(j))) + } else { huong.at(i, default: "above") } + if hien-dinh { + _ve-diem(ctx, d2.at(i), ten: t, huong: hg, bk: bk, cach: cach, mau: mau, mau-ten: mau-ten) + } else if t != none { + nhan(ctx, d2.at(i), t, huong: hg, cach: cach, mau: mau-ten) + } + } + // ---- điểm phụ: nhãn tự đặt vuông góc với cạnh chứa nó + let C2 = ( + d2.map(q => q.at(0)).sum() / d2.len(), + d2.map(q => q.at(1)).sum() / d2.len(), + ) + for m in diem { + let (P, t, hg, mc) = if _la-diem3(m) { (m, none, auto, auto) } else { + (m.at(0), m.at(1, default: none), m.at(2, default: auto), m.at(3, default: auto)) + } + if not hien-khuat and _bi-khoi-che(P, e, pt.mat) { continue } + let c = _canh-chua(dinh, pt.canh, P) + let h = if hg != auto { hg } else if c != none { + _huong-khe(ctx, p(P), (d2.at(c.i), d2.at(c.j))) + } else { _huong-xa(p(P), C2) } + let m2 = if mc != auto { mc } else if mau-diem == auto { mau } else { mau-diem } + _ve-diem(ctx, p(P), ten: t, huong: h, bk: bk-diem, cach: cach, mau: m2, mau-ten: m2) + } + if them != none { them(ctx, p) } +} + +// Các điểm 3D phụ (từ `diem`/`duong`) — để cửa sổ bao luôn phần chìa ra ngoài. +#let _diem-phu(diem, duong) = { + let ds = () + for m in diem { ds.push(if _la-diem3(m) { m } else { m.at(0) }) } + for m in duong { + ds.push(m.at(0)) + ds.push(m.at(1)) + if m.len() > 2 and "vuong" in m.at(2) { ds.push(m.at(2).at("vuong")) } + } + ds +} + +// Bản TỰ TẠO KHUNG (không nhận ctx ⇒ KHÔNG kê vào ve-voi). +#let da-dien( + dinh: (), mat: (), + cam: auto, w: 7cm, le: 0.75, co-chu: 10pt, + diem: (), duong: (), + ..dt, +) = { + let cm = _cam(cam) + let cs = _cs2((dinh + _diem-phu(diem, duong)).map(cm.p), mg: le) + hinh( + w: w, xmin: cs.xmin, xmax: cs.xmax, ymin: cs.ymin, ymax: cs.ymax, + co-chu: co-chu, + ctx => ve-da-dien( + ctx, dinh: dinh, mat: mat, cam: cm, diem: diem, duong: duong, ..dt, + ), + ) +} + +// ===================================================================== +// KHỐI DỰNG SẴN — TRẢ (dinh:, mat:, ten:), dùng với toán tử `..` +// #da-dien(..khoi-lang-tru-deu(n: 6, R: 1.8, cao: 3.4), to: blue.lighten(88%)) +// ===================================================================== +#let _chu = ("A", "B", "C", "D", "E", "F", "G", "H", "I", "K", "L", "M") +#let _ten-day(n, phay: false) = range(n).map(i => { + let t = _chu.at(calc.rem(i, _chu.len())) + eval(if phay { t + "'" } else { t }, mode: "math") +}) +// Pha mặc định của đa giác đều: đỉnh A rơi ra phía trước-trái. +#let _pha(n) = if n == 4 { 225deg } else { 210deg } +// Mảng đỉnh đa giác đều n cạnh, tâm trên trục Oz, bán kính R, cao z. +#let day-deu(n, R, z: 0, pha: auto) = { + let ph = if pha == auto { _pha(n) } else { pha } + range(n).map(i => { + let a = ph + i * 360deg / n + (R * calc.cos(a), R * calc.sin(a), z) + }) +} + +// Hình chóp: đáy là mảng điểm 3D bất kì (đa giác lồi), đỉnh S. +#let khoi-chop(day, S, ten: auto) = { + let n = day.len() + ( + dinh: day + (S,), + mat: (range(n),) + range(n).map(i => (i, calc.rem(i + 1, n), n)), + ten: if ten == auto { _ten-day(n) + ($S$,) } else { ten }, + ) +} +// Lăng trụ: đáy `day` tịnh tiến theo vectơ v (đáy trên ghi dấu phẩy). +#let khoi-lang-tru(day, v, ten: auto) = { + let n = day.len() + let tren = day.map(P => v3-cong(P, v)) + ( + dinh: day + tren, + mat: (range(n), range(n).map(i => i + n)) + + range(n).map(i => { + let j = calc.rem(i + 1, n) + (i, j, j + n, i + n) + }), + ten: if ten == auto { _ten-day(n) + _ten-day(n, phay: true) } else { ten }, + ) +} +// Chóp cụt: hai đáy CÙNG số đỉnh, tương ứng theo thứ tự. +#let khoi-chop-cut(day, day-tren, ten: auto) = { + let n = day.len() + ( + dinh: day + day-tren, + mat: (range(n), range(n).map(i => i + n)) + + range(n).map(i => { + let j = calc.rem(i + 1, n) + (i, j, j + n, i + n) + }), + ten: if ten == auto { _ten-day(n) + _ten-day(n, phay: true) } else { ten }, + ) +} +// Hình hộp tổng quát: đỉnh A + ba vectơ cạnh u, v, w. +#let khoi-hop(A, u, v, w, ten: auto) = khoi-lang-tru( + (A, v3-cong(A, u), v3-cong(v3-cong(A, u), v), v3-cong(A, v)), + w, + ten: ten, +) + +#let khoi-chop-deu(n: 4, R: 2, cao: 3.6, pha: auto, ten: auto) = khoi-chop( + day-deu(n, R, pha: pha), (0, 0, cao), ten: ten, +) +#let khoi-lang-tru-deu(n: 6, R: 1.8, cao: 3.4, pha: auto, ten: auto) = khoi-lang-tru( + day-deu(n, R, pha: pha), (0, 0, cao), ten: ten, +) +#let khoi-chop-cut-deu(n: 4, R: 2, r: 1.1, cao: 3, pha: auto, ten: auto) = khoi-chop-cut( + day-deu(n, R, pha: pha), day-deu(n, r, z: cao, pha: pha), ten: ten, +) +#let khoi-hop-chu-nhat(dai: 4, rong: 2.6, cao: 3, ten: auto) = khoi-hop( + (0, 0, 0), (dai, 0, 0), (0, rong, 0), (0, 0, cao), ten: ten, +) +#let khoi-lap-phuong(a: 3, ten: auto) = khoi-hop-chu-nhat( + dai: a, rong: a, cao: a, ten: ten, +) +// Tứ diện đều cạnh a: đáy đều bán kính a/√3, chiều cao a·√(2/3). +#let khoi-tu-dien-deu(a: 3, ten: auto) = khoi-chop( + day-deu(3, a / calc.sqrt(3)), + (0, 0, a * calc.sqrt(2 / 3)), + ten: if ten == auto { ($A$, $B$, $C$, $D$) } else { ten }, +) +// Bát diện đều cạnh a: 6 đỉnh (±s, 0, 0), (0, ±s, 0), (0, 0, ±s), s = a/√2. +#let khoi-bat-dien-deu(a: 3, ten: auto) = { + let s = a / calc.sqrt(2) + ( + dinh: ((s, 0, 0), (0, s, 0), (-s, 0, 0), (0, -s, 0), (0, 0, s), (0, 0, -s)), + mat: ( + (0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4), + (0, 1, 5), (1, 2, 5), (2, 3, 5), (3, 0, 5), + ), + ten: if ten == auto { ($A$, $B$, $C$, $D$, $S$, $S'$) } else { ten }, + ) +} + +// ===================================================================== +// MẶT PHẲNG +// mp = (n: pháp tuyến ĐƠN VỊ, d: hằng số) ⇔ n · P = d +// ===================================================================== +#let mp-qua-phap(P, n) = { + let u = v3-chuan(n) + (n: u, d: v3-vo-huong(u, P)) +} +#let mp-qua-3-diem(A, B, C) = mp-qua-phap(A, v3-co-huong(v3-tru(B, A), v3-tru(C, A))) +// Mặt phẳng cắt ba trục tại (a,0,0), (0,b,0), (0,0,c): x/a + y/b + z/c = 1. +#let mp-cat-truc(a, b, c) = mp-qua-3-diem((a, 0, 0), (0, b, 0), (0, 0, c)) +// Mặt phẳng song song với mp, đi qua P. +#let mp-song-song(mp, P) = (n: mp.n, d: v3-vo-huong(mp.n, P)) + +// Điểm 2D có nằm trong đa giác LỒI (mảng điểm 2D) hay không. +#let _trong-dg2(q, dg) = { + let duong = false + let am = false + let n = dg.len() + for i in range(n) { + let A = dg.at(i) + let B = dg.at(calc.rem(i + 1, n)) + let c = ( + (B.at(0) - A.at(0)) * (q.at(1) - A.at(1)) + - (B.at(1) - A.at(1)) * (q.at(0) - A.at(0)) + ) + if c > 1e-9 { duong = true } + if c < -1e-9 { am = true } + } + not (duong and am) +} + +// Vẽ ĐỨT phần đoạn AB (3D) bị đa giác `dg3` che: nằm SAU mặt phẳng chứa +// đa giác VÀ hình chiếu rơi vào trong đa giác. Quét mẫu nên đúng với mọi +// cấu hình (đoạn cắt ngang mặt phẳng, ló ra hai đầu…). +#let _dut-bi-che(ctx, p, nhin, dg3, A, B, n: 240, mau: black, day: 0.9pt) = { + let dg2 = dg3.map(p) + let nm = phap-da-giac(dg3) + let d0 = v3-vo-huong(nm, dg3.at(0)) + // người nhìn ở phía f > 0 nếu nm cùng hướng nhìn; điểm SAU mặt phẳng: f·phia < 0 + let phia = if v3-vo-huong(nm, nhin) >= 0 { 1 } else { -1 } + let dai = () + let dau = none + for s in range(n + 1) { + let t = s / n + let P = chia-3d(A, B, t) + let f = (v3-vo-huong(nm, P) - d0) * phia + let che = f < -1e-9 and _trong-dg2(p(P), dg2) + if che and dau == none { dau = t } + if not che and dau != none { + dai.push((dau, t)) + dau = none + } + } + if dau != none { dai.push((dau, 1)) } + for (u, v) in dai { + doan(ctx, p(chia-3d(A, B, u)), p(chia-3d(A, B, v)), mau: mau, day: day, dut: true) + } +} + +// --------------------------------------------------------------------- +// Vẽ MỘT MẶT PHẲNG (đa giác phẳng 3D) — lõi chung, nhận ctx. +// p hàm chiếu (x, y, z) -> 2D (vd t3 của #oxyz) +// dinh3 mảng đỉnh 3D của đa giác biểu diễn mặt phẳng +// che mảng đoạn 3D ((A, B), ...) sẽ được vẽ ĐỨT ở phần bị mặt phẳng che +// --------------------------------------------------------------------- +#let mat-phang( + ctx, p, dinh3, + to: auto, mau: blue.darken(15%), day: 1pt, dut: false, + ten: none, ten-tai: 0, huong: "above-left", cach: 5pt, + che: (), mau-che: black, day-che: 0.9pt, + nhin: auto, + them: none, +) = { + let e = if nhin == auto { _nhin(p) } else { nhin } + let d2 = dinh3.map(p) + let mau-to = if to == auto { rgb(80, 140, 220, 46) } else { to } + if mau-to != none { da-giac-pt(d2.map(q => toa-pt(ctx, q)), to: mau-to) } + let n = d2.len() + for i in range(n) { + doan(ctx, d2.at(i), d2.at(calc.rem(i + 1, n)), mau: mau, day: day, dut: dut) + } + for (A, B) in che { + _dut-bi-che(ctx, p, e, dinh3, A, B, mau: mau-che, day: day-che) + } + if ten != none { + nhan(ctx, d2.at(calc.rem(ten-tai, n)), ten, huong: huong, cach: cach, mau: mau) + } + if them != none { them(ctx, p) } +} + +// --------------------------------------------------------------------- +// MẶT PHẲNG CẮT BA TRỤC trong hệ #oxyz — tam giác A(a,0,0) B(0,b,0) C(0,0,c), +// tự vẽ ĐỨT phần ba trục toạ độ nằm SAU mặt phẳng (bị mặt phẳng che). +// #oxyz(x: 5, y: 6, z: 5, them: (ctx, t3) => { +// mat-phang-oxyz(ctx, t3, 4, 5, 3, ten-dinh: true, ten: $(P)$) +// }) +// `truc: auto` tự lấy phạm vi (0 -> mốc cắt trục); truyền tuple 3 khoảng để +// đổi, vd truc: ((-1, 5), (-1, 6), (-1, 5)); truc: none = không xử lí nét đứt. +// --------------------------------------------------------------------- +#let mat-phang-oxyz( + ctx, t3, a, b, c, + to: auto, mau: blue.darken(15%), day: 1pt, dut: false, + ten: none, ten-tai: 0, huong: "above-left", cach: 5pt, + ten-dinh: false, ten-abc: ($A$, $B$, $C$), hien-dinh: true, bk: 1.8pt, + truc: auto, mau-che: black, day-che: 0.9pt, + them: none, +) = { + let A = (a, 0, 0) + let B = (0, b, 0) + let C = (0, 0, c) + let ds-truc = if truc == none { () } else if truc == auto { + (((0, 0, 0), A), ((0, 0, 0), B), ((0, 0, 0), C)) + } else { + ( + ((truc.at(0).at(0), 0, 0), (truc.at(0).at(1), 0, 0)), + ((0, truc.at(1).at(0), 0), (0, truc.at(1).at(1), 0)), + ((0, 0, truc.at(2).at(0)), (0, 0, truc.at(2).at(1))), + ) + } + mat-phang( + ctx, t3, (A, B, C), + to: to, mau: mau, day: day, dut: dut, + ten: ten, ten-tai: ten-tai, huong: huong, cach: cach, + che: ds-truc, mau-che: mau-che, day-che: day-che, + ) + if ten-dinh or hien-dinh { + let bo = ((A, ten-abc.at(0), "below-left"), (B, ten-abc.at(1), "below-right"), (C, ten-abc.at(2), "above-right")) + for (P, t, h) in bo { + diem( + ctx, t3(P), + ten: if ten-dinh { t } else { none }, + huong: h, bk: if hien-dinh { bk } else { 0pt }, cach: 5pt, mau: mau, + ) + } + } + if them != none { them(ctx, t3) } +} + +// --------------------------------------------------------------------- +// MẶT PHẲNG "LƠ LỬNG" dạng hình bình hành: tâm `tam`, hai nửa-vectơ u, v +// (4 đỉnh = tam ± u ± v). Ba trục toạ độ đi sau nó cũng tự vẽ đứt. +// mat-phang-bh(ctx, t3, (2, 2, 2), (2.4, 0, 0), (0, 2.4, 0), ten: $(alpha)$) +// --------------------------------------------------------------------- +#let mat-phang-bh( + ctx, t3, tam, u, v, + to: auto, mau: blue.darken(15%), day: 1pt, dut: false, + ten: none, ten-tai: 1, huong: "above-right", cach: 5pt, + truc: none, mau-che: black, day-che: 0.9pt, + them: none, +) = { + let dinh3 = ( + v3-tru(v3-tru(tam, u), v), + v3-tru(v3-cong(tam, u), v), + v3-cong(v3-cong(tam, u), v), + v3-cong(v3-tru(tam, u), v), + ) + let ds-truc = if truc == none or truc == auto { () } else { + ( + ((truc.at(0).at(0), 0, 0), (truc.at(0).at(1), 0, 0)), + ((0, truc.at(1).at(0), 0), (0, truc.at(1).at(1), 0)), + ((0, 0, truc.at(2).at(0)), (0, 0, truc.at(2).at(1))), + ) + } + mat-phang( + ctx, t3, dinh3, + to: to, mau: mau, day: day, dut: dut, + ten: ten, ten-tai: ten-tai, huong: huong, cach: cach, + che: ds-truc, mau-che: mau-che, day-che: day-che, + them: them, + ) +} + +// ===================================================================== +// THIẾT DIỆN — cắt khối LỒI bởi một mặt phẳng +// thiet-dien TRẢ VỀ mảng điểm 3D đã SẮP THEO VÒNG (rỗng nếu không cắt). +// Cách làm: lấy mọi giao điểm của mặt phẳng với các CẠNH của khối (kể cả +// đỉnh nằm đúng trên mặt phẳng), bỏ điểm trùng, rồi sắp theo GÓC quanh tâm +// trong hệ hai vectơ của mặt phẳng — thiết diện của khối lồi là đa giác lồi +// nên phép sắp theo góc cho đúng thứ tự vòng. +// ===================================================================== +#let thiet-dien(dinh, mat, mp, eps: 1e-7) = { + let f = P => v3-vo-huong(mp.n, P) - mp.d + let bang = (:) + for m in mat { + for (i, j) in _canh-vong(m) { + bang.insert(_khoa(i, j), (calc.min(i, j), calc.max(i, j))) + } + } + let ds = () + for (i, j) in bang.values() { + let A = dinh.at(i) + let B = dinh.at(j) + let fa = f(A) + let fb = f(B) + if calc.abs(fa) < eps { ds.push(A) } + if calc.abs(fb) < eps { ds.push(B) } + if calc.abs(fa) >= eps and calc.abs(fb) >= eps and fa * fb < 0 { + ds.push(chia-3d(A, B, fa / (fa - fb))) + } + } + let kq = () + for P in ds { + if not kq.any(Q => v3-dai(v3-tru(P, Q)) < 1e-6) { kq.push(P) } + } + if kq.len() < 3 { return () } + let T = tam-3d(kq) + let u = v3-chuan(v3-tru(kq.at(0), T)) + let v = v3-co-huong(mp.n, u) + kq.sorted(key: P => { + let w = v3-tru(P, T) + calc.atan2(v3-vo-huong(w, u), v3-vo-huong(w, v)).deg() + }) +} + +// Vẽ thiết diện TRÊN một khối: tô miền + viền LIỀN ở phần nằm trên mặt +// THẤY, ĐỨT ở phần nằm trên mặt KHUẤT (nhận ctx ⇒ kê vào ve-voi). +// ve-thiet-dien(ctx, dinh: d, mat: m, mp: mp-qua-3-diem(P, Q, R)) +#let ve-thiet-dien( + ctx, + dinh: (), mat: (), mp: none, + cam: auto, + to: auto, mau: red.darken(10%), day: 1.1pt, + hien-khuat: true, mau-khuat: auto, day-khuat: auto, + ten: none, huong: auto, cach: 6pt, hien-dinh: true, bk: 1.8pt, mau-ten: auto, + them: none, +) = { + let pt = phan-tich-khoi(dinh, mat, cam: cam) + let p = pt.cam.p + let dd = thiet-dien(dinh, mat, mp) + if dd.len() < 3 { return } + let d2 = dd.map(p) + let mau-to = if to == auto { rgb(220, 70, 70, 40) } else { to } + if mau-to != none { da-giac-pt(d2.map(q => toa-pt(ctx, q)), to: mau-to) } + let n = dd.len() + for i in range(n) { + let j = calc.rem(i + 1, n) + let M = trung-diem-3d(dd.at(i), dd.at(j)) + // cạnh thiết diện nằm trên mặt nào? thấy nếu có một mặt CHỨA nó là mặt trước + let hien = pt.mat.any(f => ( + calc.abs(v3-vo-huong(f.phap, M) - v3-vo-huong(f.phap, f.tam)) < 1e-5 and f.truoc + )) + if not hien and not hien-khuat { continue } + doan( + ctx, d2.at(i), d2.at(j), + mau: if hien or mau-khuat == auto { mau } else { mau-khuat }, + day: if hien or day-khuat == auto { day } else { day-khuat }, + dut: not hien, + ) + } + for i in range(n) { + let t = if ten == none { none } else { ten.at(i, default: none) } + let hg = if huong == auto { + _huong-khe(ctx, d2.at(i), (d2.at(calc.rem(i + n - 1, n)), d2.at(calc.rem(i + 1, n)))) + } else { huong.at(i, default: "above") } + if hien-dinh { + diem(ctx, d2.at(i), ten: t, huong: hg, bk: bk, cach: cach, mau: mau, mau-ten: if mau-ten == auto { mau } else { mau-ten }) + } else if t != none { + nhan(ctx, d2.at(i), t, huong: hg, cach: cach, mau: if mau-ten == auto { mau } else { mau-ten }) + } + } + if them != none { them(ctx, p) } +} + +// Khối + thiết diện trong MỘT lệnh, tự tạo khung (không nhận ctx). +#let da-dien-thiet-dien( + dinh: (), mat: (), mp: none, + cam: auto, w: 7cm, le: 0.75, co-chu: 10pt, + ten: none, huong: auto, + mau: black, day: 1.1pt, to: none, to-mat: none, + hien-khuat: true, hien-dinh: true, mau-ten: black, + diem: (), duong: (), mau-diem: auto, bk-diem: 1.8pt, + td: (:), // tuỳ chọn riêng cho thiết diện (mau, to, ten, …) + them: none, +) = { + let cm = _cam(cam) + let cs = _cs2((dinh + _diem-phu(diem, duong)).map(cm.p), mg: le) + hinh( + w: w, xmin: cs.xmin, xmax: cs.xmax, ymin: cs.ymin, ymax: cs.ymax, + co-chu: co-chu, + ctx => { + ve-da-dien( + ctx, dinh: dinh, mat: mat, cam: cm, + ten: ten, huong: huong, mau: mau, day: day, to: to, to-mat: to-mat, + hien-khuat: hien-khuat, hien-dinh: hien-dinh, mau-ten: mau-ten, + diem: diem, duong: duong, mau-diem: mau-diem, bk-diem: bk-diem, + ) + if mp != none { + ve-thiet-dien(ctx, dinh: dinh, mat: mat, mp: mp, cam: cm, ..td) + } + if them != none { them(ctx, cm.p) } + }, + ) +} diff --git a/packages/preview/conic-toan/0.3.4/lib/do-thi.typ b/packages/preview/conic-toan/0.3.4/lib/do-thi.typ new file mode 100644 index 0000000000..9171225f75 --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/do-thi.typ @@ -0,0 +1,1632 @@ +// ===================================================================== +// do-thi.typ — ĐỒ THỊ HÀM SỐ THPT +// Hệ trục, lưới, vạch chia, đường gióng + các đồ thị dựng sẵn: +// bậc nhất, bậc hai, bậc ba, trùng phương, phân thức b1/b1, hữu tỉ b2/b1, +// mũ, lôgarit, sin/cos/tan, căn thức. +// +// CỬA SỔ THÔNG MINH: khung nhìn tự tính từ các điểm then chốt +// (cực trị, điểm uốn, tâm đối xứng, tiệm cận, giao trục) và luôn chứa gốc O. +// +// THAM SỐ NHÃN CHUNG cho các đồ thị dựng sẵn: +// cuc-tri : auto = gióng cực trị vào 2 trục + nhãn tự động +// none = không gióng +// (x: "below", y: "left") = đổi hướng nhãn (mọi điểm) +// ((x:.., y:..), (x:.., y:..), ...) = hướng riêng từng điểm +// (thứ tự x tăng dần) +// giao-ox : none | auto | "below" | ("below", "above", ...) — nhãn giao Ox +// giao-oy : none | auto | "left" — nhãn giao Oy +// Hướng nhãn (tiếng Anh): above, below, left, right, +// above-left, above-right, below-left, below-right. +// Tiệm cận (nếu có) LUÔN được vẽ kèm nhãn trên trục. +// goc-ten : vị trí nhãn tên hàm — "above-right" (mặc định) | "above-left" +// | "below-left" | "below-right" | (x, y) toạ độ tuỳ ý +// dan-x, dan-y : co/giãn cửa sổ quanh tâm theo từng trục — +// > 1 phóng to (đơn vị lớn hơn), < 1 thu nhỏ (thấy vùng rộng hơn) +// Mỗi đồ thị dựng sẵn có `them: ctx => ...` để vẽ chồng nội dung tuỳ ý. +// ===================================================================== +#import "ve.typ": * + +// Định dạng số gọn: 2 -> "2", 2.50 -> "2.5" +#let so-dep(v) = { + let r = calc.round(v, digits: 2) + if calc.fract(r) == 0 { str(int(r)) } else { str(r) } +} + +// Nhãn toán "đẹp": nhận dạng số nguyên, phân số p/q (q ≤ 20), +// căn thức k√n (n ≤ 50), dạng a ± k√n (ví dụ 1 + √2). +// Không khớp thì trả về số thập phân. +#let so-toan(v) = { + let eps = 0.0001 + if calc.abs(v - calc.round(v)) < eps { return $#int(calc.round(v))$ } + for q in range(2, 21) { + let p = calc.round(v * q) + if calc.abs(v - p / q) < eps and calc.gcd(int(calc.abs(p)), q) == 1 { + let p = int(p) + return if p < 0 { $- #(-p) / #q$ } else { $#p / #q$ } + } + } + // phân số mẫu LỚN (vd 283/27 — giá trị cực trị bậc ba, mẫu 27a²): + // khớp CHẶT (1e-9) nên không "nhận nhầm" số vô tỉ thành phân số. + for q in range(21, 401) { + let p = calc.round(v * q) + if calc.abs(v - p / q) < 1e-9 and calc.gcd(int(calc.abs(p)), q) == 1 { + let p = int(p) + return if p < 0 { $- #(-p) / #q$ } else { $#p / #q$ } + } + } + for n in range(2, 51) { + let r = calc.sqrt(n) + for k in range(1, 6) { + if calc.abs(calc.abs(v) - k * r) < eps { + let ct = if k == 1 { $sqrt(#[#n])$ } else { $#k sqrt(#[#n])$ } + return if v < 0 { $-#ct$ } else { ct } + } + } + } + for n in range(2, 31) { + let cn = calc.sqrt(n) + if calc.abs(cn - calc.round(cn)) < eps { continue } + for k in range(1, 4) { + let ct = if k == 1 { $sqrt(#[#n])$ } else { $#k sqrt(#[#n])$ } + let du = v - k * cn // v = a + k√n ? + if calc.abs(du - calc.round(du)) < eps and calc.abs(du) > eps and calc.abs(calc.round(du)) <= 12 { + return $#int(calc.round(du)) + #ct$ + } + let du = v + k * cn // v = a - k√n ? + if calc.abs(du - calc.round(du)) < eps and calc.abs(du) > eps and calc.abs(calc.round(du)) <= 12 { + return $#int(calc.round(du)) - #ct$ + } + } + } + // dạng (m ± √n)/q — nghiệm phương trình bậc hai (vd (1+√7)/3, √3/2) + for q in range(2, 13) { + let t = v * q + let m0 = int(calc.round(t)) + for dm in range(-16, 17) { + let m = m0 + dm + let s = t - m // s = ±√n + let n = calc.round(s * s) + if n >= 2 and n <= 400 and calc.abs(s * s - n) < 0.00001 { + let cn = calc.sqrt(n) + if calc.abs(cn - calc.round(cn)) < eps { continue } // n chính phương -> hữu tỉ, đã xét + // rút thừa số chính phương: √96 -> 4√6 + let n = int(n) + let k = 1 + let j = 2 + while j * j <= n { + if calc.rem(n, j * j) == 0 { k = j } + j = j + 1 + } + let n = int(n / (k * k)) + let ct = if k == 1 { $sqrt(#[#n])$ } else { $#k sqrt(#[#n])$ } + return if m == 0 { + if s > 0 { $#ct / #q$ } else { $- #ct / #q$ } + } else if s > 0 { $(#m + #ct) / #q$ } else { $(#m - #ct) / #q$ } + } + } + } + $#so-dep(v)$ +} + +// Căn thức CHÍNH XÁC: hiển thị (P + K·√n)/Q từ các thành phần đã biết +// (P, K, Q gần-nguyên; n hữu tỉ ≥ 0). Tự chuẩn hoá n hữu tỉ (√(u/den) = +// √(u·den)/den), rút thừa số chính phương, rút gọn gcd(P, K, Q). +// Không "đẹp" được thì rơi về so-toan(giá trị số) — không tệ hơn trước. +#let so-can-thuc(P, K, n, Q) = { + let eps = 0.000001 + let gan-int(x) = calc.abs(x - calc.round(x)) < eps * calc.max(1, calc.abs(x)) + if Q == 0 or n < -eps { return $#so-dep(0)$ } // phòng hờ (không xảy ra) + let gia = (P + K * calc.sqrt(calc.max(n, 0))) / Q + // chuẩn hoá n về nguyên: (P + K√(u/den))/Q = (P·den + K√(u·den))/(Q·den) + let cap = if gan-int(n) { (calc.round(n), P, Q) } else { + let ket = none + for den in range(2, 21) { + if gan-int(n * den) { + ket = (calc.round(n * den) * den, P * den, Q * den) + break + } + } + ket + } + if cap == none { return so-toan(gia) } + let (n, P, Q) = cap + if not (gan-int(P) and gan-int(K) and gan-int(Q)) { return so-toan(gia) } + let P = int(calc.round(P)) + let K = int(calc.round(K)) + let Q = int(calc.round(Q)) + let n = int(calc.round(n)) + if Q < 0 { P = -P; K = -K; Q = -Q } + // rút thừa số chính phương: √96 -> 4√6 + if K != 0 and n > 1 { + let j = 2 + let h = 1 + while j * j <= n { + if calc.rem(n, j * j) == 0 { h = j } + j = j + 1 + } + K = K * h + n = int(n / (h * h)) + } + if n == 1 { P = P + K; K = 0 } + if K == 0 or n == 0 { + // hữu tỉ P/Q chính xác + let g = calc.max(calc.gcd(calc.abs(P), Q), 1) + let (P, Q) = (int(P / g), int(Q / g)) + return if Q == 1 { $#P$ } else if P < 0 { $- #(-P) / #Q$ } else { $#P / #Q$ } + } + let g = calc.max(calc.gcd(calc.gcd(calc.abs(P), calc.abs(K)), Q), 1) + let (P, K, Q) = (int(P / g), int(K / g), int(Q / g)) + let ct = if calc.abs(K) == 1 { $sqrt(#[#n])$ } else { $#calc.abs(K) sqrt(#[#n])$ } + if Q == 1 { + if P == 0 { if K > 0 { ct } else { $-#ct$ } } + else if K > 0 { $#P + #ct$ } else { $#P - #ct$ } + } else { + if P == 0 { if K > 0 { $#ct / #Q$ } else { $- #ct / #Q$ } } + else if K > 0 { $(#P + #ct) / #Q$ } else { $(#P - #ct) / #Q$ } + } +} + +// ---- Cực trị CHÍNH XÁC theo hệ số (dùng chung cho bang.typ + khao-sat.typ) ---- +// Bậc ba y = ax³+bx²+cx+d, Δ' = b²−3ac > 0: trả (x1:, y1:, x2:, y2:) với +// x1 < x2. x = (−b ∓ σ√Δ')/(3a), σ = sign(a); +// y = (P0 ± 2σΔ'√Δ')/(27a²), P0 = 2b³ − 9abc + 27a²d (chia đa thức f cho f'). +#let cuc-tri-bac-ba(a, b, c, d) = { + let delta = b * b - 3 * a * c + let sg = if a > 0 { 1 } else { -1 } + let P0 = 2 * b * b * b - 9 * a * b * c + 27 * a * a * d + ( + x1: so-can-thuc(-b, -sg, delta, 3 * a), + y1: so-can-thuc(P0, 2 * sg * delta, delta, 27 * a * a), + x2: so-can-thuc(-b, sg, delta, 3 * a), + y2: so-can-thuc(P0, -2 * sg * delta, delta, 27 * a * a), + ) +} +// Hữu tỉ y = (ax²+bx+c)/(dx+e) có 2 cực trị: E = ae² − bde + cd² (E/a > 0), +// x = (−e ∓ σ√(E/a))/d, σ = sign(d); tại cực trị y = (2ax+b)/d +// ⇒ y = (bd − 2ae ∓ 2aσ√(E/a))/d², x1 < x2. +#let cuc-tri-huu-ti(a, b, c, d, e) = { + let E = a * e * e - b * d * e + c * d * d + let sg = if d > 0 { 1 } else { -1 } + ( + x1: so-can-thuc(-e, -sg, E / a, d), + y1: so-can-thuc(b * d - 2 * a * e, -2 * a * sg, E / a, d * d), + x2: so-can-thuc(-e, sg, E / a, d), + y2: so-can-thuc(b * d - 2 * a * e, 2 * a * sg, E / a, d * d), + ) +} + +// Nội dung "y = px + q" rút gọn (nhãn tiệm cận xiên). +#let _pt-bac-nhat(p, q) = { + let hs = if calc.abs(p - 1) < 0.001 { $x$ } else if calc.abs(p + 1) < 0.001 { $-x$ } else { $#so-toan(p) x$ } + if calc.abs(q) < 0.001 { $y = #hs$ } else if q > 0 { $y = #hs + #so-toan(q)$ } else { $y = #hs - #so-toan(-q)$ } +} + +// ---------- Đạo hàm số & tiếp tuyến ---------- +// Đạo hàm của f tại x bằng sai phân trung tâm (đủ chính xác để vẽ hình). +// let k = dao-ham(f, 1) // hệ số góc tiếp tuyến tại x = 1 +#let dao-ham(f, x, h: auto) = { + let b = if h == auto { 0.0001 * calc.max(1, calc.abs(x)) } else { h } + (f(x + b) - f(x - b)) / (2 * b) +} + +// TIẾP TUYẾN của đồ thị y = f(x) tại điểm có hoành độ x0 (hoặc DÃY hoành độ). +// Hệ số góc lấy bằng đạo hàm số nên dùng được cho MỌI hàm f viết bằng closure. +// tiep-tuyen(f, 1) // tiếp tuyến tại x = 1 +// tiep-tuyen(f, (-1, 2), mau: green) // nhiều tiếp điểm +// tiep-tuyen(f, 1, ten: auto) // tự ghi y = kx + m +// tiep-tuyen(f, 1, ten: $Delta$, ten-diem: $M$) // đặt tên tuỳ ý +// Tham số: +// dai : auto = kéo hết bề ngang khung (tự cắt gọn theo cửa sổ); +// số = nửa độ dài đoạn tiếp tuyến theo trục Ox quanh tiếp điểm +// cham : chấm tiếp điểm · ten-diem/huong-diem: nhãn tiếp điểm +// ten : nhãn đường tiếp tuyến (auto = phương trình y = kx + m rút gọn) +// tai/huong-ten/cach/ten-quay: vị trí & kiểu nhãn (như `ten:` của `doan`) +// giong : true = kẻ đường gióng đứt từ tiếp điểm về 2 trục +#let tiep-tuyen( + ctx, f, x0, + mau: red, day: 1.2pt, dut: false, dai: auto, + cham: true, mau-cham: auto, ten-diem: none, huong-diem: "above-left", + ten: none, tai: 0.9, huong-ten: auto, cach: 6pt, ten-quay: false, + giong: false, h: auto, +) = { + for x in (if type(x0) == array { x0 } else { (x0,) }) { + let y = f(x) + let k = dao-ham(f, x, h: h) + let tt = u => y + k * (u - x) + // hai đầu đoạn tiếp tuyến: cắt gọn trong cửa sổ hoặc theo `dai` + let (X1, X2) = if dai != auto { (x - dai, x + dai) } else if calc.abs(k) > 0.0001 { + let ca = ((ctx.ymin - y) / k + x, (ctx.ymax - y) / k + x).sorted() + (calc.max(ctx.xmin, ca.at(0)), calc.min(ctx.xmax, ca.at(1))) + } else { (ctx.xmin, ctx.xmax) } + if X2 - X1 > 0.0001 { + doan( + ctx, (X1, tt(X1)), (X2, tt(X2)), + mau: mau, day: day, dut: dut, + ten: if ten == auto { _pt-bac-nhat(k, y - k * x) } else { ten }, + tai: tai, huong: huong-ten, cach: cach, ten-quay: ten-quay, mau-ten: mau, + ) + } + if giong { + doan(ctx, (x, y), (x, 0), mau: gray.darken(20%), day: 0.8pt, dut: true) + doan(ctx, (x, y), (0, y), mau: gray.darken(20%), day: 0.8pt, dut: true) + } + if cham { + diem(ctx, (x, y), bk: 2.2pt, mau: if mau-cham == auto { mau } else { mau-cham }, + ten: ten-diem, huong: huong-diem) + } + } +} + +// ---------- Hệ trục Oxy ---------- +#let truc(ctx, ten-x: $x$, ten-y: $y$, ten-goc: $O$, mau: black, day: 0.9pt) = { + mui-ten(ctx, (ctx.xmin, 0), (ctx.xmax, 0), mau: mau, day: day) + mui-ten(ctx, (0, ctx.ymin), (0, ctx.ymax), mau: mau, day: day) + nhan(ctx, (ctx.xmax, 0), ten-x, huong: "below", cach: 5pt) + nhan(ctx, (0, ctx.ymax), ten-y, huong: "left", cach: 5pt) + if ten-goc != none { nhan(ctx, (0, 0), ten-goc, huong: "below-left", cach: 4pt) } +} + +// Lưới ô vuông mờ. +#let luoi(ctx, buoc: 1, mau: luma(87%), day: 0.5pt) = { + let x = calc.ceil(ctx.xmin / buoc) * buoc + while x <= ctx.xmax + 0.0001 { + doan(ctx, (x, ctx.ymin), (x, ctx.ymax), mau: mau, day: day) + x = x + buoc + } + let y = calc.ceil(ctx.ymin / buoc) * buoc + while y <= ctx.ymax + 0.0001 { + doan(ctx, (ctx.xmin, y), (ctx.xmax, y), mau: mau, day: day) + y = y + buoc + } +} + +// Vạch chia + số trên hai trục (bỏ qua 0). +#let vach-chia(ctx, buoc-x: 1, buoc-y: 1, so: true, mau: black) = { + let t = 3pt / ctx.sy // nửa chiều dài vạch, đổi sang đơn vị toạ độ + let x = calc.ceil((ctx.xmin + 0.3) / buoc-x) * buoc-x + while x <= ctx.xmax - 0.3 { + if calc.abs(x) > 0.0001 { + doan(ctx, (x, -t), (x, t), mau: mau, day: 0.8pt) + if so { nhan(ctx, (x, 0), $#so-dep(x)$, huong: "below", cach: 4pt) } + } + x = x + buoc-x + } + let s = 3pt / ctx.sx + let y = calc.ceil((ctx.ymin + 0.3) / buoc-y) * buoc-y + while y <= ctx.ymax - 0.3 { + if calc.abs(y) > 0.0001 { + doan(ctx, (-s, y), (s, y), mau: mau, day: 0.8pt) + if so { nhan(ctx, (0, y), $#so-dep(y)$, huong: "left", cach: 4pt) } + } + y = y + buoc-y + } +} + +// Hệ trục TRỌN GÓI: lưới + trục + vạch chia + số — một lệnh thay ba. +// #hinh(xmin: -4, xmax: 4, ymin: -3, ymax: 3, ctx => { he-truc(ctx) ... }) +// luoi-o: lưới ô vuông mờ · vach: vạch chia đơn vị · so: ghi số trên vạch +#let he-truc( + ctx, + ten-x: $x$, ten-y: $y$, ten-goc: $O$, + luoi-o: true, buoc-luoi: auto, // auto = theo buoc-x + vach: true, so: true, buoc-x: 1, buoc-y: 1, + mau: black, day: 0.9pt, mau-luoi: luma(87%), +) = { + if luoi-o { + luoi(ctx, buoc: if buoc-luoi == auto { buoc-x } else { buoc-luoi }, mau: mau-luoi) + } + truc(ctx, ten-x: ten-x, ten-y: ten-y, ten-goc: ten-goc, mau: mau, day: day) + if vach { vach-chia(ctx, buoc-x: buoc-x, buoc-y: buoc-y, so: so) } +} + +// Đường gióng từ điểm P về 2 trục (nét đứt) + nhãn toạ độ trên trục. +// ten-x / ten-y: auto = tự ghi số; none = không ghi; hoặc nội dung tuỳ ý. +#let giong( + ctx, P, + ten-x: auto, ten-y: auto, + huong-x: "below", huong-y: "left", + mau: gray.darken(20%), mau-diem: red, diem-to: true, +) = { + // điểm nằm ngay trên một trục thì bỏ đường gióng (tránh đè lên trục) + if calc.abs(P.at(0)) > 0.0001 and calc.abs(P.at(1)) > 0.0001 { + doan(ctx, P, (P.at(0), 0), mau: mau, day: 0.8pt, dut: true) + doan(ctx, P, (0, P.at(1)), mau: mau, day: 0.8pt, dut: true) + } + if diem-to { diem(ctx, P, bk: 1.9pt, mau: mau-diem) } + if ten-x != none and calc.abs(P.at(0)) > 0.0001 { + let tx = if ten-x == auto { so-toan(P.at(0)) } else { ten-x } + nhan(ctx, (P.at(0), 0), tx, huong: huong-x, cach: 4pt) + } + if ten-y != none and calc.abs(P.at(1)) > 0.0001 { + let ty = if ten-y == auto { so-toan(P.at(1)) } else { ten-y } + nhan(ctx, (0, P.at(1)), ty, huong: huong-y, cach: 4pt) + } +} + +// ---------- Bộ trợ giúp cửa sổ & nhãn (dùng nội bộ) ---------- +// Cửa sổ thông minh: bao các "điểm chốt" + gốc O, thêm lề tương xứng, +// bảo đảm kích thước tối thiểu. +#let _cua-so(diem-chot, le: 0.16, rong-min: 4.2, cao-min: 3.4, goc: true) = { + let pts = diem-chot + if goc { pts = pts + ((0, 0),) } + let xs = pts.map(p => p.at(0)) + let ys = pts.map(p => p.at(1)) + let (x1, x2) = (calc.min(..xs), calc.max(..xs)) + let (y1, y2) = (calc.min(..ys), calc.max(..ys)) + let px = calc.max((x2 - x1) * le, 0.65) + let py = calc.max((y2 - y1) * le, 0.6) + let (xa, xb, ya, yb) = (x1 - px, x2 + px, y1 - py, y2 + py) + if xb - xa < rong-min { + let t = (rong-min - (xb - xa)) / 2 + xa = xa - t + xb = xb + t + } + if yb - ya < cao-min { + let t = (cao-min - (yb - ya)) / 2 + ya = ya - t + yb = yb + t + } + (xmin: xa, xmax: xb, ymin: ya, ymax: yb) +} + +// Nghiệm của f trên [a, b] bằng chia đôi (chỉ bắt nghiệm đổi dấu). +#let _nghiem(f, a, b, n: 160) = { + let kq = () + let m = lay-mau(f, a, b, n: n) + for i in range(n) { + let (x1, y1) = m.at(i) + let (x2, y2) = m.at(i + 1) + if y1 * y2 <= 0 and (y1 != 0 or y2 != 0) { + let (lo, hi) = (x1, x2) + for _ in range(36) { + let g = (lo + hi) / 2 + if f(lo) * f(g) <= 0 { hi = g } else { lo = g } + } + let x = (lo + hi) / 2 + if kq.len() == 0 or x - kq.last() > (b - a) / n * 1.5 { kq.push(x) } + } + } + kq +} + +// Lấy hướng thứ i từ tuỳ chọn none|auto|chuỗi|tuple chuỗi. +#let _huong-i(tuy-chon, i, mac-dinh) = { + if type(tuy-chon) == str { tuy-chon } else if type(tuy-chon) == array and i < tuy-chon.len() { tuy-chon.at(i) } else { mac-dinh } +} + +// Gióng điểm cực trị thứ i theo tuỳ chọn cuc-tri (xem đầu file). +// ten-x/ten-y: nhãn CHÍNH XÁC (căn thức/phân số) tính sẵn từ hệ số — dùng khi +// người dùng không tự đặt tên; auto = lấy số từ toạ độ (so-toan). +#let _giong-ct(ctx, P, tuy-chon, i, ten-x: auto, ten-y: auto) = { + if tuy-chon == none or tuy-chon == false { return } + let o = if type(tuy-chon) == array { + if i < tuy-chon.len() { tuy-chon.at(i) } else { (:) } + } else if type(tuy-chon) == dictionary { tuy-chon } else { (:) } + giong( + ctx, P, + huong-x: o.at("x", default: "below"), + huong-y: o.at("y", default: "left"), + ten-x: o.at("ten-x", default: ten-x), + ten-y: o.at("ten-y", default: ten-y), + ) +} + +// Nhãn các giao điểm với Ox (cac-x: mảng hoành độ đã tính sẵn). +// ten: mảng nhãn CHÍNH XÁC song song với cac-x (thiếu thì tự lấy so-toan). +#let _nhan-giao-ox(ctx, cac-x, tuy-chon, ten: ()) = { + if tuy-chon == none { return } + let le = (ctx.xmax - ctx.xmin) * 0.02 + let i = 0 + let j = 0 + for x in cac-x { + // giao điểm trùng gốc O thì bỏ qua hẳn + if x >= ctx.xmin + le and x <= ctx.xmax - le and calc.abs(x) > 0.001 { + diem(ctx, (x, 0), bk: 1.9pt, mau: red) + let nd = if j < ten.len() { ten.at(j) } else { so-toan(x) } + nhan(ctx, (x, 0), nd, huong: _huong-i(tuy-chon, i, "below"), cach: 4pt) + i = i + 1 + } + j = j + 1 + } +} + +// Nhãn giao điểm với Oy (y = f(0)). +#let _nhan-giao-oy(ctx, y, tuy-chon) = { + if tuy-chon == none or y == none { return } + // giao điểm trùng gốc O thì bỏ qua hẳn + if y < ctx.ymin or y > ctx.ymax or ctx.xmin > 0 or ctx.xmax < 0 or calc.abs(y) <= 0.001 { return } + diem(ctx, (0, y), bk: 1.9pt, mau: red) + nhan(ctx, (0, y), so-toan(y), huong: _huong-i(tuy-chon, 0, "left"), cach: 4pt) +} + +// Chuẩn hoá true/false cũ về auto/none. +#let _chuan-tc(t) = if t == true { auto } else if t == false { none } else { t } + +// Nhãn tên hàm đặt ở 1 trong 4 góc hoặc toạ độ (x, y) tuỳ ý. +// goc-ten: "above-right" (mặc định) | "above-left" | "below-left" +// | "below-right" | (x, y) +#let _nhan-ten(ctx, ten, goc, mau) = { + if ten == none { return } + let dx = ctx.xmax - ctx.xmin + let dy = ctx.ymax - ctx.ymin + let P = if type(goc) == array { goc } else { + ( + above-right: (ctx.xmax - dx * 0.14, ctx.ymax - dy * 0.09), + above-left: (ctx.xmin + dx * 0.14, ctx.ymax - dy * 0.09), + below-left: (ctx.xmin + dx * 0.14, ctx.ymin + dy * 0.09), + below-right: (ctx.xmax - dx * 0.14, ctx.ymin + dy * 0.09), + ).at(goc, default: (ctx.xmax - dx * 0.14, ctx.ymax - dy * 0.09)) + } + // nền trắng để đường cong không xuyên qua nhãn + nhan(ctx, P, box(fill: white, inset: (x: 2.5pt, y: 1pt), radius: 1.5pt, ten), huong: "center", mau: mau) +} + +// Co/giãn cửa sổ quanh tâm: dan > 1 phóng to (đơn vị trục lớn hơn), +// dan < 1 thu nhỏ (nhìn được vùng rộng hơn). Dùng khi đơn vị quá lớn/bé. +#let _dan(cs, dan-x, dan-y) = { + if dan-x == 1 and dan-y == 1 { return cs } + let cx = (cs.xmin + cs.xmax) / 2 + let cy = (cs.ymin + cs.ymax) / 2 + let hx = (cs.xmax - cs.xmin) / 2 / dan-x + let hy = (cs.ymax - cs.ymin) / 2 / dan-y + (xmin: cx - hx, xmax: cx + hx, ymin: cy - hy, ymax: cy + hy) +} + +// ---------- Đồ thị tổng quát ---------- +// Vẽ hàm f bất kỳ trên hệ trục hoàn chỉnh, tự tạo khung. +#let do-thi-ham( + f, + w: 8cm, h: auto, + xmin: -4, xmax: 4, ymin: -3, ymax: 3, + tu: auto, den: auto, + mau: blue, day: 1.4pt, n: 150, + ten: none, // nhãn hàm (vd $y = x^2$) + goc-ten: "above-right", // vị trí nhãn: 4 góc hoặc toạ độ (x, y) + luoi-o: false, + vach: false, // true => vạch chia bước 1 + dan-x: 1, dan-y: 1, // co/giãn cửa sổ quanh tâm (xem đầu file) + them: none, // ctx => nội dung vẽ thêm +) = { + let hh = if h == auto { w * 0.68 } else { h } + let cs = _dan((xmin: xmin, xmax: xmax, ymin: ymin, ymax: ymax), dan-x, dan-y) + hinh(w: w, h: hh, xmin: cs.xmin, xmax: cs.xmax, ymin: cs.ymin, ymax: cs.ymax, ctx => { + if luoi-o { luoi(ctx) } + truc(ctx) + if vach { vach-chia(ctx) } + ve-ham(ctx, f, tu: tu, den: den, n: n, mau: mau, day: day,) + _nhan-ten(ctx, ten, goc-ten, mau) + if them != none { them(ctx) } + }) +} + +// ---------- Bậc nhất: y = ax + b ---------- +#let do-thi-bac-nhat( + a, b, + w: 7cm, mau: blue, ten: auto, goc-ten: "above-right", + giao-ox: auto, giao-oy: auto, giao-truc: true, + luoi-o: false, vach: false, + dan-x: 1, dan-y: 1, + them: none, +) = { + let f = x => a * x + b + let chot = ((0, b),) + if a != 0 { chot = chot + ((-b / a, 0),) } + let cs = _dan(_cua-so(chot, rong-min: 4.6), dan-x, dan-y) + do-thi-ham( + f, + w: w, xmin: cs.xmin, xmax: cs.xmax, ymin: cs.ymin, ymax: cs.ymax, + mau: mau, luoi-o: luoi-o, vach: vach, + ten: if ten == auto { none } else { ten }, + goc-ten: goc-ten, + them: ctx => { + if giao-truc { + if a != 0 { _nhan-giao-ox(ctx, (-b / a,), giao-ox) } + _nhan-giao-oy(ctx, b, giao-oy) + } + if them != none { them(ctx) } + }, + ) +} + +// ---------- Bậc hai: y = ax² + bx + c ---------- +// Cửa sổ tự tính từ đỉnh + giao trục. +#let do-thi-bac-hai( + a, b, c, + w: 7.6cm, mau: blue, ten: none, goc-ten: "above-right", + dinh: auto, // gióng đỉnh: auto | none | (x: "below", y: "left") + giao-ox: auto, giao-oy: auto, + truc-dx: false, // trục đối xứng + luoi-o: false, vach: false, + dan-x: 1, dan-y: 1, + them: none, +) = { + let f = x => a * x * x + b * x + c + let xv = -b / (2 * a) + let yv = c - b * b / (4 * a) + let dinh = _chuan-tc(dinh) + // điểm chốt: đỉnh, giao Oy, giao Ox (nếu có) + let chot = ((xv, yv), (0, c)) + let dlt = b * b - 4 * a * c + let ng = () + if dlt > 0 { + let s = calc.sqrt(dlt) / (2 * calc.abs(a)) + ng = (xv - s, xv + s) + chot = chot + ((xv - s, 0), (xv + s, 0)) + } + // cánh parabol: mở cửa sổ theo bề ngang các điểm chốt + let xs = chot.map(p => p.at(0)) + (0,) + let mien = calc.max(calc.max(..xs) - calc.min(..xs), 3.0) + let (e1, e2) = (f(xv - 0.62 * mien), f(xv + 0.62 * mien)) + let ye = if a > 0 { calc.min(e1, e2) } else { calc.max(e1, e2) } + let cs = _dan(_cua-so(chot + ((xv - 0.62 * mien, ye), (xv + 0.62 * mien, ye)), rong-min: 4.6), dan-x, dan-y) + do-thi-ham( + f, + w: w, xmin: cs.xmin, xmax: cs.xmax, ymin: cs.ymin, ymax: cs.ymax, + mau: mau, ten: ten, goc-ten: goc-ten, luoi-o: luoi-o, vach: vach, + them: ctx => { + if truc-dx and calc.abs(xv) > 0.001 { + doan(ctx, (xv, cs.ymin), (xv, cs.ymax), mau: red, day: 0.8pt, dut: true) + } + _giong-ct( + ctx, (xv, yv), dinh, 0, + ten-x: so-can-thuc(-b, 0, 0, 2 * a), + ten-y: so-can-thuc(4 * a * c - b * b, 0, 0, 4 * a), + ) + _nhan-giao-ox( + ctx, ng, giao-ox, + ten: if dlt > 0 { + let sg = if a > 0 { 1 } else { -1 } + (so-can-thuc(-b, -sg, dlt, 2 * a), so-can-thuc(-b, sg, dlt, 2 * a)) + } else { () }, + ) + _nhan-giao-oy(ctx, c, giao-oy) + if them != none { them(ctx) } + }, + ) +} + +// ---------- Bậc ba: y = ax³ + bx² + cx + d ---------- +// Cửa sổ tự tính từ 2 cực trị (nếu có) hoặc điểm uốn (nếu không). +#let do-thi-bac-ba( + a, b, c, d, + w: 7.6cm, mau: blue, ten: none, goc-ten: "above-right", + cuc-tri: auto, // gióng 2 điểm cực trị (nếu có) + diem-uon: none, // gióng điểm uốn: none | auto | (x:.., y:..) + giao-ox: none, giao-oy: none, + luoi-o: false, vach: false, + dan-x: 1, dan-y: 1, + them: none, +) = { + let f = x => ((a * x + b) * x + c) * x + d + let delta = b * b - 3 * a * c + let tam = -b / (3 * a) + let cuc-tri = _chuan-tc(cuc-tri) + let (x1, x2) = (none, none) + let chot = ((tam, f(tam)),) + if delta > 0 { + let s = calc.sqrt(delta) / (3 * calc.abs(a)) + x1 = tam - s + x2 = tam + s + chot = chot + ( + (x1, f(x1)), (x2, f(x2)), + (tam - 2.1 * s, f(tam - 2.1 * s)), (tam + 2.1 * s, f(tam + 2.1 * s)), + ) + } else { + // không cực trị: bán kính quanh điểm uốn sao cho biến thiên y vừa mắt + let m = c - b * b / (3 * a) + let bien = t => calc.abs(a * t * t * t + m * t) + let r = 1.0 + while bien(r) < 4 and r < 6 { r = r * 1.25 } + while bien(r) > 7 and r > 0.9 { r = r / 1.15 } + chot = chot + ((tam - r, f(tam - r)), (tam + r, f(tam + r))) + } + // giao Ox (tìm số) chỉ khi cần nhãn + let ng = if giao-ox != none { + let bk = 1 + calc.max(calc.abs(b), calc.abs(c), calc.abs(d)) / calc.abs(a) + _nghiem(f, tam - calc.min(bk, 12), tam + calc.min(bk, 12)) + } else { () } + let cs = _dan(_cua-so(chot + ng.map(x => (x, 0))), dan-x, dan-y) + do-thi-ham( + f, + w: w, xmin: cs.xmin, xmax: cs.xmax, ymin: cs.ymin, ymax: cs.ymax, + mau: mau, ten: ten, goc-ten: goc-ten, n: 180, luoi-o: luoi-o, vach: vach, + them: ctx => { + if x1 != none { + // giá trị cực trị CHÍNH XÁC (phân số/căn thức) thay vì số thập phân + let ct = cuc-tri-bac-ba(a, b, c, d) + _giong-ct(ctx, (x1, f(x1)), cuc-tri, 0, ten-x: ct.x1, ten-y: ct.y1) + _giong-ct(ctx, (x2, f(x2)), cuc-tri, 1, ten-x: ct.x2, ten-y: ct.y2) + } + _giong-ct( + ctx, (tam, f(tam)), diem-uon, 0, + ten-x: so-can-thuc(-b, 0, 0, 3 * a), + ten-y: so-can-thuc(2 * b * b * b - 9 * a * b * c + 27 * a * a * d, 0, 0, 27 * a * a), + ) + _nhan-giao-ox(ctx, ng, giao-ox) + _nhan-giao-oy(ctx, d, giao-oy) + if them != none { them(ctx) } + }, + ) +} + +// ---------- Trùng phương: y = ax⁴ + bx² + c ---------- +// Cửa sổ tự tính từ 3 cực trị (a·b < 0) hoặc 1 cực trị (0, c). +#let do-thi-trung-phuong( + a, b, c, + w: 7.6cm, mau: blue, ten: none, goc-ten: "above-right", + cuc-tri: auto, + giao-ox: none, giao-oy: none, + luoi-o: false, vach: false, + dan-x: 1, dan-y: 1, + them: none, +) = { + let f = x => a * calc.pow(x, 4) + b * x * x + c + let co-3ct = a * b < 0 + let cuc-tri = _chuan-tc(cuc-tri) + let xc = if co-3ct { calc.sqrt(-b / (2 * a)) } else { 0 } + let chot = ((0, c),) + if co-3ct { + chot = chot + ( + (xc, f(xc)), (-xc, f(xc)), + (1.45 * xc, f(1.45 * xc)), (-1.45 * xc, f(1.45 * xc)), + ) + } else { + let bien = t => calc.abs(f(t) - c) + let r = 1.0 + while bien(r) < 4 and r < 5 { r = r * 1.25 } + while bien(r) > 7 and r > 0.8 { r = r / 1.15 } + chot = chot + ((r, f(r)), (-r, f(r))) + } + let ng = if giao-ox != none { + let xs = chot.map(p => p.at(0)) + _nghiem(f, calc.min(..xs) - 0.5, calc.max(..xs) + 0.5) + } else { () } + let cs = _dan(_cua-so(chot + ng.map(x => (x, 0))), dan-x, dan-y) + do-thi-ham( + f, + w: w, xmin: cs.xmin, xmax: cs.xmax, ymin: cs.ymin, ymax: cs.ymax, + mau: mau, ten: ten, goc-ten: goc-ten, n: 180, luoi-o: luoi-o, vach: vach, + them: ctx => { + if cuc-tri != none { + if co-3ct { + let Xc = so-can-thuc(0, 1, -b / (2 * a), 1) + let nXc = so-can-thuc(0, -1, -b / (2 * a), 1) + let Yc = so-can-thuc(4 * a * c - b * b, 0, 0, 4 * a) + _giong-ct(ctx, (-xc, f(xc)), cuc-tri, 0, ten-x: nXc, ten-y: Yc) + _giong-ct(ctx, (0, c), cuc-tri, 1) + _giong-ct(ctx, (xc, f(xc)), cuc-tri, 2, ten-x: Xc, ten-y: Yc) + } else { + _giong-ct(ctx, (0, c), cuc-tri, 0) + } + } + _nhan-giao-ox(ctx, ng, giao-ox) + _nhan-giao-oy(ctx, c, giao-oy) + if them != none { them(ctx) } + }, + ) +} + +// ---------- Phân thức bậc nhất/bậc nhất: y = (ax + b)/(cx + d) ---------- +// Hai nhánh hypebol; tiệm cận LUÔN vẽ + nhãn trên trục. +// Cửa sổ tự tính từ tâm đối xứng (x0, y0), giao trục và gốc O. +#let do-thi-phan-thuc( + a, b, c, d, + w: 7.6cm, mau: blue, ten: none, goc-ten: "above-right", + giao-ox: none, giao-oy: none, + huong-x0: "below-left", huong-y0: "above-left", // hướng nhãn 2 tiệm cận + luoi-o: false, vach: false, + dan-x: 1, dan-y: 1, + them: none, +) = { + let x0 = -d / c // tiệm cận đứng + let y0 = a / c // tiệm cận ngang + let f = x => (a * x + b) / (c * x + d) + let chot = ((x0, y0), (x0 - 1.7, y0 - 1.7), (x0 + 1.7, y0 + 1.7)) + if a != 0 { chot = chot + ((-b / a, 0),) } + if d != 0 { chot = chot + ((0, b / d),) } + let cs = _dan(_cua-so(chot, le: 0.2), dan-x, dan-y) + hinh( + w: w, h: w * 0.74, + xmin: cs.xmin, xmax: cs.xmax, ymin: cs.ymin, ymax: cs.ymax, + ctx => { + if luoi-o { luoi(ctx) } + truc(ctx) + if vach { vach-chia(ctx) } + // tiệm cận + nhãn trên trục + doan(ctx, (x0, cs.ymin), (x0, cs.ymax), mau: red, day: 0.9pt, dut: true) + doan(ctx, (cs.xmin, y0), (cs.xmax, y0), mau: red, day: 0.9pt, dut: true) + if calc.abs(x0) > 0.001 { nhan(ctx, (x0, 0), so-can-thuc(-d, 0, 0, c), huong: huong-x0, cach: 4pt) } + if calc.abs(y0) > 0.001 { nhan(ctx, (0, y0), so-can-thuc(a, 0, 0, c), huong: huong-y0, cach: 4pt) } + // hai nhánh + let g = (cs.xmax - cs.xmin) * 0.012 + ve-ham(ctx, f, tu: cs.xmin, den: x0 - g, n: 130, mau: mau) + ve-ham(ctx, f, tu: x0 + g, den: cs.xmax, n: 130, mau: mau) + if a != 0 { _nhan-giao-ox(ctx, (-b / a,), giao-ox) } + if d != 0 { _nhan-giao-oy(ctx, b / d, giao-oy) } + _nhan-ten(ctx, ten, goc-ten, mau) + if them != none { them(ctx) } + }, + ) +} + +// ---------- Hữu tỉ bậc hai/bậc nhất: y = (ax² + bx + c)/(dx + e) ---------- +// Tự nhận 4 tình huống: (a/d > 0 hay < 0) × (có 2 cực trị hay không). +// Có cực trị -> cửa sổ bao 2 điểm cực trị (+ gióng nhãn). +// Không cực trị -> cửa sổ quanh tâm đối xứng (giao 2 tiệm cận). +// LUÔN vẽ tiệm cận đứng x = x0 (nhãn trên Ox) và tiệm cận xiên +// y = px + q (nhãn ten-tcx: auto = "y = px + q" | none | nội dung tuỳ ý). +#let do-thi-huu-ti( + a, b, c, d, e, + w: 7.6cm, mau: blue, ten: none, goc-ten: "above-right", + dan-x: 1, dan-y: 1, + cuc-tri: auto, + tam: auto, // gióng tâm đối xứng I(x0, px0+q) + nhãn: + // auto = chỉ khi không có cực trị | true/(x:..,y:..) = luôn | none = tắt + giao-ox: none, giao-oy: none, + huong-x0: "below-right", // hướng nhãn tiệm cận đứng trên Ox + ten-tcx: auto, huong-tcx: "above-left", // nhãn tiệm cận xiên + luoi-o: false, vach: false, + them: none, +) = { + let x0 = -e / d // tiệm cận đứng + let p = a / d // tiệm cận xiên y = px + q + let q = (b * d - a * e) / (d * d) + let r = c - q * e // y = px + q + r/(dx + e) + let f = x => (a * x * x + b * x + c) / (d * x + e) + let tcx = x => p * x + q + let cuc-tri = _chuan-tc(cuc-tri) + let co-ct = p != 0 and r * d * p > 0 + let hien-tam = if tam == none { false } else if tam == auto { not co-ct } else { true } + let (xt1, xt2) = (none, none) + let chot = ((x0, tcx(x0)),) + if co-ct { + let t = calc.sqrt(r * d / p) + let xs = ((-e - t) / d, (-e + t) / d).sorted() + xt1 = xs.at(0) + xt2 = xs.at(1) + let s = xt2 - xt1 + chot = chot + ( + (xt1, f(xt1)), (xt2, f(xt2)), + (xt1 - 0.55 * s, f(xt1 - 0.55 * s)), (xt2 + 0.55 * s, f(xt2 + 0.55 * s)), + ) + } else { + // không cực trị: mở quanh tâm sao cho nhánh tách rõ khỏi tiệm cận + let t = calc.max(calc.min(calc.abs(r / (2 * d)), 3), 0.9) + chot = chot + ( + (x0 - t, f(x0 - t)), (x0 + t, f(x0 + t)), + (x0 - 2.4 * t, f(x0 - 2.4 * t)), (x0 + 2.4 * t, f(x0 + 2.4 * t)), + ) + } + // giao Ox: nghiệm tử thức (nếu cần nhãn) + let ng = if giao-ox != none and b * b - 4 * a * c > 0 { + let s = calc.sqrt(b * b - 4 * a * c) / (2 * calc.abs(a)) + (-b / (2 * a) - s, -b / (2 * a) + s) + } else { () } + let cs = _dan(_cua-so(chot + ng.map(x => (x, 0)), le: 0.14), dan-x, dan-y) + hinh( + w: w, h: w * 0.78, + xmin: cs.xmin, xmax: cs.xmax, ymin: cs.ymin, ymax: cs.ymax, + ctx => { + if luoi-o { luoi(ctx) } + truc(ctx) + if vach { vach-chia(ctx) } + // tiệm cận đứng (khi gióng tâm thì nhãn x0 do gióng đảm nhiệm, tránh ghi 2 lần) + doan(ctx, (x0, cs.ymin), (x0, cs.ymax), mau: red, day: 0.9pt, dut: true) + if calc.abs(x0) > 0.001 and not hien-tam { nhan(ctx, (x0, 0), so-can-thuc(-e, 0, 0, d), huong: huong-x0, cach: 4pt) } + // tiệm cận xiên (cắt gọn trong cửa sổ) + let (X1, X2) = if calc.abs(p) > 0.0001 { + let ca = ((cs.ymin - q) / p, (cs.ymax - q) / p).sorted() + (calc.max(cs.xmin, ca.at(0)), calc.min(cs.xmax, ca.at(1))) + } else { (cs.xmin, cs.xmax) } + doan(ctx, (X1, tcx(X1)), (X2, tcx(X2)), mau: red, day: 0.9pt, dut: true) + if ten-tcx != none { + let nd = if ten-tcx == auto { _pt-bac-nhat(p, q) } else { ten-tcx } + let Xn = X1 + 0.88 * (X2 - X1) + nhan(ctx, (Xn, tcx(Xn)), nd, huong: huong-tcx, cach: 5pt, mau: red) + } + // hai nhánh + let g = (cs.xmax - cs.xmin) * 0.012 + ve-ham(ctx, f, tu: cs.xmin, den: x0 - g, n: 140, mau: mau) + ve-ham(ctx, f, tu: x0 + g, den: cs.xmax, n: 140, mau: mau) + // cực trị (nếu có) + if xt1 != none { + let ct = cuc-tri-huu-ti(a, b, c, d, e) + _giong-ct(ctx, (xt1, f(xt1)), cuc-tri, 0, ten-x: ct.x1, ten-y: ct.y1) + _giong-ct(ctx, (xt2, f(xt2)), cuc-tri, 1, ten-x: ct.x2, ten-y: ct.y2) + } + // tâm đối xứng I = giao 2 tiệm cận + if hien-tam { + let o = if type(tam) == dictionary { tam } else { (:) } + giong( + ctx, (x0, tcx(x0)), + huong-x: o.at("x", default: "below"), + huong-y: o.at("y", default: "left"), + ten-x: o.at("ten-x", default: so-can-thuc(-e, 0, 0, d)), + ten-y: o.at("ten-y", default: so-can-thuc(b * d - 2 * a * e, 0, 0, d * d)), + ) + } + _nhan-giao-ox( + ctx, ng, giao-ox, + ten: if ng.len() == 2 { + let dl = b * b - 4 * a * c + let sg = if a > 0 { 1 } else { -1 } + (so-can-thuc(-b, -sg, dl, 2 * a), so-can-thuc(-b, sg, dl, 2 * a)) + } else { () }, + ) + if e != 0 { _nhan-giao-oy(ctx, c / e, giao-oy) } + _nhan-ten(ctx, ten, goc-ten, mau) + if them != none { them(ctx) } + }, + ) +} + +// ---------- Mũ: y = aˣ (a > 0, a ≠ 1) ---------- +#let do-thi-mu(a, w: 7cm, mau: blue, ten: auto, goc-ten: auto, diem-db: true, luoi-o: false, vach: false, them: none) = { + // cơ số: nguyên -> số nguyên; còn lại -> số đẹp (phân số/căn) trong ngoặc + let cs = if calc.abs(a - calc.round(a)) < 0.001 { $#int(calc.round(a))$ } else { $(#so-toan(a))$ } + let nd = if ten == auto { $y = #cs^x$ } else { ten } + // a > 1 tăng (góc trống trên-trái) · a < 1 giảm (trên-phải) + let gt = if goc-ten == auto { if a > 1 { "above-left" } else { "above-right" } } else { goc-ten } + do-thi-ham( + x => calc.pow(a, x), + w: w, xmin: -2.9, xmax: 2.9, ymin: -1.2, ymax: 5.2, + mau: mau, ten: nd, goc-ten: gt, n: 160, luoi-o: luoi-o, vach: vach, + them: ctx => { + if diem-db { + giong(ctx, (0, 1), ten-x: none) + giong(ctx, (1, a)) + } + if them != none { them(ctx) } + }, + ) +} + +// ---------- Lôgarit: y = log_a(x) ---------- +#let do-thi-log(a, w: 7cm, mau: blue, ten: auto, goc-ten: auto, diem-db: true, luoi-o: false, vach: false, them: none) = { + let cs = if calc.abs(a - calc.round(a)) < 0.001 { $#int(calc.round(a))$ } else { so-toan(a) } + let nd = if ten == auto { $y = log_#cs x$ } else { ten } + // a > 1 tăng (góc trống dưới-phải) · a < 1 giảm (dưới-trái) + let gt = if goc-ten == auto { if a > 1 { "below-right" } else { "below-left" } } else { goc-ten } + do-thi-ham( + x => calc.ln(x) / calc.ln(a), + w: w, xmin: -1.4, xmax: 6, ymin: -3.1, ymax: 3.1, + tu: 0.035, den: 5.8, + mau: mau, ten: nd, goc-ten: gt, n: 220, luoi-o: luoi-o, vach: vach, + them: ctx => { + if diem-db { + giong(ctx, (1, 0), ten-y: none) + giong(ctx, (a, 1)) + } + if them != none { them(ctx) } + }, + ) +} + +// ---------- Lượng giác ---------- +// Ghi nhãn các bội của π trên trục hoành. +#let nhan-pi(ctx) = { + let cap = ( + (-2 * calc.pi, $-2pi$), (-calc.pi, $-pi$), (-calc.pi / 2, $-pi/2$), + (calc.pi / 2, $pi/2$), (calc.pi, $pi$), (2 * calc.pi, $2pi$), + ) + for (v, t) in cap { + if v > ctx.xmin + 0.3 and v < ctx.xmax - 0.3 { + let s = 3pt / ctx.sy + doan(ctx, (v, -s), (v, s), day: 0.8pt) + nhan(ctx, (v, 0), t, huong: "below", cach: 5pt) + } + } +} + +#let do-thi-sin(w: 10cm, mau: blue, ten: $y = sin x$, goc-ten: "above-right", luoi-o: false, them: none) = do-thi-ham( + x => calc.sin(x), + w: w, h: w * 0.34, + xmin: -6.9, xmax: 6.9, ymin: -1.7, ymax: 1.7, + tu: -6.6, den: 6.6, + mau: mau, ten: ten, goc-ten: goc-ten, n: 220, luoi-o: luoi-o, + them: ctx => { + nhan-pi(ctx) + nhan(ctx, (0, 1), $1$, huong: "above-left", cach: 3pt) + nhan(ctx, (0, -1), $-1$, huong: "below-left", cach: 3pt) + if them != none { them(ctx) } + }, +) + +#let do-thi-cos(w: 10cm, mau: blue, ten: $y = cos x$, goc-ten: "above-right", luoi-o: false, them: none) = do-thi-ham( + x => calc.cos(x), + w: w, h: w * 0.34, + xmin: -6.9, xmax: 6.9, ymin: -1.7, ymax: 1.7, + tu: -6.6, den: 6.6, + mau: mau, ten: ten, goc-ten: goc-ten, n: 220, luoi-o: luoi-o, + them: ctx => { + nhan-pi(ctx) + nhan(ctx, (0, 1), $1$, huong: "above-right", cach: 3pt) + if them != none { them(ctx) } + }, +) + +#let do-thi-tan(w: 8cm, mau: blue, ten: $y = tan x$, goc-ten: "above-right", luoi-o: false, them: none) = { + let p2 = calc.pi / 2 + do-thi-ham( + x => calc.tan(x), + w: w, h: w * 0.8, + xmin: -4.6, xmax: 4.6, ymin: -3.6, ymax: 3.6, + tu: -4.55, den: 4.55, + mau: mau, ten: ten, goc-ten: goc-ten, n: 260, luoi-o: luoi-o, + them: ctx => { + for k in (-3, -1, 1, 3) { + doan(ctx, (k * p2, -3.6), (k * p2, 3.6), mau: red, day: 0.8pt, dut: true) + } + nhan-pi(ctx) + if them != none { them(ctx) } + }, + ) +} + +// ---------- Côtang: y = cot x (tiệm cận đứng x = kπ) ---------- +// cot x = cos x / sin x; nhánh tự tách tại các tiệm cận (giống tan). +#let do-thi-cot(w: 8cm, mau: blue, ten: $y = cot x$, goc-ten: "above-left", luoi-o: false, them: none) = { + let pi = calc.pi + let cot = x => { + let s = calc.sin(x) + if calc.abs(s) < 0.000001 { 1000000 } else { calc.cos(x) / s } + } + do-thi-ham( + cot, + w: w, h: w * 0.8, + xmin: -4.6, xmax: 4.6, ymin: -3.6, ymax: 3.6, + tu: -4.55, den: 4.55, + mau: mau, ten: ten, goc-ten: goc-ten, n: 260, luoi-o: luoi-o, + them: ctx => { + // tiệm cận x = ±π (x = 0 trùng trục Oy nên không vẽ lại) + for k in (-1, 1) { + doan(ctx, (k * pi, -3.6), (k * pi, 3.6), mau: red, day: 0.8pt, dut: true) + } + nhan-pi(ctx) + if them != none { them(ctx) } + }, + ) +} + +// ---------- VẼ NHANH: chỉ cần công thức + màu ---------- +// Tự lấy mẫu hàm số và chọn cửa sổ y hợp lý (kể cả khi hàm tăng rất nhanh). +// #ve-do-thi(x => x*x*x - 3*x, mau: red) +// #ve-do-thi(x => calc.sin(2*x), mau: purple, ten: $y = sin 2x$) +// Lưu ý: hàm phải xác định trên [xmin, xmax] (hoặc trên [tu, den] nếu đặt); +// với hàm phân thức có tiệm cận, dùng #do-thi-phan-thuc / #do-thi-huu-ti. +#let ve-do-thi( + f, + mau: blue, + w: 8cm, + xmin: -4, xmax: 4, + tu: auto, den: auto, + ten: none, goc-ten: "above-right", + vach: false, luoi-o: false, + gioi-han-y: 12, // bề cao tối đa của cửa sổ nhìn theo trục y + giao-ox: none, giao-oy: none, + day: 1.4pt, + them: none, +) = { + let a = if tu == auto { xmin } else { tu } + let b = if den == auto { xmax } else { den } + let ys = lay-mau(f, a, b, n: 160).map(p => p.at(1)).sorted() + let lo = ys.first() + let hi = ys.last() + // gộp trục hoành vào cửa sổ nếu không làm cửa sổ quá cao + if lo > 0 and hi <= gioi-han-y { lo = 0 } + if hi < 0 and -lo <= gioi-han-y { hi = 0 } + // hàm biến thiên quá mạnh: thu cửa sổ quanh trung vị (đồ thị tự cắt nhánh) + if hi - lo > gioi-han-y { + let tv = ys.at(calc.floor(ys.len() / 2)) + lo = calc.max(lo, tv - gioi-han-y / 2) + hi = calc.min(hi, tv + gioi-han-y / 2) + } + let dem = (hi - lo) * 0.1 + 0.35 + do-thi-ham( + f, + w: w, xmin: xmin, xmax: xmax, + ymin: lo - dem, ymax: hi + dem, + tu: a, den: b, + mau: mau, day: day, n: 180, + ten: ten, goc-ten: goc-ten, vach: vach, luoi-o: luoi-o, + them: them, + ) +} + +// ===================================================================== +// NHIỀU ĐỒ THỊ TRÊN CÙNG MỘT HỆ TRỤC +// Mỗi hàm gói bằng #ham(...); truyền hàm trần x => ... cũng được. +// #do-thi-nhieu-ham( +// ham(x => x*x - 2, mau: blue, ten: $y = x^2 - 2$), +// ham(x => x, mau: red, ten: $y = x$, dut: true), +// xmin: -3, xmax: 3, giao-diem: auto, +// ) +// ===================================================================== +// Gói thông số một hàm. +// tai : auto = nhãn đặt gần đầu phải của đường; hoặc hoành độ x cụ thể +// huong-ten : hướng nhãn so với điểm đặt +#let ham( + f, + mau: blue, day: 1.3pt, dut: false, + ten: none, tai: auto, huong-ten: "above", + tu: auto, den: auto, +) = ( + f: f, mau: mau, day: day, dut: dut, + ten: ten, tai: tai, huong-ten: huong-ten, tu: tu, den: den, +) + +#let do-thi-nhieu-ham( + ..cac-ham, + w: 8cm, h: auto, + xmin: -4, xmax: 4, ymin: -3, ymax: 3, + vach: false, luoi-o: false, + giao-diem: none, // auto = chấm đỏ tại giao điểm của từng cặp đồ thị + dan-x: 1, dan-y: 1, + them: none, +) = { + let ds = cac-ham.pos().map(hs => if type(hs) == function { ham(hs) } else { hs }) + let cs = _dan((xmin: xmin, xmax: xmax, ymin: ymin, ymax: ymax), dan-x, dan-y) + let hh = if h == auto { w * 0.68 } else { h } + hinh(w: w, h: hh, xmin: cs.xmin, xmax: cs.xmax, ymin: cs.ymin, ymax: cs.ymax, ctx => { + if luoi-o { luoi(ctx) } + truc(ctx) + if vach { vach-chia(ctx) } + for hs in ds { + let a = if hs.tu == auto { cs.xmin } else { hs.tu } + let b = if hs.den == auto { cs.xmax } else { hs.den } + ve-ham(ctx, hs.f, tu: a, den: b, mau: hs.mau, day: hs.day, dut: hs.dut) + if hs.ten != none { + // vị trí nhãn: hoành độ chỉ định, hoặc điểm trong cửa sổ gần đầu phải + let P = none + if hs.tai != auto { + P = (hs.tai, (hs.f)(hs.tai)) + } else { + let le = (cs.ymax - cs.ymin) * 0.08 + let i = 40 + while i >= 0 and P == none { + let x = a + (b - a) * (0.06 + 0.88 * i / 40) + let y = (hs.f)(x) + if y > cs.ymin + le and y < cs.ymax - le { P = (x, y) } + i = i - 1 + } + } + if P != none { nhan(ctx, P, hs.ten, huong: hs.huong-ten, mau: hs.mau, cach: 7pt) } + } + } + if giao-diem == auto { + for i in range(ds.len()) { + for j in range(i + 1, ds.len()) { + let fi = ds.at(i).f + let fj = ds.at(j).f + for x in _nghiem(x => fi(x) - fj(x), cs.xmin, cs.xmax) { + let y = fi(x) + if y > cs.ymin and y < cs.ymax { diem(ctx, (x, y), bk: 2pt, mau: red) } + } + } + } + } + if them != none { them(ctx) } + }) +} + +// ===================================================================== +// MIỀN NGHIỆM BPT / HỆ BPT BẬC NHẤT HAI ẨN +// Quy ước SGK: GẠCH phần KHÔNG là miền nghiệm; miền nghiệm để trắng +// (hoặc tô nhạt bằng to-mien). +// #mien-nghiem(bpt(4, 5, -8, dau: "<"), xmin: -4, xmax: 4) +// #mien-nghiem( +// bpt(3, -2, -9, dau: "<=", mau: red), +// bpt(3, -5, -18, dau: ">=", mau: green.darken(20%)), +// to-mien: rgb(30, 100, 200, 40), +// ) +// ===================================================================== +// Một BPT: a·x + b·y (dau) c. dau: "<=", ">=", "<", ">" +// mau : màu đường thẳng; mau-gach: auto = cùng màu (nhạt hơn) +// goc-gach : GÓC gạch chéo RIÊNG cho bpt này; auto = theo goc-gach chung +// của mien-nghiem (giống lối mau-gach). buoc-gach: bước gạch riêng +// ten : nhãn đường thẳng (vd $d: 4x + 5y = -8$); ten-tai: 0..1 vị trí +// dọc theo đoạn nhìn thấy tại điểm đặt nhãn +// nghieng-ten : true (mặc định) = chữ NẰM NGHIÊNG theo chiều đường thẳng +// (tự đọc xuôi, không lộn ngược); false = chữ nằm ngang như cũ +// huong-ten : PHÍA đặt nhãn tại điểm đó — "above" hoặc "below". Khi nghiêng, +// đây là phía trên/dưới so với đường (theo pháp tuyến); khi không +// nghiêng vẫn nhận mọi hướng cũ ("above"/"below"/"left"/tuple…) +// cach-ten : khoảng cách từ đường tới nhãn +#let bpt( + a, b, c, + dau: "<=", + mau: red, day: 1.1pt, + mau-gach: auto, goc-gach: auto, buoc-gach: auto, + ten: none, ten-tai: 0.78, huong-ten: "above", + nghieng-ten: true, cach-ten: 3pt, +) = ( + a: a, b: b, c: c, dau: dau, mau: mau, day: day, + mau-gach: mau-gach, goc-gach: goc-gach, buoc-gach: buoc-gach, + ten: ten, ten-tai: ten-tai, huong-ten: huong-ten, + nghieng-ten: nghieng-ten, cach-ten: cach-ten, +) + +// Đoạn nhìn thấy của đường a·x + b·y = c trong cửa sổ (none nếu ngoài). +#let _doan-cua-so(ctx, a, b, c) = { + let gd = () + let canh = ( + ((ctx.xmin, ctx.ymin), (ctx.xmax, ctx.ymin)), + ((ctx.xmax, ctx.ymin), (ctx.xmax, ctx.ymax)), + ((ctx.xmax, ctx.ymax), (ctx.xmin, ctx.ymax)), + ((ctx.xmin, ctx.ymax), (ctx.xmin, ctx.ymin)), + ) + for (P, Q) in canh { + let fp = a * P.at(0) + b * P.at(1) - c + let fq = a * Q.at(0) + b * Q.at(1) - c + if fp * fq <= 0 and fp != fq { + let s = fp / (fp - fq) + let G = (P.at(0) + s * (Q.at(0) - P.at(0)), P.at(1) + s * (Q.at(1) - P.at(1))) + if gd.len() == 0 or khoang-cach(gd.at(0), G) > 0.0001 { gd.push(G) } + } + } + if gd.len() >= 2 { (gd.at(0), gd.at(1)) } else { none } +} + +#let mien-nghiem( + ..cac-bpt, + w: 7cm, h: auto, + xmin: -5, xmax: 5, ymin: -4, ymax: 4, + to-mien: none, // màu tô miền nghiệm (nên trong suốt), none = bỏ + giao-truc: none, // auto = ghi số giao điểm của các đường với 2 trục + buoc-gach: 6.5pt, goc-gach: 45deg, + vach: false, luoi-o: false, + them: none, +) = { + let ds = cac-bpt.pos() + let hh = if h == auto { w * (ymax - ymin) / (xmax - xmin) } else { h } + hinh(w: w, h: hh, xmin: xmin, xmax: xmax, ymin: ymin, ymax: ymax, ctx => { + let khung = ((xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)) + if luoi-o { luoi(ctx) } + // tô miền nghiệm: giao các nửa mặt phẳng + if to-mien != none { + let mn = khung + for d in ds { + // chuẩn hoá về dạng ≤: miền nghiệm là a·x + b·y ≤ c + let (a, b, c) = if d.dau in ("<=", "<") { (d.a, d.b, d.c) } else { (-d.a, -d.b, -d.c) } + mn = cat-nua-mp(mn, a, b, c) + } + if mn.len() >= 3 { da-giac-pt(mn.map(P => toa-pt(ctx, P)), to: to-mien) } + } + // gạch phần loại bỏ của từng BPT + for d in ds { + let (a, b, c) = if d.dau in ("<=", "<") { (d.a, d.b, d.c) } else { (-d.a, -d.b, -d.c) } + let loai = cat-nua-mp(khung, -a, -b, -c) // nửa mặt phẳng a·x + b·y ≥ c + let mg = if d.mau-gach == auto { d.mau.lighten(25%) } else { d.mau-gach } + // góc/bước gạch RIÊNG của từng bpt (auto = theo giá trị chung của mien-nghiem) + let gg = if d.at("goc-gach", default: auto) == auto { goc-gach } else { d.goc-gach } + let bg = if d.at("buoc-gach", default: auto) == auto { buoc-gach } else { d.buoc-gach } + if loai.len() >= 3 { + gach-mien(ctx, loai, goc: gg, buoc: bg, mau: mg) + } + } + truc(ctx) + if vach { vach-chia(ctx) } + // đường biên (đứt nếu BPT ngặt) + nhãn + for d in ds { + let dc = _doan-cua-so(ctx, d.a, d.b, d.c) + if dc != none { + let (A, B) = dc + doan(ctx, A, B, mau: d.mau, day: d.day, dut: d.dau in ("<", ">")) + if d.ten != none { + let P = chia(A, B, d.ten-tai) + let cach = d.at("cach-ten", default: 3pt) + if d.at("nghieng-ten", default: true) { + // chữ nằm nghiêng theo chiều đường; PHÍA above/below theo pháp tuyến. + // Đặt SÁT đường: lệch = cach + nửa bề DÀY chữ (vuông góc đường) — + // KHÔNG dùng offset của `nhan` (offset đó theo nửa khung ĐÃ XOAY, với + // nhãn dài + đường xiên thì khung rất to nên nhãn bị đẩy xa đường). + let xuoi = toa-pt(ctx, A).at(0) <= toa-pt(ctx, B).at(0) + let ang = if xuoi { goc-truc(ctx, A, B) } else { goc-truc(ctx, B, A) } + let n = _phap-tuyen(ctx, A, B) // pháp tuyến hướng LÊN (trang) + if d.huong-ten in ("below", "duoi") { n = (-n.at(0), -n.at(1)) } + context { + let nd = text(fill: d.mau, d.ten) + let nd2 = rotate(ang, reflow: true, nd) + let s = measure(nd2) // khung sau khi xoay (canh giữa) + let off = cach + measure(nd).height / 2 // bề dày chữ, vuông góc đường + let p = toa(ctx, P) + place( + dx: p.at(0) - s.width / 2 + n.at(0) * off, + dy: p.at(1) - s.height / 2 + n.at(1) * off, + nd2, + ) + } + } else { + nhan(ctx, P, d.ten, huong: d.huong-ten, mau: d.mau, cach: cach) + } + } + } + if giao-truc == auto { + if d.a != 0 { _nhan-giao-ox(ctx, (d.c / d.a,), auto) } + if d.b != 0 { _nhan-giao-oy(ctx, d.c / d.b, auto) } + } + } + if them != none { them(ctx) } + }) +} + +// ===================================================================== +// DIỆN TÍCH HÌNH PHẲNG GIỚI HẠN BỞI HAI ĐỒ THỊ +// g mặc định là trục hoành (y = 0). +// a, b: auto = tự lấy giao điểm ngoài cùng của f và g trong [tu, den]; +// hoặc chỉ định đoạn [a, b] cụ thể. +// Miền tô tự tách tại các giao điểm ở giữa nên tô đúng cả khi f − g đổi dấu. +// #dien-tich-2-ham(x => x*x, g: x => x + 2) // tự tìm giao +// #dien-tich-2-ham(x => calc.sin(x), a: 0, b: calc.pi) // trên [a, b] +// ===================================================================== +// Điểm đặt nhãn: điểm trên đồ thị gần mép phải mà còn nằm trong cửa sổ. +#let _diem-nhan(cs, f, a, b) = { + let le = (cs.ymax - cs.ymin) * 0.08 + let i = 40 + while i >= 0 { + let x = a + (b - a) * (0.06 + 0.88 * i / 40) + let y = f(x) + if y > cs.ymin + le and y < cs.ymax - le { return (x, y) } + i = i - 1 + } + none +} + +#let dien-tich-2-ham( + f, g: x => 0.0, + a: auto, b: auto, + tu: -5, den: 5, // phạm vi tìm giao điểm khi a, b: auto + w: 8cm, h: auto, + mau-f: blue, mau-g: red, day: 1.3pt, + ten-f: none, ten-g: none, + huong-ten-f: "above", huong-ten-g: "below", + mau-to: rgb(70, 90, 200, 70), + ten-a: auto, ten-b: auto, // nhãn cận trên Ox; none = bỏ + giong-ab: true, // đường đứt x = a, x = b + cham-giao: true, // chấm đỏ tại giao điểm + keo-dai: 0.22, // vẽ đồ thị dài thêm ra 2 phía (tỉ lệ đoạn) + dan-x: 1, dan-y: 1, + them: none, +) = { + let hieu = x => f(x) - g(x) + let giao = _nghiem(hieu, tu, den) + let aa = if a == auto { if giao.len() > 0 { giao.first() } else { tu } } else { a } + let bb = if b == auto { if giao.len() > 0 { giao.last() } else { den } } else { b } + let m = (bb - aa) * keo-dai + 0.3 + // cửa sổ: bao miền tô trên [aa, bb]; đồ thị ngoài đoạn bị cắt theo cửa sổ + let chot = lay-mau(f, aa, bb, n: 60) + lay-mau(g, aa, bb, n: 60) + let cs = _dan(_cua-so(chot + ((aa - m, 0), (bb + m, 0))), dan-x, dan-y) + let hh = if h == auto { w * 0.68 } else { h } + hinh(w: w, h: hh, xmin: cs.xmin, xmax: cs.xmax, ymin: cs.ymin, ymax: cs.ymax, ctx => { + // các đoạn con tách tại giao điểm nằm trong (aa, bb) + let moc = (aa,) + giao.filter(x => x > aa + 0.0001 and x < bb - 0.0001) + (bb,) + for i in range(moc.len() - 1) { + to-vung-2-ham(ctx, f, g, moc.at(i), moc.at(i + 1), mau: mau-to) + } + truc(ctx) + ve-ham(ctx, f, tu: aa - m, den: bb + m, mau: mau-f, day: day) + ve-ham(ctx, g, tu: aa - m, den: bb + m, mau: mau-g, day: day) + if giong-ab { + for (x, t) in ((aa, ten-a), (bb, ten-b)) { + let (y1, y2) = (calc.min(f(x), g(x), 0), calc.max(f(x), g(x), 0)) + if y2 - y1 > 0.0001 { + doan(ctx, (x, y1), (x, y2), mau: gray.darken(20%), day: 0.8pt, dut: true) + } + if t != none and calc.abs(x) > 0.001 { + nhan(ctx, (x, 0), if t == auto { so-toan(x) } else { t }, + huong: if calc.max(f(x), g(x)) < 0.0001 { "above" } else { "below" }, cach: 4pt) + } + } + } + if cham-giao { + for x in giao.filter(x => x >= aa - 0.0001 and x <= bb + 0.0001) { + diem(ctx, (x, f(x)), bk: 2pt, mau: red) + } + } + if ten-f != none { + let P = _diem-nhan(cs, f, aa - m, bb + m) + if P != none { nhan(ctx, P, ten-f, huong: huong-ten-f, mau: mau-f, cach: 7pt) } + } + if ten-g != none { + let P = _diem-nhan(cs, g, aa - m, bb + m) + if P != none { nhan(ctx, P, ten-g, huong: huong-ten-g, mau: mau-g, cach: 7pt) } + } + if them != none { them(ctx) } + }) +} + +// ---------- Căn thức: y = √x ---------- +#let do-thi-can(w: 7cm, mau: blue, ten: $y = sqrt(x)$, goc-ten: "above-right", luoi-o: false, vach: false, them: none) = do-thi-ham( + x => calc.sqrt(x), + w: w, xmin: -1.2, xmax: 6.2, ymin: -1, ymax: 3.2, + tu: 0, den: 6, + mau: mau, ten: ten, goc-ten: goc-ten, n: 160, luoi-o: luoi-o, vach: vach, + them: ctx => { + giong(ctx, (1, 1)) + giong(ctx, (4, 2)) + if them != none { them(ctx) } + }, +) + +// ===================================================================== +// TRỤC SỐ — biểu diễn đoạn / khoảng / nửa khoảng (một hoặc nhiều) +// Mỗi khoảng là tuple: (a, b, kieu) hoặc (a, b, kieu, mau) +// a, b : số HOẶC "-oo" / "+oo" (vô cực) — a < b +// kieu : chuỗi 2 kí tự đầu mút trái+phải, chọn trong: +// "[]" đoạn · "()" khoảng · "[)" · "(]" nửa khoảng +// Quy ước (SGK): GIỮ trống vùng nghiệm, GẠCH CHÉO phần loại bỏ; +// [ ] = lấy đầu mút (đặc), ( ) = không lấy (rỗng). +// #truc-so((-2, 3, "[)")) +// #truc-so((-2, 1, "()"), (3, "+oo", "[)")) +// dau: "ngoac" (mặc định) | "cham" (chấm đặc/rỗng) +// ===================================================================== +#let _vc(v) = type(v) == str // v là vô cực ("-oo"/"+oo") ? + +#let truc-so( + ..khoang, + w: 10cm, h: 1.5cm, // h: chiều cao khung cố định (hết khoảng trống dọc) + min: auto, max: auto, + ten: $x$, + mau: rgb("#1d4ed8"), // màu đoạn nghiệm + gach: true, // gạch chéo phần loại bỏ + mau-gach: red, + dau: "ngoac", // kiểu đầu mút: "ngoac" | "cham" + moc-phu: (), // mốc phụ chỉ để ghi số (không thuộc nghiệm) + so: true, + co-chu: 13pt, // cỡ chữ số tại mốc + cao-gach: 2mm, // chiều cao dải gạch chéo (đo trên trang) +) = { + let ks = khoang.pos() + // các đầu mút hữu hạn -> cửa sổ tự động + let huu-han = () + for k in ks { + if not _vc(k.at(0)) { huu-han.push(k.at(0)) } + if not _vc(k.at(1)) { huu-han.push(k.at(1)) } + } + for m in moc-phu { huu-han.push(if type(m) == array { m.at(0) } else { m }) } + let lo = if huu-han.len() > 0 { calc.min(..huu-han) } else { -4 } + let hi = if huu-han.len() > 0 { calc.max(..huu-han) } else { 4 } + let pad = calc.max((hi - lo) * 0.2, 1.5) + let xa = if min == auto { lo - pad } else { min } + let xb = if max == auto { hi + pad } else { max } + hinh(w: w, h: h, xmin: xa, xmax: xb, ymin: -1.1, ymax: 1.1, co-chu: co-chu, ctx => { + // ---- gạch chéo phần loại bỏ (bù của hợp các khoảng) ---- + if gach { + let segs = ks.map(k => { + let a = if _vc(k.at(0)) { xa } else { k.at(0) } + let b = if _vc(k.at(1)) { xb } else { k.at(1) } + (calc.min(a, b), calc.max(a, b)) + }).sorted(key: s => s.at(0)) + let merged = () + for s in segs { + if merged.len() == 0 or s.at(0) > merged.last().at(1) + 0.000001 { + merged.push(s) + } else { + let last = merged.pop() + merged.push((last.at(0), calc.max(last.at(1), s.at(1)))) + } + } + let unc = () + let cur = xa + for s in merged { + if s.at(0) > cur + 0.000001 { unc.push((cur, s.at(0))) } + cur = calc.max(cur, s.at(1)) + } + if cur < xb - 0.000001 { unc.push((cur, xb)) } + // dải gạch cao CỐ ĐỊNH trên trang (không phình theo cửa sổ toạ độ) + let hb = cao-gach / 2 / ctx.sy + for (u1, u2) in unc { + gach-mien( + ctx, ((u1, -hb), (u2, -hb), (u2, hb), (u1, hb)), + goc: 45deg, buoc: 6pt, mau: mau-gach, day: 0.5pt, + ) + } + } + // ---- trục (mũi tên 2 đầu) + nhãn x ---- + doan(ctx, (xa, 0), (xb, 0), day: 1pt) + dau-mui-ten(ctx, (xa, 0), (xb, 0)) + dau-mui-ten(ctx, (xb, 0), (xa, 0)) + nhan(ctx, (xb, 0), ten, huong: "below", cach: 6pt) + // ---- vạch + số tại đầu mút và mốc phụ ---- + let t = 3pt / ctx.sy + let ghi-moc(x) = { + doan(ctx, (x, -t), (x, t), day: 0.9pt) + if so { nhan(ctx, (x, 0), so-toan(x), huong: "below", cach: 6pt) } + } + for k in ks { + if not _vc(k.at(0)) { ghi-moc(k.at(0)) } + if not _vc(k.at(1)) { ghi-moc(k.at(1)) } + } + for m in moc-phu { ghi-moc(if type(m) == array { m.at(0) } else { m }) } + // ---- đoạn nghiệm + đầu mút ---- + let dau-mut(x, mo, ben) = { + // ben: "trai" | "phai" (quyết định chiều ngoặc / hướng) + let m = mau + if dau == "cham" { + let p = toa(ctx, (x, 0)) + if mo { + place(dx: p.at(0) - 3pt, dy: p.at(1) - 3pt, + circle(radius: 3pt, fill: white, stroke: 1pt + m)) + } else { + place(dx: p.at(0) - 3pt, dy: p.at(1) - 3pt, + circle(radius: 3pt, fill: m, stroke: none)) + } + } else { + let sym = if ben == "trai" { if mo { "(" } else { "[" } } + else { if mo { ")" } else { "]" } } + nhan(ctx, (x, 0), text(size: 1.45em, weight: "bold", fill: m)[#sym], huong: "center") + } + } + for k in ks { + let (a, b, kieu) = (k.at(0), k.at(1), k.at(2)) + let m = if k.len() >= 4 { k.at(3) } else { mau } + let xl = if _vc(a) { xa } else { a } + let xr = if _vc(b) { xb } else { b } + doan(ctx, (xl, 0), (xr, 0), mau: m, day: 2.6pt) + if not _vc(a) { + let mo = kieu.at(0) == "(" + if dau == "cham" { dau-mut(a, mo, "trai") } + else { + let sym = if mo { "(" } else { "[" } + nhan(ctx, (a, 0), text(size: 1.45em, weight: "bold", fill: m)[#sym], huong: "center") + } + } + if not _vc(b) { + let mo = kieu.at(1) == ")" + if dau == "cham" { dau-mut(b, mo, "phai") } + else { + let sym = if mo { ")" } else { "]" } + nhan(ctx, (b, 0), text(size: 1.45em, weight: "bold", fill: m)[#sym], huong: "center") + } + } + } + }) +} + +// ===================================================================== +// HYPERBOL x²/a² − y²/b² = 1 (nhập bán trục thực a, ảo b) +// Mặc định vẽ kèm: 2 tiệm cận y = ±(b/a)x, đỉnh A₁A₂, tiêu điểm F₁F₂. +// ===================================================================== +#let hyperbol( + a, b, + w: 8.4cm, + mau: blue, day: 1.4pt, + ten: auto, goc-ten: auto, + tiem-can: true, dinh: true, tieu-diem: true, + ten-dinh: ($A_1$, $A_2$), ten-tieu: ($F_1$, $F_2$), + luoi-o: false, vach: false, + them: none, +) = { + let eexp = t => calc.pow(2.718281828459045, t) + let ch = t => (eexp(t) + eexp(-t)) / 2 + let sh = t => (eexp(t) - eexp(-t)) / 2 + let c = calc.sqrt(a * a + b * b) + let X = calc.max(a * 2.4, c * 1.12) + let Y = (b / a) * X * 0.9 + let T = calc.ln(X / a + calc.sqrt((X / a) * (X / a) - 1)) + let nhanh = s => range(0, 121).map(i => { + let tt = -T + 2 * T * i / 120 + (s * a * ch(tt), b * sh(tt)) + }) + let nd = if ten == auto { + $x^2 / #so-dep(a * a) - y^2 / #so-dep(b * b) = 1$ + } else { ten } + let gt = if goc-ten == auto { (X * 0.3, Y * 0.82) } else { goc-ten } + hinh(w: w, xmin: -X, xmax: X, ymin: -Y, ymax: Y, ctx => { + if luoi-o { luoi(ctx) } + if tiem-can { + let xl = Y * a / b + doan(ctx, (-xl, -Y), (xl, Y), mau: gray.darken(15%), day: 0.9pt, dut: true) + doan(ctx, (-xl, Y), (xl, -Y), mau: gray.darken(15%), day: 0.9pt, dut: true) + } + truc(ctx) + if vach { vach-chia(ctx) } + duong-cong(ctx, nhanh(1), mau: mau, day: day) + duong-cong(ctx, nhanh(-1), mau: mau, day: day) + if dinh { + diem(ctx, (-a, 0), ten: ten-dinh.at(0), huong: "below", bk: 1.9pt, mau: red) + diem(ctx, (a, 0), ten: ten-dinh.at(1), huong: "below", bk: 1.9pt, mau: red) + } + if tieu-diem { + diem(ctx, (-c, 0), ten: ten-tieu.at(0), huong: "above", bk: 1.9pt) + diem(ctx, (c, 0), ten: ten-tieu.at(1), huong: "above", bk: 1.9pt) + } + _nhan-ten(ctx, nd, gt, mau) + if them != none { them(ctx) } + }) +} + +// ===================================================================== +// PARABOL y² = 2px (nhập tham số tiêu p ≠ 0; p > 0 mở phải, p < 0 mở trái) +// Mặc định vẽ kèm: đỉnh O, tiêu điểm F(p/2; 0), đường chuẩn x = −p/2. +// ===================================================================== +#let parabol( + p, + w: 8cm, + mau: blue, day: 1.4pt, + ten: auto, goc-ten: auto, + tieu-diem: true, duong-chuan: true, dinh: true, + ten-tieu: $F$, + luoi-o: false, vach: false, + them: none, +) = { + let s = if p > 0 { 1 } else { -1 } + let Xt = 4.2 + let Y = calc.sqrt(2 * calc.abs(p) * Xt) + let xdc = -p / 2 // đường chuẩn + let xf = p / 2 // tiêu điểm + let xs = (0, s * Xt, xdc, xf) + let xa = calc.min(..xs) - 0.9 + let xb = calc.max(..xs) + 0.9 + let ya = -Y - 0.6 + let yb = Y + 0.6 + let cur = range(0, 121).map(i => { + let yy = -Y + 2 * Y * i / 120 + (yy * yy / (2 * p), yy) + }) + let nd = if ten == auto { $y^2 = #so-dep(2 * p) x$ } else { ten } + // p > 0 mở phải -> nhãn bên trái trống; p < 0 mở trái -> nhãn bên phải + let gt = if goc-ten == auto { if s > 0 { "above-left" } else { "above-right" } } else { goc-ten } + hinh(w: w, xmin: xa, xmax: xb, ymin: ya, ymax: yb, ctx => { + if luoi-o { luoi(ctx) } + if duong-chuan { + doan(ctx, (xdc, ya + 0.2), (xdc, yb - 0.2), mau: gray.darken(15%), day: 1pt, dut: true) + nhan(ctx, (xdc, yb - 0.2), $Delta$, huong: if s > 0 { "above-left" } else { "above-right" }, cach: 3pt) + } + truc(ctx) + if vach { vach-chia(ctx) } + duong-cong(ctx, cur, mau: mau, day: day) + if dinh { diem(ctx, (0, 0), bk: 1.9pt, mau: red) } + if tieu-diem { + diem(ctx, (xf, 0), ten: ten-tieu, huong: "below", bk: 1.9pt, mau: red) + } + _nhan-ten(ctx, nd, gt, mau) + if them != none { them(ctx) } + }) +} + +// ===================================================================== +// ELIP x²/a² + y²/b² = 1 (nhập 2 bán trục a, b như hyperbol) +// Mặc định vẽ kèm: 4 đỉnh (±a; 0), (0; ±b) và 2 tiêu điểm trên trục lớn +// (a > b: F(±c; 0); b > a: F(0; ±c); c = √|a² − b²|). a = b -> đường tròn. +// ===================================================================== +#let duong-elip( + a, b, + w: 8cm, + mau: blue, day: 1.4pt, + ten: auto, goc-ten: "above-right", + dinh: true, tieu-diem: true, + ten-tieu: ($F_1$, $F_2$), + luoi-o: false, vach: false, + them: none, +) = { + let c = calc.sqrt(calc.abs(a * a - b * b)) + let mg = 0.85 + let X = a + mg + let Y = b + mg + let nd = if ten == auto { $x^2 / #so-dep(a * a) + y^2 / #so-dep(b * b) = 1$ } else { ten } + hinh(w: w, xmin: -X, xmax: X, ymin: -Y, ymax: Y, ctx => { + if luoi-o { luoi(ctx) } + truc(ctx) + if vach { vach-chia(ctx) } + elip(ctx, (0, 0), a, b, mau: mau, day: day) + if dinh { + diem(ctx, (-a, 0), ten: $A_1$, huong: "below-left", bk: 1.9pt, mau: red) + diem(ctx, (a, 0), ten: $A_2$, huong: "below-right", bk: 1.9pt, mau: red) + diem(ctx, (0, b), ten: $B_2$, huong: "above-left", bk: 1.9pt, mau: red) + diem(ctx, (0, -b), ten: $B_1$, huong: "below-left", bk: 1.9pt, mau: red) + } + if tieu-diem and c > 0.001 { + if a >= b { + diem(ctx, (-c, 0), ten: ten-tieu.at(0), huong: "above", bk: 1.9pt) + diem(ctx, (c, 0), ten: ten-tieu.at(1), huong: "above", bk: 1.9pt) + } else { + diem(ctx, (0, -c), ten: ten-tieu.at(0), huong: "right", bk: 1.9pt) + diem(ctx, (0, c), ten: ten-tieu.at(1), huong: "right", bk: 1.9pt) + } + } + _nhan-ten(ctx, nd, goc-ten, mau) + if them != none { them(ctx) } + }) +} +// (đồng bộ mount 07/2026) diff --git a/packages/preview/conic-toan/0.3.4/lib/hinh-khong-gian.typ b/packages/preview/conic-toan/0.3.4/lib/hinh-khong-gian.typ new file mode 100644 index 0000000000..5929960dd8 --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/hinh-khong-gian.typ @@ -0,0 +1,925 @@ +// ===================================================================== +// hinh-khong-gian.typ — HÌNH KHÔNG GIAN THPT +// Hình chóp, lăng trụ, hình hộp, lập phương, nón, trụ, cầu, trục Oxyz. +// Nét khuất tự động vẽ đứt theo đúng quy ước SGK. +// +// Mỗi hình đều có tham số `them`: hàm (ctx, dinh) => nội dung, cho phép +// vẽ THÊM điểm/đoạn/thiết diện lên hình bằng chính các đỉnh có sẵn. +// vd: them: (ctx, d) => { diem(ctx, chia(d.S, d.A, 0.5), ten: $M$) } +// ===================================================================== +#import "ve.typ": * +#import "hinh-phang.typ": huong-ra, trong-tam + +// Phép chiếu song song đơn giản: (x, y, z) -> mặt phẳng. +// x: sang phải, y: chiều sâu (ra sau), z: lên trên. +#let chieu(P, k: 0.45, goc: 40deg) = ( + P.at(0) + k * P.at(1) * calc.cos(goc), + P.at(2) + k * P.at(1) * calc.sin(goc), +) + +// Gọi callback `them` nếu có. +#let goi-them(them, ctx, dinh) = { + if them != none { them(ctx, dinh) } +} + +// ---------- HÌNH CHÓP TAM GIÁC S.ABC ---------- +// duong-cao: none | "tam" (chân H trong tam giác) | "dinh-a" (SA vuông đáy) +#let hinh-chop-tam-giac( + w: 7cm, + ten: ($S$, $A$, $B$, $C$), + duong-cao: none, + ten-chan: $H$, + mau: black, mau-phu: red, day: 1.1pt, + them: none, +) = hinh(w: w, xmin: -0.6, xmax: 5.0, ymin: -0.5, ymax: 5.0, ctx => { + let d = ( + A: (0.0, 1.1), + B: (3.1, 0.0), + C: (4.3, 1.9), + S: (2.1, 4.4), + ) + // nét liền + doan(ctx, d.A, d.B, mau: mau, day: day) + doan(ctx, d.S, d.A, mau: mau, day: day) + doan(ctx, d.S, d.B, mau: mau, day: day) + doan(ctx, d.S, d.C, mau: mau, day: day) + doan(ctx, d.B, d.C, mau: mau, day: day) + // nét khuất + doan(ctx, d.A, d.C, mau: mau, day: day, dut: true) + // đường cao + if duong-cao == "tam" { + let H = (2.35, 1.15) + d.insert("H", H) + doan(ctx, d.S, H, mau: mau-phu, day: 1pt, dut: true) + goc-vuong(ctx, H, d.S, d.B, r: 0.28, mau: mau-phu) + diem(ctx, H, ten: ten-chan, huong: "duoi", bk: 1.6pt, mau: mau-phu) + } else if duong-cao == "dinh-a" { + goc-vuong(ctx, d.A, d.S, d.B, r: 0.3, mau: mau-phu) + } + // nhãn đỉnh + let G = trong-tam(d.A, d.B, d.C, d.S) + for (P, t) in ((d.S, d.A, d.B, d.C).zip(ten)) { + diem(ctx, P, ten: t, huong: huong-ra(P, G)) + } + goi-them(them, ctx, d) +}) + +// ---------- HÌNH CHÓP TỨ GIÁC S.ABCD (đáy hình bình hành / vuông) ---------- +// duong-cheo: vẽ 2 đường chéo đáy (đứt) + tâm O. +// duong-cao: none | "tam" (SO vuông đáy, chóp đều) | "dinh-a" (SA vuông đáy) + +#let hinh-chop-tu-giac-thuong( + w: 7.6cm, + ten: ($S$, $A$, $B$, $C$, $D$), + duong-cheo: false, + duong-cao: none, + ten-tam: $O$, + mau: black, mau-phu: red, day: 1.1pt, + them: none, +) = hinh(w: w, xmin: -1, xmax: 5, ymin: -0.5, ymax: 7, ctx => { + let d = ( + A: (-1, 3), + B: (5, 3), + C: (4, 1), + D: (0, 0), // = A + C - B (hình bình hành) + S: (2, 7), + ) + let O = trung-diem(d.A, d.C) + // nét liền + doan(ctx, d.A, d.B, mau: mau, day: day, dut: true) + doan(ctx, d.B, d.C, mau: mau, day: day) + doan(ctx, d.S, d.A, mau: mau, day: day) + doan(ctx, d.S, d.B, mau: mau, day: day) + doan(ctx, d.S, d.C, mau: mau, day: day) + // nét khuất + doan(ctx, d.A, d.D, mau: mau, day: day) + doan(ctx, d.D, d.C, mau: mau, day: day) + doan(ctx, d.S, d.D, mau: mau, day: day) + if duong-cheo or duong-cao == "tam" { + d.insert("O", O) + doan(ctx, d.A, d.C, mau: mau, day: 0.9pt, dut: true) + doan(ctx, d.B, d.D, mau: mau, day: 0.9pt, dut: true) + diem(ctx, O, ten: ten-tam, huong: "duoi", bk: 1.6pt) + } + if duong-cao == "tam" { + doan(ctx, d.S, O, mau: mau-phu, day: 1pt, dut: true) + goc-vuong(ctx, O, d.S, d.C, r: 0.3, mau: mau-phu) + } else if duong-cao == "dinh-a" { + goc-vuong(ctx, d.A, d.S, d.B, r: 0.3, mau: mau-phu) + } + // nhãn đỉnh + let G = trong-tam(d.A, d.B, d.C, d.D, d.S) + for (P, t) in ((d.S, d.A, d.B, d.C, d.D).zip(ten)) { + diem(ctx, P, ten: t, huong: huong-ra(P, G)) + } + goi-them(them, ctx, d) +}) + +#let hinh-chop-tu-giac( + w: 7.6cm, + ten: ($S$, $A$, $B$, $C$, $D$), + duong-cheo: false, + duong-cao: none, + ten-tam: $O$, + mau: black, mau-phu: red, day: 1.1pt, + them: none, +) = hinh(w: w, xmin: 0, xmax: 9, ymin: -0.5, ymax: 6, ctx => { + let d = ( + A: (3, 2), + B: (9, 2), + C: (6, 0), + D: (0, 0), // = A + C - B (hình bình hành) + S: (4.5, 6), + ) + let O = trung-diem(d.A, d.C) + // nét liền + doan(ctx, d.A, d.B, mau: mau, day: day, dut: true) + doan(ctx, d.B, d.C, mau: mau, day: day) + doan(ctx, d.S, d.A, mau: mau, day: day, dut: true) + doan(ctx, d.S, d.B, mau: mau, day: day) + doan(ctx, d.S, d.C, mau: mau, day: day) + // nét khuất + doan(ctx, d.A, d.D, mau: mau, day: day, dut: true) + doan(ctx, d.D, d.C, mau: mau, day: day) + doan(ctx, d.S, d.D, mau: mau, day: day) + if duong-cheo or duong-cao == "tam" { + d.insert("O", O) + doan(ctx, d.A, d.C, mau: mau, day: 0.9pt, dut: true) + doan(ctx, d.B, d.D, mau: mau, day: 0.9pt, dut: true) + diem(ctx, O, ten: ten-tam, huong: "duoi", bk: 1.6pt) + } + if duong-cao == "tam" { + doan(ctx, d.S, O, mau: mau-phu, day: 1pt, dut: true) + goc-vuong(ctx, O, d.S, d.C, r: 0.3, mau: mau-phu) + } else if duong-cao == "dinh-a" { + goc-vuong(ctx, d.A, d.S, d.B, r: 0.3, mau: mau-phu) + } + // nhãn đỉnh + let G = trong-tam(d.A, d.B, d.C, d.D, d.S) + for (P, t) in ((d.S, d.A, d.B, d.C, d.D).zip(ten)) { + diem(ctx, P, ten: t, huong: huong-ra(P, G)) + } + goi-them(them, ctx, d) +}) + +#let hinh-chop-day-hinh-thang( + w: 7.6cm, + ten: ($S$, $A$, $B$, $C$, $D$), + duong-cheo: false, + duong-cao: none, + ten-tam: $O$, + mau: black, mau-phu: red, day: 1.1pt, + them: none, +) = hinh(w: w, xmin: 0, xmax: 9, ymin: -0.5, ymax: 6, ctx => { + let d = ( + A: (2, 2), + B: (9, 2), + C: (3.5, 0), + D: (0, 0), // = A + C - B (hình bình hành) + S: (2, 6), + ) + let O = trung-diem(d.A, d.C) + // nét liền + doan(ctx, d.A, d.B, mau: mau, day: day, dut: true) + doan(ctx, d.B, d.C, mau: mau, day: day) + doan(ctx, d.S, d.A, mau: mau, day: day, dut: true) + doan(ctx, d.S, d.B, mau: mau, day: day) + doan(ctx, d.S, d.C, mau: mau, day: day) + // nét khuất + doan(ctx, d.A, d.D, mau: mau, day: day, dut: true) + doan(ctx, d.D, d.C, mau: mau, day: day) + doan(ctx, d.S, d.D, mau: mau, day: day) + if duong-cheo or duong-cao == "tam" { + d.insert("O", O) + doan(ctx, d.A, d.C, mau: mau, day: 0.9pt, dut: true) + doan(ctx, d.B, d.D, mau: mau, day: 0.9pt, dut: true) + diem(ctx, O, ten: ten-tam, huong: "duoi", bk: 1.6pt) + } + if duong-cao == "tam" { + doan(ctx, d.S, O, mau: mau-phu, day: 1pt, dut: true) + goc-vuong(ctx, O, d.S, d.C, r: 0.3, mau: mau-phu) + } else if duong-cao == "dinh-a" { + goc-vuong(ctx, d.A, d.S, d.B, r: 0.3, mau: mau-phu) + } + // nhãn đỉnh + let G = trong-tam(d.A, d.B, d.C, d.D, d.S) + for (P, t) in ((d.S, d.A, d.B, d.C, d.D).zip(ten)) { + diem(ctx, P, ten: t, huong: huong-ra(P, G)) + } + goi-them(them, ctx, d) +}) + +// ---------- HÌNH CHÓP TAM GIÁC ĐỀU S.ABC ---------- +// S thẳng trên trọng tâm O của đáy; SO vuông góc đáy (nét đứt). +// trung-tuyen: vẽ thêm trung tuyến AM của đáy đi qua O. +#let hinh-chop-tam-giac-deu( + w: 7cm, + ten: ($S$, $A$, $B$, $C$), + ten-tam: $O$, + ten-trung-diem: $M$, // tên trung điểm BC (khi trung-tuyen: true) + trung-tuyen: false, + mau: black, mau-phu: red, day: 1.1pt, + them: none, +) = hinh(w: w, xmin: -0.6, xmax: 6.0, ymin: -0.5, ymax: 5.3, ctx => { + let d = (A: (0.0, 2), B: (1.5, 0.0), C: (6, 2)) + let O = trong-tam(d.A, d.B, d.C) + let S = (O.at(0), O.at(1) + 3.7) + d.insert("O", O) + d.insert("S", S) + // cạnh đáy + doan(ctx, d.A, d.B, mau: mau, day: day) + doan(ctx, d.B, d.C, mau: mau, day: day) + doan(ctx, d.A, d.C, mau: mau, day: day, dut: true) + // cạnh bên + doan(ctx, S, d.A, mau: mau, day: day) + doan(ctx, S, d.B, mau: mau, day: day) + doan(ctx, S, d.C, mau: mau, day: day) + // đường cao SO + if trung-tuyen { + let M = trung-diem(d.B, d.C) + d.insert("M", M) + doan(ctx, d.A, M, mau: mau, day: 0.9pt, dut: true) + diem(ctx, M, ten: ten-trung-diem, huong: "phai", bk: 1.5pt) + } + doan(ctx, S, O, mau: mau-phu, day: 1pt, dut: true) + goc-vuong(ctx, O, S, d.A, r: 0.26, mau: mau-phu) + diem(ctx, O, ten: ten-tam, huong: "duoi", bk: 1.5pt) + let G = trong-tam(d.A, d.B, d.C, S) + for (P, t) in ((S, d.A, d.B, d.C).zip(ten)) { + diem(ctx, P, ten: t, huong: huong-ra(P, G)) + } + goi-them(them, ctx, d) +}) + +// ---------- HÌNH CHÓP TỨ GIÁC ĐỀU S.ABCD ---------- +// (đáy hình vuông, SO vuông góc đáy tại tâm O — vẽ sẵn 2 đường chéo) +#let hinh-chop-tu-giac-deu(w: 7.6cm, ten: ($S$, $A$, $B$, $C$, $D$), ten-tam: $O$, them: none) = { + hinh-chop-tu-giac(w: w, ten: ten, ten-tam: ten-tam, duong-cao: "tam", duong-cheo: true, them: them) +} + +// ---------- HÌNH CHÓP TAM DIỆN VUÔNG O.ABC ---------- +// OA, OB, OC đôi một vuông góc (vẽ kiểu "góc phòng", 3 dấu vuông tại O). +#let hinh-chop-tam-dien-vuong( + w: 6.6cm, + ten: ($O$, $A$, $B$, $C$), + mau: black, mau-phu: red, day: 1.1pt, + them: none, +) = hinh(w: w, xmin: -2.9, xmax: 3.3, ymin: -2.7, ymax: 3.4, ctx => { + let d = (O: (0.0, 0.0), A: (-2.1, -1.9), B: (2.7, -0.4), C: (0.0, 2.8)) + doan(ctx, d.O, d.A, mau: mau, day: day, dut: true) + doan(ctx, d.O, d.B, mau: mau, day: day, dut: true) + doan(ctx, d.O, d.C, mau: mau, day: day, dut: true) + doan(ctx, d.A, d.B, mau: mau, day: day) + doan(ctx, d.B, d.C, mau: mau, day: day) + doan(ctx, d.C, d.A, mau: mau, day: day) + goc-vuong(ctx, d.O, d.A, d.B, r: 0.30, mau: mau-phu) + goc-vuong(ctx, d.O, d.B, d.C, r: 0.38, mau: mau-phu) + goc-vuong(ctx, d.O, d.A, d.C, r: 0.46, mau: mau-phu) + diem(ctx, d.O, ten: ten.at(0), huong: "tren-phai", bk: 1.5pt) + diem(ctx, d.A, ten: ten.at(1), huong: "duoi-trai", bk: 1.5pt) + diem(ctx, d.B, ten: ten.at(2), huong: "phai", bk: 1.5pt) + diem(ctx, d.C, ten: ten.at(3), huong: "tren", bk: 1.5pt) + goi-them(them, ctx, d) +}) + +// ---------- CHÓP ĐÁY TAM GIÁC VUÔNG, CẠNH BÊN VUÔNG GÓC ĐÁY ---------- +// Đáy ABC vuông tại B; SA vuông góc với đáy (S thẳng trên A). +#let hinh-chop-day-tam-giac-vuong( + w: 6.8cm, + ten: ($S$, $A$, $B$, $C$), + mau: black, mau-phu: red, day: 1.1pt, + them: none, +) = hinh(w: w, xmin: -0.7, xmax: 4.6, ymin: -0.6, ymax: 5.9, ctx => { + let d = (A: (0.0, 2), B: (3.4, 0.0), C: (4.6, 2), S: (0.0, 5.5)) + // đáy + doan(ctx, d.A, d.B, mau: mau, day: day) + doan(ctx, d.B, d.C, mau: mau, day: day) + doan(ctx, d.A, d.C, mau: mau, day: day, dut: true) + // cạnh bên + doan(ctx, d.S, d.A, mau: mau, day: day) + doan(ctx, d.S, d.B, mau: mau, day: day) + doan(ctx, d.S, d.C, mau: mau, day: day) + // SA vuông đáy + đáy vuông tại B + goc-vuong(ctx, d.A, d.S, d.B, r: 0.3, mau: mau-phu) + goc-vuong(ctx, d.B, d.A, d.C, r: 0.3, mau: mau-phu) + let G = trong-tam(d.A, d.B, d.C, d.S) + for (P, t) in ((d.S, d.A, d.B, d.C).zip(ten)) { + diem(ctx, P, ten: t, huong: huong-ra(P, G)) + } + goi-them(them, ctx, d) +}) + +// ---------- CHÓP ĐÁY HÌNH CHỮ NHẬT, CẠNH BÊN VUÔNG GÓC ĐÁY ---------- +// Đáy ABCD hình chữ nhật; SA vuông góc đáy (S thẳng trên A). +#let hinh-chop-day-chu-nhat( + w: 7.6cm, + ten: ($S$, $A$, $B$, $C$, $D$), + duong-cheo: false, + mau: black, mau-phu: red, day: 1.1pt, + them: none, +) = hinh(w: w, xmin: -3, xmax: 6.0, ymin: -2, ymax: 5.5, ctx => { + let d = (A: (0.0, 0.0), B: (6, 0.0), C: (3, -2), D: (-3, -2), S: (0.0, 4.8)) + // đáy: nét liền phía trước, nét khuất phía sau + doan(ctx, d.A, d.B, mau: mau, day: day, dut: true) + doan(ctx, d.B, d.C, mau: mau, day: day) + doan(ctx, d.A, d.D, mau: mau, day: day, dut: true) + doan(ctx, d.D, d.C, mau: mau, day: day) + // cạnh bên + doan(ctx, d.S, d.A, mau: mau, day: day, dut: true) + doan(ctx, d.S, d.B, mau: mau, day: day) + doan(ctx, d.S, d.C, mau: mau, day: day) + doan(ctx, d.S, d.D, mau: mau, day: day) + if duong-cheo { + doan(ctx, d.A, d.C, mau: mau, day: 0.9pt, dut: true) + doan(ctx, d.B, d.D, mau: mau, day: 0.9pt, dut: true) + } + // SA vuông với AB và AD + goc-vuong(ctx, d.A, d.S, d.B, r: 0.3, mau: mau-phu) + goc-vuong(ctx, d.A, d.S, d.D, r: 0.42, mau: mau-phu) + let G = trong-tam(d.A, d.B, d.C, d.D, d.S) + for (P, t) in ((d.S, d.A, d.B, d.C, d.D).zip(ten)) { + diem(ctx, P, ten: t, huong: huong-ra(P, G)) + } + goi-them(them, ctx, d) +}) + +// ---------- HÌNH HỘP CHỮ NHẬT / LẬP PHƯƠNG ABCD.A'B'C'D' ---------- +// Đáy ABCD, đỉnh trên A'B'C'D'. dai, cao: kích thước; sau: độ "lùi" phối cảnh. +#let hinh-hop( + w: 7.6cm, + dai: 3.6, cao: 2.6, sau: 1.5, nghieng: -0.7, + ten: ($A$, $B$, $C$, $D$, $A'$, $B'$, $C'$, $D'$), + duong-cheo: false, // đường chéo không gian AC' (đứt) + mau: black, mau-phu: red, day: 1.1pt, + them: none, +) = { + let dx = -sau * 0.75 + let dy = -sau * 0.62 + hinh( + w: w, + xmin: -dx - 2, xmax: dai - dx , + ymin: -dy - 2, ymax: cao - dy, + ctx => { + let d = ( + A: (0.0, 0.0), + B: (dai, 0.0), + C: (dai + dx, dy), + D: (dx, dy), + ) + d.insert("A1", (d.A.at(0) + nghieng, d.A.at(1) + cao)) + d.insert("B1", (d.B.at(0) + nghieng, d.B.at(1) + cao)) + d.insert("C1", (d.C.at(0) + nghieng, d.C.at(1) + cao)) + d.insert("D1", (d.D.at(0) + nghieng, d.D.at(1) + cao)) + // nét liền: mặt trước, mặt phải, mặt trên + duong-cong(ctx, (d.A1, d.B1, d.C1, d.D1), mau: mau, day: day, dong: true) + + + doan(ctx, d.B, d.C, mau: mau, day: day) + doan(ctx, d.D, d.C, mau: mau, day: day) + doan(ctx, d.D, d.D1, mau: mau, day: day) + doan(ctx, d.B, d.B1, mau: mau, day: day) + doan(ctx, d.C, d.C1, mau: mau, day: day) + // nét khuất: 3 cạnh tại đỉnh A + doan(ctx, d.A, d.D, mau: mau, day: day, dut: true) + doan(ctx, d.A, d.B, mau: mau, day: day, dut: true) + doan(ctx, d.A, d.A1, mau: mau, day: day, dut: true) + + if duong-cheo { + doan(ctx, d.A, d.C1, mau: mau-phu, day: 1pt, dut: true) + } + let G = trong-tam(d.A, d.C1) + for (P, t) in ((d.A, d.B, d.C, d.D, d.A1, d.B1, d.C1, d.D1).zip(ten)) { + diem(ctx, P, ten: t, huong: huong-ra(P, G)) + } + goi-them(them, ctx, d) + }, + ) +} + +// Hình lập phương cạnh chuẩn. +#let hinh-lap-phuong(w: 7cm, ten: ($A$, $B$, $C$, $D$, $A'$, $B'$, $C'$, $D'$), duong-cheo: false, them: none) = { + hinh-hop(w: w, dai: 2.8, cao: 2.8, sau: 1.5, nghieng: 0, ten: ten, duong-cheo: duong-cheo, them: them) +} + +// Hình lập phương cạnh chuẩn. +#let hinh-hop-chu-nhat(w: 7cm, ten: ($A$, $B$, $C$, $D$, $A'$, $B'$, $C'$, $D'$), duong-cheo: false, them: none) = { + hinh-hop(w: w, dai: 4, cao: 2.8, sau: 1.5, nghieng: 0, ten: ten, duong-cheo: duong-cheo, them: them) +} + + +// ---------- LĂNG TRỤ TAM GIÁC ABC.A'B'C' (đứng) ---------- +#let hinh-lang-tru-tam-giac( + w: 6.6cm, + cao: 3.0, + ten: ($A$, $B$, $C$, $A'$, $B'$, $C'$), + mau: black, day: 1.1pt, + them: none, +) = hinh(w: w, xmin: -0.6, xmax: 4.4, ymin: -0.5, ymax: cao + 1.9, ctx => { + let d = ( + A: (0.0, 0.0), + B: (3.4, -0.0), + C: (2.4, 1.3), + ) + d.insert("A1", (d.A.at(0), d.A.at(1) + cao)) + d.insert("B1", (d.B.at(0), d.B.at(1) + cao)) + d.insert("C1", (d.C.at(0), d.C.at(1) + cao)) + // đáy trên: nét liền + da-giac(ctx, (d.A1, d.B1, d.C1), mau: mau, day: day) + // cạnh bên + doan(ctx, d.A, d.A1, mau: mau, day: day) + doan(ctx, d.B, d.B1, mau: mau, day: day) + doan(ctx, d.C, d.C1, mau: mau, day: day, dut: true) + // đáy dưới + doan(ctx, d.A, d.B, mau: mau, day: day) + doan(ctx, d.A, d.C, mau: mau, day: day, dut: true) + doan(ctx, d.B, d.C, mau: mau, day: day, dut: true) + let G = trong-tam(d.A, d.B, d.C, d.A1, d.B1, d.C1) + for (P, t) in ((d.A, d.B, d.C, d.A1, d.B1, d.C1).zip(ten)) { + diem(ctx, P, ten: t, huong: huong-ra(P, G)) + } + goi-them(them, ctx, d) +}) + +// ---------- HÌNH NÓN ---------- +#let hinh-non( + w: 5.6cm, + r: 1.6, cao: 3.4, + ten-dinh: $S$, ten-tam: $O$, ten-bk: $r$, + truc: true, + mau: black, mau-phu: red, day: 1.1pt, + them: none, +) = hinh(w: w, xmin: -r - 0.7, xmax: r + 0.7, ymin: -r * 0.42 - 0.5, ymax: cao + 0.6, ctx => { + let ry = r * 0.32 + let S = (0.0, cao) + let O = (0.0, 0.0) + let d = (S: S, O: O, T1: (-r, 0.0), T2: (r, 0.0)) + // đáy: nửa trước liền, nửa sau đứt + cung-elip(ctx, O, r, ry, tu: 180deg, den: 360deg, mau: mau, day: day) + cung-elip(ctx, O, r, ry, tu: 0deg, den: 180deg, mau: mau, day: day, dut: true) + // hai đường sinh + doan(ctx, S, d.T1, mau: mau, day: day) + doan(ctx, S, d.T2, mau: mau, day: day) + if truc { + doan(ctx, S, O, mau: mau-phu, day: 1pt, dut: true) + doan(ctx, O, d.T2, mau: mau-phu, day: 1pt) + goc-vuong(ctx, O, S, d.T2, r: 0.24, mau: mau-phu) + nhan(ctx, (r / 2, 0), ten-bk, huong: "duoi", mau: mau-phu) + diem(ctx, O, ten: ten-tam, huong: "duoi-trai", bk: 1.5pt) + } + diem(ctx, S, ten: ten-dinh, huong: "tren", bk: 1.5pt) + goi-them(them, ctx, d) +}) + +// ---------- HÌNH TRỤ ---------- +#let hinh-tru( + w: 5.6cm, + r: 1.5, cao: 3.2, + ten-tam: ($O$, $O'$), ten-bk: $r$, + truc: true, + mau: black, mau-phu: red, day: 1.1pt, + them: none, +) = hinh(w: w, xmin: -r - 0.7, xmax: r + 0.7, ymin: -r * 0.42 - 0.4, ymax: cao + r * 0.42 + 0.5, ctx => { + let ry = r * 0.3 + let O = (0.0, 0.0) + let O1 = (0.0, cao) + let d = (O: O, O1: O1) + // đáy dưới: nửa trước liền, nửa sau đứt + cung-elip(ctx, O, r, ry, tu: 180deg, den: 360deg, mau: mau, day: day) + cung-elip(ctx, O, r, ry, tu: 0deg, den: 180deg, mau: mau, day: day, dut: true) + // đáy trên: liền hoàn toàn + cung-elip(ctx, O1, r, ry, tu: 0deg, den: 360deg, mau: mau, day: day) + // hai cạnh bên + doan(ctx, (-r, 0), (-r, cao), mau: mau, day: day) + doan(ctx, (r, 0), (r, cao), mau: mau, day: day) + if truc { + doan(ctx, O, O1, mau: mau-phu, day: 1pt, dut: true) + doan(ctx, O1, (r, cao), mau: mau-phu, day: 1pt) + nhan(ctx, (r / 2, cao), ten-bk, huong: "tren", mau: mau-phu) + diem(ctx, O, ten: ten-tam.at(0), huong: "trai", bk: 1.5pt) + diem(ctx, O1, ten: ten-tam.at(1), huong: "trai", bk: 1.5pt) + } + goi-them(them, ctx, d) +}) + +// ---------- HÌNH CẦU ---------- +#let hinh-cau( + w: 5.6cm, + r: 2.0, + ten-tam: $O$, ten-bk: $R$, + ban-kinh: true, + mau: black, mau-phu: red, day: 1.1pt, + them: none, +) = hinh(w: w, xmin: -r - 0.5, xmax: r + 0.5, ymin: -r - 0.5, ymax: r + 0.5, ctx => { + let O = (0.0, 0.0) + let ry = r * 0.32 + duong-tron(ctx, O, r, mau: mau, day: day) + // xích đạo: nửa trước liền, nửa sau đứt + cung-elip(ctx, O, r, ry, tu: 180deg, den: 360deg, mau: mau, day: 0.9pt) + cung-elip(ctx, O, r, ry, tu: 0deg, den: 180deg, mau: mau, day: 0.9pt, dut: true) + diem(ctx, O, ten: ten-tam, huong: "duoi-trai", bk: 1.5pt) + if ban-kinh { + let M = (r * calc.cos(40deg), r * calc.sin(40deg)) + doan(ctx, O, M, mau: mau-phu, day: 1pt) + diem(ctx, M, ten: none, bk: 1.5pt, mau: mau-phu) + nhan(ctx, trung-diem(O, M), ten-bk, huong: "tren-trai", mau: mau-phu) + } + goi-them(them, ctx, (O: O)) +}) + +// ---------- HỆ TRỤC TOẠ ĐỘ Oxyz ---------- +#let truc-oxyz( + w: 6cm, + ten: ($x$, $y$, $z$), + ten-goc: $O$, + don-vi: false, // vẽ 3 điểm đơn vị i, j, k + mau: black, day: 1pt, + them: none, +) = hinh(w: w, xmin: -2.2, xmax: 2.6, ymin: -1.9, ymax: 2.6, ctx => { + let O = (0.0, 0.0) + // Ox: chéo xuống trái (hướng về người nhìn), Oy: phải, Oz: lên + let X = (-1.55, -1.55) + let Y = (2.3, 0.0) + let Z = (0.0, 2.3) + mui-ten(ctx, O, X, mau: mau, day: day) + mui-ten(ctx, O, Y, mau: mau, day: day) + mui-ten(ctx, O, Z, mau: mau, day: day) + nhan(ctx, X, ten.at(0), huong: "duoi-trai") + nhan(ctx, Y, ten.at(1), huong: "duoi") + nhan(ctx, Z, ten.at(2), huong: "phai") + diem(ctx, O, ten: ten-goc, huong: "tren-phai", bk: 1.5pt) + if don-vi { + vecto(ctx, O, (-0.62, -0.62), ten: $arrow(i)$, huong: "duoi", mau: red) + vecto(ctx, O, (0.9, 0), ten: $arrow(j)$, huong: "tren", mau: red) + vecto(ctx, O, (0, 0.9), ten: $arrow(k)$, huong: "trai", mau: red) + } + goi-them(them, ctx, (O: O)) +}) + +// ===================================================================== +// HỆ TRỤC Oxyz CÓ TOẠ ĐỘ THẬT — điểm, vectơ, hộp gióng theo đơn vị +// Phép chiếu: Oy sang phải, Oz lên trên, Ox chéo xuống-trái về người nhìn. +// #oxyz(x: 5, y: 8, z: 8, them: (ctx, t3) => { +// diem-oxyz(ctx, t3, (5, 8, 8), ten: $B$, huong: "above-right") +// giong-oxyz(ctx, t3, (5, 8, 8)) +// vecto-oxyz(ctx, t3, (0, 0, 0), (5, 8, 8), mau: purple) +// }) +// `them` nhận (ctx, t3): t3 đổi (x, y, z) -> điểm 2D, dùng được với MỌI +// hàm vẽ phẳng: diem(ctx, t3((1,2,3)), ...), doan(ctx, t3(A), t3(B), ...). +// ===================================================================== +// Điểm 3D: chấm + nhãn. +#let diem-oxyz(ctx, t3, P, ten: none, huong: "above", bk: 2pt, mau: black) = { + diem(ctx, t3(P), ten: ten, huong: huong, bk: bk, mau: mau) +} + +// Đoạn / vectơ nối hai điểm 3D. +#let doan-oxyz(ctx, t3, A, B, mau: black, day: 1pt, dut: false) = { + doan(ctx, t3(A), t3(B), mau: mau, day: day, dut: dut) +} +#let vecto-oxyz(ctx, t3, A, B, ten: none, huong: "tren", mau: black, day: 1.1pt, dut: false) = { + vecto(ctx, t3(A), t3(B), ten: ten, huong: huong, mau: mau, day: day, dut: dut) +} + +// Hộp gióng nét đứt từ gốc O đến điểm P = (a, b, c): vẽ các cạnh của +// hình hộp chữ nhật [0,a]×[0,b]×[0,c] (trừ 3 cạnh nằm trên trục). +#let giong-oxyz(ctx, t3, P, mau: blue, day: 0.8pt) = { + let (a, b, c) = (P.at(0), P.at(1), P.at(2)) + let canh = () + if a != 0 and b != 0 { + canh += (((a, 0, 0), (a, b, 0)), ((0, b, 0), (a, b, 0))) + } + if c != 0 { + if a != 0 or b != 0 { canh += (((a, b, 0), (a, b, c)),) } + if a != 0 { canh += (((0, 0, c), (a, 0, c)), ((a, 0, 0), (a, 0, c))) } + if b != 0 { canh += (((0, 0, c), (0, b, c)), ((0, b, 0), (0, b, c))) } + if a != 0 and b != 0 { + canh += (((a, 0, c), (a, b, c)), ((0, b, c), (a, b, c))) + } + } + for (A, B) in canh { + doan(ctx, t3(A), t3(B), mau: mau, day: day, dut: true) + } +} + +// Các mốc bội của buoc trong [a, b], bỏ 0 (vạch chia/số/lưới của oxyz). +#let _moc-oxyz(a, b, buoc) = { + let kq = () + let t = calc.ceil(a / buoc - 0.0001) * buoc + while t <= b + 0.0001 { + if calc.abs(t) > 0.0001 { kq.push(t) } + t = t + buoc + } + kq +} + +// Phạm vi một trục: n -> (0, n) | số âm -> (n, 0) | tuple (mn, mx) giữ nguyên. +#let _mien-oxyz(v) = if type(v) == array { + (v.at(0), v.at(1)) +} else if v < 0 { (v, 0) } else { (0, v) } + +// Định dạng số gọn cho nhãn trục: 2.0 -> "2", 0.5 -> "0.5". +#let _so-oxyz(t) = if calc.abs(t - calc.round(t)) < 0.0001 { + str(int(calc.round(t))) +} else { str(t) } + +#let oxyz( + w: 7cm, + x: 4, y: 5, z: 4, // phạm vi trục: n = [0, n] | số âm = [n, 0] + // | tuple (mn, mx) = hai phía, vd x: (-2, 3) + am: 0.8, // kéo dài thêm trục quá phạm vi (phía không mũi tên) + ten: ($x$, $y$, $z$), ten-goc: $O$, + don-vi: true, // vẽ 3 vectơ đơn vị i, j, k + mau-don-vi: (red, purple, green.darken(30%)), + vach: false, // vạch chia đơn vị trên 3 trục + so: false, // ghi số tại các vạch (bỏ qua 0) + buoc: 1, // bước của vạch chia / số / lưới + luoi: (), // lưới trên mặt phẳng toạ độ: true = cả 3, + // hoặc tuple chọn: ("xy",), ("xy", "xz", "yz") + mau-luoi: luma(84%), + k: 0.55, goc: 35deg, // hệ số & góc chiếu của trục Ox + mau: black, day: 0.9pt, + co-chu: 10pt, + them: none, // (ctx, t3) => nội dung vẽ thêm +) = { + let t3 = P => ( + P.at(1) - k * P.at(0) * calc.cos(goc), + P.at(2) - k * P.at(0) * calc.sin(goc), + ) + let dm = 0.9 // lề cho mũi tên + nhãn + // phạm vi từng trục (chấp nhận số âm / tuple 2 phía) + let (xa, xb) = _mien-oxyz(x) + let (ya, yb) = _mien-oxyz(y) + let (za, zb) = _mien-oxyz(z) + // cửa sổ: bao 8 đỉnh hộp phạm vi và đầu mút các trục + let chot = ( + (xb + am + dm, 0, 0), (xa - am, 0, 0), + (0, yb + am + dm, 0), (0, ya - am, 0), + (0, 0, zb + am + dm), (0, 0, za - am), + ) + for cx in (xa, xb) { + for cy in (ya, yb) { + for cz in (za, zb) { chot.push((cx, cy, cz)) } + } + } + let chot = chot.map(t3) + let xs = chot.map(p => p.at(0)) + let ys = chot.map(p => p.at(1)) + let (x1, x2) = (calc.min(..xs) - 0.35, calc.max(..xs) + 0.35) + let (y1, y2) = (calc.min(..ys) - 0.35, calc.max(..ys) + 0.35) + hinh(w: w, xmin: x1, xmax: x2, ymin: y1, ymax: y2, co-chu: co-chu, ctx => { + // lưới trên các mặt phẳng toạ độ (vẽ TRƯỚC để nằm dưới trục) + let mp = if luoi == true { ("xy", "xz", "yz") } else if luoi == none or luoi == false { () } else if type(luoi) == str { (luoi,) } else { luoi } + for m in mp { + if m == "xy" { + for i in _moc-oxyz(xa, xb, buoc) { doan(ctx, t3((i, ya, 0)), t3((i, yb, 0)), mau: mau-luoi, day: 0.5pt) } + for j in _moc-oxyz(ya, yb, buoc) { doan(ctx, t3((xa, j, 0)), t3((xb, j, 0)), mau: mau-luoi, day: 0.5pt) } + } else if m == "xz" { + for i in _moc-oxyz(xa, xb, buoc) { doan(ctx, t3((i, 0, za)), t3((i, 0, zb)), mau: mau-luoi, day: 0.5pt) } + for l in _moc-oxyz(za, zb, buoc) { doan(ctx, t3((xa, 0, l)), t3((xb, 0, l)), mau: mau-luoi, day: 0.5pt) } + } else if m == "yz" { + for j in _moc-oxyz(ya, yb, buoc) { doan(ctx, t3((0, j, za)), t3((0, j, zb)), mau: mau-luoi, day: 0.5pt) } + for l in _moc-oxyz(za, zb, buoc) { doan(ctx, t3((0, ya, l)), t3((0, yb, l)), mau: mau-luoi, day: 0.5pt) } + } + } + mui-ten(ctx, t3((xa - am, 0, 0)), t3((xb + am + dm, 0, 0)), mau: mau, day: day) + mui-ten(ctx, t3((0, ya - am, 0)), t3((0, yb + am + dm, 0)), mau: mau, day: day) + mui-ten(ctx, t3((0, 0, za - am)), t3((0, 0, zb + am + dm)), mau: mau, day: day) + nhan(ctx, t3((xb + am + dm, 0, 0)), ten.at(0), huong: "below-left", cach: 5pt) + nhan(ctx, t3((0, yb + am + dm, 0)), ten.at(1), huong: "below", cach: 5pt) + nhan(ctx, t3((0, 0, zb + am + dm)), ten.at(2), huong: "left", cach: 5pt) + nhan(ctx, t3((0, 0, 0)), ten-goc, huong: "below-right", cach: 4pt) + // vạch chia đơn vị + số trên 3 trục (vạch vuông góc với trục trên trang) + if vach or so { + let bo3 = ( + ((1, 0, 0), xa, xb, tm => t3((tm, 0, 0)), "below"), + ((0, 1, 0), ya, yb, tm => t3((0, tm, 0)), "below"), + ((0, 0, 1), za, zb, tm => t3((0, 0, tm)), "left"), + ) + for (u, lo, hi, f, hg) in bo3 { + let o2 = toa-pt(ctx, t3((0, 0, 0))) + let u2 = toa-pt(ctx, t3(u)) + let (dx, dy) = (u2.at(0) - o2.at(0), u2.at(1) - o2.at(1)) + let l = calc.sqrt(dx * dx + dy * dy) + let (nx, ny) = (-dy / l, dx / l) + for tm in _moc-oxyz(lo, hi, buoc) { + let p = toa-pt(ctx, f(tm)) + if vach { + doan-pt( + (p.at(0) - 2.2 * nx, p.at(1) - 2.2 * ny), + (p.at(0) + 2.2 * nx, p.at(1) + 2.2 * ny), + mau: mau, day: 0.8pt, + ) + } + if so { nhan(ctx, f(tm), $#_so-oxyz(tm)$, huong: hg, cach: 4pt) } + } + } + } + if don-vi { + vecto(ctx, t3((0, 0, 0)), t3((1, 0, 0)), ten: $arrow(i)$, huong: "trai", mau: mau-don-vi.at(0), day: 1.2pt) + vecto(ctx, t3((0, 0, 0)), t3((0, 1, 0)), ten: $arrow(j)$, huong: "tren", mau: mau-don-vi.at(1), day: 1.2pt) + vecto(ctx, t3((0, 0, 0)), t3((0, 0, 1)), ten: $arrow(k)$, huong: "trai", mau: mau-don-vi.at(2), day: 1.2pt) + } + if them != none { them(ctx, t3) } + }) +} + +// ===================================================================== +// LỤC GIÁC ĐỀU & CHÓP CỤT ĐỀU — engine chiếu + phân loại nét khuất +// Đáy đa giác đều NẰM NGANG (z = const), nhìn chếch từ trước-trên. +// Nét khuất (đáy sau + cạnh bên tới đỉnh sau) TỰ vẽ đứt theo hình chiếu. +// #hinh-chop-luc-giac-deu() — chóp lục giác đều S.ABCDEF +// #hinh-lang-tru-luc-giac-deu() — lăng trụ lục giác đều ABCDEF.A'…F' +// #hinh-chop-cut-deu(n: 3|4|6) — chóp cụt đều tam/tứ/lục giác +// ===================================================================== +// Chiếu điểm 3D (x, y, z) -> điểm 2D trên trang (y = chiều sâu ra sau). +#let _pr(P) = ( + P.at(0) + 0.5 * P.at(1) * calc.cos(40deg), + P.at(2) + 0.5 * P.at(1) * calc.sin(40deg), +) + +// Đỉnh đa giác đều n cạnh, tâm trục Oz, bán kính R, độ cao z, lệch pha. +#let _dinh-deu(n, R, z, pha) = range(n).map(i => { + let a = pha + i * 360deg / n + (R * calc.cos(a), R * calc.sin(a), z) +}) + +// Pha mặc định: đỉnh A rơi vào phía trước-trái, các cạnh trước hiện rõ. +// n = 6 dùng 210° (như n = 3): các đỉnh toả đều 2 phía, D và E (hay D', E') +// không dồn sát nhau bên phải như pha 180° cũ. +#let _pha-deu(n) = if n == 3 { 210deg } else if n == 4 { 225deg } else { 210deg } + +// Phân loại nét khuất của đáy: trả (canh-sau, dinh-sau). +// P2: đỉnh đáy đã chiếu; y3: độ sâu 3D tương ứng. Đáy lồi -> hai chuỗi +// giữa đỉnh trái-nhất và phải-nhất; chuỗi có độ sâu lớn hơn là "sau" (khuất). +#let _khuat(P2, y3) = { + let n = P2.len() + let L = 0 + let R = 0 + for i in range(n) { + if P2.at(i).at(0) < P2.at(L).at(0) { L = i } + if P2.at(i).at(0) > P2.at(R).at(0) { R = i } + } + let ch1 = () + let i = L + while i != R { ch1.push(i); i = calc.rem(i + 1, n) } + ch1.push(R) + let ch2 = () + let j = L + while j != R { ch2.push(j); j = calc.rem(j + n - 1, n) } + ch2.push(R) + let tb(ch) = ch.map(k => y3.at(k)).sum() / ch.len() + let sau = if tb(ch1) >= tb(ch2) { ch1 } else { ch2 } + let canh-sau = () + for t in range(sau.len() - 1) { canh-sau.push((sau.at(t), sau.at(t + 1))) } + (canh-sau: canh-sau, dinh-sau: sau.slice(1, sau.len() - 1)) +} +#let _la-khuat(canh, i, j) = canh.any(c => (c.at(0) == i and c.at(1) == j) or (c.at(0) == j and c.at(1) == i)) + +// Cửa sổ bao các điểm 2D + lề. +#let _cua-so-2d(pts, mg: 0.7) = { + let xs = pts.map(p => p.at(0)) + let ys = pts.map(p => p.at(1)) + (xmin: calc.min(..xs) - mg, xmax: calc.max(..xs) + mg, ymin: calc.min(..ys) - mg, ymax: calc.max(..ys) + mg) +} + +// ---------- CHÓP LỤC GIÁC ĐỀU S.ABCDEF ---------- +#let hinh-chop-luc-giac-deu( + w: 7cm, + R: 2.0, cao: 3.8, + ten: ($S$, $A$, $B$, $C$, $D$, $E$, $F$), + ten-tam: $O$, + duong-cao: true, + mau: black, mau-phu: red, day: 1.1pt, + them: none, +) = { + let n = 6 + let B3 = _dinh-deu(n, R, 0, _pha-deu(n)) + let S3 = (0, 0, cao) + let O3 = (0, 0, 0) + let B2 = B3.map(_pr) + let S2 = _pr(S3) + let O2 = _pr(O3) + let kl = _khuat(B2, B3.map(P => P.at(1))) + let cs = _cua-so-2d(B2 + (S2,)) + hinh(w: w, xmin: cs.xmin, xmax: cs.xmax, ymin: cs.ymin, ymax: cs.ymax, ctx => { + // cạnh đáy: sau đứt + for i in range(n) { + let j = calc.rem(i + 1, n) + doan(ctx, B2.at(i), B2.at(j), mau: mau, day: day, dut: _la-khuat(kl.canh-sau, i, j)) + } + // cạnh bên: tới đỉnh sau thì đứt + for i in range(n) { + doan(ctx, S2, B2.at(i), mau: mau, day: day, dut: kl.dinh-sau.contains(i)) + } + if duong-cao { + doan(ctx, S2, O2, mau: mau-phu, day: 1pt, dut: true) + goc-vuong(ctx, O2, S2, B2.at(0), r: 0.26, mau: mau-phu) + diem(ctx, O2, ten: ten-tam, huong: "below", bk: 1.5pt, mau: mau-phu) + } + let G = trong-tam(..B2, S2) + diem(ctx, S2, ten: ten.at(0), huong: huong-ra(S2, G)) + for i in range(n) { diem(ctx, B2.at(i), ten: ten.at(i + 1), huong: huong-ra(B2.at(i), G)) } + if them != none { them(ctx, (S: S3, O: O3, day: B3)) } + }) +} + +// ---------- LĂNG TRỤ LỤC GIÁC ĐỀU ABCDEF.A'B'C'D'E'F' ---------- +#let hinh-lang-tru-luc-giac-deu( + w: 7cm, + R: 1.9, cao: 3.4, + ten: ($A$, $B$, $C$, $D$, $E$, $F$, $A'$, $B'$, $C'$, $D'$, $E'$, $F'$), + mau: black, day: 1.1pt, + them: none, +) = { + let n = 6 + let pha = _pha-deu(n) + let B3 = _dinh-deu(n, R, 0, pha) + let T3 = _dinh-deu(n, R, cao, pha) + let B2 = B3.map(_pr) + let T2 = T3.map(_pr) + let kl = _khuat(B2, B3.map(P => P.at(1))) + let cs = _cua-so-2d(B2 + T2) + hinh(w: w, xmin: cs.xmin, xmax: cs.xmax, ymin: cs.ymin, ymax: cs.ymax, ctx => { + // đáy trên: liền hết + for i in range(n) { + let j = calc.rem(i + 1, n) + doan(ctx, T2.at(i), T2.at(j), mau: mau, day: day) + } + // cạnh bên + for i in range(n) { + doan(ctx, B2.at(i), T2.at(i), mau: mau, day: day, dut: kl.dinh-sau.contains(i)) + } + // đáy dưới: sau đứt + for i in range(n) { + let j = calc.rem(i + 1, n) + doan(ctx, B2.at(i), B2.at(j), mau: mau, day: day, dut: _la-khuat(kl.canh-sau, i, j)) + } + let Gb = trong-tam(..B2) + let Gt = trong-tam(..T2) + for i in range(n) { diem(ctx, B2.at(i), ten: ten.at(i), huong: huong-ra(B2.at(i), Gt)) } + for i in range(n) { diem(ctx, T2.at(i), ten: ten.at(i + n), huong: huong-ra(T2.at(i), Gb)) } + if them != none { them(ctx, (duoi: B3, tren: T3)) } + }) +} + +// ---------- CHÓP CỤT ĐỀU (tam/tứ/lục giác) ABCD….A'B'C'D'… ---------- +// n: 3 | 4 | 6. R: bán kính đáy lớn (dưới), r: bán kính đáy nhỏ (trên). +#let hinh-chop-cut-luc-giac-deu( + n: 6, + w: 7cm, + R: 2.3, r: 1.25, cao: 3.0, + ten: auto, // auto: A,B,… (đáy dưới) + phẩy (đáy trên); hoặc mảng 2n nội dung + mau: black, day: 1.1pt, + truc: false, ten-tam: ($O$, $O'$), + mau-phu: red, + them: none, +) = { + let pha = _pha-deu(n) + let B3 = _dinh-deu(n, R, 0, pha) + let T3 = _dinh-deu(n, r, cao, pha) + let B2 = B3.map(_pr) + let T2 = T3.map(_pr) + let kl = _khuat(B2, B3.map(P => P.at(1))) + let chu = ("A", "B", "C", "D", "E", "F", "G", "H") + let ten2 = if ten == auto { + range(n).map(i => eval(chu.at(i), mode: "math")) + range(n).map(i => eval(chu.at(i) + "'", mode: "math")) + } else { ten } + let O3 = (0, 0, 0) + let O13 = (0, 0, cao) + let cs = _cua-so-2d(B2 + T2) + hinh(w: w, xmin: cs.xmin, xmax: cs.xmax, ymin: cs.ymin, ymax: cs.ymax, ctx => { + // đáy trên: liền hết + for i in range(n) { + let j = calc.rem(i + 1, n) + doan(ctx, T2.at(i), T2.at(j), mau: mau, day: day) + } + // cạnh bên + for i in range(n) { + doan(ctx, B2.at(i), T2.at(i), mau: mau, day: day, dut: kl.dinh-sau.contains(i)) + } + // đáy dưới: sau đứt + for i in range(n) { + let j = calc.rem(i + 1, n) + doan(ctx, B2.at(i), B2.at(j), mau: mau, day: day, dut: _la-khuat(kl.canh-sau, i, j)) + } + if truc { + let O2 = _pr(O3) + let O12 = _pr(O13) + doan(ctx, O2, O12, mau: mau-phu, day: 1pt, dut: true) + diem(ctx, O2, ten: ten-tam.at(0), huong: "below", bk: 1.1pt, mau: mau-phu) + diem(ctx, O12, ten: ten-tam.at(1), huong: "above", bk: 1.1pt, mau: mau-phu) + } + let Gb = trong-tam(..B2) + let Gt = trong-tam(..T2) + for i in range(n) { diem(ctx, B2.at(i), ten: ten2.at(i), huong: huong-ra(B2.at(i), Gt)) } + for i in range(n) { diem(ctx, T2.at(i), ten: ten2.at(i + n), huong: huong-ra(T2.at(i), Gb)) } + if them != none { them(ctx, (duoi: B3, tren: T3)) } + }) +} + +// Bí danh chóp cụt đều theo số cạnh đáy (cùng bộ tham số ở trên): +// #hinh-chop-cut-deu(n: 3|4|6) — gọi chung +// #hinh-chop-cut-tam-giac-deu() — ABC.A'B'C' +// #hinh-chop-cut-tu-giac-deu() — ABCD.A'B'C'D' +#let hinh-chop-cut-deu = hinh-chop-cut-luc-giac-deu +#let hinh-chop-cut-tam-giac-deu(..th) = hinh-chop-cut-luc-giac-deu(n: 3, ..th) +#let hinh-chop-cut-tu-giac-deu(..th) = hinh-chop-cut-luc-giac-deu(n: 4, ..th) diff --git a/packages/preview/conic-toan/0.3.4/lib/hinh-phang.typ b/packages/preview/conic-toan/0.3.4/lib/hinh-phang.typ new file mode 100644 index 0000000000..b4b2408e0d --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/hinh-phang.typ @@ -0,0 +1,498 @@ +// ===================================================================== +// hinh-phang.typ — HÌNH HỌC PHẲNG THPT +// Tam giác + các đường đặc biệt, tứ giác, đường tròn nội/ngoại tiếp, +// tiếp tuyến, giao hai đường tròn, đường tròn lượng giác... +// Mọi hàm nhận ctx từ #hinh (xem ve.typ). Toạ độ nên dùng 2 trục cùng tỉ lệ +// (để h: auto trong #hinh). +// ===================================================================== +#import "ve.typ": * + +// Hướng nhãn tự động: đẩy nhãn ra xa điểm G (thường là trọng tâm hình). +#let huong-ra(P, G) = { + let d = calc.atan2(P.at(0) - G.at(0), P.at(1) - G.at(1)).deg() + if d < 0 { d = d + 360 } + let idx = calc.rem(calc.floor((d + 22.5) / 45), 8) + ("phai", "tren-phai", "tren", "tren-trai", "trai", "duoi-trai", "duoi", "duoi-phai").at(idx) +} + +// Trọng tâm của dãy điểm. +#let trong-tam(..pts) = { + let ds = pts.pos() + let n = ds.len() + ( + ds.map(P => P.at(0)).sum() / n, + ds.map(P => P.at(1)).sum() / n, + ) +} + +// Vẽ đa giác + nhãn đỉnh tự động hướng ra ngoài. +// ten: mảng nội dung nhãn (cùng độ dài số đỉnh) hoặc none. +#let da-giac-ten(ctx, dinh, ten: none, mau: black, day: 1.1pt, to: none, cham: true) = { + da-giac(ctx, dinh, mau: mau, day: day, to: to) + let G = trong-tam(..dinh) + if ten != none { + for (P, t) in dinh.zip(ten) { + if cham { + diem(ctx, P, ten: t, huong: huong-ra(P, G)) + } else { + nhan(ctx, P, t, huong: huong-ra(P, G)) + } + } + } +} + +// ---------- Tam giác ---------- +#let tam-giac(ctx, A, B, C, ten: ($A$, $B$, $C$), mau: black, day: 1.1pt, to: none, cham: true) = { + da-giac-ten(ctx, (A, B, C), ten: ten, mau: mau, day: day, to: to, cham: cham) +} + +// Đường cao hạ từ P xuống đường thẳng AB (kèm ký hiệu vuông góc). +// ten-chan: nhãn chân đường cao (vd $H$). +#let duong-cao(ctx, P, A, B, ten-chan: none, mau: red, day: 1pt, dut: false, vuong: true) = { + let H = hinh-chieu(P, A, B) + doan(ctx, P, H, mau: mau, day: day, dut: dut) + if vuong { + let Q = if khoang-cach(H, A) > khoang-cach(H, B) { A } else { B } + goc-vuong(ctx, H, P, Q, mau: mau) + } + if ten-chan != none { + diem(ctx, H, ten: ten-chan, huong: huong-ra(H, trong-tam(P, A, B))) + } +} + +// Trung tuyến từ P đến trung điểm AB (kèm 2 vạch đánh dấu bằng nhau). +#let trung-tuyen(ctx, P, A, B, ten-chan: none, mau: blue, day: 1pt, dut: false, so-vach: 1) = { + let M = trung-diem(A, B) + doan(ctx, P, M, mau: mau, day: day, dut: dut) + danh-dau(ctx, A, M, so: so-vach, mau: mau) + danh-dau(ctx, M, B, so: so-vach, mau: mau) + if ten-chan != none { + diem(ctx, M, ten: ten-chan, huong: huong-ra(M, trong-tam(P, A, B))) + } +} + +// Phân giác trong góc O của tam giác OAB (kèm 2 cung đánh dấu góc bằng nhau). +// vach : số VẠCH gạch ngang cung (0/1/2/3) — ký hiệu hai góc bằng nhau +// so-cung : số CUNG đồng tâm vẽ ở mỗi góc (1/2/3) +// lech : hệ số bán kính cung thứ hai so với cung thứ nhất +// (auto = 1.18 khi KHÔNG đánh dấu, = 1 khi có vạch/nhiều cung — +// vì hai góc bằng nhau thì phải vẽ cùng bán kính mới đúng quy ước) +// ten-goc : nhãn ghi ở CẢ HAI góc (vd $alpha$) — none = không ghi +// Ví dụ: phan-giac(A, B, C, vach: 1) // mỗi góc một vạch +// phan-giac(A, B, C, so-cung: 2) // mỗi góc hai cung đồng tâm +#let phan-giac( + ctx, O, A, B, + ten-chan: none, mau: green.darken(20%), day: 1pt, dut: false, + r-cung: 0.5, vach: 0, so-cung: 1, dai-vach: 6pt, lech: auto, + ten-goc: none, cach-nhan: 1.9, +) = { + let oa = khoang-cach(O, A) + let ob = khoang-cach(O, B) + let D = chia(A, B, oa / (oa + ob)) + doan(ctx, O, D, mau: mau, day: day, dut: dut) + let k = if lech != auto { lech } else if vach > 0 or so-cung > 1 { 1 } else { 1.18 } + goc(ctx, O, A, D, r: r-cung, mau: mau, day: day, + vach: vach, so-cung: so-cung, dai-vach: dai-vach, + ten: ten-goc, cach-nhan: cach-nhan) + goc(ctx, O, D, B, r: r-cung * k, mau: mau, day: day, + vach: vach, so-cung: so-cung, dai-vach: dai-vach, + ten: ten-goc, cach-nhan: cach-nhan) + if ten-chan != none { + diem(ctx, D, ten: ten-chan, huong: huong-ra(D, trong-tam(O, A, B))) + } +} + +// Đường trung trực của đoạn AB (đoạn vuông góc tại trung điểm, dài 2*dai). +#let trung-truc(ctx, A, B, dai: 1.5, mau: purple, day: 1pt, dut: true, so-vach: 1) = { + let M = trung-diem(A, B) + let d = khoang-cach(A, B) + let ux = (B.at(0) - A.at(0)) / d + let uy = (B.at(1) - A.at(1)) / d + let P1 = (M.at(0) - dai * uy, M.at(1) + dai * ux) + let P2 = (M.at(0) + dai * uy, M.at(1) - dai * ux) + doan(ctx, P1, P2, mau: mau, day: day, dut: dut) + goc-vuong(ctx, M, B, P1, mau: mau) + danh-dau(ctx, A, M, so: so-vach, mau: mau) + danh-dau(ctx, M, B, so: so-vach, mau: mau) +} + +// ---------- Tam giác đặc biệt ---------- +// Tam giác đều cạnh `canh`, đỉnh A ở dưới-trái, đáy nằm ngang. +// vach: số vạch đánh dấu cạnh bằng nhau (0 = không vẽ). +#let tam-giac-deu(ctx, A, canh, ten: ($A$, $B$, $C$), mau: black, day: 1.1pt, to: none, vach: 1) = { + let B = (A.at(0) + canh, A.at(1)) + let C = (A.at(0) + canh / 2, A.at(1) + canh * calc.sqrt(3) / 2) + da-giac-ten(ctx, (A, B, C), ten: ten, mau: mau, day: day, to: to) + if vach > 0 { + danh-dau(ctx, A, B, so: vach, mau: mau) + danh-dau(ctx, B, C, so: vach, mau: mau) + danh-dau(ctx, C, A, so: vach, mau: mau) + } +} + +// Tam giác vuông tại A: cạnh AB ngang dài a, cạnh AC đứng dài b. +#let tam-giac-vuong(ctx, A, a, b, ten: ($A$, $B$, $C$), mau: black, day: 1.1pt, to: none) = { + let B = (A.at(0) + a, A.at(1)) + let C = (A.at(0), A.at(1) + b) + da-giac-ten(ctx, (A, B, C), ten: ten, mau: mau, day: day, to: to) + goc-vuong(ctx, A, B, C, mau: mau) +} + +// Tam giác cân tại C: đáy AB ngang dài `canh-day`, chiều cao `chieu-cao`. +#let tam-giac-can(ctx, A, canh-day, chieu-cao, ten: ($A$, $B$, $C$), mau: black, day: 1.1pt, to: none, vach: 1) = { + let B = (A.at(0) + canh-day, A.at(1)) + let C = (A.at(0) + canh-day / 2, A.at(1) + chieu-cao) + da-giac-ten(ctx, (A, B, C), ten: ten, mau: mau, day: day, to: to) + if vach > 0 { + danh-dau(ctx, C, A, so: vach, mau: mau) + danh-dau(ctx, C, B, so: vach, mau: mau) + } +} + +// Tam giác vuông cân tại A, hai cạnh góc vuông dài `canh`. +#let tam-giac-vuong-can(ctx, A, canh, ten: ($A$, $B$, $C$), mau: black, day: 1.1pt, to: none, vach: 1) = { + tam-giac-vuong(ctx, A, canh, canh, ten: ten, mau: mau, day: day, to: to) + if vach > 0 { + let B = (A.at(0) + canh, A.at(1)) + let C = (A.at(0), A.at(1) + canh) + danh-dau(ctx, A, B, so: vach, mau: mau) + danh-dau(ctx, A, C, so: vach, mau: mau) + } +} + +// ---------- Tứ giác thường gặp ---------- +// Tứ giác bất kỳ ABCD. +#let tu-giac(ctx, A, B, C, D, ten: ($A$, $B$, $C$, $D$), mau: black, day: 1.1pt, to: none) = { + da-giac-ten(ctx, (A, B, C, D), ten: ten, mau: mau, day: day, to: to) +} + +// Hình bình hành biết 3 đỉnh liên tiếp A, B, C (D tự tính = A + C - B). +#let hinh-binh-hanh(ctx, A, B, C, ten: ($A$, $B$, $C$, $D$), mau: black, day: 1.1pt, to: none) = { + let D = (A.at(0) + C.at(0) - B.at(0), A.at(1) + C.at(1) - B.at(1)) + da-giac-ten(ctx, (A, B, C, D), ten: ten, mau: mau, day: day, to: to) +} + +// Hình chữ nhật: góc dưới-trái A, chiều rộng r, chiều cao c. +#let hinh-chu-nhat(ctx, A, r, c, ten: ($A$, $B$, $C$, $D$), mau: black, day: 1.1pt, to: none, goc-vg: true) = { + let B = (A.at(0) + r, A.at(1)) + let C = (A.at(0) + r, A.at(1) + c) + let D = (A.at(0), A.at(1) + c) + da-giac-ten(ctx, (A, B, C, D), ten: ten, mau: mau, day: day, to: to) + if goc-vg { goc-vuong(ctx, A, B, D, mau: mau) } +} + +// Hình thang: đáy lớn AB (dưới, dài a), đáy nhỏ DC (trên, dài b), +// cao c, lech: độ lệch ngang của D so với A. +#let hinh-thang(ctx, A, a, b, c, lech: 0.6, ten: ($A$, $B$, $C$, $D$), mau: black, day: 1.1pt, to: none) = { + let B = (A.at(0) + a, A.at(1)) + let D = (A.at(0) + lech, A.at(1) + c) + let C = (D.at(0) + b, D.at(1)) + da-giac-ten(ctx, (A, B, C, D), ten: ten, mau: mau, day: day, to: to) +} + +// ---------- Đường tròn liên quan tam giác / đa giác ---------- +// Khoảng cách từ P tới ĐOẠN thẳng AB (không phải tới đường thẳng AB). +#let _cach-doan(P, A, B) = { + let ux = B.at(0) - A.at(0) + let uy = B.at(1) - A.at(1) + let d2 = ux * ux + uy * uy + if d2 < 1e-12 { return khoang-cach(P, A) } + let t = calc.max(0, calc.min(1, ((P.at(0) - A.at(0)) * ux + (P.at(1) - A.at(1)) * uy) / d2)) + khoang-cach(P, (A.at(0) + t * ux, A.at(1) + t * uy)) +} + +// Khoảng cách từ P tới cạnh GẦN NHẤT của đa giác (mảng đỉnh, khép kín). +#let _cach-canh(P, dinh) = calc.min( + ..range(dinh.len()).map(i => _cach-doan(P, dinh.at(i), dinh.at(calc.rem(i + 1, dinh.len())))), +) + +// 8 hướng đặt nhãn + vectơ tương ứng trong TOẠ ĐỘ TOÁN (y hướng lên). +#let _tam-huong = ( + ("phai", (1, 0)), ("tren-phai", (0.7, 0.7)), ("tren", (0, 1)), ("tren-trai", (-0.7, 0.7)), + ("trai", (-1, 0)), ("duoi-trai", (-0.7, -0.7)), ("duoi", (0, -1)), ("duoi-phai", (0.7, -0.7)), +) + +// Hướng đặt nhãn tâm sao cho chữ KHÔNG rơi lên cạnh nào của đa giác. +#let _huong-thoang(O, dinh, r) = { + let d = _tam-huong.map(((ten, v)) => ( + ten, + _cach-canh((O.at(0) + 0.3 * r * v.at(0), O.at(1) + 0.3 * r * v.at(1)), dinh), + )) + let tot = calc.max(..d.map(x => x.at(1))) + d.find(x => x.at(1) == tot).at(0) +} + +// Đường tròn ngoại tiếp TAM GIÁC hoặc ĐA GIÁC. +// duong-tron-ngoai-tiep(A, B, C) — 3 đỉnh rời (lối cũ) +// duong-tron-ngoai-tiep((A, B, C, D, E)) — MỘT MẢNG đỉnh, đa giác tuỳ ý +// Từ 4 đỉnh trở lên, tâm/bán kính khớp theo bình phương bé nhất (đa giác nội +// tiếp được cho ra đúng đường tròn của nó — xem `tron-qua-diem`). +// ten-tam : nhãn tâm (none = không ghi) +// huong-tam : auto = tự chọn phía THOÁNG nhất, không đè lên cạnh đa giác +// ban-kinh : vẽ bán kính (nét đứt) tới một đỉnh; đỉnh được chọn TỰ ĐỘNG +// sao cho bán kính không nằm đè lên cạnh (vd tam giác vuông: +// tâm ở giữa cạnh huyền, bán kính tới hai đầu huyền là trùng +// cạnh — hàm tự tránh, chọn đỉnh góc vuông) +// dinh-r : chỉ số đỉnh muốn vẽ bán kính tới (auto = tự chọn như trên) +// ten-r : nhãn ghi trên bán kính (vd $R$) +// canh : true = vẽ luôn đa giác (mặc định false, thường vẽ sẵn rồi) +#let duong-tron-ngoai-tiep( + ctx, ..vi-tri, + ten-tam: $O$, huong-tam: auto, + mau: blue, day: 1pt, dut: false, to: none, + ban-kinh: false, dinh-r: auto, ten-r: none, + canh: false, mau-canh: black, +) = { + let ds = vi-tri.pos() + let dinh = if ds.len() == 1 { ds.at(0) } else { ds } + if dinh.len() < 3 { panic("duong-tron-ngoai-tiep: cần ít nhất 3 đỉnh") } + let (O, r) = tron-qua-diem(dinh) + duong-tron(ctx, O, r, mau: mau, day: day, dut: dut, to: to) + if canh { da-giac(ctx, dinh, mau: mau-canh, day: 1.1pt) } + if ban-kinh { + // đỉnh nào có bán kính nằm THOÁNG nhất (không trùng cạnh đa giác) + let i = if dinh-r != auto { dinh-r } else { + let d = dinh.map(V => _cach-canh(trung-diem(O, V), dinh)) + let tot = calc.max(..d) + d.position(x => x == tot) + } + let V = dinh.at(i) + doan(ctx, O, V, mau: mau, day: 0.8pt, dut: true, + ten: ten-r, mau-ten: mau) + } + if ten-tam != none { + diem(ctx, O, ten: ten-tam, + huong: if huong-tam == auto { _huong-thoang(O, dinh, r) } else { huong-tam }) + } +} + +// Đường tròn nội tiếp tam giác ABC. +#let duong-tron-noi-tiep(ctx, A, B, C, ten-tam: $I$, mau: orange.darken(10%), day: 1pt, dut: false, ban-kinh: false) = { + let (I, r) = tam-noi-tiep(A, B, C) + duong-tron(ctx, I, r, mau: mau, day: day, dut: dut) + if ten-tam != none { diem(ctx, I, ten: ten-tam, huong: "tren") } + if ban-kinh { + let H = hinh-chieu(I, A, B) + doan(ctx, I, H, mau: mau, day: 0.8pt, dut: true) + goc-vuong(ctx, H, I, A, r: 0.22, mau: mau) + } +} + +// ---------- Trực tâm ---------- +// Ba đường cao của tam giác ABC + trực tâm H. +// ten : nhãn trực tâm (none = không ghi) +// ten-chan : nhãn 3 chân đường cao theo thứ tự hạ từ A, B, C +// (none = không ghi; mặc định $H_A$, $H_B$, $H_C$) +// vuong : vẽ ký hiệu góc vuông tại chân đường cao +// canh : true = vẽ luôn tam giác ABC (kèm nhãn đỉnh) +#let ve-truc-tam( + ctx, A, B, C, + ten: $H$, + ten-chan: ($H_A$, $H_B$, $H_C$), + ten-dinh: ($A$, $B$, $C$), + canh: true, + mau: black, mau-cao: red, day: 1.1pt, dut: false, vuong: true, +) = { + if canh { tam-giac(ctx, A, B, C, ten: ten-dinh, mau: mau, day: day) } + let H = truc-tam(A, B, C) + for (i, (P, X, Y)) in ((A, B, C), (B, C, A), (C, A, B)).enumerate() { + let K = hinh-chieu(P, X, Y) + // kéo dài tới chân đường cao (tam giác tù thì chân nằm ngoài cạnh) + doan(ctx, P, K, mau: mau-cao, day: 0.9pt, dut: dut) + // nối dài cạnh nếu chân đường cao rơi ra ngoài đoạn XY + let t = ( + ((K.at(0) - X.at(0)) * (Y.at(0) - X.at(0)) + + (K.at(1) - X.at(1)) * (Y.at(1) - X.at(1))) + / calc.pow(khoang-cach(X, Y), 2) + ) + if t < 0 { doan(ctx, X, K, mau: mau, day: 0.7pt, dut: true) } + if t > 1 { doan(ctx, Y, K, mau: mau, day: 0.7pt, dut: true) } + if vuong { goc-vuong(ctx, K, P, if t < 0.5 { Y } else { X }, r: 0.22, mau: mau-cao) } + if ten-chan != none { + diem(ctx, K, bk: 1.6pt, mau: mau-cao, ten: ten-chan.at(i), + huong: huong-ra(K, trong-tam(A, B, C))) + } + } + if ten != none { + diem(ctx, H, mau: mau-cao, ten: ten, huong: huong-ra(H, trong-tam(A, B, C))) + } else { + diem(ctx, H, mau: mau-cao) + } +} + +// ---------- Đường tròn bàng tiếp ---------- +// Đường tròn bàng tiếp TRONG GÓC A của tam giác ABC (tiếp xúc cạnh BC và +// phần kéo dài của AB, AC). Muốn góc B thì gọi duong-tron-bang-tiep(ctx, B, C, A). +// ten-tam : nhãn tâm (mặc định $J$) +// keo-dai : vẽ nét đứt kéo dài hai cạnh AB, AC tới tiếp điểm +// tiep-diem : chấm 3 tiếp điểm +// ban-kinh : vẽ bán kính tới cạnh BC (kèm ký hiệu vuông góc) +#let duong-tron-bang-tiep( + ctx, A, B, C, + ten-tam: $J$, ten-tiep: none, + mau: green.darken(25%), day: 1pt, + keo-dai: true, tiep-diem: true, ban-kinh: false, +) = { + let (J, r) = tam-bang-tiep(A, B, C) + let a = khoang-cach(B, C) + let b = khoang-cach(C, A) + let c = khoang-cach(A, B) + let s = (a + b + c) / 2 + let X = chia(B, C, (s - c) / a) // tiếp điểm trên cạnh BC + let Y = chia(A, B, s / c) // trên tia AB (ngoài B) + let Z = chia(A, C, s / b) // trên tia AC (ngoài C) + if keo-dai { + doan(ctx, B, Y, mau: mau, day: 0.7pt, dut: true) + doan(ctx, C, Z, mau: mau, day: 0.7pt, dut: true) + } + duong-tron(ctx, J, r, mau: mau, day: day) + if tiep-diem { + for (i, P) in (X, Y, Z).enumerate() { + diem(ctx, P, bk: 1.6pt, mau: mau, + ten: if ten-tiep == none { none } else { ten-tiep.at(i) }, + huong: huong-ra(P, J)) + } + } + if ban-kinh { + doan(ctx, J, X, mau: mau, day: 0.8pt, dut: true) + goc-vuong(ctx, X, J, B, r: 0.22, mau: mau) + } + if ten-tam != none { diem(ctx, J, mau: mau, ten: ten-tam, huong: huong-ra(J, A)) } +} + +// ---------- Tiếp tuyến ---------- +// Hai tiếp tuyến kẻ từ điểm M ngoài đường tròn (O; r). +#let tiep-tuyen-tu-diem( + ctx, O, r, M, + ten-tam: $O$, ten-diem: $M$, ten-tiep: ($T_1$, $T_2$), + mau: black, mau-tt: red, day: 1pt, +) = { + duong-tron(ctx, O, r, mau: mau, day: day) + let (T1, T2) = tiep-diem(O, r, M) + doan(ctx, M, T1, mau: mau-tt, day: day) + doan(ctx, M, T2, mau: mau-tt, day: day) + doan(ctx, O, T1, mau: mau, day: 0.8pt, dut: true) + doan(ctx, O, T2, mau: mau, day: 0.8pt, dut: true) + doan(ctx, O, M, mau: mau, day: 0.8pt, dut: true) + goc-vuong(ctx, T1, O, M, r: 0.24, mau: mau-tt) + goc-vuong(ctx, T2, O, M, r: 0.24, mau: mau-tt) + if ten-tam != none { diem(ctx, O, ten: ten-tam, huong: "duoi") } + if ten-diem != none { diem(ctx, M, ten: ten-diem, huong: "phai") } + if ten-tiep != none { + diem(ctx, T1, ten: ten-tiep.at(0), huong: huong-ra(T1, O)) + diem(ctx, T2, ten: ten-tiep.at(1), huong: huong-ra(T2, O)) + } +} + +// ---------- Tiếp tuyến TẠI một điểm trên đường tròn ---------- +// Dựng tiếp tuyến của đường tròn (O; r) TẠI điểm M (M nằm trên đường tròn): +// đoạn thẳng qua M, VUÔNG GÓC với bán kính OM. +// dai : nửa độ dài đoạn tiếp tuyến (auto = 1.4·r); đơn vị toạ độ toán. +// mau / day / dut : kiểu nét của đoạn tiếp tuyến. +// M không cần chính xác trên đường tròn — hướng tiếp tuyến lấy theo OM thực tế. +/// Tiếp tuyến của (O; r) TẠI điểm M: đoạn qua M vuông góc OM. +/// `tiep-tuyen-tai-diem(O, r, M, dai: auto, mau: black, day: 1pt, dut: false)`. +#let tiep-tuyen-tai-diem(ctx, O, r, M, dai: auto, mau: black, day: 1pt, dut: false) = { + let dx = M.at(0) - O.at(0) + let dy = M.at(1) - O.at(1) + let l = calc.sqrt(dx * dx + dy * dy) + if l < 1e-9 { panic("tiep-tuyen-tai-diem: M trùng tâm O") } + // vectơ đơn vị VUÔNG GÓC với OM (hướng tiếp tuyến) + let tx = -dy / l + let ty = dx / l + let d = if dai == auto { r * 1.4 } else { dai } + let P1 = (M.at(0) - d * tx, M.at(1) - d * ty) + let P2 = (M.at(0) + d * tx, M.at(1) + d * ty) + doan(ctx, P1, P2, mau: mau, day: day, dut: dut) +} + +// Giao điểm ĐƯỜNG THẲNG AB với ĐƯỜNG TRÒN (O; r). +// duong : cặp điểm (A, B) xác định đường thẳng +// tron : cặp (O, r) — tâm và bán kính +// TRẢ VỀ MẢNG điểm giao, sắp theo chiều A -> B: +// () rỗng nếu không cắt · một điểm (P,) nếu TIẾP XÚC · (P1, P2) nếu cắt. +// let gd = giao-duong-thang-duong-tron((A, B), (O, r)) +#let giao-duong-thang-duong-tron(duong, tron) = { + let (A, B) = duong + let (O, r) = tron + let dx = B.at(0) - A.at(0) + let dy = B.at(1) - A.at(1) + let l = calc.sqrt(dx * dx + dy * dy) + if l < 1e-9 { panic("giao-duong-thang-duong-tron: A trùng B") } + let ux = dx / l + let uy = dy / l + // chân đường vuông góc H từ O xuống AB + let t = (O.at(0) - A.at(0)) * ux + (O.at(1) - A.at(1)) * uy + let H = (A.at(0) + t * ux, A.at(1) + t * uy) + let d2 = calc.pow(khoang-cach(O, H), 2) + let r2 = r * r + if d2 > r2 + 1e-9 { return () } // không cắt + let h = calc.sqrt(calc.max(0, r2 - d2)) + if h < 1e-9 { return ((H.at(0), H.at(1)),) } // tiếp xúc + ( + (H.at(0) - h * ux, H.at(1) - h * uy), + (H.at(0) + h * ux, H.at(1) + h * uy), + ) +} + +// Giao điểm hai đường tròn (O1;r1), (O2;r2): trả về (P1, P2). +#let giao-hai-duong-tron(O1, r1, O2, r2) = { + let d = khoang-cach(O1, O2) + let a = (d * d + r1 * r1 - r2 * r2) / (2 * d) + let h = calc.sqrt(r1 * r1 - a * a) + let ux = (O2.at(0) - O1.at(0)) / d + let uy = (O2.at(1) - O1.at(1)) / d + let H = (O1.at(0) + a * ux, O1.at(1) + a * uy) + ( + (H.at(0) - h * uy, H.at(1) + h * ux), + (H.at(0) + h * uy, H.at(1) - h * ux), + ) +} + +// Giao điểm hai đường thẳng AB và CD (giả sử không song song). +#let giao-duong-thang(A, B, C, D) = { + let x1 = A.at(0); let y1 = A.at(1) + let x2 = B.at(0); let y2 = B.at(1) + let x3 = C.at(0); let y3 = C.at(1) + let x4 = D.at(0); let y4 = D.at(1) + let m = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) + ( + ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / m, + ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / m, + ) +} + +// ---------- Đường tròn lượng giác (hình hoàn chỉnh, tự tạo khung) ---------- +// goc: góc của điểm M trên đường tròn; hien: hiển thị hình chiếu sin/cos. +#let duong-tron-luong-giac( + w: 7cm, + so-do: 55deg, // số đo góc của điểm M + ten-goc: $alpha$, + hien-chieu: true, + mau-tron: blue, + mau-goc: red, +) = hinh(w: w, xmin: -1.55, xmax: 1.55, ymin: -1.5, ymax: 1.5, ctx => { + // hai trục + mui-ten(ctx, (-1.4, 0), (1.4, 0), day: 0.9pt) + mui-ten(ctx, (0, -1.35), (0, 1.35), day: 0.9pt) + nhan(ctx, (1.4, 0), $x$, huong: "duoi") + nhan(ctx, (0, 1.35), $y$, huong: "phai") + duong-tron(ctx, (0, 0), 1, mau: mau-tron, day: 1.2pt) + // các điểm đặc biệt + nhan(ctx, (1, 0), $1$, huong: "duoi-phai", cach: 4pt) + nhan(ctx, (-1, 0), $-1$, huong: "duoi-trai", cach: 4pt) + nhan(ctx, (0, 1), $1$, huong: "tren-trai", cach: 4pt) + nhan(ctx, (0, -1), $-1$, huong: "duoi-trai", cach: 4pt) + diem(ctx, (0, 0), ten: $O$, huong: "duoi-trai", bk: 1.6pt) + // điểm M và bán kính OM + let M = (calc.cos(so-do), calc.sin(so-do)) + doan(ctx, (0, 0), M, mau: mau-goc, day: 1.2pt) + diem(ctx, M, ten: $M$, huong: huong-ra(M, (0, 0)), mau: mau-goc) + goc(ctx, (0, 0), (1, 0), M, r: 0.3, ten: ten-goc, mau: mau-goc) + if hien-chieu { + doan(ctx, M, (M.at(0), 0), mau: gray, day: 0.8pt, dut: true) + doan(ctx, M, (0, M.at(1)), mau: gray, day: 0.8pt, dut: true) + diem(ctx, (M.at(0), 0), ten: $cos alpha$, huong: "duoi", bk: 1.6pt, mau: mau-goc) + diem(ctx, (0, M.at(1)), ten: $sin alpha$, huong: "trai", bk: 1.6pt, mau: mau-goc) + } +}) diff --git a/packages/preview/conic-toan/0.3.4/lib/khao-sat.typ b/packages/preview/conic-toan/0.3.4/lib/khao-sat.typ new file mode 100644 index 0000000000..3f48628ce6 --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/khao-sat.typ @@ -0,0 +1,354 @@ +// ===================================================================== +// khao-sat.typ — KHẢO SÁT & VẼ ĐỒ THỊ TỰ ĐỘNG TỪ HỆ SỐ +// +// 5 hàm, chỉ cần nhập hệ số (như bbt-*/do-thi-*) là xổ ra TRỌN lời giải +// (tập xác định, đạo hàm, giới hạn, chiều biến thiên, cực trị / tiệm cận, +// bảng biến thiên) kèm ĐỒ THỊ — bao quát mọi trường hợp của mỗi hàm: +// +// #khao-sat-ve-do-thi-ham-bac-hai(a, b, c) y = ax² + bx + c +// #khao-sat-ve-do-thi-ham-bac-ba(a, b, c, d) y = ax³ + bx² + cx + d +// #khao-sat-ve-do-thi-ham-trung-phuong(a, b, c) y = ax⁴ + bx² + c +// #khao-sat-ve-do-thi-ham-phan-thuc(a, b, c, d) y = (ax + b)/(cx + d) +// #khao-sat-ve-do-thi-ham-huu-ti(a, b, c, d, e) y = (ax² + bx + c)/(dx + e) +// +// Tham số chung: +// tieu-de: auto -> dòng đậm "Khảo sát sự biến thiên và vẽ đồ thị của +// hàm số y = ..."; none -> bỏ tiêu đề; hoặc nội dung tuỳ ý. +// w: bề rộng đồ thị (mặc định 7.6cm). giua: true -> canh giữa BBT + đồ thị. +// ===================================================================== +#import "do-thi.typ": so-toan, so-dep, so-can-thuc, cuc-tri-bac-ba, cuc-tri-huu-ti, do-thi-bac-hai, do-thi-bac-ba, do-thi-trung-phuong, do-thi-phan-thuc, do-thi-huu-ti +#import "bang.typ": bbt-bac-hai, bbt-bac-ba, bbt-trung-phuong, bbt-phan-thuc, bbt-huu-ti + +#let _eps = 0.000001 +#let _ninf = $-oo$ +#let _pinf = $+oo$ + +// ----- Đơn thức |co|·x^mu (không kèm dấu) ----- +#let _don-thuc(co, mu) = { + let mag = calc.abs(co) + let bien = if mu == 0 { none } else if mu == 1 { $x$ } else { $x^#mu$ } + if bien == none { so-toan(mag) } + else if calc.abs(mag - 1) < _eps { bien } + else { $#so-toan(mag) #bien$ } +} + +// ----- Đa thức: danh sách (hệ số, số mũ) giảm dần -> nội dung math ----- +#let _da-thuc(cac) = { + let cac = cac.filter(t => calc.abs(t.at(0)) > _eps) + if cac.len() == 0 { return $0$ } + let acc = $0$ + for (i, t) in cac.enumerate() { + let (co, mu) = t + let than = _don-thuc(co, mu) + if i == 0 { + acc = if co < 0 { $-#than$ } else { than } + } else { + acc = if co < 0 { $#acc - #than$ } else { $#acc + #than$ } + } + } + acc +} + +// ----- Khoảng (a; b) — a, b là nội dung math; dùng TRONG MARKUP: #_kh(..) ----- +#let _kh(a, b) = $(#a ; #b)$ + +// ----- Dòng tiêu đề "Khảo sát ... y = " ----- +#let _tieu-de(tieu-de, bt) = { + if tieu-de == none { return none } + let nd = if tieu-de == auto { [Khảo sát sự biến thiên và vẽ đồ thị của hàm số $y = #bt$] } else { tieu-de } + [*#nd* ] +} + +// ----- Khối canh giữa (hoặc để nguyên) cho BBT/đồ thị ----- +#let _o(giua, than) = if giua { align(center, than) } else { than } + +// ===================================================================== +// 1) BẬC HAI: y = ax² + bx + c +// ===================================================================== +#let khao-sat-ve-do-thi-ham-bac-hai(a, b, c, tieu-de: auto, w: 7.6cm, giua: false) = { + assert(calc.abs(a) > _eps, message: "bac-hai: cần a ≠ 0") + let bt = _da-thuc(((a, 2), (b, 1), (c, 0))) + let xd = -b / (2 * a) // hoành độ đỉnh + let yd = c - b * b / (4 * a) // tung độ đỉnh + let dlt = b * b - 4 * a * c + let Xd = so-toan(xd) + let Yd = so-toan(yd) + let cbt = if a > 0 { + [Hàm số nghịch biến trên khoảng #_kh(_ninf, Xd) và đồng biến trên khoảng #_kh(Xd, _pinf)#"."] + } else { + [Hàm số đồng biến trên khoảng #_kh(_ninf, Xd) và nghịch biến trên khoảng #_kh(Xd, _pinf)#"."] + } + let ct = if a > 0 { + [Hàm số đạt cực tiểu tại $x = #Xd$, $y_("CT") = #Yd$; không có cực đại.] + } else { + [Hàm số đạt cực đại tại $x = #Xd$, $y_("CĐ") = #Yd$; không có cực tiểu.] + } + let gh = if a > 0 { + [$limits(lim)_(x -> -oo) y = +oo$; $limits(lim)_(x -> +oo) y = +oo$.] + } else { + [$limits(lim)_(x -> -oo) y = -oo$; $limits(lim)_(x -> +oo) y = -oo$.] + } + let giao-ox = if dlt > _eps { + let s = calc.sqrt(dlt) / (2 * calc.abs(a)) + [cắt trục hoành tại hai điểm $(#so-toan(xd - s) ; 0)$ và $(#so-toan(xd + s) ; 0)$] + } else if dlt < -_eps { + [không cắt trục hoành] + } else { + [tiếp xúc trục hoành tại $(#Xd ; 0)$] + } + [ + #_tieu-de(tieu-de, bt) + - Tập xác định: $D = RR$. + - Sự biến thiên: + + Đạo hàm: $y' = #_da-thuc(((2 * a, 1), (b, 0)))$; $y' = 0 <=> x = #Xd$. + + Giới hạn: #gh + + Chiều biến thiên: #cbt + + Cực trị: #ct + + Bảng biến thiên: + #_o(giua, bbt-bac-hai(a, b, c)) + - Đồ thị: + + Đỉnh $I(#Xd ; #Yd)$, trục đối xứng $x = #Xd$. + + Đồ thị cắt trục tung tại $(0; #so-toan(c))$ và #giao-ox. + #_o(giua, do-thi-bac-hai(a, b, c, w: w)) + ] +} + +// ===================================================================== +// 2) BẬC BA: y = ax³ + bx² + cx + d (Δ' = b² − 3ac) +// ===================================================================== +#let khao-sat-ve-do-thi-ham-bac-ba(a, b, c, d, tieu-de: auto, w: 7.6cm, giua: false) = { + assert(calc.abs(a) > _eps, message: "bac-ba: cần a ≠ 0") + let bt = _da-thuc(((a, 3), (b, 2), (c, 1), (d, 0))) + let f = x => ((a * x + b) * x + c) * x + d + let delta = b * b - 3 * a * c + let xu = -b / (3 * a) // hoành độ điểm uốn + let yu = f(xu) + let gh = if a > 0 { + [$limits(lim)_(x -> -oo) y = -oo$; $limits(lim)_(x -> +oo) y = +oo$.] + } else { + [$limits(lim)_(x -> -oo) y = +oo$; $limits(lim)_(x -> +oo) y = -oo$.] + } + let dao = _da-thuc(((3 * a, 2), (2 * b, 1), (c, 0))) + let cbt = [] + let ct = [] + let dao-them = [] + if delta > _eps { + // hai cực trị — hoành độ & giá trị CHÍNH XÁC dạng căn thức + let ctr = cuc-tri-bac-ba(a, b, c, d) + let U = ctr.x1 + let V = ctr.x2 + let Fu = ctr.y1 + let Fv = ctr.y2 + dao-them = [ $y' = 0 <=> x = #U$ hoặc $x = #V$.] + if a > 0 { + cbt = [Hàm số đồng biến trên các khoảng #_kh(_ninf, U) và #_kh(V, _pinf)#"; "nghịch biến trên khoảng #_kh(U, V)#"."] + ct = [Hàm số đạt cực đại tại $x = #U$, $y_("CĐ") = #Fu$; đạt cực tiểu tại $x = #V$, $y_("CT") = #Fv$.] + } else { + cbt = [Hàm số nghịch biến trên các khoảng #_kh(_ninf, U) và #_kh(V, _pinf)#"; "đồng biến trên khoảng #_kh(U, V)#"."] + ct = [Hàm số đạt cực tiểu tại $x = #U$, $y_("CT") = #Fu$; đạt cực đại tại $x = #V$, $y_("CĐ") = #Fv$.] + } + } else if delta < -_eps { + // đơn điệu, y' vô nghiệm + dao-them = [ Vì $Delta' = #so-toan(delta) < 0$ nên $y'$ không đổi dấu.] + cbt = if a > 0 { + [Hàm số đồng biến trên khoảng #_kh(_ninf, _pinf)#"."] + } else { + [Hàm số nghịch biến trên khoảng #_kh(_ninf, _pinf)#"."] + } + ct = [Hàm số không có cực trị.] + } else { + // Δ' = 0: nghiệm kép, vẫn đơn điệu + dao-them = [ $y' = 0 <=> x = #so-toan(xu)$ (nghiệm kép), $y'$ không đổi dấu.] + cbt = if a > 0 { + [Hàm số đồng biến trên khoảng #_kh(_ninf, _pinf)#"."] + } else { + [Hàm số nghịch biến trên khoảng #_kh(_ninf, _pinf)#"."] + } + ct = [Hàm số không có cực trị.] + } + [ + #_tieu-de(tieu-de, bt) + - Tập xác định: $D = RR$. + - Sự biến thiên: + + Đạo hàm: $y' = #dao$.#dao-them + + Giới hạn: #gh + + Chiều biến thiên: #cbt + + Cực trị: #ct + + Bảng biến thiên: + #_o(giua, bbt-bac-ba(a, b, c, d)) + - Đồ thị: + + Đạo hàm cấp hai: $y'' = #_da-thuc(((6 * a, 1), (2 * b, 0)))$; $y'' = 0 <=> x = #so-toan(xu)$. Điểm uốn $I(#so-toan(xu) ; #so-toan(yu))$. + + Đồ thị cắt trục tung tại $(0; #so-toan(d))$. + #_o(giua, do-thi-bac-ba(a, b, c, d, w: w)) + ] +} + +// ===================================================================== +// 3) TRÙNG PHƯƠNG: y = ax⁴ + bx² + c (a·b < 0 -> 3 cực trị) +// ===================================================================== +#let khao-sat-ve-do-thi-ham-trung-phuong(a, b, c, tieu-de: auto, w: 7.6cm, giua: false) = { + assert(calc.abs(a) > _eps, message: "trung-phuong: cần a ≠ 0") + let bt = _da-thuc(((a, 4), (b, 2), (c, 0))) + let f = x => a * calc.pow(x, 4) + b * x * x + c + let dao = _da-thuc(((4 * a, 3), (2 * b, 1))) + let gh = if a > 0 { + [$limits(lim)_(x -> -oo) y = +oo$; $limits(lim)_(x -> +oo) y = +oo$.] + } else { + [$limits(lim)_(x -> -oo) y = -oo$; $limits(lim)_(x -> +oo) y = -oo$.] + } + let cbt = [] + let ct = [] + let dao-them = [] + if a * b < -_eps { + // 3 cực trị: x = 0, x = ±xc + let Xc = so-can-thuc(0, 1, -b / (2 * a), 1) + let nXc = so-can-thuc(0, -1, -b / (2 * a), 1) + let yc = so-can-thuc(4 * a * c - b * b, 0, 0, 4 * a) + let y0 = so-toan(c) + dao-them = [ $y' = 0 <=> x = 0$ hoặc $x = plus.minus #Xc$.] + if a > 0 { + cbt = [Hàm số nghịch biến trên các khoảng #_kh(_ninf, nXc) và #_kh($0$, Xc)#"; "đồng biến trên các khoảng #_kh(nXc, $0$) và #_kh(Xc, _pinf)#"."] + ct = [Hàm số đạt cực tiểu tại $x = plus.minus #Xc$, $y_("CT") = #yc$; đạt cực đại tại $x = 0$, $y_("CĐ") = #y0$.] + } else { + cbt = [Hàm số đồng biến trên các khoảng #_kh(_ninf, nXc) và #_kh($0$, Xc)#"; "nghịch biến trên các khoảng #_kh(nXc, $0$) và #_kh(Xc, _pinf)#"."] + ct = [Hàm số đạt cực đại tại $x = plus.minus #Xc$, $y_("CĐ") = #yc$; đạt cực tiểu tại $x = 0$, $y_("CT") = #y0$.] + } + } else { + // 1 cực trị tại x = 0 + dao-them = [ $y' = 0 <=> x = 0$.] + if a > 0 { + cbt = [Hàm số nghịch biến trên khoảng #_kh(_ninf, $0$) và đồng biến trên khoảng #_kh($0$, _pinf)#"."] + ct = [Hàm số đạt cực tiểu tại $x = 0$, $y_("CT") = #so-toan(c)$; không có cực đại.] + } else { + cbt = [Hàm số đồng biến trên khoảng #_kh(_ninf, $0$) và nghịch biến trên khoảng #_kh($0$, _pinf)#"."] + ct = [Hàm số đạt cực đại tại $x = 0$, $y_("CĐ") = #so-toan(c)$; không có cực tiểu.] + } + } + [ + #_tieu-de(tieu-de, bt) + - Tập xác định: $D = RR$. + - Sự biến thiên: + + Đạo hàm: $y' = #dao$.#dao-them + + Giới hạn: #gh + + Chiều biến thiên: #cbt + + Cực trị: #ct + + Bảng biến thiên: + #_o(giua, bbt-trung-phuong(a, b, c)) + - Đồ thị: + + Đồ thị cắt trục tung tại $(0; #so-toan(c))$; là hàm số chẵn nên nhận trục $O y$ làm trục đối xứng. + #_o(giua, do-thi-trung-phuong(a, b, c, w: w)) + ] +} + +// ===================================================================== +// 4) PHÂN THỨC BẬC NHẤT/BẬC NHẤT: y = (ax + b)/(cx + d) +// ===================================================================== +#let khao-sat-ve-do-thi-ham-phan-thuc(a, b, c, d, tieu-de: auto, w: 7.6cm, giua: false) = { + assert(calc.abs(c) > _eps, message: "phan-thuc: cần c ≠ 0") + let D = a * d - b * c + assert(calc.abs(D) > _eps, message: "phan-thuc: ad − bc = 0 (hàm hằng)") + let tu = _da-thuc(((a, 1), (b, 0))) + let mau = _da-thuc(((c, 1), (d, 0))) + let bt = $(#tu)/(#mau)$ + let x0 = -d / c // TCĐ + let y0 = a / c // TCN + let X0 = so-toan(x0) + let Y0 = so-toan(y0) + let dong = D > 0 + let dao = if dong { + $y' = #so-toan(D)/(#mau)^2 > 0, forall x != #X0$ + } else { + $y' = #so-toan(D)/(#mau)^2 < 0, forall x != #X0$ + } + let cbt = if dong { + [Hàm số đồng biến trên từng khoảng #_kh(_ninf, X0) và #_kh(X0, _pinf)#"."] + } else { + [Hàm số nghịch biến trên từng khoảng #_kh(_ninf, X0) và #_kh(X0, _pinf)#"."] + } + let trai = if dong { _ninf } else { _pinf } + let phai = if dong { _pinf } else { _ninf } + [ + #_tieu-de(tieu-de, bt) + - Tập xác định: $D = RR without {#X0}$. + - Sự biến thiên: + + Chiều biến thiên: #dao. #cbt + + Cực trị: Hàm số không có cực trị. + + Tiệm cận: + $limits(lim)_(x -> #X0^-) y = #trai$, $limits(lim)_(x -> #X0^+) y = #phai$ nên đường thẳng $x = #X0$ là tiệm cận đứng. + $limits(lim)_(x -> plus.minus oo) y = #Y0$ nên đường thẳng $y = #Y0$ là tiệm cận ngang. + + Bảng biến thiên: + #_o(giua, bbt-phan-thuc(a, b, c, d)) + - Đồ thị: + + Đồ thị nhận điểm $I(#X0 ; #Y0)$ (giao hai tiệm cận) làm tâm đối xứng. + #_o(giua, do-thi-phan-thuc(a, b, c, d, w: w)) + ] +} + +// ===================================================================== +// 5) HỮU TỈ BẬC HAI/BẬC NHẤT: y = (ax² + bx + c)/(dx + e) +// Viết y = px + q + r/(dx + e); p = a/d, q = (bd − ae)/d², r = c − qe. +// ===================================================================== +#let khao-sat-ve-do-thi-ham-huu-ti(a, b, c, d, e, tieu-de: auto, w: 7.6cm, giua: false) = { + assert(calc.abs(a) > _eps and calc.abs(d) > _eps, message: "huu-ti: cần a ≠ 0 và d ≠ 0") + let tu = _da-thuc(((a, 2), (b, 1), (c, 0))) + let mau = _da-thuc(((d, 1), (e, 0))) + let bt = $(#tu)/(#mau)$ + let x0 = -e / d + let p = a / d + let q = (b * d - a * e) / (d * d) + let r = c - q * e + let f = x => (a * x * x + b * x + c) / (d * x + e) + let X0 = so-toan(x0) + let tcx = _da-thuc(((p, 1), (q, 0))) // y = px + q + let co-ct = p != 0 and r * d * p > _eps + // y' = p + k/(dx+e)² với k = −rd + let k = -r * d + let dao = if k > _eps { $y' = #so-toan(p) + #so-toan(k)/(#mau)^2$ } + else if k < -_eps { $y' = #so-toan(p) - #so-toan(-k)/(#mau)^2$ } + else { $y' = #so-toan(p)$ } + let cbt = [] + let ct = [] + let dao-them = [] + if co-ct { + // hoành độ & giá trị cực trị CHÍNH XÁC dạng căn thức + let ctr = cuc-tri-huu-ti(a, b, c, d, e) + let U = ctr.x1 + let V = ctr.x2 + let Fu = ctr.y1 + let Fv = ctr.y2 + dao-them = [ $y' = 0 <=> x = #U$ hoặc $x = #V$.] + if p > 0 { + cbt = [Hàm số đồng biến trên các khoảng #_kh(_ninf, U) và #_kh(V, _pinf)#"; "nghịch biến trên các khoảng #_kh(U, X0) và #_kh(X0, V)#"."] + ct = [Hàm số đạt cực đại tại $x = #U$, $y_("CĐ") = #Fu$; đạt cực tiểu tại $x = #V$, $y_("CT") = #Fv$.] + } else { + cbt = [Hàm số nghịch biến trên các khoảng #_kh(_ninf, U) và #_kh(V, _pinf)#"; "đồng biến trên các khoảng #_kh(U, X0) và #_kh(X0, V)#"."] + ct = [Hàm số đạt cực tiểu tại $x = #U$, $y_("CT") = #Fu$; đạt cực đại tại $x = #V$, $y_("CĐ") = #Fv$.] + } + } else { + dao-them = [ (không đổi dấu trên tập xác định).] + if p > 0 { + cbt = [Hàm số đồng biến trên từng khoảng #_kh(_ninf, X0) và #_kh(X0, _pinf)#"."] + } else { + cbt = [Hàm số nghịch biến trên từng khoảng #_kh(_ninf, X0) và #_kh(X0, _pinf)#"."] + } + ct = [Hàm số không có cực trị.] + } + let trai = if r * d > 0 { _ninf } else { _pinf } + let phai = if r * d > 0 { _pinf } else { _ninf } + [ + #_tieu-de(tieu-de, bt) + - Tập xác định: $D = RR without {#X0}$. + - Sự biến thiên: + + Chiều biến thiên: #dao.#dao-them #cbt + + Cực trị: #ct + + Tiệm cận: + $limits(lim)_(x -> #X0^-) y = #trai$, $limits(lim)_(x -> #X0^+) y = #phai$ nên đường thẳng $x = #X0$ là tiệm cận đứng. + $limits(lim)_(x -> plus.minus oo) [y - (#tcx)] = 0$ nên đường thẳng $y = #tcx$ là tiệm cận xiên. + + Bảng biến thiên: + #_o(giua, bbt-huu-ti(a, b, c, d, e)) + - Đồ thị: + + Đồ thị nhận giao hai tiệm cận làm tâm đối xứng. + #_o(giua, do-thi-huu-ti(a, b, c, d, e, w: w)) + ] +} +// (đồng bộ mount 07/2026) diff --git a/packages/preview/conic-toan/0.3.4/lib/mat-cong.typ b/packages/preview/conic-toan/0.3.4/lib/mat-cong.typ new file mode 100644 index 0000000000..46aee9091a --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/mat-cong.typ @@ -0,0 +1,471 @@ +// ===================================================================== +// mat-cong.typ — ENGINE MẶT CONG (nón · trụ) CÓ NÉT KHUẤT TỰ ĐỘNG (08/2026) +// +// Bổ khuyết cho `da-dien.typ`: engine kia chỉ lo được KHỐI ĐA DIỆN LỒI và +// KHÔNG xử lý được hai khối che nhau. Ở đây dùng MỘT cơ chế duy nhất cho cả +// hai việc — BẮN TIA từ điểm đang xét về phía người nhìn; tia gặp lòng bất +// kì khối nào (kể cả chính nó) thì đoạn đó là NÉT KHUẤT. +// +// #mat-cong( +// mat-non(r: 2, cao: 4, mau: red), +// mat-tru(tam: (0, 2, 0), r: 2, cao: 4, mau: blue), +// cam: chieu-truc-giao(ngang: 15deg, cao: 22deg), +// truc: (x: 2.6, y: 4.4, z: 4.6), +// ) +// +// TRỤC KHỐI ĐẶT NGHIÊNG ĐƯỢC: `nghieng:` là góc giữa trục khối và Oz, +// `huong:` là hướng ngả. Khi trục nghiêng thì TOÀN THÂN nghiêng theo — mặt +// đáy luôn VUÔNG GÓC với trục, đường sinh nghiêng cùng trục: +// +// #mat-cong(mat-tru(r: 1.2, cao: 5, nghieng: 70deg)) // trụ nằm nghiêng +// #mat-cong(mat-non(r: 1.6, cao: 4.5, nghieng: 90deg)) // nón nằm ngang +// +// Mọi công thức đều viết theo KHUNG RIÊNG (u, v, w) của khối — w là trục, +// (u, v) là hai phương trong mặt đáy — nên trục đứng chỉ là trường hợp riêng. +// +// Phép thử che khuất là GIẢI TÍCH, KHÔNG quét mẫu: giao của tia với mặt đáy +// là phương trình bậc nhất, với mặt bên là bậc hai; trạng thái trong/ngoài +// chỉ đổi tại các nghiệm đó nên chỉ cần xét trung điểm giữa hai mốc liên +// tiếp. Sai số duy nhất còn lại là ở mắt lưới của đường được vẽ (`n:`). +// +// GIỚI HẠN: đường sinh BIÊN (silhouette) CỐ Ý không để chính khối của nó +// che — tia bắn từ đúng đường biên là tiếp tuyến, xét ở đó sẽ chập chờn làm +// nét biên lúc liền lúc đứt. Chưa có mặt cầu. +// ===================================================================== +#import "ve.typ": * +#import "da-dien.typ": v3-cong, v3-tru, v3-nhan, v3-dai, v3-vo-huong, v3-co-huong, v3-chuan, _cam, chieu-3d, chieu-xien, chieu-oxyz, chieu-truc-giao + +// ===================================================================== +// KHUNG RIÊNG CỦA KHỐI — (u, v) trong mặt đáy, w dọc trục +// ===================================================================== +// Camera MẶC ĐỊNH của engine này là chiếu TRỰC GIAO, KHÔNG phải `chieu-xien` +// như `da-dien.typ`. Lý do: chiếu xiên làm đường tròn nằm ngang chiếu ra elip +// NGHIÊNG trong khi trục khối vẫn dựng đứng — đáy nghiêng mà trục đứng, nhìn +// vô lý. Chiếu trực giao cho elip đáy có trục lớn NẰM NGANG, khối vẽ ra NGAY +// NGẮN đúng lối sách giáo khoa. Muốn lối cũ thì khai rõ `cam: chieu-xien()`. +#let _cam-mc(c) = if c == auto { + chieu-truc-giao(ngang: 15deg, cao: 22deg) +} else { _cam(c) } + +#let _khung(w0) = { + let w = v3-chuan(w0) + // chọn vectơ mồi KHÔNG song song với w, kẻo tích có hướng ra vectơ không + let moi = if calc.abs(w.at(2)) > 0.9 { (1.0, 0.0, 0.0) } else { (0.0, 0.0, 1.0) } + let u = v3-chuan(v3-co-huong(moi, w)) + (u, v3-co-huong(w, u), w) +} + +// Nhận CẢ kiểu angle (`30deg`) LẪN số trần (`30` = 30 ĐỘ) — cùng lối +// `toa-cuc` của ve.typ. Không có dòng này thì `nghieng: 210` bị Typst hiểu +// là 210 RADIAN, khối quay đi đâu không biết mà KHÔNG hề báo lỗi. +#let _goc-do(a) = if type(a) == angle { a } else { a * 1deg } + +// Trục khối suy từ hai góc: `nghieng` = góc so với Oz, `huong` = hướng ngả. +#let _truc-tu-goc(nghieng0, huong0) = { + let nghieng = _goc-do(nghieng0) + let huong = _goc-do(huong0) + ( + calc.sin(nghieng) * calc.cos(huong), + calc.sin(nghieng) * calc.sin(huong), + calc.cos(nghieng), + ) +} + +#let _khoi(loai, tam, r, cao, nghieng, huong, truc, mau, to) = { + let w0 = if truc == auto { _truc-tu-goc(nghieng, huong) } else { truc } + let (u, v, w) = _khung(w0) + (loai: loai, tam: tam, r: r, cao: cao, u: u, v: v, w: w, mau: mau, to: to) +} + +// ===================================================================== +// MÔ TẢ KHỐI — TRẢ GIÁ TRỊ (KHÔNG kê vào ve-voi) +// tam : tâm mặt đáy (đáy DƯỚI nếu trục đứng) +// r, cao : bán kính đáy, chiều cao ĐO DỌC TRỤC +// nghieng : góc giữa trục khối và Oz (0 = đứng, 90° = nằm ngang) +// huong : hướng ngả của trục, đo trong mặt phẳng Oxy +// truc : ghi đè thẳng bằng vectơ 3D (auto = suy từ nghieng/huong) +// ===================================================================== +#let mat-non( + tam: (0, 0, 0), r: 2, cao: 4, + nghieng: 0deg, huong: 0deg, truc: auto, + mau: auto, to: auto, +) = _khoi("non", tam, r, cao, nghieng, huong, truc, mau, to) + +#let mat-tru( + tam: (0, 0, 0), r: 2, cao: 4, + nghieng: 0deg, huong: 0deg, truc: auto, + mau: auto, to: auto, +) = _khoi("tru", tam, r, cao, nghieng, huong, truc, mau, to) + +// Đỉnh nón (cũng là tâm đáy trên của trụ). +#let dinh-non(k) = v3-cong(k.tam, v3-nhan(k.cao, k.w)) + +// Mảng điểm 3D của một ĐƯỜNG TRÒN tâm `tam`, bán kính r, nằm trong mặt phẳng +// vuông góc với `truc` — đưa vào `duong:` của `ve-mat-cong` để vẽ thiết diện, +// đường tròn phụ… mà vẫn được tự chia liền/đứt. TRẢ GIÁ TRỊ (không kê). +#let tron-ngang(tam, r, truc: (0, 0, 1), n: 48) = { + let (u, v, w) = _khung(truc) + range(n + 1).map(i => { + let t = 360deg * (i / n) + v3-cong(tam, v3-cong(v3-nhan(r * calc.cos(t), u), v3-nhan(r * calc.sin(t), v))) + }) +} + +// Điểm P có nằm HẲN trong lòng khối không? +// `lui` CO KHỐI LẠI một chút — KHÔNG phải để làm đẹp mà là BẮT BUỘC: +// đường tròn được vẽ bằng đa giác n cạnh, nên TRUNG ĐIỂM DÂY CUNG (điểm mà +// `_duong-tu-dong` đem đi thử) thụt vào trong vành đúng bằng độ võng +// `r·(1 − cos(180°/n))`. Tia bắn từ đó xuyên qua một LÁT MỎNG của chính khối +// rồi ra ngay ⇒ phép thử giải tích bắt đúng lát đó và báo KHUẤT oan, làm cả +// vành đáy thành nét đứt. Vì vậy `ve-mat-cong` tính `lui` theo `n` rồi gắn +// vào từng khối. (Bản quét mẫu cũ vô tình NHẢY QUA lát này nên không lộ lỗi.) +#let _trong-khoi(k, P) = { + let lui = k.at("lui", default: 1e-4) + let q = v3-tru(P, k.tam) + let z = v3-vo-huong(q, k.w) // toạ độ DỌC TRỤC + if z < lui or z > k.cao - lui { return false } + let d = v3-dai(v3-tru(q, v3-nhan(z, k.w))) // khoảng cách tới trục + let bk = if k.loai == "non" { k.r * (1 - z / k.cao) } else { k.r } + d < bk - lui +} + +// Các MỐC s > 0 mà trạng thái trong/ngoài khối k CÓ THỂ đổi dọc tia P + s·e: +// hai mặt đáy (mặt phẳng) + mặt bên (phương trình bậc hai). +// ⚠️ ĐỪNG quay lại lối QUÉT MẪU đều: bước quét luôn có thể dài hơn dây cung +// đi qua khối ⇒ tia "nhảy qua" khối và nét khuất bị bỏ sót. Đã vấp đúng lỗi +// đó (đường sinh trụ nằm trong lòng nón mà vẫn vẽ liền): bước 1.13 trong khi +// dây cung chỉ 0.5. +#let _moc-tia(k, P, e) = { + let q = v3-tru(P, k.tam) + let z0 = v3-vo-huong(q, k.w) + let ez = v3-vo-huong(e, k.w) + let a = v3-tru(q, v3-nhan(z0, k.w)) // phần vuông góc trục + let b = v3-tru(e, v3-nhan(ez, k.w)) + let moc = () + if calc.abs(ez) > 1e-12 { + moc.push(-z0 / ez) + moc.push((k.cao - z0) / ez) + } + // mặt bên: |a + s·b|² = bán kính(s)² + let he = if k.loai == "tru" { + (v3-vo-huong(b, b), 2 * v3-vo-huong(a, b), v3-vo-huong(a, a) - k.r * k.r) + } else { + let m = k.r / k.cao + let a0 = k.r - m * z0 + let k1 = m * ez + ( + v3-vo-huong(b, b) - k1 * k1, + 2 * (v3-vo-huong(a, b) + a0 * k1), + v3-vo-huong(a, a) - a0 * a0, + ) + } + let (A, B, C) = he + if calc.abs(A) > 1e-12 { + let D = B * B - 4 * A * C + if D > 0 { + let sq = calc.sqrt(D) + moc.push((-B - sq) / (2 * A)) + moc.push((-B + sq) / (2 * A)) + } + } else if calc.abs(B) > 1e-12 { + moc.push(-C / B) + } + moc +} + +// Điểm P có bị khối nào che không: bắn tia P + s·e về phía người nhìn. +// CHÍNH XÁC (không quét mẫu): trạng thái trong/ngoài chỉ đổi tại các mốc +// của `_moc-tia`, nên chỉ cần xét TRUNG ĐIỂM giữa hai mốc liên tiếp. +// `bo` = chỉ số khối được BỎ QUA (không tự che). +#let _bi-che(P, e, ds-khoi, bo: -1) = { + let moc = (1e-6,) + for j in range(ds-khoi.len()) { + if j == bo { continue } + for s in _moc-tia(ds-khoi.at(j), P, e) { + if s > 1e-6 { moc.push(s) } + } + } + let moc = moc.sorted() + for i in range(moc.len() - 1) { + let Q = v3-cong(P, v3-nhan((moc.at(i) + moc.at(i + 1)) / 2, e)) + for j in range(ds-khoi.len()) { + if j != bo and _trong-khoi(ds-khoi.at(j), Q) { return true } + } + } + false +} + +// ===================================================================== +// VẼ MỘT ĐƯỜNG 3D, TỰ CHIA LIỀN / ĐỨT +// Mỗi mắt lưới xét ở TRUNG ĐIỂM rồi gom các mắt liên tiếp cùng trạng thái +// (cùng lối với `_doan-tu-dong` của da-dien.typ). +// ===================================================================== +#let _duong-tu-dong( + ctx, p, e, ds-khoi, pts, + mau: black, day: 1.1pt, hien-khuat: true, + mau-khuat: auto, day-khuat: auto, bo: -1, +) = { + let m = pts.len() + if m < 2 { return } + let trang = range(m - 1).map(i => { + let M = v3-nhan(0.5, v3-cong(pts.at(i), pts.at(i + 1))) + _bi-che(M, e, ds-khoi, bo: bo) + }) + let dau = 0 + let i = 1 + while i <= m - 1 { + let het = i == m - 1 + if het or trang.at(i) != trang.at(dau) { + let cuoi = if het { m - 1 } else { i } + let che = trang.at(dau) + if not che or hien-khuat { + duong-cong( + ctx, range(dau, cuoi + 1).map(j => p(pts.at(j))), + mau: if not che or mau-khuat == auto { mau } else { mau-khuat }, + day: if not che or day-khuat == auto { day } else { day-khuat }, + dut: che, + ) + } + dau = i + } + i = i + 1 + } +} + +// ===================================================================== +// HÌNH HỌC CỦA TỪNG KHỐI — viết theo KHUNG RIÊNG (u, v, w) +// ===================================================================== +// Điểm trên đường tròn bán kính R quanh trục, cách đáy `h` dọc trục. +#let _diem-vanh(k, R, t, h) = v3-cong( + v3-cong(k.tam, v3-nhan(h, k.w)), + v3-cong(v3-nhan(R * calc.cos(t), k.u), v3-nhan(R * calc.sin(t), k.v)), +) +#let _cung-vanh(k, R, h, tu, den, n: 48) = range(n + 1).map(i => { + _diem-vanh(k, R, tu + (den - tu) * (i / n), h) +}) + +// Hiệu hai điểm 2D · tích có hướng 2D. +#let _tru2(a, b) = (a.at(0) - b.at(0), a.at(1) - b.at(1)) +#let _cheo2(a, b) = a.at(0) * b.at(1) - a.at(1) * b.at(0) + +// Góc ứng với điểm GẦN người nhìn nhất trên vành: độ sâu tỉ lệ với +// cos t·(u·e) + sin t·(v·e), lớn nhất tại atan2(u·e, v·e). +#let _goc-gan(k, e) = calc.atan2(v3-vo-huong(k.u, e), v3-vo-huong(k.v, e)) + +// Hai góc ứng với ĐƯỜNG SINH BIÊN của khối. +// • trụ : pháp tuyến mặt bên vuông góc tia nhìn ⇔ _goc-gan ± 90°. +// • nón : điều kiện hình chiếu đường sinh TIẾP XÚC elip đáy — +// (W×U)·sin t − (W×V)·cos t = −r·(U×V)/cao, +// với U, V, W là ảnh của khung riêng (u, v, w) qua phép chiếu. +// (Kiểm chứng với trục đứng + chieu-oxyz: ra đúng +// cos t + k·cos(goc)·sin t = −r·k·sin(goc)/cao.) +#let _goc-bien(k, p, e) = { + let g0 = _goc-gan(k, e) + if k.loai != "non" { return (g0 - 90deg, g0 + 90deg) } + let o = p((0, 0, 0)) + let U = _tru2(p(k.u), o) + let V = _tru2(p(k.v), o) + let W = _tru2(p(k.w), o) + let a = _cheo2(W, U) + let b = -_cheo2(W, V) + let bien-do = calc.sqrt(a * a + b * b) + if bien-do < 1e-12 { return (g0 - 90deg, g0 + 90deg) } + let ve-phai = -k.r * _cheo2(U, V) / (k.cao * bien-do) + let dt = calc.acos(calc.max(-1.0, calc.min(1.0, ve-phai))) + let ph = calc.atan2(b, a) + (ph - dt, ph + dt) +} + +// Các đường phải vẽ của một khối: mảng (pts, tu-che). `tu-che = false` +// nghĩa là đường BIÊN — không để chính khối đó che. +#let _duong-khoi(k, p, e, n: 48) = { + let (t1, t2) = _goc-bien(k, p, e) + // ÉP KIỂU int: calc.round và phép chia của Typst trả FLOAT, mà `range` + // chỉ nhận số nguyên ("expected integer, found float"). + let ns = calc.max(2, int(n / 2)) + let ds = ((_cung-vanh(k, k.r, 0, 0deg, 360deg, n: n), true),) + if k.loai == "non" { + let S = dinh-non(k) + for t in (t1, t2) { + let B = _diem-vanh(k, k.r, t, 0) + ds.push(( + range(ns + 1).map(i => v3-cong(B, v3-nhan(i / ns, v3-tru(S, B)))), + false, + )) + } + } else { + ds.push((_cung-vanh(k, k.r, k.cao, 0deg, 360deg, n: n), true)) + for t in (t1, t2) { + let A = _diem-vanh(k, k.r, t, 0) + ds.push(( + range(ns + 1).map(i => v3-cong(A, v3-nhan(k.cao * i / ns, k.w))), + false, + )) + } + } + ds +} + +// Đường bao ngoài (dùng để tô màu): nón = cung đáy thấy được + đỉnh; +// trụ = cung đáy phía trước + cung nắp phía sau. +#let _bao-khoi(k, p, e, n: 48) = { + let (t1, t2) = _goc-bien(k, p, e) + if k.loai == "non" { + _cung-vanh(k, k.r, 0, t1, t2, n: n) + (dinh-non(k),) + } else { + // BẮT BUỘC bọc ngoặc: `+` ở ĐẦU dòng nối bị Typst đọc thành dấu dương + // một ngôi ("cannot apply unary '+' to array"). + ( + _cung-vanh(k, k.r, 0, t1, t2, n: n) + + _cung-vanh(k, k.r, k.cao, t2, t1 + 360deg, n: n) + ) + } +} + +// Các điểm bao của khối (để dựng cửa sổ khung hình). Lấy 12 hướng quanh mỗi +// vành chứ không phải 4 — hình chiếu của đường tròn là elip XIÊN nên bốn +// điểm theo trục toạ độ hụt mất phần chìa ra hai bên. +#let _chot-khoi(k, m: 12) = { + let ds = () + for h in (0, k.cao) { + let bk = if k.loai == "non" and h == k.cao { 0.0 } else { k.r } + for i in range(m) { ds.push(_diem-vanh(k, bk, 360deg * (i / m), h)) } + } + ds +} +#let _chot-tat-ca(ds) = { + let kq = () + for k in ds { kq = kq + _chot-khoi(k) } + kq +} + +// ===================================================================== +// HỆ TRỤC Oxyz VẼ BẰNG CHÍNH CAMERA CỦA KHỐI +// Lý do phải có: `oxyz` của hinh-khong-gian.typ tự dựng phép chiếu XIÊN +// riêng của nó. Trong phép chiếu xiên, đường tròn nằm ngang chiếu ra elip +// NGHIÊNG trong khi Oz vẫn dựng đứng — nhìn vô lý. Vẽ trục bằng chính +// camera của khối thì cả khung hình chung MỘT góc nhìn. Với +// `chieu-truc-giao(cao:)` thì elip đáy của khối ĐỨNG có trục lớn NẰM NGANG +// và trục khối THẲNG ĐỨNG — đúng như nhìn vật thật. +// NHẬN ctx ⇒ PHẢI kê vào `ve-voi` và khối `_voi-ctx`. +// ===================================================================== +#let ve-truc-3d( + ctx, cam, + x: 3, y: 4, z: 4, + am: 0.4, // kéo dài thêm về phía âm + dm: 0.55, // chừa chỗ cho mũi tên + nhãn ở đầu dương + ten: ($x$, $y$, $z$), ten-goc: $O$, + huong-ten: ("below-left", "below-right", "left"), + huong-goc: "below-right", + mau: black, day: 0.9pt, cach: 5pt, +) = { + let p = _cam(cam).p + let bo3 = ( + ((1, 0, 0), x, ten.at(0), huong-ten.at(0)), + ((0, 1, 0), y, ten.at(1), huong-ten.at(1)), + ((0, 0, 1), z, ten.at(2), huong-ten.at(2)), + ) + for (u, L, t, hg) in bo3 { + let B = v3-nhan(L + am + dm, u) + mui-ten(ctx, p(v3-nhan(-am, u)), p(B), mau: mau, day: day) + if t != none { nhan(ctx, p(B), t, huong: hg, cach: cach) } + } + if ten-goc != none { + nhan(ctx, p((0, 0, 0)), ten-goc, huong: huong-goc, cach: 4pt) + } +} + +// Hai đầu mút ba trục (để cửa sổ khung hình bao được cả hệ trục). +#let _chot-truc(tr) = { + let am = tr.at("am", default: 0.4) + let dm = tr.at("dm", default: 0.55) + let kq = () + for (khoa, u) in (("x", (1, 0, 0)), ("y", (0, 1, 0)), ("z", (0, 0, 1))) { + let L = tr.at(khoa, default: if khoa == "x" { 3 } else { 4 }) + kq.push(v3-nhan(-am, u)) + kq.push(v3-nhan(L + am + dm, u)) + } + kq +} + +// ===================================================================== +// VE-MAT-CONG — NHẬN ctx ⇒ PHẢI kê vào `ve-voi` và khối `_voi-ctx` +// ===================================================================== +#let ve-mat-cong( + ctx, + ..khoi, + cam: auto, + mau: black, day: 1.1pt, to: none, + hien-khuat: true, mau-khuat: auto, day-khuat: auto, + n: 48, + duong: (), + truc: none, // dict tuỳ chọn của `ve-truc-3d`, vd truc: (x: 3, y: 5, z: 5) + truoc: none, // vẽ TRƯỚC khối · them: vẽ SAU khối + them: none, +) = { + let ds0 = khoi.pos() + if ds0.len() == 0 { return } + // gắn `lui` cho từng khối: phải LỚN HƠN độ võng dây cung của lưới n cạnh, + // xem giải thích ở `_trong-khoi`. Hệ số 1.6 là biên an toàn. + let ds = ds0.map(k => k + ( + lui: calc.max(1e-4, 1.6 * k.r * (1 - calc.cos(180deg / n))), + )) + let cm = _cam-mc(cam) + let p = cm.p + let e = cm.nhin + // 0) hệ trục + lớp vẽ nền — nằm DƯỚI khối + if truc != none { ve-truc-3d(ctx, cm, ..truc) } + if truoc != none { truoc(ctx, p) } + // 1) tô màu trước, để nét không bị màu đè lên + for k in ds { + let mt = if k.to == auto { to } else { k.to } + if mt != none { + da-giac-pt(_bao-khoi(k, p, e, n: n).map(P => toa-pt(ctx, p(P))), to: mt) + } + } + // 2) rồi mới kẻ nét, tự chia liền / đứt + for j in range(ds.len()) { + let k = ds.at(j) + let mk = if k.mau == auto { mau } else { k.mau } + for (pts, tu-che) in _duong-khoi(k, p, e, n: n) { + _duong-tu-dong( + ctx, p, e, ds, pts, + mau: mk, day: day, hien-khuat: hien-khuat, + mau-khuat: mau-khuat, day-khuat: day-khuat, + bo: if tu-che { -1 } else { j }, + ) + } + } + // 3) các đường vẽ thêm (thiết diện, đường tròn phụ…) — cũng tự chia liền/đứt. + // Mỗi mục là một DICT: (pts: mảng điểm 3D, mau: …, day: …, hien-khuat: …) + for m in duong { + _duong-tu-dong( + ctx, p, e, ds, m.pts, + mau: m.at("mau", default: mau), + day: m.at("day", default: day), + hien-khuat: m.at("hien-khuat", default: hien-khuat), + mau-khuat: mau-khuat, day-khuat: day-khuat, + ) + } + if them != none { them(ctx, p) } +} + +// ===================================================================== +// MAT-CONG — TỰ tạo khung hình (KHÔNG kê vào ve-voi) +// Mọi tuỳ chọn khác đều chảy thẳng xuống `ve-mat-cong` qua sink `..khoi`. +// ===================================================================== +#let mat-cong(..khoi, cam: auto, w: 7cm, le: 0.75, co-chu: 10pt) = { + let cm = _cam-mc(cam) + let tr = khoi.named().at("truc", default: none) + let chot = _chot-tat-ca(khoi.pos()) + (if tr == none { () } else { _chot-truc(tr) }) + let pts = chot.map(cm.p) + let xs = pts.map(q => q.at(0)) + let ys = pts.map(q => q.at(1)) + hinh( + w: w, + xmin: calc.min(..xs) - le, xmax: calc.max(..xs) + le, + ymin: calc.min(..ys) - le, ymax: calc.max(..ys) + le, + co-chu: co-chu, + ctx => ve-mat-cong(ctx, ..khoi, cam: cm), + ) +} diff --git a/packages/preview/conic-toan/0.3.4/lib/oxyz-toan.typ b/packages/preview/conic-toan/0.3.4/lib/oxyz-toan.typ new file mode 100644 index 0000000000..c4fec33ca7 --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/oxyz-toan.typ @@ -0,0 +1,544 @@ +// ===================================================================== +// oxyz-toan.typ — TÍNH TOÁN trong không gian toạ độ Oxyz +// Toàn bộ hàm ở file này TRẢ VỀ GIÁ TRỊ (số / điểm / dict) hoặc NỘI DUNG +// toán đã định dạng ⇒ KHÔNG kê vào `ve-voi` / `_voi-ctx` của baigiang.typ. +// +// Quy ước kiểu dữ liệu +// điểm / vectơ : (x, y, z) +// mặt phẳng : (a: , b: , c: , d: ) ⇔ ax + by + cz + d = 0 +// (cũng nhận dạng (n: , d: ) của da-dien.typ) +// đường thẳng : (P: điểm đi qua, u: vectơ chỉ phương) +// mặt cầu : (I: tâm, R: bán kính) +// +// Nhóm hàm `hien-*` trả về NỘI DUNG toán CHÍNH XÁC (căn thức, phân số), +// nhóm còn lại trả số thực để tính tiếp / vẽ hình. +// ===================================================================== +#import "da-dien.typ": * +#import "do-thi.typ": so-toan, so-dep, so-can-thuc + +#let _e0 = 1e-9 + +#let _gan-int(x, eps: 1e-9) = calc.abs(x - calc.round(x)) < eps * calc.max(1, calc.abs(x)) + +#let _gcd2(a, b) = { + let (a, b) = (calc.abs(int(a)), calc.abs(int(b))) + while b != 0 { let r = calc.rem(a, b); a = b; b = r } + a +} + +// Nhân dãy hệ số với số hữu tỉ nhỏ nhất để thành NGUYÊN rồi chia ước chung; +// dấu chuẩn hoá: hệ số khác 0 đầu tiên > 0. Không nguyên hoá được -> none. +#let _rut-gon(ds) = { + let ok = none + for q in range(1, 61) { + let t = ds.map(v => v * q) + if t.all(v => _gan-int(v, eps: 1e-7)) { + ok = t.map(v => int(calc.round(v))) + break + } + } + if ok == none { return none } + let g = 0 + for v in ok { g = _gcd2(g, v) } + if g == 0 { return ok } + let r = ok.map(v => int(v / g)) + let am = false + for v in r { + if v != 0 { am = v < 0; break } + } + if am { r = r.map(v => -v) } + r +} + +// Số -> CHUỖI toán (nguyên, phân số p/q rút gọn, cùng lắm là thập phân 3 chữ số). +// Lượt mẫu LỚN (tới 5000, khớp chặt 1e-11) để tâm ngoại tiếp/trực tâm — vốn là +// phân số mẫu vài trăm — vẫn hiện CHÍNH XÁC thay vì số thập phân. +#let _ms(v) = { + if _gan-int(v) { return str(int(calc.round(v))) } + for q in range(2, 101) { + let p = v * q + if _gan-int(p) { + let p = int(calc.round(p)) + if _gcd2(p, q) == 1 { return str(p) + "/" + str(q) } + } + } + for q in range(101, 5001) { + let p = v * q + if _gan-int(p, eps: 1e-11) { + let p = int(calc.round(p)) + if _gcd2(p, q) == 1 { return str(p) + "/" + str(q) } + } + } + let r = calc.round(v, digits: 3) + if calc.fract(r) == 0 { str(int(r)) } else { str(r) } +} + +// Có phải chuỗi số NGUYÊN (để biết cần bọc ngoặc hay không). +#let _la-nguyen(v) = _gan-int(v) + +// Vế trái "2x - 3y + z - 5" từ hệ số + tên biến ("" = hạng tử tự do). +#let _ve-trai(hs, bien) = { + let s = "" + for (v, b) in hs.zip(bien) { + if calc.abs(v) < 1e-12 { continue } + let am = v < 0 + let a = calc.abs(v) + let t = if b == "" { + _ms(a) + } else if _la-nguyen(a) and calc.round(a) == 1 { + b + } else if _la-nguyen(a) { + _ms(a) + b + } else { + "(" + _ms(a) + ")" + b + } + if s == "" { + s = if am { "-" + t } else { t } + } else { + s = s + (if am { " - " } else { " + " }) + t + } + } + if s == "" { s = "0" } + s +} + +// Bọc ngoặc khi số âm hoặc là phân số (dùng làm mẫu số, hệ số đứng riêng). +#let _ms-ngoac(v) = { + let s = _ms(v) + if v < 0 or not _la-nguyen(v) { "(" + s + ")" } else { s } +} + +// ===================================================================== +// 1. VECTƠ +// (các phép cơ bản v3-cong, v3-tru, v3-nhan, v3-chuan… có sẵn ở da-dien.typ) +// ===================================================================== + +// Toạ độ vectơ AB = B - A. Gọi 1 đối số thì trả về chính vectơ đó. +#let vecto-3d(..a) = { + let p = a.pos() + if p.len() == 1 { p.at(0) } else { v3-tru(p.at(1), p.at(0)) } +} + +#let tich-vo-huong(u, v) = v3-vo-huong(u, v) +#let tich-co-huong(u, v) = v3-co-huong(u, v) +// Tích hỗn tạp [u, v, w] = (u × v) · w (định thức cấp ba). +#let tich-hon-tap(u, v, w) = v3-vo-huong(v3-co-huong(u, v), w) + +#let do-dai-vecto(u) = v3-dai(u) + +// cos của góc giữa hai vectơ (giá trị thực, đã chặn trong [-1; 1]). +#let cos-goc-vecto(u, v) = { + let m = v3-dai(u) * v3-dai(v) + if m < _e0 { return 0.0 } + calc.max(-1.0, calc.min(1.0, v3-vo-huong(u, v) / m)) +} +// Góc giữa hai vectơ (0° -> 180°), trả về kiểu angle. +#let goc-vecto(u, v) = calc.acos(cos-goc-vecto(u, v)) + +#let cung-phuong(u, v) = v3-dai(v3-co-huong(u, v)) < 1e-7 * calc.max(1, v3-dai(u) * v3-dai(v)) +#let vuong-goc(u, v) = calc.abs(v3-vo-huong(u, v)) < 1e-7 * calc.max(1, v3-dai(u) * v3-dai(v)) +#let dong-phang(u, v, w) = calc.abs(tich-hon-tap(u, v, w)) < 1e-7 + +// ===================================================================== +// 2. ĐIỂM — khoảng cách, điểm đặc biệt của tam giác +// (trung-diem-3d, chia-3d, tam-3d đã có ở da-dien.typ) +// ===================================================================== +#let khoang-cach-3d(A, B) = v3-dai(v3-tru(B, A)) +#let do-dai-doan-3d(A, B) = khoang-cach-3d(A, B) + +#let trong-tam-3d(..P) = { + let ds = P.pos() + v3-nhan(1 / ds.len(), ds.fold((0.0, 0.0, 0.0), v3-cong)) +} + +// Tâm đường tròn NGOẠI tiếp tam giác ABC (toạ độ tâm tỉ cự). +#let tam-ngoai-tiep-3d(A, B, C) = { + let a2 = v3-vo-huong(v3-tru(C, B), v3-tru(C, B)) + let b2 = v3-vo-huong(v3-tru(C, A), v3-tru(C, A)) + let c2 = v3-vo-huong(v3-tru(B, A), v3-tru(B, A)) + let al = a2 * (b2 + c2 - a2) + let be = b2 * (c2 + a2 - b2) + let ga = c2 * (a2 + b2 - c2) + let s = al + be + ga + if calc.abs(s) < _e0 { return trong-tam-3d(A, B, C) } + v3-nhan(1 / s, v3-cong(v3-cong(v3-nhan(al, A), v3-nhan(be, B)), v3-nhan(ga, C))) +} + +// Trực tâm — hệ thức Euler: H = A + B + C - 2·O. +#let truc-tam-3d(A, B, C) = v3-tru( + v3-cong(v3-cong(A, B), C), + v3-nhan(2, tam-ngoai-tiep-3d(A, B, C)), +) + +// Tâm đường tròn NỘI tiếp: I = (a·A + b·B + c·C)/(a + b + c). +#let tam-noi-tiep-3d(A, B, C) = { + let a = khoang-cach-3d(B, C) + let b = khoang-cach-3d(C, A) + let c = khoang-cach-3d(A, B) + let s = a + b + c + if s < _e0 { return A } + v3-nhan(1 / s, v3-cong(v3-cong(v3-nhan(a, A), v3-nhan(b, B)), v3-nhan(c, C))) +} + +// Tâm đường tròn BÀNG tiếp trong góc A. +#let tam-bang-tiep-3d(A, B, C) = { + let a = khoang-cach-3d(B, C) + let b = khoang-cach-3d(C, A) + let c = khoang-cach-3d(A, B) + let s = -a + b + c + if calc.abs(s) < _e0 { return A } + v3-nhan(1 / s, v3-cong(v3-cong(v3-nhan(-a, A), v3-nhan(b, B)), v3-nhan(c, C))) +} + +// ===================================================================== +// 3. DIỆN TÍCH — THỂ TÍCH +// ===================================================================== +#let dien-tich-tam-giac-3d(A, B, C) = 0.5 * v3-dai(v3-co-huong(v3-tru(B, A), v3-tru(C, A))) +#let dien-tich-hbh-3d(A, B, C) = v3-dai(v3-co-huong(v3-tru(B, A), v3-tru(C, A))) +#let the-tich-tu-dien(A, B, C, D) = calc.abs( + tich-hon-tap(v3-tru(B, A), v3-tru(C, A), v3-tru(D, A)), +) / 6 +#let the-tich-hinh-hop(A, B, C, D) = calc.abs( + tich-hon-tap(v3-tru(B, A), v3-tru(C, A), v3-tru(D, A)), +) +#let bon-diem-dong-phang(A, B, C, D) = dong-phang( + v3-tru(B, A), v3-tru(C, A), v3-tru(D, A), +) +#let ban-kinh-ngoai-tiep-3d(A, B, C) = { + let s = dien-tich-tam-giac-3d(A, B, C) + if s < _e0 { return 0.0 } + (khoang-cach-3d(B, C) * khoang-cach-3d(C, A) * khoang-cach-3d(A, B)) / (4 * s) +} +#let ban-kinh-noi-tiep-3d(A, B, C) = { + let p = (khoang-cach-3d(B, C) + khoang-cach-3d(C, A) + khoang-cach-3d(A, B)) / 2 + if p < _e0 { return 0.0 } + dien-tich-tam-giac-3d(A, B, C) / p +} + +// ===================================================================== +// 4. MẶT PHẲNG ax + by + cz + d = 0 +// ===================================================================== + +// Nhận cả dạng (a,b,c,d) lẫn dạng (n: đơn vị, d:) của da-dien.typ. +#let _hs-mp(m) = { + if "a" in m { (m.a, m.b, m.c, m.d) } else { (m.n.at(0), m.n.at(1), m.n.at(2), -m.d) } +} +#let phap-tuyen-mp(m) = { + let h = _hs-mp(m) + (h.at(0), h.at(1), h.at(2)) +} +// Đổi sang dạng (n: , d: ) để VẼ bằng mat-phang-bh / hinh-chieu-mp của da-dien. +#let mp-de-ve(m) = { + let h = _hs-mp(m) + let n = v3-chuan((h.at(0), h.at(1), h.at(2))) + let l = v3-dai((h.at(0), h.at(1), h.at(2))) + (n: n, d: if l < _e0 { 0.0 } else { -h.at(3) / l }) +} + +#let mat-phang-qua-phap(A, n) = { + let h = _rut-gon((n.at(0), n.at(1), n.at(2), -v3-vo-huong(n, A))) + let h = if h == none { (n.at(0), n.at(1), n.at(2), -v3-vo-huong(n, A)) } else { h } + (a: h.at(0), b: h.at(1), c: h.at(2), d: h.at(3)) +} +#let mat-phang-qua-3-diem(A, B, C) = mat-phang-qua-phap( + A, v3-co-huong(v3-tru(B, A), v3-tru(C, A)), +) +// Mặt phẳng đoạn chắn: x/p + y/q + z/r = 1. +#let mat-phang-doan-chan(p, q, r) = mat-phang-qua-3-diem((p, 0, 0), (0, q, 0), (0, 0, r)) +#let mat-phang-song-song(m, A) = mat-phang-qua-phap(A, phap-tuyen-mp(m)) +#let mat-phang-trung-truc(A, B) = mat-phang-qua-phap(trung-diem-3d(A, B), v3-tru(B, A)) +// Mặt phẳng qua A và vuông góc đường thẳng d. +#let mat-phang-vuong-goc-duong(A, d) = mat-phang-qua-phap(A, d.u) +// Mặt phẳng qua A, chứa hai phương u, v. +#let mat-phang-qua-2-phuong(A, u, v) = mat-phang-qua-phap(A, v3-co-huong(u, v)) + +// Thay toạ độ điểm vào vế trái: ax + by + cz + d. +#let the-vao-mp(A, m) = { + let h = _hs-mp(m) + h.at(0) * A.at(0) + h.at(1) * A.at(1) + h.at(2) * A.at(2) + h.at(3) +} +#let khoang-cach-diem-mp(A, m) = { + let n = phap-tuyen-mp(m) + let l = v3-dai(n) + if l < _e0 { return 0.0 } + calc.abs(the-vao-mp(A, m)) / l +} +#let hinh-chieu-len-mp(A, m) = { + let n = phap-tuyen-mp(m) + let l2 = v3-vo-huong(n, n) + if l2 < _e0 { return A } + v3-tru(A, v3-nhan(the-vao-mp(A, m) / l2, n)) +} +#let doi-xung-qua-mp(A, m) = { + let n = phap-tuyen-mp(m) + let l2 = v3-vo-huong(n, n) + if l2 < _e0 { return A } + v3-tru(A, v3-nhan(2 * the-vao-mp(A, m) / l2, n)) +} +// Khung tứ giác biểu diễn mặt phẳng để VẼ bằng mat-phang-bh của da-dien.typ: +// let (T, u, v) = khung-mp(P, tam: A, r: 2.2) +// mat-phang-bh(ctx, t3, T, u, v) +#let khung-mp(m, tam: auto, r: 2) = { + let n = v3-chuan(phap-tuyen-mp(m)) + let T = hinh-chieu-len-mp(if tam == auto { (0, 0, 0) } else { tam }, m) + let t = if calc.abs(n.at(2)) < 0.9 { (0, 0, 1) } else { (1, 0, 0) } + let u = v3-nhan(r, v3-chuan(v3-co-huong(n, t))) + let v = v3-nhan(r, v3-chuan(v3-co-huong(n, u))) + (T, u, v) +} + +#let goc-2-mp(m1, m2) = calc.acos( + calc.min(1.0, calc.abs(cos-goc-vecto(phap-tuyen-mp(m1), phap-tuyen-mp(m2)))), +) +#let vi-tri-2-mp(m1, m2) = { + let (n1, n2) = (phap-tuyen-mp(m1), phap-tuyen-mp(m2)) + if not cung-phuong(n1, n2) { return "cắt nhau" } + let (h1, h2) = (_hs-mp(m1), _hs-mp(m2)) + let k = 0.0 + for i in range(3) { + if calc.abs(h2.at(i)) > _e0 { k = h1.at(i) / h2.at(i) } + } + if calc.abs(h1.at(3) - k * h2.at(3)) < 1e-7 { "trùng nhau" } else { "song song" } +} + +// Phương trình mặt phẳng: nội dung $2x - 3y + z - 5 = 0$ +#let pt-mat-phang(m, an: ("x", "y", "z")) = { + let h = _hs-mp(m) + let r = _rut-gon(h) + let h = if r == none { h } else { r } + eval(_ve-trai(h, (an.at(0), an.at(1), an.at(2), "")) + " = 0", mode: "math") +} + +// ===================================================================== +// 5. ĐƯỜNG THẲNG (P: điểm, u: vectơ chỉ phương) +// ===================================================================== +// Vectơ chỉ phương "đẹp": nhân lên thành nguyên, chia ước chung, thành phần +// khác 0 đầu tiên dương. Không nguyên hoá được thì giữ nguyên. +#let vtcp-dep(u) = { + let r = _rut-gon(u) + if r == none { u } else { r } +} +#let duong-thang-qua-vtcp(A, u) = (P: A, u: u) +#let duong-thang-qua-2-diem(A, B) = (P: A, u: v3-tru(B, A)) +#let duong-thang-vuong-goc-mp(A, m) = (P: A, u: phap-tuyen-mp(m)) + +#let khoang-cach-diem-duong(A, d) = { + let l = v3-dai(d.u) + if l < _e0 { return khoang-cach-3d(A, d.P) } + v3-dai(v3-co-huong(v3-tru(A, d.P), d.u)) / l +} +#let hinh-chieu-len-duong(A, d) = { + let l2 = v3-vo-huong(d.u, d.u) + if l2 < _e0 { return d.P } + v3-cong(d.P, v3-nhan(v3-vo-huong(v3-tru(A, d.P), d.u) / l2, d.u)) +} +#let doi-xung-qua-duong(A, d) = v3-tru(v3-nhan(2, hinh-chieu-len-duong(A, d)), A) +#let goc-2-duong(d1, d2) = calc.acos(calc.min(1.0, calc.abs(cos-goc-vecto(d1.u, d2.u)))) +#let goc-duong-mp(d, m) = calc.asin( + calc.min(1.0, calc.abs(cos-goc-vecto(d.u, phap-tuyen-mp(m)))), +) + +#let vi-tri-2-duong(d1, d2) = { + let w = v3-tru(d2.P, d1.P) + if cung-phuong(d1.u, d2.u) { + if v3-dai(v3-co-huong(w, d1.u)) < 1e-7 { "trùng nhau" } else { "song song" } + } else if calc.abs(tich-hon-tap(d1.u, d2.u, w)) < 1e-7 { "cắt nhau" } else { "chéo nhau" } +} +#let khoang-cach-2-duong(d1, d2) = { + let n = v3-co-huong(d1.u, d2.u) + if v3-dai(n) < 1e-7 { return khoang-cach-diem-duong(d2.P, d1) } + calc.abs(v3-vo-huong(n, v3-tru(d2.P, d1.P))) / v3-dai(n) +} +// Giao điểm đường thẳng với mặt phẳng — song song / nằm trong thì trả none. +#let giao-duong-mp(d, m) = { + let n = phap-tuyen-mp(m) + let t0 = v3-vo-huong(n, d.u) + if calc.abs(t0) < 1e-9 { return none } + v3-cong(d.P, v3-nhan(-the-vao-mp(d.P, m) / t0, d.u)) +} +// Giao điểm hai đường thẳng (chéo nhau / song song -> none). +#let giao-2-duong(d1, d2) = { + if vi-tri-2-duong(d1, d2) != "cắt nhau" { return none } + let w = v3-tru(d2.P, d1.P) + let n = v3-co-huong(d1.u, d2.u) + let t = v3-vo-huong(v3-co-huong(w, d2.u), n) / v3-vo-huong(n, n) + v3-cong(d1.P, v3-nhan(t, d1.u)) +} +// Giao tuyến hai mặt phẳng (song song / trùng -> none). +#let giao-2-mp(m1, m2) = { + let (n1, n2) = (phap-tuyen-mp(m1), phap-tuyen-mp(m2)) + let u = v3-co-huong(n1, n2) + if v3-dai(u) < 1e-7 { return none } + let (h1, h2) = (_hs-mp(m1), _hs-mp(m2)) + let (k1, k2) = (-h1.at(3), -h2.at(3)) + let (a11, a12) = (v3-vo-huong(n1, n1), v3-vo-huong(n1, n2)) + let a22 = v3-vo-huong(n2, n2) + let de = a11 * a22 - a12 * a12 + let al = (k1 * a22 - k2 * a12) / de + let be = (k2 * a11 - k1 * a12) / de + (P: v3-cong(v3-nhan(al, n1), v3-nhan(be, n2)), u: vtcp-dep(u)) +} + +// Phương trình THAM SỐ: cases(x = ..., y = ..., z = ...) +#let pt-tham-so(d, t: "t", an: ("x", "y", "z")) = { + let dong = range(3).map(i => ( + an.at(i) + " = " + _ve-trai((d.P.at(i), d.u.at(i)), ("", t)) + )) + eval("cases(" + dong.join(", ") + ")", mode: "math") +} +// Phương trình CHÍNH TẮC (thành phần chỉ phương bằng 0 thì tách ra sau dấu phẩy). +#let pt-chinh-tac(d, an: ("x", "y", "z")) = { + let ph = () + let le = () + for i in range(3) { + if calc.abs(d.u.at(i)) < _e0 { + le.push(an.at(i) + " = " + _ms(d.P.at(i))) + } else { + ph.push( + "(" + _ve-trai((1, -d.P.at(i)), (an.at(i), "")) + ")/" + _ms-ngoac(d.u.at(i)), + ) + } + } + let s = if ph.len() >= 2 { (ph.join(" = "),) + le } else { ph + le } + eval(s.join(", "), mode: "math") +} + +// ===================================================================== +// 6. MẶT CẦU (I: tâm, R: bán kính) +// ===================================================================== +#let mat-cau(I, R) = (I: I, R: calc.abs(R)) +#let mat-cau-duong-kinh(A, B) = (I: trung-diem-3d(A, B), R: khoang-cach-3d(A, B) / 2) +#let mat-cau-tam-tiep-xuc-mp(I, m) = (I: I, R: khoang-cach-diem-mp(I, m)) +// Mặt cầu qua 4 điểm không đồng phẳng (đồng phẳng -> none). +#let mat-cau-qua-4-diem(A, B, C, D) = { + let q = P => v3-vo-huong(P, P) + let ha = (v3-tru(B, A), v3-tru(C, A), v3-tru(D, A)) + let ve = ((q(B) - q(A)) / 2, (q(C) - q(A)) / 2, (q(D) - q(A)) / 2) + let de = tich-hon-tap(ha.at(0), ha.at(1), ha.at(2)) + if calc.abs(de) < 1e-9 { return none } + let _cot-j(j) = { + let m = ha.map(r => r) + let k = range(3).map(i => { + let r = m.at(i) + range(3).map(t => if t == j { ve.at(i) } else { r.at(t) }) + }) + tich-hon-tap(k.at(0), k.at(1), k.at(2)) + } + let I = (_cot-j(0) / de, _cot-j(1) / de, _cot-j(2) / de) + (I: I, R: khoang-cach-3d(I, A)) +} +#let vi-tri-mp-mat-cau(m, S) = { + let k = khoang-cach-diem-mp(S.I, m) + if k > S.R + 1e-7 { "không cắt" } else if k > S.R - 1e-7 { "tiếp xúc" } else { "cắt nhau" } +} +// Đường tròn giao tuyến của mặt phẳng và mặt cầu -> (I: tâm, R: bán kính). +#let duong-tron-giao(m, S) = { + let k = khoang-cach-diem-mp(S.I, m) + if k > S.R + 1e-9 { return none } + (I: hinh-chieu-len-mp(S.I, m), R: calc.sqrt(calc.max(0.0, S.R * S.R - k * k))) +} + +// Phương trình mặt cầu dạng CHÍNH TẮC: (x - a)^2 + (y - b)^2 + (z - c)^2 = R^2 +#let pt-mat-cau(S, an: ("x", "y", "z")) = { + let ve = range(3).map(i => { + let t = _ve-trai((1, -S.I.at(i)), (an.at(i), "")) + if t.contains(" ") { "(" + t + ")^2" } else { t + "^2" } + }) + let r2 = S.R * S.R + eval(ve.join(" + ") + " = " + _ms(r2), mode: "math") +} +// Phương trình mặt cầu dạng KHAI TRIỂN: x^2+y^2+z^2 - 2ax - 2by - 2cz + d = 0 +#let pt-mat-cau-khai-trien(S, an: ("x", "y", "z")) = { + let d0 = v3-vo-huong(S.I, S.I) - S.R * S.R + let dau = _ve-trai( + (-2 * S.I.at(0), -2 * S.I.at(1), -2 * S.I.at(2), d0), + (an.at(0), an.at(1), an.at(2), ""), + ) + let dau = if dau == "0" { "" } else if dau.starts-with("-") { + " - " + dau.slice(1) + } else { " + " + dau } + eval( + an.at(0) + "^2 + " + an.at(1) + "^2 + " + an.at(2) + "^2" + dau + " = 0", + mode: "math", + ) +} + +// ===================================================================== +// 7. HIỂN THỊ — nội dung toán CHÍNH XÁC (căn thức, phân số) +// ===================================================================== +// Số CHÍNH XÁC nếu là nguyên / phân số (mẫu tới 5000), ngược lại là thập phân +// 3 chữ số. KHÔNG "đoán" căn thức như so-toan (dễ ra dạng đẹp mà SAI với số vô +// tỉ); cần căn thức chính xác thì dùng hien-can / hien-do-dai / hien-dien-tich-*. +#let hien-so(v) = eval(_ms(v), mode: "math") +// Giá trị gần đúng (mặc định 2 chữ số thập phân). +#let hien-gan-dung(v, chu-so: 2) = { + let r = calc.round(v, digits: chu-so) + eval(if calc.fract(r) == 0 { str(int(r)) } else { str(r) }, mode: "math") +} +// Căn bậc hai CHÍNH XÁC của số nguyên n (tự rút thừa số chính phương). +#let hien-can(n) = so-can-thuc(0, 1, n, 1) +// Tên điểm/vectơ: chuỗi "AB" -> $A B$ (math của Typst coi chuỗi nhiều chữ cái +// là MỘT biến nên phải tách ra); truyền thẳng nội dung toán cũng được. +#let _ten-toan(t) = if type(t) == str { eval(t.clusters().join(" "), mode: "math") } else { t } +#let _bo-ba(u) = eval("(" + u.map(v => _ms(v)).join("; ") + ")", mode: "math") + +// Toạ độ điểm: A(1; 2; -3) +#let hien-diem(A, ten: none) = { + let s = _bo-ba(A) + if ten == none { s } else { $#_ten-toan(ten)#s$ } +} +// Toạ độ vectơ: arrow(u) = (1; 2; -3) +#let hien-vecto(u, ten: none) = { + let s = _bo-ba(u) + if ten == none { s } else { $arrow(#_ten-toan(ten)) = #s$ } +} +#let hien-goc(g, chu-so: 1) = { + let d = g.deg() + let s = if _gan-int(d, eps: 1e-6) { + str(int(calc.round(d))) + } else { so-dep(calc.round(d, digits: chu-so)) } + eval(s + " degree", mode: "math") +} +// Độ dài vectơ / khoảng cách hai điểm — dạng căn thức chính xác. +#let hien-do-dai(u) = so-can-thuc(0, 1, v3-vo-huong(u, u), 1) +#let hien-khoang-cach(A, B) = hien-do-dai(v3-tru(B, A)) +// cos góc giữa hai vectơ — chính xác: (u·v)/(|u||v|) = t√(N)/N +#let hien-cos-goc(u, v) = { + let t = v3-vo-huong(u, v) + let n = v3-vo-huong(u, u) * v3-vo-huong(v, v) + if n < _e0 { return $0$ } + so-can-thuc(0, t, n, n) +} +#let hien-khoang-cach-diem-mp(A, m) = { + let h = _hs-mp(m) + let r = _rut-gon(h) + let h = if r == none { h } else { r } + let n = h.at(0) * h.at(0) + h.at(1) * h.at(1) + h.at(2) * h.at(2) + if n < _e0 { return $0$ } + let t = calc.abs(h.at(0) * A.at(0) + h.at(1) * A.at(1) + h.at(2) * A.at(2) + h.at(3)) + so-can-thuc(0, t, n, n) +} +#let hien-khoang-cach-diem-duong(A, d) = { + let n2 = v3-vo-huong(d.u, d.u) + if n2 < _e0 { return hien-khoang-cach(A, d.P) } + let c = v3-co-huong(v3-tru(A, d.P), d.u) + so-can-thuc(0, 1, v3-vo-huong(c, c) * n2, n2) +} +#let hien-khoang-cach-2-duong(d1, d2) = { + let n = v3-co-huong(d1.u, d2.u) + let n2 = v3-vo-huong(n, n) + if n2 < 1e-12 { return hien-khoang-cach-diem-duong(d2.P, d1) } + let t = v3-vo-huong(n, v3-tru(d2.P, d1.P)) + so-can-thuc(0, 1, t * t / n2, 1) +} +// Diện tích hình bình hành dựng trên hai vectơ = √M (chính xác). +#let hien-dien-tich-hbh(A, B, C) = { + let c = v3-co-huong(v3-tru(B, A), v3-tru(C, A)) + so-can-thuc(0, 1, v3-vo-huong(c, c), 1) +} +// Diện tích tam giác = √M/2 (chính xác). +#let hien-dien-tich-tam-giac(A, B, C) = { + let c = v3-co-huong(v3-tru(B, A), v3-tru(C, A)) + so-can-thuc(0, 1, v3-vo-huong(c, c), 2) +} +#let hien-the-tich-tu-dien(A, B, C, D) = so-toan(the-tich-tu-dien(A, B, C, D)) diff --git a/packages/preview/conic-toan/0.3.4/lib/slide.typ b/packages/preview/conic-toan/0.3.4/lib/slide.typ new file mode 100644 index 0000000000..bb4c088dcb --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/slide.typ @@ -0,0 +1,1005 @@ +// ===================================================================== +// slide.typ — ENGINE TRÌNH CHIẾU THUẦN TYPST (16:9) +// +// Cách dùng: +// #show: bai-giang.with( +// tieu-de: [CHƯƠNG I. HÀM SỐ], gv: "Thầy/Cô ...", ... +// ) +// #muc[1. Định nghĩa] +// #slide(tieu-de: [Khái niệm])[ ... nội dung ... ] +// +// Khung nội dung: #dinh-nghia[...], #dinh-ly[...], #vi-du[...], +// #loi-giai[...], #chu-y[...], #ghi-nho[...], #nhan-xet[...], #luyen-tap[...] +// Bố cục: #chia-cot(trai, phai, ti-le: (3fr, 2fr)) +// ===================================================================== + +// Bảng màu (đọc qua state để mọi hàm dùng chung). +#let _mau = state("bg-mau", ( + chinh: rgb("#0f4c81"), // xanh đậm chủ đạo + nhan: rgb("#e67e22"), // cam nhấn + nen: rgb("#f7f9fc"), +)) +#let _thong-tin = state("bg-thong-tin", (tieu-de: [], gv: none, ngan: none)) +#let _muc-ht = state("bg-muc", none) +#let _buoc-ht = state("bg-buoc", 1) // bước hoạt hình hiện tại +#let _dem-slide = counter("bg-slide") // số thứ tự slide (không tính bước) + +// ----- Hồ sơ hiển thị: "beamer" (trình chiếu) hoặc bản in A4 ----- +// "sach-dethi" : A4, ví dụ hiện lời giải, 4 loại câu ẩn đáp án. +// "sach-loigiai": A4, mọi thứ hiện lời giải + đánh dấu đáp án. +#let _ho-so = state("bg-ho-so", "beamer") +#let _chuan-hs(hs) = lower(hs).replace("-", "").replace("_", "") +#let _la-sach(hs) = _chuan-hs(hs) != "beamer" // khác beamer = bản in A4 + +// ----- KÍCH THƯỚC THÂN SLIDE 16:9 ----- +// Rút thành hằng số để `slide()` và bộ TỰ NGẮT MÀN (che-do.typ) dùng CHUNG +// một bộ số — đổi lề ở đây là chỗ đo cũng đổi theo, không lệch nhau. +// `presentation-16-9` của Typst = 297mm × 167,06mm. +#let _kho-slide = (rong: 841.89pt, cao: 473.55pt) +#let _le-slide = ( + tren: 73pt, // chừa thanh tiêu đề slide + tren-tron: 26pt, // slide không có tiêu đề + duoi: 34pt, // chừa thanh điều hướng + ngang: 28pt, + du: 8pt, // hụt an toàn khi đo (đo bao giờ cũng có sai số) +) +// Vùng chữ thật sự của một slide — dùng để biết lời giải có tràn trang không. +#let _vung-than(co-tieu-de: true) = ( + rong: _kho-slide.rong - 2 * _le-slide.ngang, + cao: _kho-slide.cao - _le-slide.duoi - _le-slide.du + - (if co-tieu-de { _le-slide.tren } else { _le-slide.tren-tron }), +) + +// ----- GIÃN DÒNG (khoảng cách giữa các dòng) ----- +// `nen` : leading NỀN của hồ sơ đang dùng (bai-giang/de-toan ghi vào); +// `he-so` : HỆ SỐ NHÂN do người dùng đặt — 1.0 = mốc mặc định, +// 1.3 = giãn thêm 30%, 0.9 = thu lại 10%. +// Cách đặt: +// #bai-giang(gian-dong: 1.25) / #de-toan(gian-dong: 1.25) — toàn tài liệu +// #gian-dong(1.4) — đổi giữa bài (áp cho các slide/khung sau đó) +// #tn(..., gian-dong: 1.4) — ghi đè RIÊNG một câu +// Dùng khi phân số / căn thức / chỉ số chồng tầng làm hai dòng dính nhau. +// `doan` = khoảng cách GIỮA HAI ĐOẠN (khi nội dung có DÒNG TRỐNG). Phải giãn +// theo cùng hệ số với `leading`, kẻo dòng nối bằng `\` (cùng một đoạn) thì +// giãn ra mà chỗ cách bằng dòng trống (sang đoạn mới) lại y nguyên — đó là lý +// do trước đây #voi-gian-dong "chỉ ăn đoạn đầu". +// 1.2em là mốc mặc định của Typst ⇒ he-so = 1.0 giữ nguyên bố cục mọi bài cũ. +#let _doan-nen = 1.2em +#let _gd = state("bg-gian-dong", (nen: 0.62em, doan: _doan-nen, he-so: 1.0)) +#let _dat-gian(nen, k, doan: _doan-nen) = _gd.update((nen: nen, doan: doan, he-so: k)) +#let gian-dong(k) = _gd.update(g => ( + nen: g.nen, doan: g.at("doan", default: _doan-nen), he-so: k)) +// Ba helper dưới đây CHỈ gọi được trong `context`. +#let _he-so-gian() = _gd.get().he-so +#let _gian-ht() = { + let g = _gd.get() + g.nen * g.he-so +} +#let _gian-doan() = { + let g = _gd.get() + g.at("doan", default: _doan-nen) * g.he-so +} +// Áp hệ số giãn dòng cho MỘT KHỐI nội dung bất kì (phần còn lại không đổi): +// #voi-gian-dong(1.4)[ ... đoạn nhiều phân số ... ] +// Giãn CẢ khoảng cách dòng lẫn khoảng cách đoạn. +#let voi-gian-dong(k, body) = context { + let g = _gd.get() + set par(leading: g.nen * k, spacing: g.at("doan", default: _doan-nen) * k) + body +} + +// ----- CHIỀU CAO THẬT CỦA CÔNG THỨC TRONG DÒNG (chống dính chữ) ----- +// VÌ SAO CẦN: Typst đóng khung công thức TRONG DÒNG theo SỐ ĐO PHÔNG CHỮ +// (cap-height → đường chân chữ, ≈6.83pt ở cỡ 10pt) chứ KHÔNG theo nét vẽ thật. +// `measure($1/2$)` và `measure($0,5$)` vì thế ra CÙNG chiều cao, trong khi phân +// số vẽ ra cao ≈12pt và TRÀN cả trên lẫn dưới khung. Hệ quả: +// • trong một đoạn: dòng không cao lên ⇒ tử số dòng dưới chạm mẫu dòng trên; +// • trong ô của `grid` (phương án #tn, ý #ds, `cot-item`): ô cũng lấy chiều +// cao HỤT đó ⇒ phân số đè sang hàng trên, mà `gian-dong` (chỉ chạm +// par.leading/par.spacing) KHÔNG với tới được. +// Trước đây phải bù bằng `gian-dong: 3` — giãn ĐỀU cả bài, chỗ cần thì vừa, +// chỗ chữ thường thì trống hoác. +// +// CÁCH CHỮA: đo nét vẽ thật (top-edge/bottom-edge: "bounds" — xem `co-net` của +// ve.typ) rồi chèn một CỘT CHỐNG vô hình RỘNG 0 ngay trước công thức, cao đúng +// phần nét nhô lên/thò xuống. Cột chống là hộp trong dòng nên Typst tính nó vào +// chiều cao dòng VÀ chiều cao ô ⇒ dòng/ô tự nới ĐÚNG chỗ cần, chữ thường không +// đổi một pt nào. Vì rộng 0 nên `measure(...).width` (chỗ `cot: auto` chọn số +// cột) cũng không đổi. +// CỐ Ý dùng cột chống chứ KHÔNG bọc công thức trong `box`: bọc box sẽ chặn +// Typst ngắt dòng giữa công thức dài. +// bat : bật/tắt toàn bộ cơ chế. +// nguong : nét tràn dưới mức này thì bỏ qua (khỏi chèn cột chống vô ích). +// them : nới thêm bấy nhiêu ở MỖI phía (chỉ dùng khi muốn thoáng hơn nữa). +#let _cao-that = state("bg-cao-that", (bat: true, nguong: 1pt, them: 0pt, chia: 0.5, hien: false)) + +// #cao-that(false) — tắt từ đây trở đi; #cao-that() — bật lại; +// #cao-that(them: 1pt) — nới thêm 1pt mỗi phía cho mọi công thức có tràn nét. +// Dùng SINK để nhận `bat` ở dạng ĐỐI SỐ VỊ TRÍ mà vẫn gọi được #cao-that(). +// hien: true — CHẨN ĐOÁN, in phần `thieu` (pt) màu đỏ ngay sau mỗi công thức. +// Không thấy số đỏ nào hiện ra ⇒ bản lib đang chạy KHÔNG phải bản này. +#let cao-that(..a, nguong: auto, them: auto, chia: auto, hien: auto) = { + let bat = if a.pos().len() > 0 { a.pos().first() } else { true } + _cao-that.update(c => ( + bat: bat, + nguong: if nguong == auto { c.at("nguong", default: 1pt) } else { nguong }, + them: if them == auto { c.at("them", default: 0pt) } else { them }, + chia: if chia == auto { c.at("chia", default: 0.5) } else { chia }, + hien: if hien == auto { c.at("hien", default: false) } else { hien }, + )) +} + +// (tren, duoi) = phần NÉT VẼ nhô lên trên / thò xuống dưới đường chân chữ. +// CHỈ gọi trong `context`. +#let _do-net(nd) = ( + measure({ set text(top-edge: "bounds", bottom-edge: "baseline"); nd }).height, + measure({ set text(top-edge: "baseline", bottom-edge: "bounds"); nd }).height, +) + +// ⚠️ KHÔNG tách được nét vẽ thành (trên, dưới) một cách đáng tin. Đã thử và +// SAI: với công thức CAO (nhất là khi tài liệu bật `math.display` cho công +// thức trong dòng — kiểu \displaystyle của LaTeX, người dùng rất hay dùng để +// phân số to đẹp), hai phép đo một phía trả về CÙNG một số (đo được 17.31 / +// 17.31 cho `$1/2$`), tức mỗi phép đo đã gồm cả hai phía ⇒ cộng lại là nới +// GẤP ĐÔI. Đừng quay lại lối đó. +// +// LỐI ĐÚNG — TỰ HIỆU CHỈNH, chỉ dùng phép đo CẢ DÒNG (đáng tin): +// du = nét vẽ dòng "mẫu + công thức" trừ nét vẽ dòng "mẫu" → chỗ ink CẦN +// hop = khung dòng "mẫu + công thức" trừ khung dòng "mẫu" → chỗ Typst CHO +// thieu = du − hop → phần TRÀN +// `thieu` chính là phần đè sang hàng trên/dòng trên. Cách này không phụ thuộc +// vào việc Typst tính khung công thức thế nào, nên đúng với cả math.display, +// cả phông chữ khác, cả cỡ chữ khác. +#let _mau-chu = [Ág] + +#let _cao-ink(nd) = measure({ + set text(top-edge: "bounds", bottom-edge: "bounds") + nd +}).height + +// Cột chống vô hình + chính công thức. Thiếu ít hơn `nguong` ⇒ TRẢ NGUYÊN. +// `chia` = phần `thieu` dồn xuống DƯỚI đường chân chữ (0.5 = chia đôi, hợp với +// phân số vì tử tràn lên còn mẫu tràn xuống gần bằng nhau). +#let _chong-net(nd) = context { + let c = _cao-that.get() + if not c.at("bat", default: true) { nd } else { + let mau = _mau-chu + let ca = [#mau#nd] + let du = _cao-ink(ca) - _cao-ink(mau) + let hop = measure(ca).height - measure(mau).height + let thieu = calc.max(0pt, du - hop) + let ng = c.at("nguong", default: 1pt) + let them = c.at("them", default: 0pt) + let chia = c.at("chia", default: 0.5) + // Chỉ chèn cột chống khi thật sự tràn; không tràn thì KHÔNG thêm gì cả. + if thieu > ng { + box(width: 0pt, height: measure(nd).height + thieu + 2 * them, + baseline: thieu * chia + them) + } + nd + // Chẩn đoán: #cao-that(hien: true) -> in số `thieu` màu đỏ sau công thức. + if c.at("hien", default: false) { + text(size: 6pt, fill: red, weight: "bold")[ (#calc.round(thieu.pt(), digits: 2))] + } + } +} + +// Áp cơ chế cho MỘT khối bất kì (bai-giang/de-toan đã tự áp cho cả tài liệu): +// #voi-cao-that[ ... ] +#let voi-cao-that(body) = { + show math.equation.where(block: false): _chong-net + body +} + +// ----- Bộ đếm "đóng băng" theo slide ----- +// Slide hoạt hình được in thành nhiều trang lặp; nếu đếm kiểu thường thì +// "Ví dụ 1" sẽ thành "Ví dụ 2, 3..." qua từng bước. Cách giải quyết: +// goc = tổng tích luỹ đến HẾT slide trước (state), +// cnt = đếm trong slide, được ĐẶT LẠI ở mỗi bước lặp, +// số hiển thị = goc + cnt => không đổi giữa các bước. +#let _bd-cau = (goc: state("bg-goc-cau", 0), cnt: counter("bg-cnt-cau")) +#let _bd-vd = (goc: state("bg-goc-vd", 0), cnt: counter("bg-cnt-vd")) +#let _bd-lt = (goc: state("bg-goc-lt", 0), cnt: counter("bg-cnt-lt")) +#let _bd-hd = (goc: state("bg-goc-hd", 0), cnt: counter("bg-cnt-hd")) +#let _bd-vdtt = (goc: state("bg-goc-vdtt", 0), cnt: counter("bg-cnt-vdtt")) +#let _bd-tat-ca = (_bd-cau, _bd-vd, _bd-lt, _bd-hd, _bd-vdtt) + +// Tăng bộ đếm và hiển thị số hiện tại. +#let _so-moi(bd) = { + bd.cnt.step() + context [#(bd.goc.get() + bd.cnt.get().first())] +} + +// ===================================================================== +// TRANG BÌA — 5 kiểu (kieu-bia: 1..5 hoặc tên) +// 1 "toi-gian" Tối giản & Thanh lịch +// 2 "tre-trung" Trẻ trung & Sáng tạo +// 3 "co-dien" Chuẩn mực Học thuật Cổ điển +// 4 "chuyen-nghiep" Chuyên nghiệp & Khoa học +// 5 "ky-thuat" Tiêu chuẩn cho Tài liệu Kỹ thuật +// Mỗi kiểu có tông màu MẶC ĐỊNH riêng; nếu người dùng truyền mau-chinh / +// mau-nhan (khác auto) thì màu đó GHI ĐÈ tông mặc định. +// ===================================================================== + +// Tông màu mặc định của từng kiểu bìa: (chính, nhấn). +#let _bia-mau(kieu) = ( + (chinh: rgb("#0f4c81"), nhan: rgb("#e67e22")), // 1 tối giản — navy + cam (mặc định, giữ tương thích) + (chinh: rgb("#5b2a86"), nhan: rgb("#ff5d73")), // 2 trẻ trung — tím + hồng san hô + (chinh: rgb("#14213d"), nhan: rgb("#c8a24b")), // 3 cổ điển — navy + vàng đồng + (chinh: rgb("#0f4c81"), nhan: rgb("#17a2b8")), // 4 chuyên nghiệp — xanh + lam ngọc + (chinh: rgb("#263238"), nhan: rgb("#ef6c00")), // 5 kỹ thuật — xám lam + cam +).at(kieu - 1) + +// Chuẩn hoá kieu-bia: nhận số 1..5 hoặc chuỗi tên. +#let _bia-so(k) = { + if type(k) == int { return calc.max(1, calc.min(5, k)) } + let s = lower(str(k)).replace("-", "").replace("_", "").replace(" ", "") + ( + toigian: 1, thanhlich: 1, minimal: 1, + tretrung: 2, sangtao: 2, creative: 2, + codien: 3, hocthuat: 3, classic: 3, + chuyennghiep: 4, khoahoc: 4, pro: 4, + kythuat: 5, tailieu: 5, tech: 5, + ).at(s, default: 1) +} + +// Logo: PHẢI truyền image(...) — không nhận chuỗi đường dẫn. Vì image() gọi +// trong file này (bên trong package) sẽ tìm ảnh CẠNH package, không phải cạnh +// file bài giảng của người dùng. Bọc image(...) trong file người dùng thì Typst +// mới tìm đúng thư mục. Truyền chuỗi => panic có hướng dẫn. +#let _bia-logo(logo, cao) = if logo != none { + if type(logo) == str { + panic("logo phải bọc image(...): dùng logo: image(\"" + logo + "\") thay cho chuỗi \"" + logo + "\" (ảnh đặt cùng thư mục file .typ của bạn).") + } + box(height: cao, logo) +} + +// Gom các trường phụ có giá trị thành mảng (nhan, gia-tri). +#let _bia-truong(mon, lop, gv, ngay) = { + let ds = () + if mon != none { ds.push(("Môn", mon)) } + if lop != none { ds.push(("Lớp", lop)) } + if gv != none { ds.push(("Giáo viên", gv)) } + if ngay != none { ds.push(("Ngày", ngay)) } + ds +} + +#let _co(x) = x != none + +// ---------- Kiểu 1: Tối giản & Thanh lịch ---------- +#let _bia-1(c, n, don-vi, logo, tieu-de, phu-de, mon, lop, gv, ngay) = { + place(rect(width: 100%, height: 100%, fill: white)) + place(bottom, rect(width: 100%, height: 5pt, fill: n)) + place(top + right, dx: -2.4cm, dy: 2.2cm, _bia-logo(logo, 1.9cm)) + place(left + horizon, dx: 2.6cm, block(width: 19.5cm, { + rect(width: 42pt, height: 4pt, fill: n) + v(18pt) + if _co(don-vi) { + text(size: 12.5pt, tracking: 2.5pt, fill: luma(45%), upper(don-vi)) + v(14pt) + } + text(fill: c, weight: "bold", size: 40pt, tieu-de) + if _co(phu-de) { v(10pt); text(size: 19pt, fill: luma(38%), phu-de) } + v(22pt) + line(length: 6.5cm, stroke: 0.6pt + luma(55%)) + v(16pt) + let ds = _bia-truong(mon, lop, gv, ngay) + if ds.len() > 0 { + text(size: 14pt, fill: c.darken(5%), ds.map(p => + [#text(fill: luma(50%))[#p.at(0): ]#p.at(1)]).join(text(fill: n)[#h(10pt)•#h(10pt)])) + } + })) +} + +// ---------- Kiểu 2: Trẻ trung & Sáng tạo ---------- +#let _bia-2(c, n, don-vi, logo, tieu-de, phu-de, mon, lop, gv, ngay) = { + place(rect(width: 100%, height: 100%, + fill: gradient.linear(c, n, angle: 40deg))) + // Khối hình học trang trí (bán trong suốt). + place(top + right, dx: 2.5cm, dy: -3cm, + circle(radius: 4.4cm, fill: white.transparentize(80%))) + place(bottom + left, dx: -2.6cm, dy: 2.6cm, + circle(radius: 3.4cm, fill: white.transparentize(85%))) + place(top + left, dx: 3.2cm, dy: 2.4cm, + box(rotate(18deg, rect(width: 26pt, height: 26pt, radius: 6pt, + fill: white.transparentize(70%))))) + place(bottom + right, dx: -3.6cm, dy: -2.4cm, + box(rotate(12deg, rect(width: 34pt, height: 34pt, radius: 8pt, + fill: white.transparentize(78%))))) + place(top + right, dx: -2.2cm, dy: 2cm, _bia-logo(logo, 1.7cm)) + place(center + horizon, align(center, block(width: 21cm, { + if _co(don-vi) { + box(fill: white.transparentize(78%), radius: 20pt, inset: (x: 16pt, y: 7pt), + text(fill: white, size: 13pt, weight: "medium", tracking: 1pt, upper(don-vi))) + v(20pt) + } + text(fill: white, weight: "bold", size: 42pt, tieu-de) + if _co(phu-de) { + v(12pt) + text(fill: white.transparentize(12%), size: 21pt, phu-de) + } + v(26pt) + let ds = _bia-truong(mon, lop, gv, ngay) + if ds.len() > 0 { + grid(columns: ds.len(), column-gutter: 12pt, + ..ds.map(p => box(fill: white.transparentize(82%), radius: 10pt, + inset: (x: 14pt, y: 9pt), align(center, { + text(fill: white.transparentize(20%), size: 10pt, upper(p.at(0))) + linebreak() + text(fill: white, size: 14pt, weight: "bold", p.at(1)) + })))) + } + }))) +} + +// ---------- Kiểu 3: Chuẩn mực Học thuật Cổ điển ---------- +#let _bia-3(c, n, don-vi, logo, tieu-de, phu-de, mon, lop, gv, ngay) = { + place(rect(width: 100%, height: 100%, fill: rgb("#f6f1e6"))) + // Khung viền kép cổ điển. + place(center + horizon, rect(width: 100% - 1.6cm, height: 100% - 1.6cm, + stroke: 1.4pt + c)) + place(center + horizon, rect(width: 100% - 2.0cm, height: 100% - 2.0cm, + stroke: 0.6pt + n)) + place(center + horizon, align(center, block(width: 20cm, { + _bia-logo(logo, 2.1cm) + if _co(logo) { v(14pt) } + if _co(don-vi) { + text(size: 14pt, tracking: 3pt, fill: c, upper(don-vi)) + v(6pt) + } + // Đường phân cách trang trí ✦ + grid(columns: (1fr, auto, 1fr), column-gutter: 12pt, align: horizon, + line(length: 100%, stroke: 0.6pt + n), + text(fill: n, size: 13pt, "✦"), + line(length: 100%, stroke: 0.6pt + n)) + v(18pt) + text(fill: c, weight: "bold", size: 38pt, + font: ("Charis SIL", "Noto Serif", "Libertinus Serif"), tieu-de) + if _co(phu-de) { + v(12pt) + text(size: 19pt, style: "italic", fill: c.lighten(15%), phu-de) + } + v(18pt) + grid(columns: (1fr, auto, 1fr), column-gutter: 12pt, align: horizon, + line(length: 100%, stroke: 0.6pt + n), + text(fill: n, size: 13pt, "✦"), + line(length: 100%, stroke: 0.6pt + n)) + v(20pt) + let ds = _bia-truong(mon, lop, gv, ngay) + if ds.len() > 0 { + text(size: 14pt, fill: c, ds.map(p => + [#emph[#p.at(0):] #p.at(1)]).join(h(18pt))) + } + }))) +} + +// ---------- Kiểu 4: Chuyên nghiệp & Khoa học ---------- +#let _bia-4(c, n, don-vi, logo, tieu-de, phu-de, mon, lop, gv, ngay) = { + place(rect(width: 100%, height: 100%, fill: white)) + // Dải màu dọc bên trái (~38%). + place(left + top, rect(width: 38%, height: 100%, + fill: gradient.linear(c, c.darken(30%), angle: 90deg))) + place(left + top, dx: 38%, rect(width: 6pt, height: 100%, fill: n)) + // Nội dung panel trái. + place(left + horizon, dx: 1.6cm, block(width: 8cm, { + _bia-logo(logo, 2cm) + if _co(logo) { v(20pt) } + if _co(don-vi) { + text(fill: white, size: 15pt, weight: "bold", don-vi) + v(16pt) + } + let ds = _bia-truong(mon, lop, gv, ngay) + for p in ds { + text(fill: white.transparentize(45%), size: 10.5pt, upper(p.at(0))) + linebreak() + text(fill: white, size: 14pt, weight: "medium", p.at(1)) + v(9pt) + } + })) + // Nội dung bên phải. + place(left + horizon, dx: 38% + 1.4cm, block(width: 13cm, { + rect(width: 46pt, height: 5pt, fill: n) + v(16pt) + text(fill: c, weight: "bold", size: 34pt, tieu-de) + if _co(phu-de) { + v(12pt) + text(fill: luma(35%), size: 19pt, phu-de) + } + })) +} + +// ---------- Kiểu 5: Tiêu chuẩn cho Tài liệu Kỹ thuật ---------- +#let _bia-5(c, n, don-vi, logo, tieu-de, phu-de, mon, lop, gv, ngay) = { + place(rect(width: 100%, height: 100%, fill: white)) + // Thanh đầu trang. + place(top, block(width: 100%, height: 1.7cm, fill: c, + align(horizon, pad(x: 1.6cm, grid(columns: (1fr, auto), align: horizon + left, + text(fill: white, size: 14pt, weight: "bold", tracking: 1pt, + if _co(don-vi) { upper(don-vi) } else { [TÀI LIỆU] }), + _bia-logo(logo, 1.1cm)))))) + place(top, dy: 1.7cm, rect(width: 100%, height: 4pt, fill: n)) + // Nhãn loại tài liệu + tiêu đề. + place(left + horizon, dx: 1.7cm, dy: -1.2cm, block(width: 21cm, { + box(fill: n, inset: (x: 10pt, y: 4pt), radius: 3pt, + text(fill: white, size: 11pt, weight: "bold", tracking: 2pt, + font: ("DejaVu Sans Mono", "Consolas"), "TAI-LIEU / DOC")) + v(16pt) + text(fill: c, weight: "bold", size: 36pt, tieu-de) + if _co(phu-de) { + v(10pt) + text(fill: luma(35%), size: 19pt, phu-de) + } + v(22pt) + // Bảng siêu dữ liệu. + let ds = _bia-truong(mon, lop, gv, ngay) + if ds.len() > 0 { + block(stroke: (left: 3pt + n), inset: (left: 14pt, y: 2pt), + grid(columns: (auto, auto), column-gutter: 16pt, row-gutter: 7pt, + ..ds.map(p => ( + text(fill: luma(45%), size: 12.5pt, + font: ("DejaVu Sans Mono", "Consolas"), upper(p.at(0)) + " :"), + text(fill: c, size: 13.5pt, weight: "medium", p.at(1)), + )).flatten())) + } + })) + // Thanh chân trang. + place(bottom, block(width: 100%, height: 0.8cm, fill: c.lighten(8%), + align(horizon, pad(x: 1.6cm, text(fill: white.transparentize(25%), size: 9.5pt, + font: ("DejaVu Sans Mono", "Consolas"), + if _co(ngay) { [\/\/ #ngay] } else { [\/\/ bài giảng · typst] }))))) +} + +// Dispatch trang bìa theo kiểu. +#let _ve-bia(kieu, c, n, don-vi, logo, tieu-de, phu-de, mon, lop, gv, ngay) = { + let f = (_bia-1, _bia-2, _bia-3, _bia-4, _bia-5).at(kieu - 1) + block(width: 100%, height: 100%, + f(c, n, don-vi, logo, tieu-de, phu-de, mon, lop, gv, ngay)) +} + +// ---------- Thiết lập tổng ---------- +// (bai-giang: điểm vào chính của hệ thống trình chiếu) +#let bai-giang( + tieu-de: [BÀI GIẢNG TOÁN], + tieu-de-ngan: none, // tên bài rút gọn hiện trên DẢI ĐẦU TRANG của mọi + // slide (beamer); none => dùng nguyên tieu-de + phu-de: none, + gv: none, + don-vi: none, + ngay: none, + lop: none, // Lớp/khối hiện trên BÌA (vd "Lớp 12A1") + mon: none, // Môn/chương hiện trên BÌA (vd "Đại số & Giải tích") + logo: none, // Logo trường trên BÌA: đường dẫn ảnh HOẶC nội dung ảnh + kieu-bia: 1, // KIỂU TRANG BÌA: 1..5 hoặc tên + // 1 "toi-gian" 2 "tre-trung" 3 "co-dien" + // 4 "chuyen-nghiep" 5 "ky-thuat" + mau-chinh: auto, // auto = dùng TÔNG MẶC ĐỊNH của kieu-bia; đặt màu để GHI ĐÈ + mau-nhan: auto, // auto = dùng tông nhấn mặc định của kieu-bia + nen: "trang", // NỀN SLIDE (beamer): "trang" | "kem" | "xanh-nhat" + // | "luc-nhat" | "xam" — hoặc màu tuỳ ý: rgb("#fdf8ee") + // (chọn tông SÁNG để hình vẽ/bảng nét sẫm còn rõ) + // Charis SIL đứng ĐẦU: font của SIL, thiết kế riêng cho dấu CHỒNG tiếng Việt + // (Ể Ổ Ữ Ẩ...). Với chữ HOA (tiêu đề đề thi) nó vẫn giữ dấu hỏi/ngã NGUYÊN CỠ + // và chồng THẲNG trên dấu mũ; Noto Serif thu nhỏ + đẩy lệch phải, còn + // Libertinus/Times vẽ hook nhỏ nằm ngang -> nhìn như mất dấu. + phong: ("Charis SIL", "Noto Serif", "Libertinus Serif", "Times New Roman"), + co-chu: 19pt, + ti-le-chu: 1.0, // HỆ SỐ PHÓNG CỠ CHỮ THÂN NỘI DUNG (toàn file): + // 1.0 = giữ nguyên; 1.1 = to hơn 10%; 0.9 = nhỏ đi 10%. + // Nhân vào cỡ chữ nền -> mọi phần thân + khung dùng em + // (định nghĩa/ví dụ/lời giải...) co giãn theo; các thanh + // tiêu đề/header/footer (pt tuyệt đối) giữ nguyên. + gian-dong: 1.0, // HỆ SỐ GIÃN DÒNG thân nội dung (toàn file): + // 1.0 = mốc mặc định (A4 0.6em, trình chiếu 0.62em); + // 1.25 = giãn thêm 25%; 0.9 = thu lại 10%. Tăng khi + // phân số/căn thức nhiều tầng làm hai dòng dính nhau. + // Đổi giữa bài: #gian-dong(1.4); riêng một câu: + // #vd/#tn/#ds/... (gian-dong: 1.4). + mau-cong-thuc: auto, // MÀU MỌI CÔNG THỨC/KÍ HIỆU trong $...$ (toàn file): + // auto (mặc định) = thừa kế màu chữ xung quanh — thân + // bài màu đen nên công thức hiển thị ĐEN, còn công thức + // nằm trong tiêu đề/bìa (chữ trắng) vẫn TRẮNG. Đặt một + // màu cụ thể, vd rgb("#0f4c81"), để nhuộm TẤT CẢ công + // thức theo màu đó (kể cả trong thanh tiêu đề). + ho-so: "beamer", // "beamer" | "sach-dethi" | "sach-loigiai" + body, +) = { + _ho-so.update(_chuan-hs(ho-so)) + // Giải màu theo kiểu bìa: auto -> tông mặc định của kieu-bia; nếu người + // dùng truyền màu cụ thể thì GHI ĐÈ. mau-chinh/mau-nhan cũng là màu chủ đạo + // của toàn slide (header/footer/khung) nên bìa và bài luôn đồng bộ. + let _kb = _bia-so(kieu-bia) + let _tm = _bia-mau(_kb) + let mau-chinh = if mau-chinh == auto { _tm.chinh } else { mau-chinh } + let mau-nhan = if mau-nhan == auto { _tm.nhan } else { mau-nhan } + // Nền slide: tên preset hoặc màu tuỳ ý; chân trang tự pha đậm hơn một chút. + let _giay = if type(nen) == str { + ( + trang: white, + kem: rgb("#fdf8ee"), + xanhnhat: rgb("#eef4fb"), + lucnhat: rgb("#f1f8f2"), + xam: rgb("#f4f5f7"), + ).at(lower(nen).replace("-", "").replace("_", ""), default: white) + } else { nen } + _mau.update(( + chinh: mau-chinh, nhan: mau-nhan, + nen: if type(_giay) == color { _giay.darken(3%) } else { rgb("#f7f9fc") }, + )) + _thong-tin.update(( + tieu-de: tieu-de, gv: gv, + ngan: if tieu-de-ngan == none { tieu-de } else { tieu-de-ngan }, + )) + // Cỡ công thức bằng thân; màu: auto = thừa kế (đen ở thân, trắng ở tiêu đề), + // hoặc nhuộm theo mau-cong-thuc nếu chỉ định. + show math.equation: it => { + set text(size: 1em) + // Lưu ý: set text(fill) phải ĐI TRƯỚC `it` trong CÙNG nhánh mới áp được; + // đặt trong `if {...}` rồi để `it` ngoài thì fill KHÔNG tác dụng. + if mau-cong-thuc == auto { it } else { + set text(fill: mau-cong-thuc) + it + } + } + + if _la-sach(ho-so) { + // ================= BẢN IN A4 (sách / đề) ================= + // Công tắc đáp án: "loigiai" -> hiện tất cả; ngược lại (dethi) -> ẩn. + state("ch-hien-da", false).update(_chuan-hs(ho-so).contains("loigiai")) + _buoc-ht.update(1000000) // mọi #lo(n) hiện hết trên bản in + set page( + paper: "a4", + margin: (x: 2cm, top: 2cm, bottom: 2cm), + fill: white, + footer: context align(center, text(size: 9pt, fill: luma(40%), + [Trang #counter(page).display() / #counter(page).final().first()])), + ) + set text(font: phong, size: 11.5pt * ti-le-chu, lang: "vi", fill: rgb("#1c2833")) + _dat-gian(0.6em, gian-dong) + set par(justify: true, leading: 0.6em * gian-dong, spacing: _doan-nen * gian-dong) + // Công thức trong dòng khai ĐÚNG chiều cao nét vẽ ⇒ dòng/ô tự nới đúng chỗ. + show math.equation.where(block: false): _chong-net + set heading(numbering: none) // không đánh số — chỉ dùng thanh màu + // Thanh tiêu đề màu thay cho heading (giữ heading để có bookmark + mục lục). + show heading.where(level: 1): it => block(width: 100%, above: 16pt, below: 10pt, + block(width: 100%, fill: mau-chinh, inset: (x: 12pt, y: 7pt), radius: 4pt, + text(fill: white, weight: "bold", size: 1.15em, it.body))) + show heading.where(level: 2): it => block(width: 100%, above: 13pt, below: 7pt, { + block(width: 100%, fill: mau-chinh, inset: (x: 10pt, y: 5pt), + radius: (top: 4pt, rest: 0pt), + text(fill: white, weight: "bold", size: 1.05em, it.body)) + block(width: 100%, height: 2.5pt, fill: mau-nhan) + }) + // ----- Tiêu đề tài liệu ----- + align(center, { + if don-vi != none { text(size: 12pt, fill: luma(30%), don-vi); linebreak() } + text(weight: "bold", size: 17pt, fill: mau-chinh, tieu-de) + if phu-de != none { linebreak(); v(2pt); text(size: 12pt, fill: luma(25%), phu-de) } + if gv != none { linebreak(); v(3pt); text(size: 11pt, style: "italic", [Giáo viên: #gv]) } + if ngay != none { linebreak(); text(size: 10pt, fill: luma(40%), ngay) } + }) + v(4pt) + line(length: 100%, stroke: 0.8pt + mau-chinh) + v(2pt) + body + } else { + // ================= BẢN TRÌNH CHIẾU 16:9 ================= + // Bật công tắc đáp án: khi trình chiếu, đáp án luôn được đánh dấu + // ở bước lo-da (bước cuối) của mỗi câu. Muốn ẩn hẳn: #tat-dap-an(). + state("ch-hien-da", false).update(true) + set page(paper: "presentation-16-9", margin: 0pt, fill: _giay) + set text(font: phong, size: co-chu * ti-le-chu, lang: "vi", fill: rgb("#1c2833")) + _dat-gian(0.62em, gian-dong) + set par(justify: false, leading: 0.62em * gian-dong, spacing: _doan-nen * gian-dong) + show math.equation.where(block: false): _chong-net + + // ----- Trang bìa (1 trong 5 kiểu) ----- + _ve-bia(_kb, mau-chinh, mau-nhan, don-vi, logo, tieu-de, phu-de, + mon, lop, gv, ngay) + + body + } +} + +// ---------- Gỡ enum trong tiêu đề ---------- +// Nội dung "[1. Tên mục]" bị Typst parse thành MỤC DANH SÁCH đánh số (enum); +// khi căn phải/giữa, số "1." bị tách xa phần chữ. Hàm này gỡ về chữ thường. +#let _go-enum(nd) = { + if type(nd) != content { return nd } + if nd.func() == enum.item { + let so = nd.at("number", default: none) + if so == none { nd.body } else { [#so. #nd.body] } + } else if nd.func() == list.item { + nd.body + } else if nd.has("children") { + nd.children.map(c => _go-enum(c)).join() + } else { nd } +} + +// ---------- Slide mục (chuyển phần) ---------- +// ngan: tên rút gọn hiển thị trên thanh điều hướng (mặc định = ten). +#let muc(ten, ngan: none) = { + let ten = _go-enum(ten) + let ngan = _go-enum(ngan) + _muc-ht.update(ten) + context { + if _la-sach(_ho-so.get()) { + // Bản in: mở phần mới ở đầu trang, tiêu đề = heading cấp 1 (thanh màu). + pagebreak(weak: true) + heading(level: 1, ten) + } else { + pagebreak(weak: true) + // mốc điều hướng: tên mục + vị trí (để tạo liên kết) + place([#metadata(( + loai: "muc", ten: ten, + ngan: if ngan == none { ten } else { ngan }, + ))]) + let m = _mau.get() + block(width: 100%, height: 100%, { + place(rect(width: 100%, height: 100%, fill: m.chinh)) + place(left + horizon, rect(width: 10pt, height: 42%, fill: m.nhan)) + place(center + horizon, align(center, + text(fill: white, weight: "bold", size: 30pt, ten) + )) + }) + } + } +} + +// ---------- Slide thường ---------- +// so-buoc: số bước hoạt hình — slide được in thành so-buoc trang, +// kết hợp với #lo(n)[...] / #chi(n)[...] / lo-da của câu hỏi. +// Nội dung dài quá một trang sẽ TỰ NGẮT sang trang kế tiếp: +// đầu/chân trang lặp lại, số slide và chấm điều hướng giữ nguyên. +#let slide(tieu-de: none, so-buoc: 1, body) = context { + let tieu-de = _go-enum(tieu-de) + // Giãn dòng hiện hành (nen × he-so) — đặt lại ở mỗi slide để #gian-dong(k) + // gọi giữa bài có tác dụng cho các slide phía sau. Với he-so = 1.0 giá trị + // này TRÙNG mốc mặc định nên bố cục không đổi. + let _gl = _gian-ht() + let _gs = _gian-doan() + if _la-sach(_ho-so.get()) { + // Bản in A4: thanh tiêu đề màu (heading cấp 2) + nội dung chảy liên tục. + if tieu-de != none { heading(level: 2, tieu-de) } + block(above: 4pt, below: 12pt, { + set par(leading: _gl, spacing: _gs) + body + }) + } else { + for k in range(1, so-buoc + 1) { + set page( + margin: ( + top: if tieu-de != none { _le-slide.tren } else { _le-slide.tren-tron }, + bottom: _le-slide.duoi, x: 0pt, + ), + header-ascent: 12pt, + footer-descent: 12pt, + // đầu trang: thanh tiêu đề (lặp lại nếu slide tràn trang) + header: if tieu-de != none { + context { + let m = _mau.get() + let tt = _thong-tin.get() + let mh = _muc-ht.get() + // Dải trên (đậm hơn): TÊN BÀI HỌC nổi bật trên MỌI slide, + // kèm tên mục hiện tại ở góc phải. + block(width: 100%, height: 17pt, fill: m.chinh.darken(30%), below: 0pt, + align(horizon, pad(x: 26pt, grid( + columns: (auto, 1fr), column-gutter: 14pt, + align: (left + horizon, right + horizon), + if tt.ngan != none { + text(fill: white, weight: "bold", size: 10.5pt, tt.ngan) + } else { [] }, + if mh != none { + text(fill: white.transparentize(40%), size: 9.5pt, mh) + } else { [] }, + )))) + // Thanh tiêu đề slide (như cũ, đủ chỗ cho tiêu đề dài). + block(width: 100%, height: 44pt, fill: m.chinh, above: 0pt, + align(horizon, pad(x: 26pt, + text(fill: white, weight: "bold", size: 21pt, tieu-de)))) + } + }, + // chân trang: thanh điều hướng + số slide (không tăng theo bước) + footer: context { + let m = _mau.get() + block(width: 100%, height: 22pt, fill: m.nen, { + place(top, line(length: 100%, stroke: 0.5pt + m.chinh.lighten(55%))) + let tat-ca = query() + let mh = _muc-ht.get() + let so-ht = _dem-slide.get().first() + align(horizon, pad(x: 26pt, grid( + columns: (auto, 1fr, auto), + column-gutter: 14pt, + // tên các mục — bấm để nhảy tới đầu mục + { + for (i, e) in tat-ca.filter(e => e.value.loai == "muc").enumerate() { + if i > 0 { h(11pt) } + let ht = e.value.ten == mh + link(e.location(), text(size: 9pt, + weight: if ht { "bold" } else { "regular" }, + fill: if ht { m.chinh } else { m.chinh.lighten(42%) }, + e.value.ngan)) + } + }, + // chấm tròn: các slide trong mục hiện tại — bấm để nhảy tới slide + align(right, { + for e in tat-ca.filter(e => e.value.loai == "slide" and e.value.muc == mh) { + h(4pt) + link(e.location(), box(baseline: 30%, circle(radius: 2.6pt, + fill: if e.value.so == so-ht { m.chinh } else { white }, + stroke: 0.7pt + m.chinh))) + } + }), + text(size: 10pt, fill: m.chinh, + [#so-ht / #_dem-slide.final().first()]), + ))) + }) + }, + ) + pagebreak(weak: true) + _buoc-ht.update(k) + // đặt lại bộ đếm trong-slide ở mỗi bước để số thứ tự không tăng + for bd in _bd-tat-ca { bd.cnt.update(0) } + // mốc điều hướng (chỉ đặt ở bước 1 — đích của liên kết) + if k == 1 { + _dem-slide.step() + place(context [#metadata(( + loai: "slide", ten: tieu-de, + muc: _muc-ht.get(), so: _dem-slide.get().first(), + ))]) + } + pad(x: _le-slide.ngang, { + set par(leading: _gl, spacing: _gs) + body + }) + // hết slide: cộng dồn số đã dùng vào tổng tích luỹ (đúng một lần) + if k == so-buoc { + context { + let so = _bd-tat-ca.map(bd => bd.cnt.get().first()) + _bd-cau.goc.update(v => v + so.at(0)) + _bd-vd.goc.update(v => v + so.at(1)) + _bd-lt.goc.update(v => v + so.at(2)) + _bd-hd.goc.update(v => v + so.at(3)) + _bd-vdtt.goc.update(v => v + so.at(4)) + } + } + } + } +} + +// ---------- Hoạt hình xuất hiện từng bước ---------- +// Dùng trong slide có so-buoc > 1: +// #lo(2)[...] — hiện TỪ bước 2. Trước đó KHÔNG chiếm chỗ +// (như \pause của LaTeX beamer) — nhờ vậy +// slide dài không sinh trang trắng đệm; +// nội dung hiện dần từ trên xuống nên bố cục +// phần đã hiện không đổi giữa các bước. +// #lo(2, giu-cho: true)[...] — ẩn nhưng vẫn giữ chỗ (khi cần cố định +// vị trí phần tử phía dưới; chỉ nên dùng +// cho slide chắc chắn gói gọn 1 trang). +// #chi(2)[...] — CHỈ hiện ở bước 2 +// #chi(2, giu-cho: false)[..] — chỉ hiện ở bước 2, không giữ chỗ +#let lo(n, giu-cho: false, body) = context { + if _la-sach(_ho-so.get()) or _buoc-ht.get() >= n { body } + else if giu-cho { hide(body) } +} + +#let chi(n, giu-cho: true, body) = context { + if _la-sach(_ho-so.get()) { body } // bản in: hiện hết + else if _buoc-ht.get() == n { body } else if giu-cho { hide(body) } +} + +// ---------- Hoạt hình BIẾN MẤT từng bước (đối xứng #lo) ---------- +// Như hiệu ứng "thoát" (exit) của PowerPoint: phần tử hiện từ đầu rồi +// BIẾN MẤT khi tới bước n. +// #an(2)[...] — hiện ở bước 1, BIẾN MẤT từ bước 2 trở đi. +// Mặc định KHÔNG giữ chỗ: nội dung phía dưới +// dồn lên lấp chỗ (như #lo). +// #an(2, giu-cho: true)[...] — biến mất nhưng CHỪA khoảng trống, bố cục +// phía dưới đứng yên (dùng cho slide gói gọn +// một trang). +// Kết hợp #lo + #an để thay thế phần tử tại một vị trí, ví dụ hiện đáp án +// sai ở bước 2 rồi xoá đi ở bước 3 và cho đáp án đúng hiện lên: +// #an(3)[đáp án tạm] #lo(3)[đáp án đúng] +// Bản in (dethi/loigiai) HIỆN HẾT như #lo — muốn bản in chỉ giữ trạng thái +// cuối thì đặt phần biến mất trong #chi (chỉ hiện đúng một bước). +#let an(n, giu-cho: false, body) = context { + if _la-sach(_ho-so.get()) or _buoc-ht.get() < n { body } + else if giu-cho { hide(body) } +} + +// Hiện TRONG MỘT KHOẢNG bước: xuất hiện ở bước `tu`, biến mất ở bước `den` +// (khoảng nửa mở [tu, den) — tức hiện ở các bước tu..den−1). Bằng đúng +// #lo(tu) + #an(den) gộp lại. +// #hien-khoang(2, 4)[...] — hiện ở bước 2 và 3, biến mất từ bước 4. +#let hien-khoang(tu, den, giu-cho: false, body) = context { + if _la-sach(_ho-so.get()) { body } // bản in: hiện hết + else { + let k = _buoc-ht.get() + if k >= tu and k < den { body } + else if giu-cho { hide(body) } + } +} + +// Danh sách hiện dần từng ý; ý đầu tiên xuất hiện ở bước `tu`. +// #tung-buoc([ý 1], [ý 2], [ý 3]) // trong slide có so-buoc đủ lớn +#let tung-buoc(tu: 2, ..cac-y) = { + for (i, y) in cac-y.pos().enumerate() { + lo(tu + i, block(above: 6pt, below: 6pt, y)) + } +} + +// ---------- Khung nội dung ---------- +// Hai block rời (đầu khung "dính" với thân) thay vì stack, +// để khung dài có thể tự ngắt sang trang/slide kế tiếp. +#let _khung(nhan, mau, body) = { + block( + width: 100%, fill: mau, inset: (x: 12pt, y: 6.5pt), + radius: (top: 5pt), above: 11pt, below: 0pt, sticky: true, + text(fill: white, weight: "bold", size: 0.78em, nhan), + ) + block( + width: 100%, fill: mau.lighten(94%), inset: (x: 12pt, y: 10pt), + radius: (bottom: 5pt), stroke: 1pt + mau, above: 0pt, below: 11pt, + body, + ) +} + +#let dinh-nghia(body, ten: none) = _khung( + if ten == none { [ĐỊNH NGHĨA] } else { [ĐỊNH NGHĨA — #ten] }, + rgb("#0f4c81"), body, +) + +#let dinh-ly(body, ten: none) = _khung( + if ten == none { [ĐỊNH LÝ] } else { [ĐỊNH LÝ — #ten] }, + rgb("#a93226"), body, +) + +#let tinh-chat(body, ten: none) = _khung( + if ten == none { [TÍNH CHẤT] } else { [TÍNH CHẤT — #ten] }, + rgb("#6c3483"), body, +) + +#let cong-thuc(body, ten: none) = _khung( + if ten == none { [CÔNG THỨC] } else { [CÔNG THỨC — #ten] }, + rgb("#148f77"), body, +) + +#let vi-du(body, ten: none) = _khung( + { + [VÍ DỤ ] + _so-moi(_bd-vd) + if ten != none { [ — #ten] } + }, + rgb("#1e8449"), body, +) + +#let luyen-tap(body, ten: none) = _khung( + { + [LUYỆN TẬP ] + _so-moi(_bd-lt) + if ten != none { [ — #ten] } + }, + rgb("#b9770e"), body, +) + +// Lời giải: khung trắng viền trái. +#let loi-giai(body) = block( + width: 100%, inset: (left: 12pt, y: 6pt, right: 6pt), + stroke: (left: 2.5pt + rgb("#1e8449")), + above: 10pt, below: 10pt, + { + align(center, text(fill: rgb("#1e8449"), weight: "bold", size: 0.82em, [Lời giải])) + body + }, +) + +#let chu-y(body) = block( + width: 100%, radius: 5pt, fill: rgb("#fef9e7"), + stroke: 1pt + rgb("#f1c40f"), inset: (x: 12pt, y: 9pt), + above: 11pt, below: 11pt, + { + text(fill: rgb("#b7950b"), weight: "bold", size: 0.82em, [⚠ Chú ý. ]) + body + }, +) + +#let ghi-nho(body) = block( + width: 100%, radius: 5pt, fill: rgb("#eaf2f8"), + stroke: (left: 3pt + rgb("#0f4c81")), inset: (x: 12pt, y: 9pt), + above: 11pt, below: 11pt, + { + text(fill: rgb("#0f4c81"), weight: "bold", size: 0.82em, [★ Ghi nhớ. ]) + body + }, +) + +// Nhận xét (mục "Nhận xét" trong SGK): khung nhẹ nền lục nhạt, viền trái. +#let nhan-xet(body) = block( + width: 100%, radius: 5pt, fill: rgb("#e8f6f3"), + stroke: (left: 3pt + rgb("#148f77")), inset: (x: 12pt, y: 9pt), + above: 11pt, below: 11pt, + { + text(fill: rgb("#117a65"), weight: "bold", size: 0.82em, [✎ Nhận xét. ]) + body + }, +) + +// ---------- Bố cục ---------- +// Hai (hoặc nhiều) cột: #chia-cot(a, b) hoặc #chia-cot(a, b, ti-le: (3fr, 2fr)) +#let chia-cot(ti-le: auto, khoang: 16pt, ..noi-dung) = { + let cells = noi-dung.pos() + grid( + columns: if ti-le == auto { (1fr,) * cells.len() } else { ti-le }, + column-gutter: khoang, + ..cells, + ) +} + +// Danh sách bước giải đánh số tròn. +// hien-dan: true => trong slide có so-buoc, các bước lần lượt xuất hiện +// từ bước hoạt hình `tu` (mặc định bước 2). +#let buoc(hien-dan: false, tu: 2, ..cac-buoc) = { + for (i, b) in cac-buoc.pos().enumerate() { + let nd = block(above: 7pt, below: 7pt, { + box(baseline: 22%, circle(radius: 9pt, fill: rgb("#0f4c81"), + align(center + horizon, text(fill: white, size: 11pt, weight: "bold", str(i + 1))))) + h(8pt) + b + }) + if hien-dan { lo(tu + i, nd) } else { nd } + } +} + +// ---------- Slide mục lục điều hướng ---------- +// Tự liệt kê mọi mục và tiêu đề slide; bấm vào là nhảy thẳng tới nơi. +// Đặt ngay sau trang bìa: #muc-luc() +#let muc-luc(tieu-de: [Nội dung bài học], cot-so: 2) = context { + if _la-sach(_ho-so.get()) { + // Bản in: mục lục tự sinh từ các thanh tiêu đề (heading cấp 1 & 2). + block(above: 8pt, below: 12pt, { + text(weight: "bold", size: 1.1em, fill: _mau.get().chinh, tieu-de) + v(5pt) + outline(title: none, depth: 2, indent: 1.2em) + }) + } else { + slide(tieu-de: tieu-de)[ + #context { + let m = _mau.get() + let tat-ca = query() + let cac-muc = tat-ca.filter(e => e.value.loai == "muc") + grid( + columns: (1fr,) * cot-so, + column-gutter: 24pt, row-gutter: 18pt, + ..cac-muc.map(mm => block({ + link(mm.location(), text(weight: "bold", size: 0.88em, fill: m.chinh, mm.value.ten)) + for s in tat-ca.filter(e => e.value.loai == "slide" and e.value.muc == mm.value.ten) { + block(above: 5pt, below: 0pt, link(s.location(), text(size: 0.74em, { + text(fill: m.nhan, [▸ ]) + if s.value.ten == none { [Slide #s.value.so] } else { s.value.ten } + }))) + } + })), + ) + } + ] + } +} + +// Trang kết thúc. +#let trang-cam-on(loi: [Cảm ơn các em đã tích cực hợp tác!]) = context { + if _la-sach(_ho-so.get()) { + // Bản in: kết thúc gọn, không tạo trang bìa cuối. + v(8pt) + line(length: 100%, stroke: 0.5pt + luma(60%)) + align(center, text(style: "italic", fill: luma(40%), loi)) + } else { + pagebreak(weak: true) + let m = _mau.get() + block(width: 100%, height: 100%, { + place(rect(width: 100%, height: 100%, + fill: gradient.linear(m.chinh, m.chinh.darken(35%), angle: 55deg))) + place(bottom, rect(width: 100%, height: 8pt, fill: m.nhan)) + place(center + horizon, text(fill: white, weight: "bold", size: 30pt, loi)) + }) + } +} diff --git a/packages/preview/conic-toan/0.3.4/lib/so-do-cay.typ b/packages/preview/conic-toan/0.3.4/lib/so-do-cay.typ new file mode 100644 index 0000000000..5c65ac4cac --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/so-do-cay.typ @@ -0,0 +1,156 @@ +// ===================================================================== +// so-do-cay.typ — SƠ ĐỒ CÂY (bài toán xác suất, đếm...) +// Cây tổng quát, sâu tuỳ ý. Mỗi nút là ô bo tròn, cạnh có nhãn xác suất, +// nút lá có thể kèm ô KẾT QUẢ (cột phải cùng hàng). +// +// #so-do-cay( +// goc: $1$, +// nhanh: ( +// nut($A$, xs: $1/6$, con: ( +// nut($B$, xs: $1/2$, kq: $A B: 1/12$), +// nut($overline(B)$, xs: $1/2$, kq: $A overline(B): 1/12$), +// )), +// nut($overline(A)$, xs: $5/6$, con: ( +// nut($B$, xs: $1/3$, kq: $overline(A) B: 5/18$), +// nut($overline(B)$, xs: $2/3$, kq: $overline(A) overline(B): 5/9$), +// )), +// ), +// ) +// ===================================================================== +#import "ve.typ": * + +// Một nút: ten (nội dung ô), xs (nhãn trên cạnh đi tới nút), +// kq (ô kết quả bên phải — thường dùng cho nút lá), con (mảng nút con), +// mau (màu nền ô; auto = theo cấp). +#let nut(ten, xs: none, kq: none, con: (), mau: auto) = ( + ten: ten, xs: xs, kq: kq, con: con, mau: mau, +) + +// Ô chữ bo tròn đặt tâm tại P (toạ độ toán). +// Kích thước ô lấy theo max(khung, biên nét chữ): công thức trong dòng như +// $1/6$ bị Typst tính hụt chiều cao khung nên nếu chỉ đo khung thì phân số +// sẽ thò ra ngoài ô (xem `co-net` trong ve.typ). +#let o-bo-tron(ctx, P, nd, to: white, vien: auto, dem-x: 8pt, dem-y: 5pt, bk: 5pt, day: 0.8pt) = context { + let s = measure(nd) + let b = co-net(nd) + let sw = calc.max(s.width, b.width) + let sh = calc.max(s.height, b.height) + let p = toa(ctx, P) + let v = if vien == auto { to.darken(35%) } else { vien } + place( + dx: p.at(0) - sw / 2 - dem-x, + dy: p.at(1) - sh / 2 - dem-y, + rect( + width: sw + 2 * dem-x, height: sh + 2 * dem-y, + radius: bk, fill: to, stroke: day + v, + ), + ) + place(dx: p.at(0) - s.width / 2, dy: p.at(1) - s.height / 2, nd) +} + +// ---------- duyệt cây (nội bộ) ---------- +#let _la-so(n) = if n.con.len() == 0 { 1 } else { n.con.map(_la-so).sum() } +#let _sau-max(n) = if n.con.len() == 0 { 0 } else { 1 + calc.max(..n.con.map(_sau-max)) } + +// Trả về (y: tung độ tâm, cuoi: y0 còn lại, nut/canh/kq: dữ liệu vẽ). +#let _duyet(n, sau, y0) = { + if n.con.len() == 0 { + (y: y0 - 0.5, cuoi: y0 - 1, nut: (), canh: (), kq: ()) + } else { + let yc = y0 + let nuts = () + let canhs = () + let kqs = () + let ys = () + for c in n.con { + let r = _duyet(c, sau + 1, yc) + yc = r.cuoi + ys.push(r.y) + nuts += r.nut + canhs += r.canh + kqs += r.kq + nuts.push((P: (sau + 1, r.y), nd: c.ten, sau: sau + 1, mau: c.mau)) + if c.kq != none { kqs.push((y: r.y, nd: c.kq)) } + } + let y = (ys.first() + ys.last()) / 2 + for (i, c) in n.con.enumerate() { + canhs.push((A: (sau, y), B: (sau + 1, ys.at(i)), xs: c.xs)) + } + (y: y, cuoi: yc, nut: nuts, canh: canhs, kq: kqs) + } +} + +#let so-do-cay( + goc: $1$, + nhanh: (), // mảng nut(...) cấp 1 + w: 11.5cm, + cao-hang: 1.5cm, // khoảng cách dọc giữa hai lá kề nhau + cach-kq: 0.55, // cột kết quả cách cột lá (đơn vị cột) + mau-cap: (rgb("#cfe9f5"), rgb("#dcd7f5"), rgb("#d5eed5"), rgb("#fdeacd")), + mau-kq: rgb("#fdecec"), vien-kq: rgb("#e08a8a"), + mau-canh: black, day-canh: 0.9pt, + co-chu: 10pt, + dem-x: 8pt, dem-y: 5pt, // đệm trong của ô + them: none, // ctx => vẽ thêm +) = { + let root = nut(goc, con: nhanh) + let L = _la-so(root) + let D = _sau-max(root) + let r = _duyet(root, 0, L) + let cac-nut = r.nut + ((P: (0, r.y), nd: root.ten, sau: 0, mau: root.mau),) + let co-kq = r.kq.len() > 0 + let xkq = D + cach-kq + let xmax = if co-kq { xkq + 1.0 } else { D + 0.55 } + hinh( + w: w, h: cao-hang * L, + xmin: -0.55, xmax: xmax, ymin: 0, ymax: L, + co-chu: co-chu, + ctx => context { + // cạnh + nhãn xác suất (đầu mút lùi vào theo bề rộng ô đã đo) + for c in r.canh { + let ncha = cac-nut.find(k => k.P == c.A) + let ncon = cac-nut.find(k => k.P == c.B) + let wa = if ncha != none { measure(ncha.nd).width / 2 + dem-x + 3pt } else { 0pt } + let wb = if ncon != none { measure(ncon.nd).width / 2 + dem-x + 3pt } else { 0pt } + let pa = toa-pt(ctx, c.A) + let pb = toa-pt(ctx, c.B) + let A2 = toa-nguoc(ctx, (pa.at(0) + wa.pt(), pa.at(1))) + let B2 = toa-nguoc(ctx, (pb.at(0) - wb.pt(), pb.at(1))) + mui-ten(ctx, A2, B2, mau: mau-canh, day: day-canh, kich: 6pt) + if c.xs != none { + // Nhánh đi XUỐNG (nút con thấp hơn nút cha) đặt nhãn phía DƯỚI cạnh + // cho cân đối và khỏi chen vào chỗ rẽ; nhánh đi lên/ngang giữ phía + // trên như cũ. Chữ vẫn quay theo cạnh (đọc xuôi). + let xuong = c.B.at(1) < c.A.at(1) + nhan( + ctx, trung-diem(A2, B2), c.xs, + huong: if xuong { "below" } else { "above" }, + cach: 3pt, quay: goc-truc(ctx, A2, B2), + ) + } + } + // các ô nút + for k in cac-nut { + let to = if k.mau == auto { mau-cap.at(calc.rem(k.sau, mau-cap.len())) } else { k.mau } + o-bo-tron(ctx, k.P, k.nd, to: to, dem-x: dem-x, dem-y: dem-y) + } + // cột kết quả + for q in r.kq { + let s = measure(q.nd) + let b = co-net(q.nd) + let sw = calc.max(s.width, b.width) + let sh = calc.max(s.height, b.height) + let p = toa(ctx, (xkq, q.y)) + place( + dx: p.at(0), dy: p.at(1) - sh / 2 - dem-y, + rect( + width: sw + 2.4 * dem-x, height: sh + 2 * dem-y, + radius: 5pt, fill: mau-kq, stroke: 0.8pt + vien-kq, + ), + ) + place(dx: p.at(0) + 1.2 * dem-x, dy: p.at(1) - s.height / 2, q.nd) + } + if them != none { them(ctx) } + }, + ) +} diff --git a/packages/preview/conic-toan/0.3.4/lib/tron-xoay.typ b/packages/preview/conic-toan/0.3.4/lib/tron-xoay.typ new file mode 100644 index 0000000000..9606d03a99 --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/tron-xoay.typ @@ -0,0 +1,359 @@ +// ===================================================================== +// tron-xoay.typ — MIỀN PHẲNG & KHỐI TRÒN XOAY (Giải tích 12) +// +// #khoi-tron-xoay(x => calc.sqrt(x), 0, 4) +// -> TRÁI: miền phẳng giới hạn bởi y = f(x), trục hoành, x = a, x = b +// (đã tô màu); PHẢI: khối tròn xoay sinh ra khi quay miền đó quanh Ox. +// +// Tham số chính: +// truc : "Ox" (mặc định) hoặc "Oy". Với "Oy" thì f là BÁN KÍNH theo y, +// tức miền giới hạn bởi x = f(y), trục tung, y = a, y = b. +// hien : "ca-hai" (mặc định) | "mien" (chỉ miền phẳng) | "khoi" (chỉ khối) +// g : biên trong (khối rỗng — vành khăn), none = khối đặc +// mat-cat : c -> vẽ thiết diện (đĩa/vành khăn) tại x = c kèm bán kính +// the-tich : true -> ghi công thức thể tích dưới hình +// +// Hàm lõi `ve-khoi-xoay(ctx, f, a, b, ...)` vẽ RIÊNG khối vào một khung +// #hinh có sẵn (gọi được không cần ctx như mọi hàm vẽ khác). +// ===================================================================== +#import "ve.typ": * +#import "do-thi.typ": so-toan + +// ---------- Tiện ích nội bộ ---------- +// Điểm trong hệ "chuẩn hoá": u dọc theo TRỤC QUAY, v vuông góc trục quay. +#let _pxy(ngang, u, v) = if ngang { (u, v) } else { (v, u) } + +// Điểm trên đường tròn thiết diện đã chiếu thành elip (k = tỉ lệ bán trục): +// t = 0° đỉnh trên · 90° mép "sau" · 180° đỉnh dưới · 270° mép "trước" +#let _e-diem(ngang, k, u, R, t) = _pxy( + ngang, + u + k * R * calc.sin(t), + R * calc.cos(t), +) + +// Cung elip thiết diện từ t1 đến t2. +#let _cung-td( + ctx, ngang, k, u, R, t1, t2, + mau: black, day: 1pt, dut: false, n: 40, +) = { + if R <= 0.0001 { return } + duong-cong( + ctx, + range(n + 1).map(i => _e-diem(ngang, k, u, R, t1 + (t2 - t1) * i / n)), + mau: mau, day: day, dut: dut, + ) +} + +// Elip thiết diện ĐẦY ĐỦ (dùng để tô). +#let _to-td(ctx, ngang, k, u, R, mau) = { + if R <= 0.0001 { return } + da-giac-pt( + range(49).map(i => toa-pt(ctx, _e-diem(ngang, k, u, R, 360deg * i / 48))), + to: mau, + ) +} + +// Giá trị lớn nhất của |f| trên [a, b] (lấy mẫu). +#let _ban-kinh-max(f, a, b, n: 120) = calc.max( + 0.0001, + ..range(n + 1).map(i => calc.abs(f(a + (b - a) * i / n))), +) + +// Hệ trục cho hình: trục quay (có mũi tên + tên) và trục vuông góc nếu 0 thuộc +// phạm vi. `ngang = true` -> trục quay là Ox. +#let _truc-xoay( + ctx, ngang, umin, umax, vmin, vmax, + mau: black, day: 0.9pt, ten-goc: $O$, +) = { + let (ten-u, ten-v) = if ngang { ($x$, $y$) } else { ($y$, $x$) } + mui-ten(ctx, _pxy(ngang, umin, 0), _pxy(ngang, umax, 0), mau: mau, day: day) + nhan( + ctx, _pxy(ngang, umax, 0), ten-u, + huong: if ngang { "duoi-phai" } else { "tren-phai" }, cach: 3pt, + ) + if vmin <= 0 and vmax >= 0 and umin <= 0 and umax >= 0 { + mui-ten(ctx, _pxy(ngang, 0, vmin), _pxy(ngang, 0, vmax), mau: mau, day: day) + nhan( + ctx, _pxy(ngang, 0, vmax), ten-v, + huong: if ngang { "tren-phai" } else { "duoi-phai" }, cach: 3pt, + ) + if ten-goc != none { + nhan(ctx, (0, 0), ten-goc, huong: "duoi-trai", cach: 4pt, mau: mau) + } + } +} + +// ===================================================================== +// ve-khoi-xoay — vẽ KHỐI TRÒN XOAY vào khung #hinh có sẵn +// (trục quay đi qua gốc; `ngang: true` = quay quanh Ox, false = quanh Oy) +// ===================================================================== +#let ve-khoi-xoay( + ctx, f, a, b, + g: none, // bán kính trong (vành khăn); none = đặc + ngang: true, + k: 0.26, // tỉ lệ bán trục ngang của elip / bán kính + mau: blue, day: 1.2pt, + mau-to: rgb(70, 130, 200, 60), + duong-sinh: true, // vẽ biên trên/dưới của khối + truc-mo: true, // đoạn trục quay bên trong khối vẽ nét đứt + mat-cat: none, // vẽ thiết diện tại u = c + mau-mat-cat: rgb(235, 130, 40, 120), + ten-ban-kinh: auto, // nhãn bán kính thiết diện (none = bỏ) + ten-mat-cat: auto, // nhãn mốc c trên trục (none = bỏ) + n: 120, +) = { + let R = u => calc.abs(f(u)) + let Rt = if g == none { u => 0.0 } else { u => calc.abs(g(u)) } + let Ra = R(a) + let Rb = R(b) + let M = _ban-kinh-max(f, a, b) + let tren = range(n + 1).map(i => { + let u = a + (b - a) * i / n + _pxy(ngang, u, R(u)) + }) + let duoi = range(n + 1).map(i => { + let u = b + (a - b) * i / n + _pxy(ngang, u, -R(u)) + }) + // ---- tô khối: nắp trái (nửa ngoài) + biên trên + nắp phải + biên dưới + let nap-t = range(41).map(i => _e-diem(ngang, k, a, Ra, 180deg + 180deg * i / 40)) + let nap-p = range(41).map(i => _e-diem(ngang, k, b, Rb, 180deg * i / 40)) + da-giac-pt((nap-t + tren + nap-p + duoi).map(P => toa-pt(ctx, P)), to: mau-to) + // ---- trục quay bên trong khối (bị che -> nét đứt) + if truc-mo { + doan( + ctx, _pxy(ngang, a, 0), _pxy(ngang, b, 0), + mau: black, day: 0.7pt, dut: true, + ) + } + // ---- thiết diện + if mat-cat != none { + let c = mat-cat + let Rc = R(c) + _to-td(ctx, ngang, k, c, Rc, mau-mat-cat) + if g != none { _to-td(ctx, ngang, k, c, Rt(c), white) } + _cung-td(ctx, ngang, k, c, Rc, 0deg, 360deg, mau: mau.darken(10%), day: 0.9pt) + if g != none { + _cung-td(ctx, ngang, k, c, Rt(c), 0deg, 360deg, mau: mau.darken(10%), day: 0.9pt) + } + doan(ctx, _pxy(ngang, c, 0), _pxy(ngang, c, Rc), mau: red.darken(10%), day: 0.9pt) + if ten-ban-kinh != none { + let nd = if ten-ban-kinh == auto { + if ngang { $f(x)$ } else { $f(y)$ } + } else { ten-ban-kinh } + nhan( + ctx, _e-diem(ngang, k, c, Rc, 90deg + 45deg), nd, + huong: if ngang { "phai" } else { "duoi" }, + cach: 3pt, mau: red.darken(10%), + ) + } + if ten-mat-cat != none { + let nd = if ten-mat-cat == auto { so-toan(c) } else { ten-mat-cat } + doan( + ctx, _pxy(ngang, c, -Rc), _pxy(ngang, c, -M - 0.3), + mau: gray.darken(20%), day: 0.7pt, dut: true, + ) + nhan( + ctx, _pxy(ngang, c, -M - 0.3), nd, + huong: if ngang { "duoi" } else { "trai" }, cach: 3pt, + ) + } + } + // ---- đường sinh (biên trên / biên dưới) + if duong-sinh { + duong-cong(ctx, tren, mau: mau, day: day) + duong-cong(ctx, duoi, mau: mau, day: day) + } + // ---- nắp hai đầu: trái nửa trong bị che (đứt), phải thấy trọn + _cung-td(ctx, ngang, k, a, Ra, 180deg, 360deg, mau: mau, day: day) + _cung-td(ctx, ngang, k, a, Ra, 0deg, 180deg, mau: mau, day: 0.8pt, dut: true) + _cung-td(ctx, ngang, k, b, Rb, 0deg, 360deg, mau: mau, day: day) + // ---- khối rỗng: mặt trong + lỗ ở nắp phải + if g != none { + _to-td(ctx, ngang, k, b, Rt(b), white) + _cung-td(ctx, ngang, k, b, Rt(b), 0deg, 360deg, mau: mau, day: day) + _cung-td(ctx, ngang, k, a, Rt(a), 0deg, 360deg, mau: mau, day: 0.8pt, dut: true) + for s in (1, -1) { + duong-cong( + ctx, + range(n + 1).map(i => { + let u = a + (b - a) * i / n + _pxy(ngang, u, s * Rt(u)) + }), + mau: mau, day: 0.9pt, dut: true, + ) + } + } +} + +// ===================================================================== +// ve-mien-xoay — vẽ MIỀN PHẲNG sinh ra khối (tô màu + cận a, b) +// ===================================================================== +#let ve-mien-xoay( + ctx, f, a, b, + g: none, + ngang: true, + mau: blue, day: 1.3pt, + mau-to: rgb(70, 130, 200, 90), + ten-ham: auto, ten-ham-trong: none, + ten-a: auto, ten-b: auto, + giong-ab: true, + n: 120, +) = { + let R = u => calc.abs(f(u)) + let Rt = if g == none { u => 0.0 } else { u => calc.abs(g(u)) } + let tren = range(n + 1).map(i => { + let u = a + (b - a) * i / n + _pxy(ngang, u, R(u)) + }) + let duoi = range(n + 1).map(i => { + let u = b + (a - b) * i / n + _pxy(ngang, u, Rt(u)) + }) + da-giac-pt((tren + duoi).map(P => toa-pt(ctx, P)), to: mau-to) + duong-cong(ctx, tren, mau: mau, day: day) + if g != none { duong-cong(ctx, duoi, mau: mau.darken(25%), day: day) } + if giong-ab { + for (u, t) in ((a, ten-a), (b, ten-b)) { + doan( + ctx, _pxy(ngang, u, Rt(u)), _pxy(ngang, u, R(u)), + mau: gray.darken(25%), day: 0.8pt, dut: true, + ) + // cận trùng gốc toạ độ thì bỏ nhãn (kẻo đè chữ O) + if t != none and not (t == auto and calc.abs(u) < 0.001) { + nhan( + ctx, _pxy(ngang, u, 0), if t == auto { so-toan(u) } else { t }, + huong: if ngang { "duoi" } else { "trai" }, cach: 4pt, + ) + } + } + } + if ten-ham != none { + let u = a + (b - a) * 0.78 + let nd = if ten-ham == auto { + if ngang { $y = f(x)$ } else { $x = f(y)$ } + } else { ten-ham } + nhan( + ctx, _pxy(ngang, u, R(u)), nd, + huong: if ngang { "tren-phai" } else { "phai" }, cach: 5pt, mau: mau, + ) + } + if ten-ham-trong != none and g != none { + let u = a + (b - a) * 0.3 + nhan( + ctx, _pxy(ngang, u, Rt(u)), ten-ham-trong, + huong: if ngang { "duoi-phai" } else { "trai" }, cach: 5pt, mau: mau.darken(25%), + ) + } +} + +// ===================================================================== +// khoi-tron-xoay — HÌNH HOÀN CHỈNH: miền phẳng -> khối tròn xoay +// ===================================================================== +#let khoi-tron-xoay( + f, a, b, + g: none, + truc: "Ox", + hien: "ca-hai", // "ca-hai" | "mien" | "khoi" + w: 6cm, + k: 0.26, + mau: blue, day: 1.3pt, + mau-to: rgb(70, 130, 200, 60), + mau-mien: rgb(70, 130, 200, 95), + ten-ham: auto, ten-ham-trong: none, + ten-a: auto, ten-b: auto, + mat-cat: none, + ten-mat-cat: auto, ten-ban-kinh: auto, + the-tich: false, // ghi công thức thể tích dưới hình + nhan-giua: auto, // nội dung giữa 2 hình + ten-goc: $O$, // nhãn gốc toạ độ (none = bỏ) + co-chu: 10pt, + khung: false, + them: none, them-mien: none, +) = { + let ngang = truc != "Oy" + let M = _ban-kinh-max(f, a, b) + let d = calc.abs(b - a) + let (a, b) = (calc.min(a, b), calc.max(a, b)) + let le = 0.16 * calc.max(M, d / 2) + 0.22 + let e = k * M + let umin = a - e - le + let umax = b + e + le + 0.12 * d + let vmax = M + le + let vmin = -M - le - (if mat-cat != none and ten-mat-cat != none { 0.42 } else { 0 }) + // cửa sổ của hình miền phẳng: chỉ cần chút chỗ dưới trục để ghi cận + let vmin-m = -0.4 * M - 0.22 + let ti-le = w / (umax - umin) // giữ ĐÚNG một tỉ lệ cho cả 2 hình + + let khung-khoi(rong, x1, x2, y1, y2, ve) = hinh( + w: rong, xmin: x1, xmax: x2, ymin: y1, ymax: y2, + co-chu: co-chu, khung: khung, ve, + ) + + let h-khoi = { + let (x1, x2, y1, y2) = if ngang { + (umin, umax, vmin, vmax) + } else { + (vmin, vmax, umin, umax) + } + khung-khoi(ti-le * (x2 - x1), x1, x2, y1, y2, ctx => { + _truc-xoay(ctx, ngang, umin, umax, vmin, vmax, ten-goc: ten-goc) + ve-khoi-xoay( + ctx, f, a, b, g: g, ngang: ngang, k: k, + mau: mau, day: day, mau-to: mau-to, + mat-cat: mat-cat, ten-mat-cat: ten-mat-cat, ten-ban-kinh: ten-ban-kinh, + ) + if them != none { them(ctx) } + }) + } + + let h-mien = { + let (x1, x2, y1, y2) = if ngang { + (umin, umax, vmin-m, vmax) + } else { + (vmin-m, vmax, umin, umax) + } + khung-khoi(ti-le * (x2 - x1), x1, x2, y1, y2, ctx => { + _truc-xoay(ctx, ngang, umin, umax, vmin-m, vmax, ten-goc: ten-goc) + ve-mien-xoay( + ctx, f, a, b, g: g, ngang: ngang, + mau: mau, day: day, mau-to: mau-mien, + ten-ham: ten-ham, ten-ham-trong: ten-ham-trong, + ten-a: ten-a, ten-b: ten-b, + ) + if them-mien != none { them-mien(ctx) } + }) + } + + let giua = if nhan-giua == auto { + align(center, { + text(size: 0.82 * co-chu)[quay quanh #truc] + linebreak() + text(size: 1.3 * co-chu)[$arrow.r.long$] + }) + } else { nhan-giua } + + let ct = if the-tich { + let (bien, kx) = if ngang { ($x$, $dif x$) } else { ($y$, $dif y$) } + let (na, nb) = (so-toan(a), so-toan(b)) + if g == none { + $V = pi integral_(#na)^(#nb) f^2 (#bien) #kx$ + } else { + $V = pi integral_(#na)^(#nb) [f^2 (#bien) - g^2 (#bien)] #kx$ + } + } else { none } + + let than = if hien == "mien" { + h-mien + } else if hien == "khoi" { + h-khoi + } else { + grid( + columns: 3, column-gutter: 10pt, align: horizon, + h-mien, giua, h-khoi, + ) + } + if ct == none { than } else { + align(center, { than; v(2pt); ct }) + } +} diff --git a/packages/preview/conic-toan/0.3.4/lib/ve.typ b/packages/preview/conic-toan/0.3.4/lib/ve.typ new file mode 100644 index 0000000000..0f5b37344a --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/lib/ve.typ @@ -0,0 +1,1556 @@ +// ===================================================================== +// ve.typ — ENGINE VẼ HÌNH THUẦN TYPST (không phụ thuộc package nào) +// Hệ toạ độ toán học: gốc do người dùng chọn, trục y hướng LÊN. +// Mọi hàm vẽ nhận `ctx` (do #hinh cung cấp) + toạ độ dạng (x, y) là số. +// Quy ước tham số chung: mau (màu), day (độ dày nét), dut (nét đứt). +// ===================================================================== + +// ---------- Khung vẽ ---------- +// Tạo khung vẽ. `ve` là hàm nhận ctx và trả về nội dung vẽ. +// h: auto => tự tính để 2 trục CÙNG TỈ LỆ (nên giữ auto với hình hình học). +// Ngăn xếp ctx của các khung hình đang vẽ — cho phép GỌI HÀM VẼ KHÔNG CẦN ctx +// (xem _voi-ctx ngay dưới hinh). Đẩy vào khi vào khung, lấy ra khi rời khung. +#let _ctx-ht = state("bg-ctx-ht", ()) + +/// Khung vẽ: đặt cửa sổ toạ độ toán (xmin..xmax, ymin..ymax) rồi vẽ bên trong. +/// Mọi lệnh vẽ đặt trong thân hàm này là tự có `ctx`, không phải gõ. +/// -> content +#let hinh( + w: 8cm, + h: auto, + xmin: -5, xmax: 5, + ymin: -4, ymax: 4, + co-chu: 10pt, + khung: false, // vẽ viền khung (để căn chỉnh khi soạn) + cat: false, // cắt phần tràn ra ngoài khung + ve, +) = { + let hh = if h == auto { w * (ymax - ymin) / (xmax - xmin) } else { h } + let ctx = ( + w: w, h: hh, + xmin: xmin, xmax: xmax, ymin: ymin, ymax: ymax, + sx: w / (xmax - xmin), + sy: hh / (ymax - ymin), + ) + box( + width: w, height: hh, + stroke: if khung { 0.4pt + luma(70%) } else { none }, + clip: cat, + { + set text(size: co-chu) + _ctx-ht.update(s => s + (ctx,)) + ve(ctx) + _ctx-ht.update(s => s.slice(0, -1)) + }, + ) +} + +// Bọc hàm vẽ f(ctx, ...) thành hàm gọi được KHÔNG CẦN ctx: nếu đối số đầu là +// ctx thì gọi thẳng (lối cũ), không thì tự lấy ctx của khung hình đang vẽ. +// Dùng ở baigiang.typ để bọc đồng loạt các hàm vẽ (xem cuối file đó). +// +// ⚠ NGĂN XẾP RỖNG — KHÔNG ĐƯỢC panic (xem CLAUDE.md, mục "CTX NGẦM TRONG +// MEASURE"). Nội dung nằm trong `measure(...)` KHÔNG được dựng vào tài liệu nên +// mọi `state.update` bên trong nó vô hiệu: `_ctx-ht.get()` trả về giá trị CUỐI +// tài liệu = () ⇒ trước đây panic, làm beamer chết ở phép đo tự ngắt màn +// (_cat-man-vua) và ở `cot: auto` của cau-mc. Nay: +// · trong measure ⇒ BỎ QUA, không vẽ gì. Phép đo KHÔNG đổi vì #hinh là `box` +// có width/height CỐ ĐỊNH, nét vẽ bên trong đều `place` (không chiếm chỗ); +// bản in thật thì ngăn xếp đầy đủ nên vẫn vẽ đúng như cũ. +// · thật sự quên #hinh(...) ⇒ ghi DẤU ĐỎ (dùng `place` nên không phá bố cục) +// thay cho panic — đủ để thấy mà không giết cả lần biên dịch. +#let _voi-ctx(f) = (..a) => { + let p = a.pos() + if p.len() > 0 and type(p.at(0)) == dictionary and "sx" in p.at(0) { + f(..a) + } else { + context { + let ds = _ctx-ht.get() + if ds.len() > 0 { + f(ds.last(), ..a) + } else { + place(text(fill: red, size: 7pt)[⚠ lệnh vẽ đặt NGOÀI khung hình]) + } + } + } +} + +// ---------- Đổi toạ độ ---------- +// Toạ độ toán (x, y) -> toạ độ trang (length, length), gốc trên-trái. +#let toa(ctx, P) = { + let Q = if "bien-doi" in ctx and ctx.bien-doi != none { (ctx.bien-doi)(P) } else { P } + ((Q.at(0) - ctx.xmin) * ctx.sx, (ctx.ymax - Q.at(1)) * ctx.sy) +} +// Như trên nhưng trả về số float tính bằng pt (để tính toán). +#let toa-pt(ctx, P) = { + let q = toa(ctx, P) + (q.at(0).pt(), q.at(1).pt()) +} + +// ---------- Phép biến hình (toạ độ toán) ---------- +// Quay điểm P quanh tâm `tam` góc `goc` (dương = ngược chiều kim đồng hồ). +#let quay-diem(P, tam, goc) = { + let dx = P.at(0) - tam.at(0) + let dy = P.at(1) - tam.at(1) + let c = calc.cos(goc) + let s = calc.sin(goc) + (tam.at(0) + dx * c - dy * s, tam.at(1) + dx * s + dy * c) +} +// Tịnh tiến điểm P theo vectơ v. +#let tinh-tien-diem(P, v) = (P.at(0) + v.at(0), P.at(1) + v.at(1)) +// Toạ độ CỰC kiểu TikZ: điểm cách `tam` một khoảng `bk`, quay góc `goc` +// (số trần = ĐỘ, nhận cả kiểu angle 30deg/0.5rad; dương = ngược kim đồng hồ). +// toa-cuc((0,0), 2, 30) ~ TikZ (30:2) +/// Toạ độ cực kiểu TikZ: điểm cách `tam` một khoảng `bk`, theo `goc` +/// (số trần = độ, dương = ngược kim đồng hồ). TikZ `(30:2)` <=> `toa-cuc((0,0), 2, 30)`. +/// TRẢ VỀ điểm, không vẽ gì. +#let toa-cuc(tam, bk, goc) = { + let g = if type(goc) == angle { goc } else { goc * 1deg } + (tam.at(0) + bk * calc.cos(g), tam.at(1) + bk * calc.sin(g)) +} +// Dựng điểm M sao cho tia AM = tia AB QUAY quanh A một góc lượng giác `goc` +// (số trần = ĐỘ, dương = ngược kim đồng hồ) và AM = `r`. Nói cách khác, góc +// định hướng (AB, AM) = `goc`. Dùng dựng đỉnh tam giác biết một cạnh + một góc. +// let M = dung-diem(A, B, 60, 3) // góc BAM = 60°, AM = 3 +/// TRẢ VỀ điểm M: tia AM là tia AB quay quanh A góc lượng giác `goc` +/// (số trần = độ, dương ngược kim đồng hồ), độ dài AM = `r`. Không vẽ gì. +#let dung-diem(A, B, goc, r) = { + let g = if type(goc) == angle { goc } else { goc * 1deg } + let base = calc.atan2(B.at(0) - A.at(0), B.at(1) - A.at(1)) + toa-cuc(A, r, base + g) +} +// Ghép thêm một phép biến hình f vào ctx (nội bộ; phép cũ làm trước, f làm sau). +#let _ghep-bien-doi(ctx, f) = { + let cu = ctx.at("bien-doi", default: none) + ctx + (bien-doi: if cu == none { f } else { P => f((cu)(P)) }) +} +// Bản sao ctx bị QUAY: mọi hàm vẽ dùng ctx này đều quay quanh `tam` góc `goc`. +// let cq = ctx-quay(ctx, 30deg, tam: (1, 1)); tam-giac(cq, A, B, C) +// Bản sao ctx bị TỊNH TIẾN theo vectơ v; ghép nối tiếp được nhiều phép: +// ctx-tinh-tien(ctx-quay(ctx, 30deg, tam: G), (2, 0)) — quay trước, tịnh tiến sau +// Lưu ý: duong-tron/elip vẫn đúng dưới 2 phép này (tâm tự biến đổi); riêng +// gach-vung mô tả miền theo toạ độ GỐC của khung (không qua bien-doi). +#let ctx-quay(ctx, goc, tam: (0, 0)) = _ghep-bien-doi(ctx, P => quay-diem(P, tam, goc)) +#let ctx-tinh-tien(ctx, v) = _ghep-bien-doi(ctx, P => tinh-tien-diem(P, v)) + +// Kiểu nét +#let net(mau, day, dut) = ( + paint: mau, + thickness: day, + dash: if dut { "dashed" } else { none }, +) + +// ---------- Nguyên thuỷ cấp thấp (toạ độ pt, dùng nội bộ) ---------- +#let doan-pt(p, q, mau: black, day: 1pt, dut: false) = place( + line( + start: (p.at(0) * 1pt, p.at(1) * 1pt), + end: (q.at(0) * 1pt, q.at(1) * 1pt), + stroke: net(mau, day, dut), + ) +) + +// Đa giác tô màu từ toạ độ pt (tự chuẩn hoá về gốc bao). +#let da-giac-pt(pts, to: black, vien: none) = { + let xs = pts.map(p => p.at(0)) + let ys = pts.map(p => p.at(1)) + let x0 = calc.min(..xs) + let y0 = calc.min(..ys) + place( + dx: x0 * 1pt, dy: y0 * 1pt, + polygon( + fill: to, stroke: vien, + ..pts.map(p => ((p.at(0) - x0) * 1pt, (p.at(1) - y0) * 1pt)), + ), + ) +} + +// Đầu mũi tên tại b, hướng a->b (toạ độ pt, dùng nội bộ). +#let _dau-pt(a, b, mau: black, kich: 7pt) = { + let dx = b.at(0) - a.at(0) + let dy = b.at(1) - a.at(1) + let l = calc.sqrt(dx * dx + dy * dy) + if l < 0.001 { return } + let k = kich.pt() + let ux = dx / l + let uy = dy / l + let px = -uy + let py = ux + da-giac-pt( + ( + b, + (b.at(0) - k * ux + 0.38 * k * px, b.at(1) - k * uy + 0.38 * k * py), + (b.at(0) - 0.72 * k * ux, b.at(1) - 0.72 * k * uy), + (b.at(0) - k * ux - 0.38 * k * px, b.at(1) - k * uy - 0.38 * k * py), + ), + to: mau, + ) +} + +// Đầu mũi tên tại B, hướng A->B. +#let dau-mui-ten(ctx, A, B, mau: black, kich: 7pt) = _dau-pt( + toa-pt(ctx, A), toa-pt(ctx, B), mau: mau, kich: kich, +) + +// ---------- Nguyên thuỷ chính ---------- +// (định nghĩa `doan` nằm NGAY SAU `nhan` vì có tuỳ chọn nhãn giữa đoạn) + +// Hướng VUÔNG GÓC với đoạn AB trên TRANG (vectơ đơn vị dùng cho `nhan`): +// ưu tiên phía trên đoạn; đoạn thẳng đứng thì ra phía trái. +#let _phap-tuyen(ctx, A, B) = { + let a = toa-pt(ctx, A) + let b = toa-pt(ctx, B) + let (dx, dy) = (b.at(0) - a.at(0), b.at(1) - a.at(1)) + let l = calc.sqrt(dx * dx + dy * dy) + if l < 0.0001 { return (0, -1) } + let n = (-dy / l, dx / l) + if n.at(1) > 0.000001 { n = (-n.at(0), -n.at(1)) } // xoay lên trên + if calc.abs(n.at(1)) <= 0.000001 and n.at(0) > 0 { n = (-1, 0) } // đoạn dọc -> trái + n +} + +// Góc xoay (kiểu angle) để chữ NẰM DỌC THEO đoạn/đường A -> B (toạ độ toán), +// có tính cả tỉ lệ 2 trục sx, sy. Dùng cho nhãn tiệm cận xiên, nhãn dọc theo trục... +// nhan(ctx, P, $y = x - 1$, quay: goc-truc(ctx, (0, -1), (1, 0))) +// nhan(ctx, (1, y), $x = 1$, quay: goc-truc(ctx, (1, 0), (1, 1))) // dọc, đọc từ dưới lên +#let goc-truc(ctx, A, B) = { + let dx = (B.at(0) - A.at(0)) * ctx.sx.pt() + let dy = (B.at(1) - A.at(1)) * ctx.sy.pt() + -calc.atan2(dx, dy) +} + +// ---------- Đo nhãn theo BIÊN NÉT CHỮ ---------- +// Typst tính HỤT khung của công thức TRONG DÒNG: `measure($1/2$)` chỉ trả về +// chiều cao MỘT DÒNG CHỮ (≈6.8pt ở cỡ 10pt) trong khi phân số vẽ ra cao gấp +// đôi (≈12pt) và tràn cả trên lẫn dưới khung. Nhãn đặt theo khung đó nên bị +// đường vẽ CẮT NGANG (phân số, căn bậc hai, chỉ số trên/dưới…). Đo lại bằng +// top-edge/bottom-edge: "bounds" cho ra đúng biên nét chữ. +// PHẢI gọi trong `context`. Số chữ thường (không tràn) cho kết quả nhỏ hơn +// hoặc bằng khung, nên nơi dùng luôn lấy phần TRÀN = max(0, biên − khung): +// chữ thường bù 0 ⇒ mọi hình cũ giữ nguyên bố cục. +#let co-net(nd) = measure({ + set text(top-edge: "bounds", bottom-edge: "bounds") + nd +}) + +// Phần nét chữ tràn ra NGOÀI khung, chia đều hai bên: (dx, dy) tính bằng length. +#let _tran-net(nd) = { + let k = measure(nd) + let b = co-net(nd) + ( + calc.max(0pt, b.width - k.width) / 2, + calc.max(0pt, b.height - k.height) / 2, + ) +} + +// Nhãn văn bản đặt cạnh điểm P. +// huong: "above", "below", "left", "right", "above-left", "above-right", +// "below-left", "below-right", "center" +// (tên tiếng Việt cũ "tren", "duoi", "trai", "phai", ... vẫn dùng được) +// quay: góc xoay chữ (vd 90deg, -45deg, hoặc goc-truc(...)). Mặc định 0deg. +/// Đặt chữ/công thức tại điểm P. `huong` là phía đặt chữ so với P +/// ("above", "below", "left", "right", "above-left"… hoặc tuple (dx, dy)), +/// `cach` là khoảng cách tới P, `quay` để xoay chữ theo đường. +#let nhan(ctx, P, noi-dung, huong: "above", cach: 6pt, mau: black, quay: 0deg) = { + let p = toa(ctx, P) + // huong: tên hướng, HOẶC vectơ (dx, dy) trên trang (y hướng XUỐNG) — dùng + // cho nhãn đặt vuông góc với một đoạn bất kỳ (xem _phap-tuyen). + let hd = if type(huong) == array { huong } else { + ( + above: (0, -1), below: (0, 1), left: (-1, 0), right: (1, 0), + above-left: (-0.7, -0.7), above-right: (0.7, -0.7), + below-left: (-0.7, 0.7), below-right: (0.7, 0.7), + center: (0, 0), + // bí danh tiếng Việt (tương thích ngược) + tren: (0, -1), duoi: (0, 1), trai: (-1, 0), phai: (1, 0), + tren-trai: (-0.7, -0.7), tren-phai: (0.7, -0.7), + duoi-trai: (-0.7, 0.7), duoi-phai: (0.7, 0.7), + giua: (0, 0), + ).at(huong) + } + context { + let nd = text(fill: mau, noi-dung) + let nd2 = if quay == 0deg { nd } else { rotate(quay, reflow: true, nd) } + let s = measure(nd2) + // bù phần nét chữ tràn ra ngoài khung (phân số, căn…) — xem `co-net` + let (bx, by) = _tran-net(nd2) + place( + dx: p.at(0) - s.width / 2 + hd.at(0) * (cach + s.width / 2 + bx), + dy: p.at(1) - s.height / 2 + hd.at(1) * (cach + s.height / 2 + by), + nd2, + ) + } +} + +// Đoạn thẳng AB, kèm NHÃN CHÚ THÍCH đặt trên đoạn (tuỳ chọn): +// ten : nội dung nhãn (vd $a$, [3 cm]) — none = không ghi +// tai : vị trí nhãn theo TỈ LỆ từ A đến B (0 = tại A, 1 = tại B, .5 = giữa) +// huong : auto = tự đặt VUÔNG GÓC phía trên đoạn (đoạn dọc thì ra trái); +// hoặc tên hướng ("above", "below", "left", ...), hoặc vectơ (dx, dy) +// cach : khoảng cách từ đoạn tới nhãn +// ten-quay: true = chữ NẰM DỌC theo đoạn (tự xoay, luôn đọc xuôi) +// mau-ten : màu chữ (auto = theo màu đoạn) +// Ví dụ: doan(A, B, ten: $2a$) // giữa đoạn, phía trên +// doan(A, B, ten: $h$, tai: 0.7, huong: "right", cach: 4pt) +// doan(A, B, ten: [đường chéo], ten-quay: true) +/// Đoạn thẳng A–B. Kèm nhãn giữa đoạn bằng `ten:` (đặt chỗ khác bằng `tai:` +/// tỉ lệ 0..1, lệch sang bên bằng `huong:`, xoay theo đoạn bằng `ten-quay: true`). +#let doan( + ctx, A, B, + mau: black, day: 1pt, dut: false, + ten: none, tai: 0.5, huong: auto, cach: 6pt, ten-quay: false, mau-ten: auto, +) = { + place(line(start: toa(ctx, A), end: toa(ctx, B), stroke: net(mau, day, dut))) + if ten != none { + let P = (A.at(0) + tai * (B.at(0) - A.at(0)), A.at(1) + tai * (B.at(1) - A.at(1))) + // chữ dọc theo đoạn: chọn chiều A->B hay B->A để không bị lộn ngược + let xuoi = toa-pt(ctx, A).at(0) <= toa-pt(ctx, B).at(0) + nhan( + ctx, P, ten, + huong: if huong == auto { _phap-tuyen(ctx, A, B) } else { huong }, + cach: cach, + mau: if mau-ten == auto { mau } else { mau-ten }, + quay: if ten-quay { if xuoi { goc-truc(ctx, A, B) } else { goc-truc(ctx, B, A) } } else { 0deg }, + ) + } +} + +// Điểm (chấm tròn) + nhãn tuỳ chọn. +// cach: khoảng cách nhãn tới điểm · mau-ten: màu chữ (auto = màu chấm). +/// Chấm một điểm, kèm tên: `diem(A, ten: $A$, huong: "below-left")`. +/// Nhiều điểm một lệnh thì dùng `cac-diem`. +#let diem(ctx, P, ten: none, huong: "tren", bk: 2pt, mau: black, cach: 6pt, mau-ten: black) = { + let p = toa(ctx, P) + place(dx: p.at(0) - bk, dy: p.at(1) - bk, circle(radius: bk, fill: mau, stroke: none)) + if ten != none { + nhan(ctx, P, ten, huong: huong, cach: cach, mau: if mau-ten == auto { mau } else { mau-ten }) + } +} + +// Đặt nhãn cho nhiều điểm, VỊ TRÍ nhãn xác định bằng GÓC LƯỢNG GIÁC (thay cho +// tên hướng "above"/"below"…). Mỗi mục là một tuple: +// (P, nội-dung, goc) — bán kính & màu lấy mặc định +// (P, nội-dung, goc, ban-kinh) — ban-kinh là ĐỘ DÀI trang (vd 8pt) +// (P, nội-dung, goc, ban-kinh, mau) — kèm màu riêng +// `goc` là góc lượng giác (số trần = ĐỘ, dương = ngược kim đồng hồ; nhãn đặt +// về phía góc đó so với điểm). Phần tử sau `goc` nhận diện theo kiểu: color -> +// màu, còn lại -> bán kính, nên thứ tự ban-kinh/mau linh hoạt. +// nhan-goc((A, $A$, 210), (B, $B$, -30, 9pt), (C, $C$, 90, 8pt, red)) +/// Đặt nhãn nhiều điểm theo GÓC LƯỢNG GIÁC. Mỗi mục: +/// `(P, nội-dung, goc[, ban-kinh][, mau])` — `goc` số trần = độ, dương ngược +/// kim đồng hồ; `ban-kinh` là độ dài trang (khoảng cách điểm→nhãn). +#let nhan-goc(ctx, ..muc, mau: black, ban-kinh: 6pt) = { + for m in muc.pos() { + let P = m.at(0) + let nd = m.at(1) + let goc = m.at(2) + let r = ban-kinh + let c = mau + for x in m.slice(3) { + if type(x) == color { c = x } else { r = x } + } + let g = if type(goc) == angle { goc } else { goc * 1deg } + // hướng trên TRANG (y hướng XUỐNG) theo góc lượng giác (y toán hướng lên) + let hd = (calc.cos(g), -calc.sin(g)) + nhan(ctx, P, nd, huong: hd, cach: r, mau: c) + } +} + +// Mũi tên A -> B. +#let mui-ten(ctx, A, B, mau: black, day: 1pt, kich: 7pt, dut: false) = { + doan(ctx, A, B, mau: mau, day: day, dut: dut) + dau-mui-ten(ctx, A, B, mau: mau, kich: kich) +} + +// Vectơ A -> B kèm tên đặt ở trung điểm. +// dut: true = thân nét đứt (vectơ nằm trên cạnh khuất, kiểu hình SGK); +// đầu mũi tên luôn nét liền. +#let vecto(ctx, A, B, ten: none, huong: "tren", mau: black, day: 1.1pt, dut: false) = { + mui-ten(ctx, A, B, mau: mau, day: day, dut: dut) + if ten != none { + nhan(ctx, ((A.at(0) + B.at(0)) / 2, (A.at(1) + B.at(1)) / 2), ten, huong: huong, mau: mau) + } +} + +// ---------- Mũi tên HAI ĐẦU (đường ghi số đo) ---------- +// Mũi tên có đầu ở CẢ HAI phía, kiểu đường ghi kích thước trong bản vẽ: +// thường dùng để chú thích số đo đặt Ở GIỮA đoạn mũi tên. +// ten : nội dung chú thích (vd $2a$, [5 cm]) — none = chỉ vẽ mũi tên +// trong : chữ nằm CHÍNH GIỮA thân, thân bị cắt chừa chỗ (|<-- 5 cm -->|) +// auto = tự bật khi có `ten` và đoạn đủ dài để chứa chữ, +// không đủ dài thì tự đặt chữ ra ngoài (phía trên đoạn) +// huong : chỗ đặt chữ khi KHÔNG nằm giữa thân +// (auto = vuông góc phía trên đoạn, như `doan`) +// ten-quay : true = chữ NẰM DỌC theo mũi tên (luôn đọc xuôi) +// vach : true = kẻ vạch chặn vuông góc ở hai đầu (kiểu ghi kích thước) +// le : lùi hai đầu mũi tên vào trong, để hở khỏi vật đang đo +// nen : màu nền lót sau chữ khi chữ nằm giữa thân (none = không lót) +// Ví dụ: +// mui-ten-2-dau(A, B, ten: [5 cm]) // chữ giữa thân +// mui-ten-2-dau(A, B, ten: $2a$, trong: false) // chữ trên đoạn +// mui-ten-2-dau((0, -0.4), (4, -0.4), ten: $h$, vach: true) +/// Mũi tên HAI ĐẦU giữa A và B, kiểu đường ghi kích thước. `ten:` là số đo +/// đặt GIỮA thân (`trong: false` để đưa chữ ra ngoài), `vach: true` kẻ vạch +/// chặn hai đầu, `le:` lùi hai đầu vào trong cho hở khỏi vật đang đo. +#let mui-ten-2-dau( + ctx, A, B, + mau: black, day: 1pt, kich: 7pt, dut: false, + ten: none, huong: auto, cach: 5pt, ten-quay: false, mau-ten: auto, + trong: auto, dem: 3pt, nen: white, + vach: false, dai-vach: 9pt, + le: 0pt, +) = context { + let a0 = toa-pt(ctx, A) + let b0 = toa-pt(ctx, B) + let (dx, dy) = (b0.at(0) - a0.at(0), b0.at(1) - a0.at(1)) + let l0 = calc.sqrt(dx * dx + dy * dy) + if l0 < 0.001 { return } + let (ux, uy) = (dx / l0, dy / l0) + let lp = calc.min(le.pt(), l0 / 2 - 0.5) + let a = (a0.at(0) + lp * ux, a0.at(1) + lp * uy) + let b = (b0.at(0) - lp * ux, b0.at(1) - lp * uy) + let l = l0 - 2 * lp + let mc = ((a.at(0) + b.at(0)) / 2, (a.at(1) + b.at(1)) / 2) + let mt = ((A.at(0) + B.at(0)) / 2, (A.at(1) + B.at(1)) / 2) + let mau-t = if mau-ten == auto { mau } else { mau-ten } + + // Kích thước chữ (đo cả biên nét để phân số/căn không bị thân mũi tên cắt). + let nd = if ten == none { none } else { + let n = text(fill: mau-t, ten) + let q = if ten-quay { + let g = if a.at(0) <= b.at(0) { goc-truc(ctx, A, B) } else { goc-truc(ctx, B, A) } + rotate(g, reflow: true, n) + } else { n } + q + } + let (kw, kh, bw, bh) = if nd == none { (0pt, 0pt, 0pt, 0pt) } else { + let k = measure(nd) + let bn = co-net(nd) + (k.width, k.height, calc.max(k.width, bn.width), calc.max(k.height, bn.height)) + } + // Bề ngang chỗ chữ chiếm DỌC THEO thân mũi tên + hai đầu mũi tên phải đủ chỗ. + let ho = bw.pt() * calc.abs(ux) + bh.pt() * calc.abs(uy) + 2 * dem.pt() + let o-giua = ( + ten != none + and (if trong == auto { l > ho + 3.2 * kich.pt() } else { trong }) + ) + + // Thân: cắt đôi chừa chỗ cho chữ nếu chữ nằm giữa + if o-giua { + let h2 = calc.min(ho / 2, l / 2 - 0.2) + doan-pt(a, (mc.at(0) - h2 * ux, mc.at(1) - h2 * uy), mau: mau, day: day, dut: dut) + doan-pt((mc.at(0) + h2 * ux, mc.at(1) + h2 * uy), b, mau: mau, day: day, dut: dut) + } else { + doan-pt(a, b, mau: mau, day: day, dut: dut) + } + _dau-pt(b, a, mau: mau, kich: kich) + _dau-pt(a, b, mau: mau, kich: kich) + + // Vạch chặn hai đầu (kiểu đường ghi kích thước) + if vach { + let hv = dai-vach.pt() / 2 + let (px, py) = (-uy, ux) + for q in (a0, b0) { + doan-pt( + (q.at(0) - hv * px, q.at(1) - hv * py), + (q.at(0) + hv * px, q.at(1) + hv * py), + mau: mau, day: day, + ) + } + } + + // Chú thích + if ten != none { + if o-giua { + if nen != none { + place( + dx: (mc.at(0) - bw.pt() / 2 - dem.pt() / 2) * 1pt, + dy: (mc.at(1) - bh.pt() / 2) * 1pt, + rect(width: bw + dem, height: bh, fill: nen, stroke: none), + ) + } + place(dx: (mc.at(0) - kw.pt() / 2) * 1pt, dy: (mc.at(1) - kh.pt() / 2) * 1pt, nd) + } else { + nhan( + ctx, mt, ten, + huong: if huong == auto { _phap-tuyen(ctx, A, B) } else { huong }, + cach: cach, mau: mau-t, + quay: if ten-quay { + if a.at(0) <= b.at(0) { goc-truc(ctx, A, B) } else { goc-truc(ctx, B, A) } + } else { 0deg }, + ) + } + } +} + +// Đường gấp khúc qua dãy điểm. dut: nét đứt (vẽ cách đoạn -> đều trên đường cong). +#let duong-cong(ctx, cac-diem, mau: black, day: 1pt, dut: false, dong: false) = { + let pts = cac-diem + if dong and pts.len() > 1 { pts = pts + (pts.at(0),) } + for i in range(pts.len() - 1) { + if not dut or calc.rem(i, 2) == 0 { + doan(ctx, pts.at(i), pts.at(i + 1), mau: mau, day: day) + } + } +} + +// Đường gấp khúc nối lần lượt A - B - C - ... KHÁC duong-cong ở chỗ: +// dut: true là nét đứt THẬT trên TỪNG đoạn (hợp polyline ít đoạn; +// duong-cong đứt kiểu "bỏ đoạn xen kẽ" chỉ hợp đường nhiều mẫu). +// dong: true nối điểm cuối về điểm đầu. +#let duong-gap-khuc(ctx, cac-diem, mau: black, day: 1pt, dut: false, dong: false) = { + let pts = cac-diem + if dong and pts.len() > 1 { pts = pts + (pts.at(0),) } + for i in range(pts.len() - 1) { + doan(ctx, pts.at(i), pts.at(i + 1), mau: mau, day: day, dut: dut) + } +} + +// Đa giác kín: viền + tô màu tuỳ chọn (to: màu hoặc none). +/// Đa giác khép kín. Đỉnh truyền vào dưới dạng MỘT MẢNG: +/// `da-giac((A, B, C))` — không phải `da-giac(A, B, C)`. `to:` để tô màu. +#let da-giac(ctx, cac-diem, mau: black, day: 1pt, dut: false, to: none) = { + if to != none { + da-giac-pt(cac-diem.map(P => toa-pt(ctx, P)), to: to) + } + duong-cong(ctx, cac-diem, mau: mau, day: day, dut: dut, dong: true) +} + +// ---------- cac-doan: VẼ NHIỀU NÉT TRONG MỘT LỆNH (kiểu \draw của TikZ) ---- +// Đối số vị trí có thể trộn thoải mái: +// • các ĐIỂM liền nhau -> nối thành MỘT đường gấp khúc: +// cac-doan(A, B) // một đoạn +// cac-doan(A, B, C, D) // A-B-C-D +// • một MẢNG ĐIỂM -> một nét riêng: +// cac-doan((A, B), (C, D)) // hai đoạn rời +// • duong(...) -> một nét riêng CÓ KIỂU RIÊNG (đè lên kiểu chung): +// cac-doan( +// duong(A, B, C, dong: true, to: blue.lighten(85%)), // tam giác tô +// duong(S, H, dut: true, ten: $h$), // nét đứt có nhãn +// (M, N), // đoạn thường +// mau: blue, day: 1.2pt, // kiểu CHUNG +// ) +// Tuỳ chọn mỗi nét (đặt ở duong(...) hoặc dùng chung ở cac-doan): +// mau · day · dut · dong (khép kín) · to (tô màu, tự khép kín) +// mui-ten (vẽ đầu mũi tên ở điểm cuối) · ten/tai/huong/cach/ten-quay/mau-ten +// (nhãn đặt tại tỉ lệ `tai` tính theo TỔNG chiều dài nét — xem `doan`) +#let duong( + ..noi-dung, + mau: auto, day: auto, dut: auto, dong: auto, to: auto, mui-ten: auto, + ten: auto, tai: auto, huong: auto, cach: auto, ten-quay: auto, mau-ten: auto, +) = { + let p = noi-dung.pos() + // duong((A, B, C)) — truyền nguyên một mảng điểm + if (p.len() == 1 and type(p.at(0)) == array and p.at(0).len() > 0 + and type(p.at(0).at(0)) == array) { p = p.at(0) } + ( + bg-duong: true, diem: p, + mau: mau, day: day, dut: dut, dong: dong, to: to, mui-ten: mui-ten, + ten: ten, tai: tai, huong: huong, cach: cach, ten-quay: ten-quay, mau-ten: mau-ten, + ) +} + +#let _la-duong(x) = type(x) == dictionary and x.at("bg-duong", default: false) +#let _la-diem(x) = type(x) == array and x.len() == 2 and type(x.at(0)) != array + +/// Vẽ NHIỀU nét trong một lệnh: điểm liền nhau thành một đường gấp khúc, +/// mỗi MẢNG điểm là một nét riêng — `cac-doan((A, B), (C, D), (M, N))`. +/// Nét cần kiểu riêng thì bọc `duong(...)`. +#let cac-doan( + ctx, ..noi-dung, + mau: black, day: 1pt, dut: false, dong: false, to: none, mui-ten: false, + ten: none, tai: 0.5, huong: auto, cach: 6pt, ten-quay: false, mau-ten: auto, +) = { + // gom đối số vị trí thành danh sách các nét + let nets = () + let dem = () + for x in noi-dung.pos() { + if _la-diem(x) { dem.push(x) } else { + if dem.len() > 0 { nets.push(duong(..dem)); dem = () } + if _la-duong(x) { nets.push(x) } else if type(x) == array { nets.push(duong(x)) } + } + } + if dem.len() > 0 { nets.push(duong(..dem)) } + // vẽ từng nét: giá trị auto của nét lấy theo tuỳ chọn chung + for n in nets { + let ly(khoa, chung) = if n.at(khoa) == auto { chung } else { n.at(khoa) } + let (m, d, du) = (ly("mau", mau), ly("day", day), ly("dut", dut)) + let (dg, tt) = (ly("dong", dong), ly("to", to)) + let pts = n.diem + if pts.len() == 0 { continue } + if pts.len() == 1 { diem(ctx, pts.at(0), mau: m); continue } + if tt != none { da-giac-pt(pts.map(P => toa-pt(ctx, P)), to: tt) } + let cd = if dg or tt != none { pts + (pts.at(0),) } else { pts } + for i in range(cd.len() - 1) { + doan(ctx, cd.at(i), cd.at(i + 1), mau: m, day: d, dut: du) + } + if ly("mui-ten", mui-ten) { + dau-mui-ten(ctx, cd.at(cd.len() - 2), cd.at(cd.len() - 1), mau: m) + } + // nhãn: đặt tại tỉ lệ `tai` tính theo TỔNG chiều dài nét (trên trang) + let nh = ly("ten", ten) + if nh != none { + let t = ly("tai", tai) + let dai = range(cd.len() - 1).map(i => { + let a = toa-pt(ctx, cd.at(i)) + let b = toa-pt(ctx, cd.at(i + 1)) + calc.sqrt(calc.pow(b.at(0) - a.at(0), 2) + calc.pow(b.at(1) - a.at(1), 2)) + }) + let tong = dai.sum() + let moc = calc.max(calc.min(t, 1), 0) * tong + let i = 0 + let con = moc + while i < dai.len() - 1 and con > dai.at(i) { con = con - dai.at(i); i = i + 1 } + let ti = if dai.at(i) > 0.0001 { con / dai.at(i) } else { 0.5 } + let (U, V) = (cd.at(i), cd.at(i + 1)) + let P = (U.at(0) + ti * (V.at(0) - U.at(0)), U.at(1) + ti * (V.at(1) - U.at(1))) + let hg = ly("huong", huong) + let mt = ly("mau-ten", mau-ten) + let xuoi = toa-pt(ctx, U).at(0) <= toa-pt(ctx, V).at(0) + nhan( + ctx, P, nh, + huong: if hg == auto { _phap-tuyen(ctx, U, V) } else { hg }, + cach: ly("cach", cach), + mau: if mt == auto { m } else { mt }, + quay: if ly("ten-quay", ten-quay) { + if xuoi { goc-truc(ctx, U, V) } else { goc-truc(ctx, V, U) } + } else { 0deg }, + ) + } + } +} + +// ---------- cac-diem: CHẤM + ĐẶT TÊN NHIỀU ĐIỂM TRONG MỘT LỆNH ---------- +// Mỗi đối số vị trí là một điểm, viết theo 1 trong 4 dạng: +// A -> chỉ chấm, không nhãn +// (A, $A$) -> chấm + nhãn, hướng lấy theo `huong` chung +// (A, $A$, "left") -> + hướng riêng +// (A, $A$, "left", red) -> + màu riêng (cả chấm lẫn chữ nếu mau-ten: auto) +// Tuỳ chọn chung: mau · bk (bán kính chấm) · huong · cach (nhãn cách chấm) +// · mau-ten (auto = theo màu chấm). +// cac-diem( +// (A, $A$, "below-left"), (B, $B$, "below-right"), (C, $C$, "above"), +// mau: blue, bk: 2.2pt, +// ) +/// Chấm + đặt tên nhiều điểm một lệnh. Mỗi mục: `A` | `(A, $A$)` | +/// `(A, $A$, "left")` | `(A, $A$, "left", red)`. +#let cac-diem( + ctx, ..noi-dung, + mau: black, bk: 2pt, huong: "above", cach: 6pt, mau-ten: black, +) = { + for x in noi-dung.pos() { + let la-diem = type(x) == array and x.len() == 2 and type(x.at(0)) != array + let P = if la-diem { x } else { x.at(0) } + let t = if la-diem { none } else { x.at(1, default: none) } + let h = if la-diem { auto } else { x.at(2, default: auto) } + let m = if la-diem { auto } else { x.at(3, default: auto) } + let mc = if m == auto { mau } else { m } + diem( + ctx, P, ten: t, + huong: if h == auto { huong } else { h }, + bk: bk, mau: mc, cach: cach, + mau-ten: if mau-ten == auto or m != auto { mc } else { mau-ten }, + ) + } +} + +// Dãy điểm trên cung elip tâm O bán trục (a, b), góc tu -> den. +#let diem-cung(O, a, b, tu, den, n: 48) = range(n + 1).map(i => { + let t = tu + (den - tu) * (i / n) + (O.at(0) + a * calc.cos(t), O.at(1) + b * calc.sin(t)) +}) + +// Đường tròn tâm O bán kính r (theo đơn vị trục x; nếu 2 trục cùng tỉ lệ +// thì là đường tròn thật, khác tỉ lệ sẽ thành elip tương ứng). +#let duong-tron(ctx, O, r, mau: black, day: 1pt, dut: false, to: none) = { + let c = toa(ctx, O) + let rx = r * ctx.sx + let ry = r * ctx.sy + place( + dx: c.at(0) - rx, dy: c.at(1) - ry, + ellipse(width: 2 * rx, height: 2 * ry, stroke: net(mau, day, dut), fill: to), + ) +} + +// Elip tâm O, bán trục a (ngang), b (dọc); quay: góc xoay quanh tâm +// (dương = ngược chiều kim đồng hồ), n: số mẫu khi vẽ elip xoay. +#let elip(ctx, O, a, b, mau: black, day: 1pt, dut: false, to: none, quay: 0deg, n: 72) = { + if quay == 0deg and not ("bien-doi" in ctx and ctx.bien-doi != none) { + let c = toa(ctx, O) + let rx = a * ctx.sx + let ry = b * ctx.sy + place( + dx: c.at(0) - rx, dy: c.at(1) - ry, + ellipse(width: 2 * rx, height: 2 * ry, stroke: net(mau, day, dut), fill: to), + ) + } else { + // elip xoay (hoặc ctx có phép biến hình): vẽ theo dãy điểm quanh chu vi + let pts = diem-cung(O, a, b, 0deg, 360deg, n: n).map(P => quay-diem(P, O, quay)) + if to != none { da-giac-pt(pts.map(P => toa-pt(ctx, P)), to: to) } + duong-cong(ctx, pts, mau: mau, day: day, dut: dut, dong: true) + } +} + +// Cung tròn tâm O bán kính r từ góc `tu` đến `den` (kiểu angle, vd 30deg). +#let cung(ctx, O, r, tu: 0deg, den: 180deg, mau: black, day: 1pt, dut: false, n: 48, quay: 0deg) = { + duong-cong(ctx, diem-cung(O, r, r, tu + quay, den + quay, n: n), mau: mau, day: day, dut: dut) +} + +// Cung elip (dùng cho hình không gian: đáy nón, trụ...). +#let cung-elip(ctx, O, a, b, tu: 0deg, den: 180deg, mau: black, day: 1pt, dut: false, n: 48, quay: 0deg) = { + let pts = diem-cung(O, a, b, tu, den, n: n) + if quay != 0deg { pts = pts.map(P => quay-diem(P, O, quay)) } + duong-cong(ctx, pts, mau: mau, day: day, dut: dut) +} + +// ---------- Đường xoắn ốc ---------- +// Xoắn ốc Archimedes tâm O: bán kính TĂNG ĐỀU theo góc, quét từ `tu` đến `den`. +// Góc quét |den - tu| ĐƯỢC PHÉP lớn hơn 360° (nhiều vòng); +// den > tu = ngược chiều kim đồng hồ (chiều dương), den < tu = cùng chiều kim đồng hồ. +// r : bán kính tại góc `tu` +// r-cuoi : bán kính tại góc `den` (auto = r + buoc · số vòng quét) +// buoc : khoảng cách giữa hai vòng liên tiếp (dùng khi r-cuoi: auto) +// mui-ten : true|"cuoi" (đầu mũi tên ở điểm cuối) · "dau" · "ca-hai" · false +// ten/huong/cach/mau-ten : nhãn đặt cạnh điểm cuối của xoắn ốc +#let xoan-oc( + ctx, O, + tu: 0deg, den: 360deg, + r: 0.12, r-cuoi: auto, buoc: 0.16, + mau: black, day: 1pt, dut: false, + mui-ten: true, kich: 7pt, + n: auto, + ten: none, huong: "above", cach: 6pt, mau-ten: auto, +) = { + let quet = den - tu + let so-vong = calc.abs(quet.deg()) / 360 + let r2 = if r-cuoi == auto { r + buoc * so-vong } else { r-cuoi } + let m = if n == auto { calc.max(32, int(calc.ceil(so-vong * 72))) } else { n } + let pts = range(m + 1).map(i => { + let s = i / m + let a = tu + quet * s + let rr = r + (r2 - r) * s + (O.at(0) + rr * calc.cos(a), O.at(1) + rr * calc.sin(a)) + }) + duong-cong(ctx, pts, mau: mau, day: day, dut: dut) + if mui-ten == true or mui-ten == "cuoi" or mui-ten == "ca-hai" { + dau-mui-ten(ctx, pts.at(m - 1), pts.at(m), mau: mau, kich: kich) + } + if mui-ten == "dau" or mui-ten == "ca-hai" { + dau-mui-ten(ctx, pts.at(1), pts.at(0), mau: mau, kich: kich) + } + if ten != none { + nhan( + ctx, pts.at(m), ten, + huong: huong, cach: cach, + mau: if mau-ten == auto { mau } else { mau-ten }, + ) + } +} + +// Góc lượng giác (OA, OM): xoắn ốc từ tia OA quay tới tia OM (kiểu SGK 11). +// chieu : "duong" = ngược chiều kim đồng hồ (mặc định) · "am" = cùng chiều +// vong : số vòng quay THÊM (0, 1, 2, ...) trước khi dừng ở tia OM +// so-do : true = tự ghi số đo (vd -430°) cạnh mũi tên; `ten` được ưu tiên +// Ví dụ hình "AOM = 70°, (OA, OM) = -430°": goc-luong-giac(O, A, M, chieu: "am", vong: 1) +#let goc-luong-giac( + ctx, O, A, M, + chieu: "duong", vong: 0, + r: auto, r-cuoi: auto, buoc: auto, + mau: blue, day: 0.9pt, dut: false, kich: 6pt, + ten: none, so-do: false, huong: "above", cach: 6pt, mau-ten: auto, + n: auto, +) = { + let am = chieu in ("am", "-", -1) or chieu == false + // r, buoc mặc định tính THEO chiều dài tia (để hình nào cũng cân đối) + let dai = calc.min( + calc.sqrt(calc.pow(A.at(0) - O.at(0), 2) + calc.pow(A.at(1) - O.at(1), 2)), + calc.sqrt(calc.pow(M.at(0) - O.at(0), 2) + calc.pow(M.at(1) - O.at(1), 2)), + ) + if dai <= 0 { dai = 1 } + let r = if r == auto { 0.12 * dai } else { r } + let buoc = if buoc == auto { 0.14 * dai } else { buoc } + let a1 = calc.atan2(A.at(0) - O.at(0), A.at(1) - O.at(1)) + let a2 = calc.atan2(M.at(0) - O.at(0), M.at(1) - O.at(1)) + let d = a2 - a1 + while d < 0deg { d = d + 360deg } + while d >= 360deg { d = d - 360deg } + if am and d != 0deg { d = d - 360deg } + let tong = d + (if am { -1 } else { 1 }) * vong * 360deg + let nd = if ten != none { ten } else if so-do { + let v = calc.round(tong.deg(), digits: 1) + let s = if calc.abs(v - calc.round(v)) < 0.01 { str(int(calc.round(v))) } else { str(v) } + [#s#sym.degree] + } else { none } + xoan-oc( + ctx, O, tu: a1, den: a1 + tong, + r: r, r-cuoi: r-cuoi, buoc: buoc, + mau: mau, day: day, dut: dut, kich: kich, n: n, + ten: nd, huong: huong, cach: cach, mau-ten: mau-ten, + ) +} + +// ---------- Đường cong uốn lượn (Bézier kiểu \draw ... controls ... của TikZ) ---------- +// `dieu-khien(c1, c2)` — điểm điều khiển của MỘT đoạn cong, đặt XEN GIỮA hai +// điểm neo (giống `.. controls (c1) and (c2) ..` của TikZ): +// duong-luon(A, dieu-khien(c1, c2), B, dieu-khien(c3, c4), C) +// dieu-khien(c1) // một điểm điều khiển (c2 = c1) +// dieu-khien(c1, auto) // c1 tự chọn cho mềm, c2 do người dùng đặt (auto lấy Catmull-Rom) +// Đoạn KHÔNG có dieu-khien giữa hai neo -> tự làm MỀM bằng Catmull-Rom +// (đường trơn đi qua đúng các điểm neo, khỏi tính điểm điều khiển bằng tay). +#let dieu-khien(..a) = { + let p = a.pos() + let c1 = p.at(0, default: auto) + let c2 = p.at(1, default: c1) + (bg-dk: true, c1: c1, c2: c2) +} +#let _la-dk(x) = type(x) == dictionary and x.at("bg-dk", default: false) + +// Lấy MẢNG điểm mẫu của đường uốn lượn (dùng chung cho vẽ và cho nhãn bám đường). +// items: dãy đối số vị trí (điểm neo trộn với dieu-khien(...)); n: mẫu mỗi đoạn. +#let _luon-mau(items, n, dong) = { + let anchors = () + let ctrls = () // ctrls.at(k) = dieu-khien của đoạn anchors[k] -> anchors[k+1] + let pending = none + for x in items { + if _la-dk(x) { pending = x } + else if _la-diem(x) { + if anchors.len() > 0 { ctrls.push(pending); pending = none } + anchors.push(x) + } + } + if anchors.len() < 2 { return anchors } + if dong { + ctrls.push(pending) // điều khiển của đoạn khép kín (cuối -> đầu) + anchors.push(anchors.at(0)) + } + let m = anchors.len() + let getc = i => anchors.at(calc.max(0, calc.min(m - 1, i))) + let getw = i => { + if dong { + let base = m - 1 + anchors.at(calc.rem(calc.rem(i, base) + base, base)) + } else { getc(i) } + } + let pts = (anchors.at(0),) + for k in range(m - 1) { + let p1 = anchors.at(k) + let p2 = anchors.at(k + 1) + let p0 = getw(k - 1) + let p3 = getw(k + 2) + // điểm điều khiển Catmull-Rom (đường trơn qua các neo) + let ec1 = (p1.at(0) + (p2.at(0) - p0.at(0)) / 6, p1.at(1) + (p2.at(1) - p0.at(1)) / 6) + let ec2 = (p2.at(0) - (p3.at(0) - p1.at(0)) / 6, p2.at(1) - (p3.at(1) - p1.at(1)) / 6) + let dk = ctrls.at(k, default: none) + let c1 = if dk == none or dk.c1 == auto { ec1 } else { dk.c1 } + let c2 = if dk == none or dk.c2 == auto { ec2 } else { dk.c2 } + for j in range(1, n + 1) { + let t = j / n + let u = 1 - t + let x = u * u * u * p1.at(0) + 3 * u * u * t * c1.at(0) + 3 * u * t * t * c2.at(0) + t * t * t * p2.at(0) + let y = u * u * u * p1.at(1) + 3 * u * u * t * c1.at(1) + 3 * u * t * t * c2.at(1) + t * t * t * p2.at(1) + pts.push((x, y)) + } + } + pts +} + +// TRẢ VỀ mảng điểm mẫu của đường uốn lượn (dùng cho nhan-cong hoặc để vẽ lại). +// KHÔNG cần ctx (chỉ tính trên toạ độ toán). +#let diem-luon(..noi-dung, n: 16, dong: false) = _luon-mau(noi-dung.pos(), n, dong) + +// Vẽ đường cong uốn lượn qua các điểm neo, điểm điều khiển kiểu `controls` TikZ. +// duong-luon(A, B, C, D) // trơn tự động qua 4 neo +// duong-luon(A, dieu-khien(c1, c2), B) // Bézier bậc ba một đoạn +// duong-luon(A, dieu-khien(c1,c2), B, C, dong: true, to: blue.lighten(85%)) +// Tuỳ chọn: mau · day · dut · dong (khép kín) · to (tô) · mui-ten · kich · +// ten/tai/huong/cach/ten-quay/mau-ten (nhãn đặt theo TỈ LỆ độ dài, như cac-doan) · +// n (số mẫu mỗi đoạn, tăng cho mượt). +#let duong-luon( + ctx, ..noi-dung, + mau: black, day: 1pt, dut: false, dong: false, to: none, + mui-ten: false, kich: 7pt, + ten: none, tai: 0.5, huong: auto, cach: 6pt, ten-quay: false, mau-ten: auto, + n: 16, +) = { + let pts = _luon-mau(noi-dung.pos(), n, dong) + if pts.len() < 2 { return } + if to != none { da-giac-pt(pts.map(P => toa-pt(ctx, P)), to: to) } + duong-cong(ctx, pts, mau: mau, day: day, dut: dut) + if mui-ten { + dau-mui-ten(ctx, pts.at(pts.len() - 2), pts.at(pts.len() - 1), mau: mau, kich: kich) + } + if ten != none { + let pg = pts.map(P => toa-pt(ctx, P)) + let dai = range(pg.len() - 1).map(i => { + let a = pg.at(i) + let b = pg.at(i + 1) + calc.sqrt(calc.pow(b.at(0) - a.at(0), 2) + calc.pow(b.at(1) - a.at(1), 2)) + }) + let tong = dai.sum() + let moc = calc.max(calc.min(tai, 1), 0) * tong + let i = 0 + let con = moc + while i < dai.len() - 1 and con > dai.at(i) { con = con - dai.at(i); i = i + 1 } + let ti = if dai.at(i) > 0.0001 { con / dai.at(i) } else { 0.5 } + let U = pts.at(i) + let V = pts.at(i + 1) + let P = (U.at(0) + ti * (V.at(0) - U.at(0)), U.at(1) + ti * (V.at(1) - U.at(1))) + let xuoi = toa-pt(ctx, U).at(0) <= toa-pt(ctx, V).at(0) + nhan( + ctx, P, ten, + huong: if huong == auto { _phap-tuyen(ctx, U, V) } else { huong }, + cach: cach, + mau: if mau-ten == auto { mau } else { mau-ten }, + quay: if ten-quay { if xuoi { goc-truc(ctx, U, V) } else { goc-truc(ctx, V, U) } } else { 0deg }, + ) + } +} + +// ---------- Nhãn chữ BÁM THEO đường cong ---------- +// Chuỗi văn bản từ nội dung (str hoặc content đơn giản). +#let _chuoi-cong(c) = { + if type(c) == str { c } + else if type(c) == content { + if c.has("text") { c.text } + else if c.has("children") { c.children.map(_chuoi-cong).fold("", (a, b) => a + b) } + else if c.has("body") { _chuoi-cong(c.body) } + else { "" } + } else { str(c) } +} + +// Đặt từng ký tự của `chu` DỌC THEO đường `duong` (mảng điểm toạ độ toán, vd +// lấy từ diem-luon / diem-cung / lay-mau), tự xoay tiếp tuyến, canh theo độ dài cung. +// duong: mảng điểm (>= 2). chu: str (khuyên dùng) hoặc content văn bản thuần. +// tu : vị trí BẮT ĐẦU theo tỉ lệ độ dài đường (0 = đầu, 1 = cuối, .5 = giữa) +// can : "trai" (bắt đầu tại `tu`) · "giua" (canh giữa chữ quanh `tu`) · "phai" +// khoang: giãn cách thêm giữa các ký tự (length) +// co : cỡ chữ (auto = theo cỡ hiện hành) · mau : màu chữ +// phia : "tren" (chữ nằm TRÊN đường) · "duoi" · "giua" (tâm chữ trên đường) +// cach : khoảng hở giữa chữ và đường (khi phia = tren/duoi) +// dao : true = đi ngược đường (khi đường vẽ từ phải sang trái, chữ khỏi lộn ngược) +// Ví dụ: nhan-cong(diem-cung((0,0), 2, 2, 20deg, 160deg), "cung tron") +#let nhan-cong( + ctx, duong, chu, + tu: 0, can: "trai", khoang: 0pt, + co: auto, mau: black, phia: "tren", cach: 2pt, dao: false, +) = { + let pts = if dao { duong.rev() } else { duong } + let txt = _chuoi-cong(chu) + let cl = txt.clusters() + if pts.len() < 2 or cl.len() == 0 { return } + let pg = pts.map(P => toa-pt(ctx, P)) + let cum = (0.0,) + for i in range(pg.len() - 1) { + let a = pg.at(i) + let b = pg.at(i + 1) + cum.push(cum.at(i) + calc.sqrt(calc.pow(b.at(0) - a.at(0), 2) + calc.pow(b.at(1) - a.at(1), 2))) + } + let tong = cum.at(cum.len() - 1) + context { + let els = cl.map(g => if co == auto { text(fill: mau, g) } else { text(size: co, fill: mau, g) }) + let ws = els.map(e => measure(e).width.pt()) + let totw = ws.sum() + khoang.pt() * calc.max(0, els.len() - 1) + let s0 = tu * tong + if can == "giua" { s0 = s0 - totw / 2 } else if can == "phai" { s0 = s0 - totw } + let cursor = s0 + for idx in range(els.len()) { + let w = ws.at(idx) + let mid = calc.max(0, calc.min(tong, cursor + w / 2)) + let i = 0 + while i < cum.len() - 2 and cum.at(i + 1) < mid { i = i + 1 } + let seg = cum.at(i + 1) - cum.at(i) + let t = if seg > 0.000001 { (mid - cum.at(i)) / seg } else { 0 } + let A = pg.at(i) + let B = pg.at(i + 1) + let px = A.at(0) + t * (B.at(0) - A.at(0)) + let py = A.at(1) + t * (B.at(1) - A.at(1)) + let dx = B.at(0) - A.at(0) + let dy = B.at(1) - A.at(1) + let ll = calc.sqrt(dx * dx + dy * dy) + let (ux, uy) = if ll > 0.000001 { (dx / ll, dy / ll) } else { (1, 0) } + let ang = calc.atan2(dx, dy) // góc của vectơ (dx, dy) trên TRANG (y xuống) + let el = els.at(idx) + let hh = measure(el).height.pt() + let off = if phia == "tren" { cach.pt() + hh / 2 } else if phia == "duoi" { -(cach.pt() + hh / 2) } else { 0 } + let cx = px + uy * off // pháp tuyến hướng "lên" trên trang: (uy, -ux) + let cy = py + (-ux) * off + place( + dx: (cx - w / 2) * 1pt, + dy: (cy - hh / 2) * 1pt, + rotate(ang, reflow: false, el), + ) + cursor = cursor + w + khoang.pt() + } + } +} + +// ---------- Đánh dấu góc ---------- +// Góc (thường, không vuông) giữa hai tia O->A và O->B: +// cung nhỏ + nhãn + số cung (1..3) + tô quạt + tự ghi số đo. +// to : màu tô hình quạt (nên trong suốt, vd rgb(255, 170, 0, 70)) +// so-do : true = tự ghi số đo góc (vd 60°) khi không đặt `ten` +// vach : 1..3 vạch CẮT NGANG cung (ký hiệu hai góc bằng nhau, kiểu SGK). +// Khác `so-cung` (vẽ nhiều cung đồng tâm) — `vach` chỉ vẽ MỘT cung +// rồi gạch 1/2/3 vạch nhỏ vuông góc cung tại giữa cung. +// Bí danh `vach-danh-dau:` dùng được y hệt. +/// Đánh dấu góc (không vuông) TẠI O, giữa hai tia O→A và O→B. +/// ĐỈNH là đối số ĐẦU. Quen lối TikZ (đỉnh ở giữa) thì dùng `ve-goc(A, O, B)`. +/// `ten:` nhãn · `so-do: true` tự ghi số đo · `vach: 1..3` vạch góc bằng nhau · +/// `so-cung: 1..3` cung đồng tâm · `to:` tô quạt · `cach-nhan:` nhãn xa/gần cung. +#let goc( + ctx, O, A, B, + r: 0.45, // bán kính cung (đơn vị toạ độ) + ten: none, + so-do: false, // tự ghi số đo (làm tròn 0.1°); `ten` được ưu tiên + so-cung: 1, + vach: 0, // số vạch đánh dấu cắt ngang cung (0 = không) + vach-danh-dau: auto, // bí danh của `vach` + dai-vach: 6pt, // độ dài mỗi vạch + to: none, // tô màu hình quạt của góc + mau: black, day: 0.8pt, + cach-nhan: 1.9, // hệ số đặt nhãn so với r +) = { + let vach = if vach-danh-dau == auto { vach } else { vach-danh-dau } + let a1 = calc.atan2(A.at(0) - O.at(0), A.at(1) - O.at(1)) + let a2 = calc.atan2(B.at(0) - O.at(0), B.at(1) - O.at(1)) + let d = a2 - a1 + if d > 180deg { d = d - 360deg } + if d < -180deg { d = d + 360deg } + if to != none { + da-giac-pt( + ((O,) + diem-cung(O, r, r, a1, a1 + d, n: 32)).map(P => toa-pt(ctx, P)), + to: to, + ) + } + for i in range(so-cung) { + let rr = r * (1 + 0.22 * i) + cung(ctx, O, rr, tu: a1, den: a1 + d, mau: mau, day: day, n: 24) + } + // Vạch đánh dấu cắt ngang cung (ký hiệu góc bằng nhau) + if vach > 0 { + let tm = a1 + d / 2 + let opt = toa-pt(ctx, O) + let ppt = toa-pt(ctx, (O.at(0) + r * calc.cos(tm), O.at(1) + r * calc.sin(tm))) + let rp = calc.sqrt( + calc.pow(ppt.at(0) - opt.at(0), 2) + calc.pow(ppt.at(1) - opt.at(1), 2) + ) + if rp > 0.01 { + let da = (3.4 / rp) * 1rad // khoảng cách 2 vạch ~3.4pt trên cung + let hd = dai-vach.pt() / 2 + for i in range(vach) { + let ang = tm + (i - (vach - 1) / 2) * da + let q = toa-pt(ctx, (O.at(0) + r * calc.cos(ang), O.at(1) + r * calc.sin(ang))) + let l = calc.sqrt( + calc.pow(q.at(0) - opt.at(0), 2) + calc.pow(q.at(1) - opt.at(1), 2) + ) + let (ux, uy) = ((q.at(0) - opt.at(0)) / l, (q.at(1) - opt.at(1)) / l) + doan-pt( + (q.at(0) - hd * ux, q.at(1) - hd * uy), + (q.at(0) + hd * ux, q.at(1) + hd * uy), + mau: mau, day: day, + ) + } + } + } + let nd = if ten != none { ten } else if so-do { + let dd = if d < 0deg { -d } else { d } + let v = calc.round(dd.deg(), digits: 1) + let s = if calc.abs(v - calc.round(v)) < 0.01 { str(int(calc.round(v))) } else { str(v) } + $#s degree$ + } else { none } + if nd != none { + let tm = a1 + d / 2 + nhan( + ctx, + (O.at(0) + r * cach-nhan * calc.cos(tm), O.at(1) + r * cach-nhan * calc.sin(tm)), + nd, huong: "giua", mau: mau, + ) + } +} + +// Ký hiệu góc vuông tại O (giữa hai tia O->A, O->B). +/// Ký hiệu góc vuông TẠI O (giữa hai tia O→A, O→B). ĐỈNH là đối số ĐẦU. +/// Quen lối TikZ (đỉnh ở giữa) thì dùng `ve-goc-vuong(A, O, B)`. +#let goc-vuong(ctx, O, A, B, r: 0.32, mau: black, day: 0.8pt) = { + let ka = calc.sqrt(calc.pow(A.at(0) - O.at(0), 2) + calc.pow(A.at(1) - O.at(1), 2)) + let kb = calc.sqrt(calc.pow(B.at(0) - O.at(0), 2) + calc.pow(B.at(1) - O.at(1), 2)) + if ka == 0 or kb == 0 { return } + let u = ((A.at(0) - O.at(0)) / ka * r, (A.at(1) - O.at(1)) / ka * r) + let v = ((B.at(0) - O.at(0)) / kb * r, (B.at(1) - O.at(1)) / kb * r) + let P1 = (O.at(0) + u.at(0), O.at(1) + u.at(1)) + let P2 = (O.at(0) + u.at(0) + v.at(0), O.at(1) + u.at(1) + v.at(1)) + let P3 = (O.at(0) + v.at(0), O.at(1) + v.at(1)) + duong-cong(ctx, (P1, P2, P3), mau: mau, day: day) +} + +// ---------- Lối viết TikZ: ĐỈNH góc đặt ở GIỮA ---------- +// `goc`/`goc-vuong` ở trên đặt ĐỈNH góc làm đối số ĐẦU: goc-vuong(O, A, B). +// TikZ lại viết đỉnh ở GIỮA (pic angle = A--O--B), nên hai hàm dưới đây nhận +// thứ tự quen thuộc đó — đỉnh là đối số THỨ HAI: +// ve-goc(A, O, B, ...) <=> goc(O, A, B, ...) +// ve-goc-vuong(A, O, B, ...) <=> goc-vuong(O, A, B, ...) +// Mọi tuỳ chọn (r, ten, so-do, so-cung, vach, to, mau, day, cach-nhan…) giữ +// nguyên tên và được truyền thẳng xuống hàm gốc, nên hai lối dùng lẫn được +// trong cùng một hình. Hàm gốc KHÔNG đổi ⇒ mọi bài cũ chạy y như trước. +/// Đánh dấu góc TẠI O theo lối TikZ: ĐỈNH nằm GIỮA — `ve-goc(A, O, B)`. +/// Cùng bộ tuỳ chọn với `goc` (r, ten, so-do, so-cung, vach, to, mau, day, cach-nhan). +#let ve-goc(ctx, A, O, B, ..tuy-chon) = goc(ctx, O, A, B, ..tuy-chon) + +/// Ký hiệu góc vuông TẠI O theo lối TikZ: ĐỈNH nằm GIỮA — `ve-goc-vuong(A, O, B)`. +/// Cùng bộ tuỳ chọn với `goc-vuong` (r, mau, day). +#let ve-goc-vuong(ctx, A, O, B, ..tuy-chon) = goc-vuong(ctx, O, A, B, ..tuy-chon) + +// ---------- Đánh dấu đoạn bằng nhau (1..3 vạch tại trung điểm) ---------- +// so : số vạch song song (1/2/3…) tại trung điểm đoạn. +// nghieng: góc NGHIÊNG của vạch so với pháp tuyến đoạn (0deg = vuông góc như cũ); +// đặt ~20–30deg cho kiểu vạch chéo "/" "//" "///". +// cheo : true = mỗi mốc vẽ dấu CHÉO NHAU "✕" (hai vạch ±góc); khi đó `nghieng` +// = nửa góc mở của dấu ✕ (mặc định 30deg nếu để 0deg). +#let danh-dau(ctx, A, B, so: 1, dai: 6pt, mau: black, day: 1pt, + nghieng: 0deg, cheo: false) = { + let a = toa-pt(ctx, A) + let b = toa-pt(ctx, B) + let dx = b.at(0) - a.at(0) + let dy = b.at(1) - a.at(1) + let l = calc.sqrt(dx * dx + dy * dy) + if l == 0 { return } + let ux = dx / l + let uy = dy / l + let m = ((a.at(0) + b.at(0)) / 2, (a.at(1) + b.at(1)) / 2) + let hd = dai.pt() / 2 + // vẽ MỘT vạch tại tâm c, nghiêng góc g so với pháp tuyến (−uy, ux) của đoạn. + let mot-vach(c, g) = { + let cs = calc.cos(g) + let sn = calc.sin(g) + // pháp tuyến p = (−uy, ux); tiếp tuyến u = (ux, uy) + // vạch nghiêng: v = cos(g)·p + sin(g)·u + let vx = -uy * cs + ux * sn + let vy = ux * cs + uy * sn + doan-pt( + (c.at(0) - hd * vx, c.at(1) - hd * vy), + (c.at(0) + hd * vx, c.at(1) + hd * vy), + mau: mau, day: day, + ) + } + for i in range(so) { + let t = (i - (so - 1) / 2) * 3.2 + let c = (m.at(0) + t * ux, m.at(1) + t * uy) + if cheo { + let g = if nghieng == 0deg { 30deg } else { nghieng } + mot-vach(c, g) + mot-vach(c, -g) + } else { + mot-vach(c, nghieng) + } + } +} + +// ---------- Tiện ích hình học phẳng (tính toán) ---------- +/// TRẢ VỀ trung điểm của A và B (chỉ tính, không vẽ). +#let trung-diem(A, B) = ((A.at(0) + B.at(0)) / 2, (A.at(1) + B.at(1)) / 2) + +#let khoang-cach(A, B) = calc.sqrt( + calc.pow(B.at(0) - A.at(0), 2) + calc.pow(B.at(1) - A.at(1), 2) +) + +// Điểm chia: A + t*(B - A). +/// TRẢ VỀ điểm M trên đường AB với AM/AB = t (t = 0.5 là trung điểm, +/// t > 1 là kéo dài quá B, t < 0 là ngược về phía A). +#let chia(A, B, t) = ( + A.at(0) + t * (B.at(0) - A.at(0)), + A.at(1) + t * (B.at(1) - A.at(1)), +) + +// Chân đường vuông góc hạ từ P xuống đường thẳng AB. +/// TRẢ VỀ hình chiếu vuông góc của P lên đường thẳng AB (chân đường cao). +#let hinh-chieu(P, A, B) = { + let ux = B.at(0) - A.at(0) + let uy = B.at(1) - A.at(1) + let t = ((P.at(0) - A.at(0)) * ux + (P.at(1) - A.at(1)) * uy) / (ux * ux + uy * uy) + (A.at(0) + t * ux, A.at(1) + t * uy) +} + +// ---------- Cửa sổ khung vừa khít hình (TRẢ GIÁ TRỊ, không vẽ) ---------- +// Gom mọi điểm mà một đối tượng chiếm chỗ (nội bộ của khung-vua). +#let _diem-cua(m) = { + if type(m) != array or m.len() == 0 { return () } + let so = (int, float) + if m.len() == 2 and type(m.at(0)) in so and type(m.at(1)) in so { + (m,) // một ĐIỂM (x, y) + } else if m.len() == 2 and type(m.at(0)) == array and type(m.at(1)) in so { + let (O, r) = m // một ĐƯỜNG TRÒN (tâm, bán kính) + ( + (O.at(0) - r, O.at(1)), (O.at(0) + r, O.at(1)), + (O.at(0), O.at(1) - r), (O.at(0), O.at(1) + r), + ) + } else { + let ds = () + for x in m { ds += _diem-cua(x) } // mảng điểm / mảng lồng nhau + ds + } +} + +// Cửa sổ toạ độ vừa khít các đối tượng truyền vào — để hình KHÔNG tràn ra +// ngoài khung. Mỗi đối số là một ĐIỂM (x, y), một ĐƯỜNG TRÒN (tâm, bán kính), +// hoặc một MẢNG gồm các thứ đó. +// #hinh(w: 5cm, ..khung-vua((A, B, C), (O, R)), ctx => { ... }) +// le: chừa lề quanh hình, tính theo TỈ LỆ cạnh lớn của hình (0.12 = 12%). +/// TRẢ VỀ cửa sổ `(xmin:, xmax:, ymin:, ymax:)` vừa khít các điểm/đường tròn +/// truyền vào, để rải thẳng vào `#hinh`: +/// `#hinh(w: 5cm, ..khung-vua((A, B, C), (O, R)), ctx => { … })`. +#let khung-vua(..muc, le: 0.12) = { + let ds = () + for m in muc.pos() { ds += _diem-cua(m) } + if ds.len() == 0 { return (xmin: -5, xmax: 5, ymin: -4, ymax: 4) } + let xs = ds.map(P => P.at(0)) + let ys = ds.map(P => P.at(1)) + let (x1, x2) = (calc.min(..xs), calc.max(..xs)) + let (y1, y2) = (calc.min(..ys), calc.max(..ys)) + let d = calc.max(x2 - x1, y2 - y1, 0.001) + let m = d * le + (xmin: x1 - m, xmax: x2 + m, ymin: y1 - m, ymax: y2 + m) +} + +// Tâm đường tròn ngoại tiếp tam giác ABC. +#let tam-ngoai-tiep(A, B, C) = { + let d = 2 * (A.at(0) * (B.at(1) - C.at(1)) + B.at(0) * (C.at(1) - A.at(1)) + C.at(0) * (A.at(1) - B.at(1))) + let a2 = A.at(0) * A.at(0) + A.at(1) * A.at(1) + let b2 = B.at(0) * B.at(0) + B.at(1) * B.at(1) + let c2 = C.at(0) * C.at(0) + C.at(1) * C.at(1) + ( + (a2 * (B.at(1) - C.at(1)) + b2 * (C.at(1) - A.at(1)) + c2 * (A.at(1) - B.at(1))) / d, + (a2 * (C.at(0) - B.at(0)) + b2 * (A.at(0) - C.at(0)) + c2 * (B.at(0) - A.at(0))) / d, + ) +} + +// Đường tròn ĐI QUA một dãy điểm — dùng cho đường tròn ngoại tiếp ĐA GIÁC. +// 3 điểm: đúng bằng đường tròn ngoại tiếp tam giác. Nhiều hơn 3: khớp theo +// bình phương bé nhất (đa giác NỘI TIẾP ĐƯỢC vẫn cho đúng đường tròn của nó, +// đa giác vẽ hơi lệch thì cho đường tròn sát nhất — không lệch hẳn về 3 đỉnh +// đầu như khi chỉ lấy 3 điểm). +// TRẢ VỀ (tâm, bán kính). +#let tron-qua-diem(ds) = { + let n = ds.len() + if n < 3 { panic("tron-qua-diem: cần ít nhất 3 điểm") } + let xm = ds.map(P => P.at(0)).sum() / n + let ym = ds.map(P => P.at(1)).sum() / n + let us = ds.map(P => P.at(0) - xm) + let vs = ds.map(P => P.at(1) - ym) + let s(f) = range(n).map(i => f(us.at(i), vs.at(i))).sum() + let suu = s((u, v) => u * u) + let svv = s((u, v) => v * v) + let suv = s((u, v) => u * v) + let a1 = (s((u, v) => u * u * u) + s((u, v) => u * v * v)) / 2 + let a2 = (s((u, v) => v * v * v) + s((u, v) => v * u * u)) / 2 + let d = suu * svv - suv * suv + if calc.abs(d) < 1e-12 { panic("tron-qua-diem: các điểm thẳng hàng") } + let uc = (a1 * svv - a2 * suv) / d + let vc = (a2 * suu - a1 * suv) / d + ( + (xm + uc, ym + vc), + calc.sqrt(uc * uc + vc * vc + (suu + svv) / n), + ) +} + +// Tâm đường tròn nội tiếp + bán kính: trả về (I, r). +#let tam-noi-tiep(A, B, C) = { + let a = khoang-cach(B, C) + let b = khoang-cach(C, A) + let c = khoang-cach(A, B) + let p = a + b + c + let I = ( + (a * A.at(0) + b * B.at(0) + c * C.at(0)) / p, + (a * A.at(1) + b * B.at(1) + c * C.at(1)) / p, + ) + let s = p / 2 + let r = calc.sqrt((s - a) * (s - b) * (s - c) / s) + (I, r) +} + +// Trực tâm tam giác ABC (giao ba đường cao). +// Dùng hệ thức Euler: vec(OH) = vec(OA) + vec(OB) + vec(OC) => H = A + B + C - 2O. +#let truc-tam(A, B, C) = { + let O = tam-ngoai-tiep(A, B, C) + ( + A.at(0) + B.at(0) + C.at(0) - 2 * O.at(0), + A.at(1) + B.at(1) + C.at(1) - 2 * O.at(1), + ) +} + +// Tâm bàng tiếp + bán kính đường tròn bàng tiếp TRONG GÓC A +// (tiếp xúc cạnh BC và phần kéo dài của AB, AC): trả về (J, r). +// J_A = (-a*A + b*B + c*C) / (-a + b + c), r_A = S / (p - a) +// Muốn tâm bàng tiếp trong góc B thì gọi tam-bang-tiep(B, C, A). +#let tam-bang-tiep(A, B, C) = { + let a = khoang-cach(B, C) + let b = khoang-cach(C, A) + let c = khoang-cach(A, B) + let t = -a + b + c + let J = ( + (-a * A.at(0) + b * B.at(0) + c * C.at(0)) / t, + (-a * A.at(1) + b * B.at(1) + c * C.at(1)) / t, + ) + let s = (a + b + c) / 2 + let S = calc.sqrt(s * (s - a) * (s - b) * (s - c)) + (J, S / (s - a)) +} + +// Hàm bậc nhất y = f(x) của đường thẳng qua 2 điểm A, B (AB không thẳng đứng) +// — dùng ghép với giao-ham để tìm giao đường thẳng với đường cong. +#let ham-qua-2-diem(A, B) = { + let a = (B.at(1) - A.at(1)) / (B.at(0) - A.at(0)) + x => A.at(1) + a * (x - A.at(0)) +} + +// Giao điểm của 2 đồ thị y = f(x) và y = g(x) trên [tu, den]: +// quét n mẫu tìm chỗ f − g đổi dấu rồi chia đôi 40 lần (chính xác ~1e-12). +// Trả về MẢNG các điểm (x, f(x)) theo x tăng dần (rỗng nếu không cắt). +// Giao đường thẳng AB với đường cong f: giao-ham(f, ham-qua-2-diem(A, B), ...); +// đường thẳng ĐỨNG x = k thì giao là (k, f(k)) — không cần hàm này. +// +// CÁCH GỌI (hàm này chỉ TÍNH, không vẽ — không cần ctx): +// giao-ham(f, g, -3, 2) // cận positional +// giao-ham(f, g, tu: -3, den: 2) // hoặc cận đặt tên (như ve-ham) +// for P in giao-ham(f, g, -3, 2) { diem(ctx, P, mau: red) } +// Lỡ truyền ctx ở đầu (giao-ham(ctx, f, g, ...)) thì tự bỏ qua, không lỗi. +// Chỗ f, g "nhảy" qua tiệm cận đứng cũng đổi dấu — các điểm giả đó bị LOẠI +// (kiểm tra lại |f − g| tại nghiệm), nên quét qua tiệm cận vẫn an toàn. +#let giao-ham(..thamso, tu: auto, den: auto, n: 200) = { + let p = thamso.pos() + // bỏ qua ctx nếu lỡ truyền vào (ctx là dictionary, f/g là hàm) + if p.len() > 0 and type(p.at(0)) == dictionary { p = p.slice(1) } + let f = p.at(0) + let g = p.at(1) + let tu = if p.len() > 2 { p.at(2) } else { tu } + let den = if p.len() > 3 { p.at(3) } else { den } + let h = x => f(x) - g(x) + let kq = () + // lệch mẫu một lượng cực nhỏ để không rơi ĐÚNG vào điểm hàm không xác định + // (vd x = −1 của (x+2)/(x+1) — Typst báo lỗi "cannot divide by zero") + let le = 0.0000001 + for i in range(n) { + let x1 = tu + (den - tu) * (i + le) / n + let x2 = tu + (den - tu) * (i + 1 + le) / n + let (y1, y2) = (h(x1), h(x2)) + if y1 * y2 <= 0 and (y1 != 0 or y2 != 0) { + let (lo, hi) = (x1, x2) + for _ in range(40) { + let m = (lo + hi) / 2 + if h(lo) * h(m) <= 0 { hi = m } else { lo = m } + } + let x = (lo + hi) / 2 + let y = f(x) + // nghiệm THẬT: f(x) ≈ g(x); tiệm cận đứng cho |f − g| rất lớn -> bỏ + let that = calc.abs(h(x)) < 0.000001 * calc.max(1, calc.abs(y)) + if that and (kq.len() == 0 or x - kq.last().at(0) > (den - tu) / n * 0.5) { + kq.push((x, y)) + } + } + } + kq +} + +// Hai tiếp điểm của tiếp tuyến kẻ từ điểm M ngoài đường tròn (O; r). +#let tiep-diem(O, r, M) = { + let d = khoang-cach(O, M) + let a = r * r / d // khoảng cách từ O đến hình chiếu tiếp điểm trên OM + let hcao = r * calc.sqrt(d * d - r * r) / d + let ux = (M.at(0) - O.at(0)) / d + let uy = (M.at(1) - O.at(1)) / d + let H = (O.at(0) + a * ux, O.at(1) + a * uy) + ( + (H.at(0) - hcao * uy, H.at(1) + hcao * ux), + (H.at(0) + hcao * uy, H.at(1) - hcao * ux), + ) +} + +// ---------- Lấy mẫu hàm số ---------- +#let lay-mau(f, a, b, n: 100) = range(n + 1).map(i => { + let x = a + (b - a) * (i / n) + (x, f(x)) +}) + +// Vẽ đồ thị hàm f trên [tu, den]; tự tách nhánh khi ra ngoài cửa sổ y +// (dùng được cho hàm có tiệm cận đứng như 1/x, tan x). +#let ve-ham( + ctx, f, + tu: auto, den: auto, + n: 150, mau: blue, day: 1.3pt, dut: false, +) = { + let a = if tu == auto { ctx.xmin } else { tu } + let b = if den == auto { ctx.xmax } else { den } + let le-y = (ctx.ymax - ctx.ymin) * 0.02 + let nhanh = () + let hien-tai = () + for p in lay-mau(f, a, b, n: n) { + if p.at(1) >= ctx.ymin - le-y and p.at(1) <= ctx.ymax + le-y { + hien-tai.push(p) + } else { + if hien-tai.len() > 1 { nhanh.push(hien-tai) } + hien-tai = () + } + } + if hien-tai.len() > 1 { nhanh.push(hien-tai) } + for nh in nhanh { + duong-cong(ctx, nh, mau: mau, day: day, dut: dut) + } +} + +// Tô miền giữa đồ thị f và trục hoành trên [a, b] (mau nên có độ trong suốt). +#let to-vung(ctx, f, a, b, mau: rgb(30, 100, 200, 60), n: 80) = { + let pts = lay-mau(f, a, b, n: n) + da-giac-pt(((a, 0), ..pts, (b, 0)).map(P => toa-pt(ctx, P)), to: mau) +} + +// Tô miền giữa HAI đồ thị f và g trên [a, b] (mau nên có độ trong suốt). +#let to-vung-2-ham(ctx, f, g, a, b, mau: rgb(30, 100, 200, 60), n: 80) = { + let tren = lay-mau(f, a, b, n: n) + let duoi = lay-mau(g, a, b, n: n).rev() + da-giac-pt((tren + duoi).map(P => toa-pt(ctx, P)), to: mau) +} + +// ---------- Gạch chéo (miền loại bỏ của BPT, vùng cấm...) ---------- +// Toạ độ trang (pt) -> toạ độ toán (nghịch đảo của toa-pt). +#let toa-nguoc(ctx, p) = ( + ctx.xmin + p.at(0) / ctx.sx.pt(), + ctx.ymax - p.at(1) / ctx.sy.pt(), +) + +// Gạch chéo một đa giác LỒI cho trước bằng các đoạn song song. +// pts: toạ độ pt. goc: hướng vạch gạch. buoc: khoảng cách 2 vạch. +#let _gach-loi-pt(pts, goc: 45deg, buoc: 6.5pt, mau: red, day: 0.55pt) = { + if pts.len() < 3 { return } + let d = (calc.cos(goc), -calc.sin(goc)) // trục y trang hướng xuống + let nv = (-d.at(1), d.at(0)) + let ts = pts.map(p => p.at(0) * nv.at(0) + p.at(1) * nv.at(1)) + let t = calc.min(..ts) + buoc.pt() / 2 + let t1 = calc.max(..ts) + while t < t1 { + // giao của đường {X · nv = t} với từng cạnh đa giác + let gd = () + for i in range(pts.len()) { + let P = pts.at(i) + let Q = pts.at(calc.rem(i + 1, pts.len())) + let a1 = P.at(0) * nv.at(0) + P.at(1) * nv.at(1) + let a2 = Q.at(0) * nv.at(0) + Q.at(1) * nv.at(1) + if calc.abs(a2 - a1) > 0.000001 { + let s = (t - a1) / (a2 - a1) + if s >= 0 and s < 1 { + gd.push((P.at(0) + s * (Q.at(0) - P.at(0)), P.at(1) + s * (Q.at(1) - P.at(1)))) + } + } + } + let gd = gd.sorted(key: p => p.at(0) * d.at(0) + p.at(1) * d.at(1)) + let i = 0 + while i + 1 < gd.len() { + doan-pt(gd.at(i), gd.at(i + 1), mau: mau, day: day) + i = i + 2 + } + t = t + buoc.pt() + } +} + +// Gạch chéo đa giác lồi theo toạ độ TOÁN. +#let gach-mien(ctx, cac-dinh, goc: 45deg, buoc: 6.5pt, mau: red, day: 0.55pt) = { + _gach-loi-pt(cac-dinh.map(P => toa-pt(ctx, P)), goc: goc, buoc: buoc, mau: mau, day: day) +} + +// Cắt đa giác bởi nửa mặt phẳng a*x + b*y <= c (Sutherland–Hodgman, toạ độ toán). +#let cat-nua-mp(pts, a, b, c) = { + let kq = () + let n = pts.len() + for i in range(n) { + let P = pts.at(i) + let Q = pts.at(calc.rem(i + 1, n)) + let fp = a * P.at(0) + b * P.at(1) - c + let fq = a * Q.at(0) + b * Q.at(1) - c + if fp <= 0.000001 { kq.push(P) } + if fp * fq < 0 { + let s = fp / (fp - fq) + kq.push((P.at(0) + s * (Q.at(0) - P.at(0)), P.at(1) + s * (Q.at(1) - P.at(1)))) + } + } + kq +} + +// Điểm P có thuộc hình tròn tâm O bán kính r không (toạ độ toán)? +#let trong-tron(P, O, r) = ( + calc.pow(P.at(0) - O.at(0), 2) + calc.pow(P.at(1) - O.at(1), 2) <= r * r +) + +// Điểm P có thuộc hình elip tâm O, bán trục a (ngang), b (dọc) không? +// quay: cùng góc xoay với hàm vẽ elip(ctx, O, a, b, quay: ...). +#let trong-elip(P, O, a, b, quay: 0deg) = { + let Q = if quay == 0deg { P } else { quay-diem(P, O, -quay) } + calc.pow((Q.at(0) - O.at(0)) / a, 2) + calc.pow((Q.at(1) - O.at(1)) / b, 2) <= 1 +} + +// ---------- Miền hình học ĐẶT TÊN (khai báo MỘT lần, vẽ + gạch dùng chung) ---------- +// let A = mien-tron((-0.5, 0.3), 0.9) // đổi bán kính MỘT chỗ duy nhất +// ve-mien(ctx, A) // vẽ biên +// gach-vung(ctx, giao(A, B, bu(C))) // gạch (A ∩ B) \ C — tự khớp +#let mien-tron(O, r) = (kieu: "tron", O: O, r: r) +#let mien-elip(O, a, b, quay: 0deg) = (kieu: "elip", O: O, a: a, b: b, quay: quay) + +// Điểm P có thuộc miền m không? m là mien-tron/mien-elip HOẶC hàm P => bool. +#let trong(P, m) = { + if type(m) == function { m(P) } else if m.kieu == "tron" { + trong-tron(P, m.O, m.r) + } else { + trong-elip(P, m.O, m.a, m.b, quay: m.quay) + } +} + +// Phép tập hợp trên miền: giao ∩, hợp ∪, bù (phần ngoài) — lồng nhau tuỳ ý: +// giao(A, hop(B, C)), giao(A, B, bu(C)), ... Kết quả dùng thẳng cho gach-vung. +#let giao(..ms) = P => ms.pos().all(m => trong(P, m)) +#let hop(..ms) = P => ms.pos().any(m => trong(P, m)) +#let bu(m) = P => not trong(P, m) + +// Vẽ biên miền (đường tròn/elip tương ứng với khai báo). +#let ve-mien(ctx, m, mau: black, day: 1pt, dut: false, to: none) = { + if m.kieu == "tron" { + duong-tron(ctx, m.O, m.r, mau: mau, day: day, dut: dut, to: to) + } else { + elip(ctx, m.O, m.a, m.b, quay: m.quay, mau: mau, day: day, dut: dut, to: to) + } +} + +// Gạch chéo MIỀN BẤT KÌ (biên cong tuỳ ý). +// kiem: miền đặt tên/tổ hợp giao-hop-bu (khuyên dùng) HOẶC hàm P => true/false: +// gach-vung(ctx, giao(A, B, bu(C))) +// gach-vung(ctx, P => trong(P, A) and P.at(1) > 0) +// goc: hướng vạch; buoc: khoảng cách 2 vạch; n: số mẫu mỗi vạch (biên càng mịn). +#let gach-vung(ctx, kiem, goc: 45deg, buoc: 6.5pt, mau: red, day: 0.55pt, n: 180) = { + let W = ctx.w.pt() + let H = ctx.h.pt() + let u = (calc.cos(goc), -calc.sin(goc)) // hướng vạch trên trang (y trang hướng xuống) + let v = (-u.at(1), u.at(0)) // pháp tuyến đơn vị + let b = buoc.pt() + let cac-goc = ((0, 0), (W, 0), (0, H), (W, H)) + let ds = cac-goc.map(p => p.at(0) * v.at(0) + p.at(1) * v.at(1)) + let ts = cac-goc.map(p => p.at(0) * u.at(0) + p.at(1) * u.at(1)) + let dmin = calc.min(..ds) + let dmax = calc.max(..ds) + let tmin = calc.min(..ts) + let tmax = calc.max(..ts) + for i in range(calc.floor((dmax - dmin) / b) + 1) { + let d = dmin + i * b + let dau = none + let truoc = none + for j in range(n + 1) { + let t = tmin + (tmax - tmin) * j / n + let p = (d * v.at(0) + t * u.at(0), d * v.at(1) + t * u.at(1)) + let thuoc = ( + p.at(0) >= 0 and p.at(0) <= W and p.at(1) >= 0 and p.at(1) <= H + and trong(toa-nguoc(ctx, p), kiem) + ) + if thuoc and dau == none { dau = p } + if not thuoc and dau != none { + doan-pt(dau, truoc, mau: mau, day: day) + dau = none + } + truoc = p + } + if dau != none { doan-pt(dau, truoc, mau: mau, day: day) } + } +} diff --git a/packages/preview/conic-toan/0.3.4/typst.toml b/packages/preview/conic-toan/0.3.4/typst.toml new file mode 100644 index 0000000000..de301e12cb --- /dev/null +++ b/packages/preview/conic-toan/0.3.4/typst.toml @@ -0,0 +1,13 @@ +[package] +name = "conic-toan" +version = "0.3.4" +entrypoint = "baigiang.typ" +authors = ["Kim Thu "] +license = "MIT" +description = "Draw math figures, graphs and variation tables." +repository = "https://github.com/Thutran1891/conic-toan" +keywords = ["math", "geometry", "plot", "slides", "exam", "vietnamese", "toan", "de-thi"] +categories = ["visualization", "presentation", "components"] +disciplines = ["mathematics", "education"] +compiler = "0.13.0" +exclude = ["/assets/"]