← 都来码 / Dolaima← 例程索引c/test/mask_bool.c原文

c/test/mask_bool.c · 19805 字节 · 下载原文

/* mask_bool.c —— 蒙版**布尔运算**与**路径当蒙版**的体检:逐点取像素,不靠眼睛。
 * mask_bool.c -- a check-up for mask **boolean operations** and **using a path as a mask**: sample pixels point by point instead of using your eyes.
 *
 * ## 为什么必须"取像素"
 * ## Why "sampling pixels" is required
 *
 * 交集、异或这类错误是"**少一块 / 多一块**":整张图看着都对,只有某处该空的没空。
 * Errors such as intersection and XOR are "**one piece missing or one piece extra**": the whole picture looks right, except that somewhere that should be empty is not.
 * 盯着屏幕看是看不出来的(本仓在这上面栽过:v0.96.427 的验收判据是"出帧非空白",
 * You cannot spot it by staring at the screen (this repo was burned by that before: the v0.96.427 acceptance criterion was "the frame is not blank",
 * 而**纯背景色的帧不算空白**,于是整类缺陷都被放过去了)。
 * and **a frame of pure background color does not count as blank**, so a whole class of defects slipped through).
 * 这里每条判据都是 `ui_2d_get_pixel(x,y) == 期望色`,对错由程序说,不由眼睛说。
 * Here every criterion is `ui_2d_get_pixel(x,y) == expected color`; the program says right or wrong, not your eyes.
 *
 * ## 覆盖的七条
 * ## The seven groups covered
 *
 * | # | 测什么 | 蒙版 |
 * | # | what it tests | mask |
 * |---|---|---|
 * | 1~3   | `SUBTRACT` 挖洞(甜甜圈) | 大圆 − 小圆 |
 * | 1~3   | `SUBTRACT` punches a hole (donut) | big circle minus small circle |
 * | 4~6   | `INTERSECT` 取交 | 圆 ∩ 矩形 |
 * | 4~6   | `INTERSECT` intersects | circle and rectangle |
 * | 7~9   | `UNION` 取并 | 两个分离的圆 |
 * | 7~9   | `UNION` unites | two separate circles |
 * | 10~12 | `XOR` 取异或 | 两个重叠的圆(重叠处应**空**)|
 * | 10~12 | `XOR` exclusive-or | two overlapping circles (the overlap should be **empty**) |
 * | 13~15 | `ui_2d_path` 当蒙版(曲线展平) | 三角形路径 |
 * | 13~15 | `ui_2d_path` used as a mask (curve flattening) | a triangle path |
 * | 16~18 | **洞后面正好有"日/月"**(三层里的背景层)| 楼 − 洞,洞开在太阳上 |
 * | 16~18 | **the sun or moon sits right behind the hole** (the background layer of the three) | building minus hole, with the hole over the sun |
 * | 19~22 | **蒙版当碰撞体**(`ui_mask_is_inside`)| 同上,判"这一点在不在楼体里" |
 * | 19~22 | **a mask as a collision body** (`ui_mask_is_inside`) | the same, testing whether a point is inside the building body |
 *
 * 采样点全都落在区域中心附近,**离边界 5px 以上** —— 判据不该被抗锯齿的边缘像素左右。
 * The sample points all land near the centers of the regions, **more than 5px from any edge** -- a criterion should not be swayed by antialiased edge pixels.
 *
 * 最后一组是**用户点名的场景**:三层模型里"建筑挖洞、背景透出来"最容易露馅的时候,
 * The last group is the **scenario the user called out**: in the three-layer model, the moment when "the building has a hole and the background shows through" is most likely to give itself away
 * 就是洞后面正好压着太阳或月亮 —— 那时若还在洞里补画"当时的天空色",
 * is when the sun or the moon sits right behind the hole -- if you then still paint the sky color of that moment inside the hole,
 * 一眼就能看出**圆圆的太阳被啃掉一块**。判据因此是"洞心必须是太阳色"。
 * you can see at once that **a bite has been taken out of the round sun**. The criterion is therefore "the hole center must be the sun color".
 *
 * ## 用法
 * ## Usage
 *
 * 手机上:`vml run examples/c/mask_bool.c`(C 前端 + 汇编 + 链接要一分多钟,别当成卡死)。
 * On a phone: `vml run examples/c/mask_bool.c` (the C front end plus assembly and linking takes over a minute; do not mistake that for a hang).
 * 桌面上:`scripts/vmlcli Examples/c/mask_bool.c -r .`(秒级,改完前端先用它)。
 * On the desktop: `scripts/vmlcli Examples/c/mask_bool.c -r .` (seconds; use this first after changing the front end).
 * 屏幕正中会写 `ALL PASS` 或 `FAIL`;失败时再按下面的 `报告` 弹窗看是哪几条。
 * The middle of the screen shows `ALL PASS` or `FAIL`; on failure, check the report dialog below to see which items failed.
 */
#include <vml_ui.h>

#define C  0xFF3C6EB4      /* 采样色:r/g/b 三通道互不相同,串色一眼可辨 */
                           /* Sample color: the r/g/b channels all differ, so a mix-up is obvious at a glance */
#define BG 0xFFFFFFFF      /* 背景(ui_2d_clear 用的色)*/
                           /* Background (the color used by ui_2d_clear) */

int passed;
int failed;
char report[80];

/* 极简整数转字符串。
 * A minimal integer-to-string converter.
 *
 * ⚠ ~~**不调库**(不 `strcpy`/`strcat`/`itoa`):`Lib/` 里两套栈清理约定并存,调用那些函数会让调用方的栈指针多释放一次。~~
 * ⚠ ~~**No library calls** (no `strcpy`/`strcat`/`itoa`): two stack-cleanup conventions coexisted in `Lib/`, and calling those functions made the caller's stack pointer release one time too many.~~
 *   ✅ **已修好(2026-10-04 复验)**:`strcpy`+`strcat` 之后做算术/多参函数调用,实测全部正确
 *   ✅ **fixed (re-checked 2026-10-04)**: after `strcpy`+`strcat`, arithmetic and multi-argument calls measured all correct
 *   (`add3(10,20,30)`=**60**、`mul2(60,2)+add3(1,2,3)`=**126**、`buf`=**AB**);CHANGELOG 亦记该约定**已消解**(旧约定收尾都在无人引用的 `Lib/shared/backup/`)。
 *   (`add3(10,20,30)`=**60**, `mul2(60,2)+add3(1,2,3)`=**126**, `buf`=**AB**); CHANGELOG also records that convention as **resolved** (the old-convention epilogues all live in the unreferenced `Lib/shared/backup/`).
 *   本程序是**判据**,判据本身不该建立在会漂的东西上 —— 所以这里**照旧**只用局部数组 + 指针寻址,一个库函数都不碰(能跑就别动)。
 *   This program is a **criterion**, and a criterion should not rest on something that drifts -- so here it **still** uses only local arrays and pointer arithmetic (if it runs, do not touch it).
 */
int put_num(char* p, int at, int v) {
    char tmp[12];
    int n;
    int i;
    if (v < 0) { p[at] = '-'; at = at + 1; v = 0 - v; }
    n = 0;
    if (v == 0) { tmp[0] = '0'; n = 1; }
    while (v > 0) { tmp[n] = '0' + (v % 10); n = n + 1; v = v / 10; }
    i = n - 1;
    while (i >= 0) { p[at] = tmp[i]; at = at + 1; i = i - 1; }
    return at;
}

/* 判一条:不中就把它编号追加进 `report`(形如 `F:3,7,11`)。 */
/* Check one item: on failure, append its number to `report` (in the form `F:3,7,11`). */
void expect(int idx, int got, int want) {
    int at;
    if (got == want) { passed = passed + 1; return; }
    failed = failed + 1;
    at = 0;
    if (failed == 1) { report[0] = 'F'; report[1] = ':'; at = 2; }
    else { report[at] = ','; at = at + 1; }
    at = put_num(report, at, idx);
    report[at] = 0;
}

int main(void) {
    int msg[8];
    int hitHole;
    int hitWall;
    int hitSky;
    int hitClear;

    passed = 0;
    failed = 0;
    report[0] = 0;

    ui_win_open("mask bool", 320, 400);
    ui_2d_clear(BG);

    /* ══ 区域 1:甜甜圈 = 大圆 SUBTRACT 小圆(x 10..110, y 5..105)══ */
    /* == Region 1: a donut = big circle SUBTRACT small circle (x 10..110, y 5..105) == */
    ui_mask_begin();
    ui_2d_circle(60, 55, 40, 0xFFFFFFFF, 1, 0);
    ui_mask_end(1);
    ui_mask_begin();
    ui_2d_circle(60, 55, 20, 0xFFFFFFFF, 1, 0);
    ui_mask_end_ex(VML_MASK_SUBTRACT);
    ui_2d_rect(10, 5, 100, 100, C, 1, 0, 0);
    ui_mask_clear();

    /* ══ 区域 2:INTERSECT —— 圆 ∩ 矩形(x 150..225, y 20..90)══
     * == Region 2: INTERSECT -- circle ∩ rectangle (x 150..225, y 20..90) ==
     * 圆在 (190,55) r=35,裁剪矩形是圆的**右半**那块方形 ⇒ 交集 = 圆的右半。
     * The circle is at (190,55) with r=35 and the clip rectangle is the **right half** square of the circle => the intersection is the right half of the circle.
     */
    ui_mask_begin();
    ui_2d_circle(190, 55, 35, 0xFFFFFFFF, 1, 0);
    ui_mask_end(1);
    ui_mask_begin();
    ui_2d_rect(190, 20, 35, 70, 0xFFFFFFFF, 1, 0, 0);
    ui_mask_end_ex(VML_MASK_INTERSECT);
    ui_2d_rect(150, 20, 75, 70, C, 1, 0, 0);
    ui_mask_clear();

    /* ══ 区域 3:UNION —— 两个**分离**的圆(x 30..150, y 150..210)══ */
    /* == Region 3: UNION -- two **separate** circles (x 30..150, y 150..210) == */
    ui_mask_begin();
    ui_2d_circle(60, 180, 25, 0xFFFFFFFF, 1, 0);
    ui_mask_end(1);
    ui_mask_begin();
    ui_2d_circle(120, 180, 25, 0xFFFFFFFF, 1, 0);
    ui_mask_end_ex(VML_MASK_UNION);
    ui_2d_rect(30, 150, 120, 60, C, 1, 0, 0);
    ui_mask_clear();

    /* ══ 区域 4:XOR —— 两个**重叠**的圆(x 170..270, y 150..210)══
     * == Region 4: XOR -- two **overlapping** circles (x 170..270, y 150..210) ==
     * 重叠处两圆都命中 ⇒ 互相抵消 ⇒ **应该是空的**(这条最容易错:写错成并集就看不出)。
     * In the overlap both circles match => they cancel out => it **should be empty** (this is the easiest one to get wrong: mistaking it for a union is invisible).
     */
    ui_mask_begin();
    ui_2d_circle(200, 180, 28, 0xFFFFFFFF, 1, 0);
    ui_mask_end(1);
    ui_mask_begin();
    ui_2d_circle(240, 180, 28, 0xFFFFFFFF, 1, 0);
    ui_mask_end_ex(VML_MASK_XOR);
    ui_2d_rect(170, 150, 100, 60, C, 1, 0, 0);
    ui_mask_clear();

    /* ══ 区域 5:`ui_2d_path` 当蒙版(x 20..110, y 240..310)══
     * == Region 5: `ui_2d_path` used as a mask (x 20..110, y 240..310) ==
     * 三角形 (30,300) (100,300) (65,250)。走的是**曲线展平器**那条路 ⇒
     * A triangle (30,300) (100,300) (65,250). It goes through the **curve flattener** path =>
     * 顺带证明"路径当蒙版"与"路径当图元"用的是同一个 `DrawPath.Flatten`。
     * which incidentally proves that "a path as a mask" and "a path as a figure" use the same `DrawPath.Flatten`.
     */
    ui_mask_begin();
    ui_2d_path("M30 300 L100 300 L65 250 Z", 0xFFFFFFFF, 1, 0xFFFFFFFF, "", 1, 0);
    ui_mask_end(1);
    ui_2d_rect(20, 240, 90, 70, C, 1, 0, 0);
    ui_mask_clear();

    /* ══ 区域 6:**洞后面压着太阳**(x 20..110, y 320..400)══
     * == Region 6: **the sun sits behind the hole** (x 20..110, y 320..400) ==
     * 三层模型最容易露馅的一刻:背景层的太阳被建筑层的洞"咬掉一块",
     * The moment the three-layer model is most likely to give itself away: whether the sun of the background layer has "a bite taken out of it" by the hole in the building layer,
     * 还是**完整地从洞里露出来**。
     * or is still **fully visible through the hole**.
     * ⚠ 上一版(在洞里补画"当时的天空色")在这里会露馅:补的是一个纯色圆,
     * ⚠ The previous version (painting the sky color of that moment inside the hole) gives itself away here: what it painted was a solid circle,
     *   正好把太阳盖成一个缺角的形状。
     * which covers the sun into a shape with a corner missing.
     */
    ui_2d_circle(60, 360, 30, 0xFFFFE060, 1, 0);              /* 太阳(背景层)*/
                                                           /* The sun (background layer) */
    ui_mask_begin();
    ui_2d_rect(20, 320, 90, 80, 0xFFFFFFFF, 1, 0, 0);         /* 楼体轮廓 */
                                                           /* The building outline */
    ui_mask_end(1);
    ui_mask_begin();
    ui_2d_circle(60, 360, 16, 0xFFFFFFFF, 1, 0);              /* 洞:正开在太阳上 */
                                                           /* The hole: cut right over the sun */
    ui_mask_end_ex(VML_MASK_SUBTRACT);
    ui_2d_rect(20, 320, 90, 80, 0xFF6FA8DC, 1, 0, 0);         /* 楼(蓝)*/
                                                           /* The building (blue) */

    /* 蒙版还生效时问一句"这些点在不在可见区域里" —— **蒙版当碰撞体**。
     * While the mask is still active, ask "are these points inside the visible area" -- **a mask as a collision body**.
     * 建筑层"炸一块缺一块"之后,程序不必再自己维护一份洞的坐标表:
     * After the building layer has "chunks blown out of it", the program does not need to keep its own table of hole coordinates:
     * 洞口是"可见的"(子弹能穿过去),楼体是"不可见的"(打到墙了)。
     * the hole is "visible" (a bullet can pass through) and the building body is "not visible" (a hit on the wall).
     * ⚠ 必须在 ui_mask_clear() **之前**问 —— 清掉之后处处可见,三个都是 1。
     * ⚠ This must be asked **before** ui_mask_clear() -- after clearing, everything is visible and all three are 1.
     */
    hitHole = ui_mask_is_inside(60, 360);      /* 洞心:被挖掉 ⇒ 0 */
                                          /* The hole center: punched out => 0 */
    hitWall = ui_mask_is_inside(60, 328);      /* 楼体:可见 ⇒ 1 */
                                          /* The building body: visible => 1 */
    hitSky  = ui_mask_is_inside(300, 380);     /* 楼外:不可见 ⇒ 0 */
                                          /* Outside the building: not visible => 0 */

    ui_mask_clear();

    /* 清掉之后处处可见 —— 这条同时钉住"没有蒙版时恒为 1"的口径 */
    /* After clearing, everything is visible -- this also pins down the convention that "with no mask it is always 1" */
    hitClear = ui_mask_is_inside(300, 380);

    /* ══ 逐点判定(全部画完之后统一采样)══ */
    /* == Point-by-point checks (all sampled at once after everything is drawn) == */
    expect(1, ui_2d_get_pixel(90, 55), C & 0xFFFFFF);      /* 环上 */
                                                      /* On the ring */
    expect(2, ui_2d_get_pixel(60, 55), BG & 0xFFFFFF);     /* 洞心:挖掉了 */
                                                      /* The hole center: punched out */
    expect(3, ui_2d_get_pixel(15, 10), BG & 0xFFFFFF);     /* 圆外(填充矩形内)*/
                                                      /* Outside the circle (inside the fill rectangle) */

    expect(4, ui_2d_get_pixel(200, 55), C & 0xFFFFFF);     /* 交集中:圆内且矩形内 */
                                                      /* In the intersection: inside the circle and inside the rectangle */
    expect(5, ui_2d_get_pixel(170, 55), BG & 0xFFFFFF);    /* 圆内、矩形外 ⇒ 被裁掉 */
                                                      /* Inside the circle but outside the rectangle => clipped away */
    expect(6, ui_2d_get_pixel(220, 25), BG & 0xFFFFFF);    /* 矩形内、圆外 ⇒ 被裁掉 */
                                                      /* Inside the rectangle but outside the circle => clipped away */

    expect(7, ui_2d_get_pixel(60, 180), C & 0xFFFFFF);     /* 并:左圆心 */
                                                      /* Union: the left center */
    expect(8, ui_2d_get_pixel(120, 180), C & 0xFFFFFF);    /* 并:右圆心 */
                                                      /* Union: the right center */
    expect(9, ui_2d_get_pixel(90, 180), BG & 0xFFFFFF);    /* 并:两圆之间(都不在)*/
                                                      /* Union: between the two circles (inside neither) */

    expect(10, ui_2d_get_pixel(180, 180), C & 0xFFFFFF);   /* 异或:只在左圆 */
                                                      /* XOR: only in the left circle */
    expect(11, ui_2d_get_pixel(220, 180), BG & 0xFFFFFF);  /* 异或:两圆重叠处 ⇒ 空 */
                                                      /* XOR: the overlap of the two circles => empty */
    expect(12, ui_2d_get_pixel(260, 180), C & 0xFFFFFF);   /* 异或:只在右圆 */
                                                      /* XOR: only in the right circle */

    expect(13, ui_2d_get_pixel(65, 290), C & 0xFFFFFF);    /* 三角形内 */
                                                      /* Inside the triangle */
    expect(14, ui_2d_get_pixel(35, 250), BG & 0xFFFFFF);   /* 三角形外(左上角)*/
                                                      /* Outside the triangle (top left corner) */
    expect(15, ui_2d_get_pixel(85, 250), BG & 0xFFFFFF);   /* 三角形外(右上角)*/
                                                      /* Outside the triangle (top right corner) */

    /* 洞后面压着太阳:洞心必须是**太阳色**(0xFFE060),不能被楼的蓝盖住 */
    /* The sun sits behind the hole: the hole center must be the **sun color** (0xFFE060) and must not be covered by the blue of the building */
    expect(16, ui_2d_get_pixel(60, 360), 0xFFE060);        /* 洞心 = 太阳 */
                                                      /* The hole center = the sun */
    expect(17, ui_2d_get_pixel(52, 366), 0xFFE060);        /* 洞内偏一点,仍是太阳 */
                                                      /* Slightly off center inside the hole, still the sun */
    expect(18, ui_2d_get_pixel(60, 328), 0x6FA8DC);        /* 对照:洞**外**是楼体蓝 */
                                                      /* Control: **outside** the hole is the building blue */

    /* 蒙版当碰撞体(`ui_mask_is_inside`)—— 判据与上面三条画面判据**同源**:
     * A mask as a collision body (`ui_mask_is_inside`) -- these criteria are **of the same origin** as the three picture criteria above:
     * 洞里"看得见太阳"⇒ 那一点就该报"在蒙版里"?**不** —— 这里报的是
     * "The sun is visible" inside the hole => so that point should report "inside the mask"? **No** -- what is reported here is
     * "会不会画出来"。洞是挖掉的(不画楼)但**太阳透出来了**……
     * "whether it gets drawn". The hole is punched out (no building drawn) but **the sun shows through**...
     * ⚠ 所以口径是**几何**的:蒙版 = "楼减洞" 这块**楼体**的形状。
     * ⚠ So the convention is **geometric**: the mask is the shape of the **building body** = "building minus hole".
     *   洞心不在楼体里 ⇒ 0;楼体上 ⇒ 1。程序据此判"打到墙了没有"。
     *   The hole center is not inside the building body => 0; on the building body => 1. The program uses this to decide "did I hit a wall".
     */
    expect(19, hitHole, 0);
    expect(20, hitWall, 1);
    expect(21, hitSky, 0);
    expect(22, hitClear, 1);                            /* 清掉蒙版 ⇒ 处处可见 */
                                                      /* After clearing the mask => everything is visible */

    if (failed == 0) ui_2d_text(160, 160, "ALL PASS", 0xFF20A020, 20, VML_ANCHOR_CENTER);
    else ui_2d_text(160, 160, "FAIL", 0xFFD02020, 20, VML_ANCHOR_CENTER);
    ui_2d_present();

    if (failed != 0) ui_dlg_msg("mask bool", report, VML_DLG_INFO);

    while (ui_win_is_closed() == 0) { ui_msg_wait(msg, 0); }
    ui_win_close();
    return failed;
}