# 昆虫世界
**简体中文** · [English](README.en.md)
[](https://github.com/xr843/insect-world/actions/workflows/ci.yml)
[](LICENSE)
[](https://linux.do)
可交互的 3D 昆虫图鉴 —— 旋转、缩放、点击标注点,认识 63 种昆虫的身体构造、生活史与生态角色。
**线上:https://insect-world.pages.dev** · **English: https://insect-world.pages.dev/en/**
**每一只虫都是代码实时生成的,仓库里没有一个模型文件。**
63 个物种覆盖 14 个目:鞘翅目 28、双翅目 5、鳞翅目 5、膜翅目 5、半翅目 4、直翅目 4,蜻蜓目、螳螂目、脉翅目、蜚蠊目各 2,䗛目、革翅目、广翅目、毛翅目各 1。
右边这只帝王蝶正在**羽化**:翅从皱缩的一小团撑开成全尺寸,同时从下垂抬回静止姿态。
它没有另做一套动画资产 —— **画成虫的那套代码,同样画得出卵、幼虫和蛹**。
这是参数化几何做得到、而扫描来的模型做不到的事,二十八个物种有完整的生活史阶段模型。
**长期目标 60 种 / 14 目已达成**,此后第 7 轮补上了三种日常昆虫(家蝇、淡色库蚊、德国小蠊)——读者指出图鉴里甲虫占了近半、身边最常见的虫却一种没有,这是兑现「后续会陆续添加」的公开承诺。物种史与打磨史见 [docs/roadmap.md](docs/roadmap.md) 与 [docs/polish-plan.md](docs/polish-plan.md),模型审计方法见 [docs/model-audit-notes.md](docs/model-audit-notes.md)。

双主题视觉,同一个馆的日与夜:默认**「标本台」**(冷灰绿档案衬板 + 青铜铭牌 +
标本标签的印刷墨色),一键切换**「博物馆之夜」**(展厅黑做底、黄铜做骨,标本靠
聚光灯锥与光池从暗场里托出——金属鞘翅、薄膜虹彩、萤火虫的尾灯都在深底上起飞)。
| 浅色「标本台」(默认) | 暗色「博物馆之夜」 |
| --- | --- |
|  |  |
> ⚠️ **关于讲解文本**:63 种昆虫的总述、关键数据、生活史与冷知识**由 AI 撰写**。
> 截至 2026-10-17,浏览量最高的 14 种(独角仙、七星瓢虫、西方蜜蜂、中华大刀螳、帝王蝶、
> 碧伟蜓、日本弓背蚁、黑蚱蝉、星天牛、东亚飞蝗、中华虎甲、玉带凤蝶、中华大锹甲、东方蝼蛄,
> 约占物种页浏览的 57%)已对照文献**逐断言部分核校**,
> 每条来源及其支持的说法列在页面「来源与核校」区,核校记录在 `docs/content-audit-*.md`;
> 其余 49 种尚未核校,全部内容都**未经昆虫学专家复审**。当作「看着好玩、顺便认个形态」的读物,
> 别当作可引用的资料;发现错处欢迎开 issue。
## 跑起来
需要 Node.js 22.13 或更新版本(与 CI 的 Node 22 一致;接口测试使用内置 SQLite)。
```bash
npm install
npm run dev # 主站 http://localhost:5178
# 模型调试台 http://localhost:5178/preview.html
npm test # 4000 多个测试
npm run build # tsc --noEmit + vite build
npx playwright install chromium # 首次运行浏览器检查前安装
npm run test:browser # 构建产物的手机布局、换虫、存储故障回归
npm run deploy # 构建并发布到 Cloudflare Pages(需先 npx wrangler login)
```
三个辅助脚本(都不进依赖,用到时临时装):
```bash
bash scripts/make-og.sh # 重生成分享卡 public/og.png 与图标三件套(需 ImageMagick)
node scripts/shots.mjs # 重拍 README 截图(无头 Chromium,用法见文件头注释)
node scripts/perf-firstframe.mjs # 首帧分段计时(配合 ?perf=1 的打点通道,见 docs/perf-notes.md)
```
页面由 Cloudflare Pages 静态托管;语言分流与纠错提交使用 Pages Functions,反馈存入 D1。`public/_headers` 配了缓存策略:
产物文件名带内容哈希,按一年不可变缓存;HTML 每次回源校验,保证发新版立刻生效。
根路径 `/` 另外挂了一个 Cloudflare Pages Function(`functions/index.ts`):按
`Accept-Language` 把没有中文语言标签的访客 302 到 `/en/`(爬虫不分流,已有语言选择 cookie 时尊重该选择,根路径响应带 `Vary: Accept-Language, Cookie, User-Agent` 与 `Cache-Control: private, no-store`)。`functions/` 目录在仓库根、
不在 `dist/` 里,但**不需要改上面这条 `deploy` 命令**——`wrangler pages deploy`
的 functions 目录默认是「执行命令那一刻的当前工作目录 + `functions`」,跟被部署
的目录参数(这里是 `dist`)无关(读的是 wrangler 4.x 源码里 `deploy2()` 的
`functionsDirectory = customFunctionsDirectory || path.join(process.cwd(), "functions")`,
不是猜的)。`npm run deploy` 就是在仓库根跑的,会自动发现并打包进部署。本地可以用
`npx wrangler pages dev dist` 起一份服务,用不同 `Accept-Language`/`Cookie` 头
`curl` 根路径来复验这条链路。
## 怎么用
左栏点选,或用 `↑` `↓` 逐只翻。拖动转动虫体,滚轮拉近,点彩色圆点看部位说明。
左侧工具条:**聚焦**把镜头凑到当前标注的部位上,**剖切**沿矢状面切开,**分层**让外骨骼半透明,**对比**在展台底部浮出对照条。
右栏的**读它的图鉴详解**是分步讲解,每翻一步镜头会自己移到讲到的部位上;**小测**是真能作答的,判对错、给解释、统计得分。


## 它们是活的
站定的虫在做极小幅度的姿势微调(约 1.7°,六条腿按着生位置错开相位)。
**本来就会悬停的八只**——蜻蜓、豆娘、蜜蜂、熊蜂、食蚜蝇、草蛉、天蛾、大蚊——
在原地振翅;蜻蜓目的前后翅**反相扑动**,那是它们能悬停的原因,也是一眼认出
蜻蜓的动态特征。
拍翅频率是**压过**的,而且是明摆着的取舍:蜜蜂真实 230Hz、食蚜蝇 200Hz,
而 60fps 屏幕的奈奎斯特上限是 30Hz —— 照实数驱动的结果不是「快」是**混叠**,
翅膀会看起来慢慢倒着扇。用对数压到 4~12Hz,**保住物种之间的相对次序**;
真实频率留在代码的数据表里,压缩函数写在旁边,不藏。
没做完整步态,理由也写在这儿:展台是转台、虫子居中,腿在迈而身体不位移,
读出来不是「走路」是**踏步机**。悬停没有这个问题——悬停本来就是原地不动的
真实行为。
## 一生
底部的**生活史**卡片点开,展台会跟着弹窗逐步换成卵、幼虫、蛹的立体标本。
目前做了二十八种,**74 个阶段模型**:
| 完全变态 | 招牌 |
| --- | --- |
| 独角仙 | 乳白的蛴螬 → **已长出角雏形的蛹** → 黑亮的成虫,反差最大的一条 |
| 帝王蝶 | 三色横带的毛虫、金斑绿蛹(垂蛹,尾钩倒挂) |
| 柞蚕蛾 | 蚕宝宝 + **纵向半剖的茧**,能看见褐色的蛹躺在里面 |
| 西方蜜蜂 | 全程在**六角巢房**里;蛹的复眼先于躯干显色 |
| 中华黄萤 | **卵、幼虫、蛹全都发光**;幼虫的叠瓦状背板 |
| 神农洁蜣螂 | 卵与幼虫都在**育儿粪梨**里;与独角仙的蛴螬形态刻意分开 |
| 七星瓢虫 | 一簇竖立的卵 → 石板蓝黑带橙斑与疣突的幼虫 → 裸蛹**尾端还挂着蜕下的幼虫皮** |
| 日本弓背蚁 | 全程在巢里;**茧的侧壁开一扇窗**,看得见里面蜷着的蛹 |
| 星天牛 | 卵嵌在树皮的**人字刻槽**里;幼虫笔直、前胸背板宽大如盾;蛹的**长触角沿体侧盘了一圈** |
| 中华大锹甲 | 卵陷在朽木的浅坑里;乳白蛴螬配**橙黄头壳与黑色大上颚**(与独角仙的分得开);雄蛹的**一对大颚已在蛹上成形** |
| 中华虎甲 | 幼虫是穴居伏击者:**金属色的头盾堵住洞口**,背上一个**带钩刺的驼峰**把自己钩在洞壁上;蛹靠背刺仰卧在蛹室里 |
| 玉带凤蝶 | 光滑的球形卵;末龄幼虫胸部一对**假眼斑**、伸出橙红色**臭角**;**缢蛹**靠腰间一根丝带斜吊在枝上(对照帝王蝶倒挂的垂蛹) |
| 家蝇 | 香蕉形白卵;**无足无头的蛆**,尖端只露出一对黑口钩、钝尾端一对 D 形后气门板;蛹其实是末龄幼虫表皮硬化成的**深红褐色围蛹**,外面看不到足和翅 |
| 日本吉丁 | 扁盘卵嵌在树皮裂缝里;幼虫是典型的**扁头蛀虫**——前胸扁宽如锤头、背上一道 Y 形沟;乳白离蛹仰卧,足折贴腹面 |
| 竹象 | 卵嵌在**笋壁咬开的小孔**里;褐色头壳、无足的 C 形幼虫;仰卧的离蛹**长喙沿腹面中线贴伏** |
| 梭毒隐翅虫 | 球形卵;**有三对胸足、腹末带尾突的蛃型幼虫**,背板深色;附肢贴腹的离蛹 |
| 金环胡蜂 | 全程在**灰褐色纸质六角巢房**里(对照蜜蜂的蜡质巢房),开口朝下;蛹期巢房封上浅色丝盖 |
| 大蚊 | 黑色长卵;灰褐的「**皮夹克虫**」,尾端一圈肉质突起围着一对气门;被蛹头端一对呼吸角、腹节带刺列 |
| 不完全变态(对照) | 招牌 |
| --- | --- |
| 黑蚱蝉 | 知了猴:开掘前足 + 背上一对翅芽 |
| 碧伟蜓 | 水虿:可折叠的**面罩状下唇** + 四片翅芽 |
| 中华大刀螳 | **卵鞘**挂在枝上,背脊一条孵化带;若虫是缩小版成虫 + 短翅芽 |
| 东亚飞蝗 | **卵囊**竖在土里,上段是泡沫塞;蝻的**翅芽是倒转的**(前后位置与成虫相反) |
| 德国小蠊 | 带分节纹与锯齿顶脊的**卵鞘**;若虫深褐、**背中一条浅色纵带**,是无翅的缩小版成虫 |
| 棒䗛 | 卵像**植物种子**:网纹硬壳 + 卵盖 + 卵孔板;若虫就是一根更小更嫩绿的「枯枝」(棒䗛成虫本来无翅,若虫也就没有翅芽) |
| 东方蝼蛄 | 灰白椭圆卵;若虫已带**铲状开掘前足**,背后一对贴伏的小翅芽 |
| 水黾 | 长卵成串粘在水草茎上;若虫无翅,**中后足极长**,是缩小版的「水上溜冰者」 |
| 豆娘 | 卵插产在水草茎内;稚虫腹末**三片叶状尾鳃**——与蜻蜓水虿一眼分开 |
| 海滨蠼螋 | 雌虫在土室里看护的一窝白卵;若虫浅色、**尾铗短而直** |
**若虫背上那对(或四片)翅芽就是「翅还停在芽的阶段」的可视证据** —— 完全变态与
不完全变态的区别,正是中小学讲昆虫的第一课,也是这个功能存在的理由。

四格都是同一个相机、同一个取景框裁出来的,所以「卵是一小撮、蛴螬是一大条」的体型差是模型自己的,不是排版凑的。这张图由 `scripts/make-lifecycle-strip.mjs` 从线上同一份代码渲出来。
阶段之间**不做连续形变**:毛虫变蝴蝶在生物学上本来就不是连续形变(组织解离 +
成虫盘发育),硬 morph 出来好看但错。唯一做成连续的是**羽化展翅**——因为它
本来就是连续的:刚出蛹的翅是一小团皱缩的软组织,靠血淋巴撑开再硬化。
(本页顶部那只正在羽化的帝王蝶就是这一段。)
它是实录,不是逐帧手调:`scripts/make-emerge-gif.mjs` 打开线上的生活史、翻到蛹、再翻一步,把浏览器合成器推出来的帧收下来。曲线是 1-(1-u)³——先快后慢,血淋巴一开始压力最大;翅在撑开的同时从下垂逐渐抬回静止姿态。
## 一个必须先讲清楚的决定:模型是代码生成的
参考站每个器官是一份约 3.2 MB 的 GLB 资产(`/models/heart.glb` 等),属于采购或扫描来的素材。昆虫没有对应的现成资产,所以这里换了条路:**每个物种的几何体由 TypeScript 在浏览器里实时生成**,一行外部模型文件都没有。
这不只是妥协。昆虫的形态比内脏规律得多 —— 体分头/胸/腹三段,胸部生三对足、两对翅,附肢是分节的锥管,翅面由放射状翅脉支撑。这些结构参数化以后,加一个物种只需要写一份尺寸与配色的描述,而不是再买一个模型。代价是写实度不如三维扫描,换来的是零资产依赖、任意可扩展,以及每处形态都能在代码里追溯到形态学依据。
单个物种 1.3 万~3.6 万三角面(最小是淡色库蚊 13,308,最大是柞蚕蛾 35,732),在浏览器里构建耗时 30~90 毫秒 —— 但**第一只要 230~730 毫秒**,因为程序化表面贴图是全库共享的、由它一次性生成(实测与首帧分段账见 [docs/perf-notes.md](docs/perf-notes.md))。生产构建首屏 JS gzip 约 424 KB(three 主导的 vendor 340 KB + 主包 84 KB),桌面后期管线 103 KB 为懒加载独立 chunk,手机不下载,物种代码按需分包,点到谁才下载谁。
## 借来的与自写的
学自参考站的是**信息架构**:三栏工作台(图鉴列表 / 3D 展台 / 详情面板)、模型上的彩色
标注点、底部扩展卡片、顶栏五个入口,以及剖切与对比这两件功能的设想 —— 这一层不藏着。
具体实现全部为自写。
自己定的是一条交互原则:每个控件都得有真实响应,不留只有样子的按钮。逐项落到:
| 控件 | 这里的行为 |
| --- | --- |
| 顶栏导航与搜索 | 五个入口都有激活态;搜索实时过滤下拉,总览按目分组 |
| 工具条 Isolate / Layers / Zoom / Reset | 镜头聚焦部位 / 外骨骼半透明 / 拉近一档 / 复位 |
| 剖切与对比 | 裁剪平面真的切开模型;展台底部浮出对比条,另可换对照物种 |
| Quiz 小测 | 真能作答:判对错、给解释、统计得分 |
| 讲解弹窗 | 分 3~4 步,每步把 3D 镜头移到讲到的部位 |
视觉的来历,说清楚:**v1 的配色与版式是照着参考站实测取值复刻的**(奶油纸底
`#f7f0e7` + 珊瑚 `#eb7c6b` + Cormorant Garamond,讲解弹窗连宽度圆角都照抄)。
仓库转公开时这些借来的数值**已全部替换**为自研的两套主题:暗色「博物馆之夜」
(`#131110` 展厅黑 + `#b08d57` 黄铜)与浅色「标本台」(`#e9ebe4` 档案衬板 +
`#7d6128` 青铜 + 砖红/靛蓝/苔绿/赭黄四色标签墨),排印换成 Playfair Display +
Noto Serif SC,弹窗尺寸也换成自己的一套。
## 结构
```
src/
three/
builders/
kit.ts 建模工具箱 —— 所有物种的地基
surface.ts 程序生成表面微观贴图(刻点/纵沟/微颗粒,运行时 CanvasTexture)
eyes.ts 复眼六边形小眼面法线贴图
venation.ts 参数化翅脉网(纵脉+渐密横脉围出真翅室)
.ts 每个物种一个文件,导出 build(): InsectModel
stages/-<阶段>.ts 卵/幼虫/蛹/若虫,一个文件一个阶段
registry.ts 按 id 动态加载物种模块(Vite 代码分割 + LRU 显存回收)
InsectCanvas.tsx 3D 展台:工作室光照、按需渲染、触角微动、背面圆点淡出
PostFX.tsx 桌面后期(N8AO + Bloom + 链尾 ACES),懒加载独立 chunk
stages.ts 生活史阶段的注册表(与物种注册表分开,懒加载边界设在这里)
motion/ 动作层:纯函数,只写关节 rotation、不碰几何
types.ts Motion 契约、进出场的幅度权重、真实拍翅频率→屏幕频率的压缩
idle.ts 静息微动(全体)
hover.ts 悬停振翅(8 只)与各自的真实拍翅频率
emerge.ts 羽化展翅(唯一一段连续形变)
components/
TopBar / LibraryPanel / Stage / DetailPanel / BottomCards 三栏工作台
Discovery.tsx 讲解弹窗(讲解 / 动态演示 / 小测 / 栖境 / 生活史 五个变体)
CompareBar.tsx 展台底部的内联对比条
Gallery.tsx 按目分组的全部物种总览
InsectGlyph.tsx 63 个手写 SVG 剪影
data/
types.ts 数据契约
insects.zh.ts / insects.en.ts 63 种昆虫的图鉴数据(中英各一份)
guides.zh.ts / guides.en.ts 63 种的分步讲解与测验(中英各一份)
preview.tsx 模型调试台(/preview.html)
```
### kit.ts 提供什么
| 类别 | API |
| --- | --- |
| 放样核心 | `loft` `spindle` `segmentedAbdomen`(默认节间凹槽)`segmentedAbdomenMembranes`(节间软膜环) |
| 附肢 | `leg` `legPair` `antenna` `antennaPair` |
| 翅 | `wing` `wingPair` `wingGeometry` `wingVeins` |
| 头部器官 | `compoundEye` `compoundEyePair` `ocelli` `mandibles` `rostrum` |
| 材质 | `chitin`(surface 刻点/纵沟/绒面、translucent 半透)`elytra`(iridescent 虹彩)`membrane`(薄膜虹彩) |
| 收尾 | `finalize` `boundingRadius` `mirrorZ` |
`loft` 是全部几何的地基:给一串椭圆截面,沿路径放样成封闭实体。它对退化输入(重合点、零半径、竖直路径)有专门的测试,因为这些情况一旦产生 NaN,整个模型会静默变成空白。
物种文件只依赖 kit(与 surface/eyes/venation 三个工具模块),彼此不依赖 —— 63 个物种是七轮多 agent 并行写出来的。
### 坐标与数据的约定
模型局部坐标:`+X` 向前(头部方向),`+Y` 向上(背方),`+Z` 向右,单位 1 = 1 厘米真实体长。
`finalize()` 把模型居中并同步平移锚点,返回 `{ group, anchors, radius }`。`radius` 供相机自动取景 —— 竹节虫 10 cm 和瓢虫 0.7 cm 差着 20 倍,靠它归一化。
`anchors` 的 key 是建模层与数据层之间唯一的耦合点:`insects.*.ts` 里每个 `hotspot.anchor`、`guides.*.ts` 里每个 `LessonStep.anchor`,都必须能在对应物种的 `anchors` 里找到,否则那个标注点会**静默消失** —— 页面不报错、两边各自的测试也都是绿的。`three/__tests__/integration.test.ts` 专门钉住了这层接缝。
## 加一个物种要做什么
注册表用 `import.meta.glob('./builders/*.ts')` 扫描目录,**文件名直接当 id**,导出的 `buildXxx()` 按 `build` 前缀找。所以放一个新文件进去就自动注册,不必回来改任何现有代码。
但内容还是要一份份写,四处的 id 必须一致:
| 写什么 | 放哪 | 分量 |
| --- | --- | --- |
| 3D 几何生成代码 | `src/three/builders/.ts` | 200–400 行,最费事 |
| 图鉴数据(学名、6 条关键数据、5–6 个标注点、生态、冷知识…) | `src/data/insects.zh.ts` + `insects.en.ts` | 各一条记录 |
| 讲解与测验(3–4 步 + 2 道题) | `src/data/guides.zh.ts` + `guides.en.ts` | 各一条记录 |
| 24×24 剪影图标 | `src/components/InsectGlyph.tsx` | 一个小组件 |
漏了会被测试拦下:数据里有记录却没有 builder 文件 → 集成测试失败;某个 `anchor` 在模型上找不到 → 接缝测试失败。
## 踩过的坑
几个只有实际跑起来才会撞上、且都不会被类型检查或 NaN 检查抓到的问题:
**`` 会挂起整棵子树。** drei 的 `Environment` 即使只用内联 `Lightformer`(不加载外部 HDR)也可能 suspend。它和模型、灯光共处一个 Suspense 边界时,表现是:canvas 尺寸正常、WebGL 上下文正常、控制台无报错、画面全空。必须给它自己的 ``。
**内联回调会造成无限重载。** `onLoaded` 这类回调若是父组件每次渲染新建的闭包,又进了加载 effect 的依赖数组,就会形成「加载完 → 通知父组件 → 父组件重渲染 → 新回调 → 重新加载」的死循环,同样表现为 3D 区永远空白。回调要放进 ref 再用。
**`legPair` 曾经不是镜像。** 原写法是「把 `base.z` 取负,再翻 `scale.z`」。但 `leg()` 内部算出的腿节方向 z 分量恒为正、不随 `base.z` 变号,于是左腿的基节被翻回右侧,整条腿从右侧根部斜穿过身体中线。三个物种作者独立报告了这个现象。几何完全合法、没有 NaN,只是长错了地方 —— 这类问题只能靠专门的对称性断言抓住(`__tests__/mirror.test.ts`)。
**`wing()` 的 `spread` 语义与直觉相反,且文档自己也错过一回。** 实测是 `180` = 向本侧完全展开、`90` = 沿体轴向前、`0` = 横穿身体伸向**对侧**。早期文档写成「0 = 侧展」,而当时的测试只比对了展开跨度 —— 0 与 180 的跨度一模一样、只是方向相反,于是错误文档被绿灯测试背书,先后坑了两位物种作者。现在 `mirror.test.ts` 连方向一起钉住了,别顺手「修正」实现。
**3D 只在浏览器窗口「可见」时才会渲染,截图因此一度做不出来。** 隐藏标签页没有
`requestAnimationFrame`,r3f 的 Canvas 量不到容器尺寸就永远不挂载子树——症状是
canvas 在、无报错、加载转圈永不消失,而 DOM 外壳照常渲染,于是**截图工具拍到的
是一张「正在生成…」**。判据一行:`document.visibilityState !== 'visible'` 或 600ms 内
rAF 计数为 0,就别再排查渲染代码了。出图改走无头 Chromium(`scripts/shots.mjs`),
那里 visibilityState 恒为 visible,顺带让 README 截图变成可复现的一条命令。
**`elytra()` 的清漆层调太高会整片过曝。** 原值 0.85 配 Environment 的面光源,正对光的角度会把固有色和隆起的体积感一起吃掉 —— 深栗褐的独角仙从正面看像两个白球。压到 0.55 才对。
**`wingVeins()` 的翅脉半径是硬编码的绝对值。** 在 3 单位以上的大翅上细到看不见,帝王蝶因此一度整片糊成均匀褐色。大翅要写按翅宽缩放的局部翅脉。
**标注点必须挂进旋转的那个 group 里。** anchors 是模型的**局部坐标**。如果把 `` 热点当作兄弟节点摆在场景里,它们会被当成世界坐标固定在空中 —— 静止时看着完全正常、位置分毫不差,一开自动旋转就露馅:虫在转,点不动。修法是让热点成为旋转 group 的 children。
连带还有一处:自转角度是**累加**的,所以「聚焦到某个部位」时不能直接拿局部锚点当目标,必须先套上 group 的世界矩阵,否则自动旋转开着时镜头会对到空处去。聚焦期间也顺手停掉自转 —— 镜头锁死在一点而虫还在转,那个部位会自己溜走。
**测试全绿不等于长得像。** 几何合法性、包围盒比例、面数预算都能自动验,但「这只虫看起来像不像萤火虫」只能用眼睛看。`/preview.html` 就是为此存在的:单物种放大、线框、网格地面、面数与锚点统计。萤火虫的前胸盾片一度被做成两个大椭球(看着像两只虫黏在一起),而所有测试都是绿的。
## 测试
```bash
npm test
```
50 多个文件、4000 多个测试。下表是一次快照,示意各层的分量(逐项数字随迭代漂移,以 `npm test` 实际输出为准):
| 文件 | 数量 | 管什么 |
| --- | --- | --- |
| `data/__tests__/guides.test.ts` | 1074 | 讲解与测验的结构、字数、anchor 逐物种校验 |
| `data/__tests__/insects.test.ts` | 952 | 图鉴数据契约、anchor 白名单、trivia 不得复述 summary |
| `data/__tests__/parity.test.ts` | 632 | 中英两版逐字段对齐(学名一致、条目不缺不多) |
| `data/__tests__/lengths.en.test.ts` | 252 | 英文文案长度闸门 —— 并行翻译的验收线 |
| `components/__tests__/glyph.test.tsx` | 192 | 63 个剪影的结构与坐标越界 |
| `three/__tests__/integration.test.ts` | 192 | 建模层 × 数据层的接缝 |
| `__tests__/layers.test.ts` | 68 | 三层齐备性 + 展示文案字符白名单 |
| `three/__tests__/anchors-have-geometry.test.ts` | 64 | 每个 anchor 落点上真有几何体 |
| `builders/__tests__/kit.test.ts` | 30 | 放样地基与退化输入 |
| `builders/__tests__/*.test.ts`(其余 31 个) | 602 | 各物种形态断言、表面材质落位、翅脉/节间膜/触角钩子普查 |
| `builders/__tests__/mirror.test.ts` | 12 | 成对附肢对称性、`wing` 的 `spread` 语义与方向 |
| 其余零散文件 | 219 | 搜索、滚动、页脚、对比度、免责声明等 |
形态类断言写的时候有个自检标准:**把代码改回出问题的版本,这条断言会失败吗?** 不会就等于没写。
「后足腿节最粗处 ≥ 腿节长的 1/3」这类边界值断言尤其容易写成恰好擦边通过而实际无效 —— 蝗虫那条最初就是 0.333 压线过,看着是绿的其实什么也没管住。后来改成量取真实网格的最大半径,外加一条「两端不许收细到峰值的 28% 以下」,才真正拦得住。
但这条自检标准仍有它管不到的地方,而且是最贵的一块:**断言量的是数字,人看的是长相,两者可以毫无关系。**
兰花螳螂的花瓣状腿节「宽度 ≥ 厚度的 3.5 倍」测出来是 5.75,绿的;渲染出来却是几片侧立的薄板,
整只虫像一只苍白的虾 —— 因为决定朝向的那个四元数只约束了长度轴,绕它的滚转是随意的,
扁平面正好侧对着镜头,而宽厚比这个**数字**丝毫不受影响。
同理,白蚁兵蚁的两颚在世界坐标里分得很开,但默认机位的视线方向恰好把这个分离压扁,
屏幕上糊成一根独角。
能钉住这类问题的断言,必须**量到用户真正看见的那个量**:
兰花螳螂改成断言花瓣的三个维度里有**两个**远大于第三个(一块板只有一个);
白蚁兵蚁改成把两颚顶点投影到默认机位的成像平面上,沿颚长切 20 段,
断言其中有连续一长段两者的投影包围盒**不相交**。
写形态断言前值得先问一句:这个数字,和我要看的那件事是同一件事吗?
这一类「测试查不出长相」的完整审计方法 —— 出图规格、扫背面剔除的剪影差集、
定位像素归属的 ID 图 —— 沉淀在 [docs/model-audit-notes.md](docs/model-audit-notes.md)。
## 已知不足
- 写实度上限仍低于三维扫描 —— 表面微观(刻点/沟纹/绒面/蜂窝复眼)已由程序贴图补足,但没有手绘贴图与真实绒毛物理;换来的是零资产文件与全库风格统一。
- 观察笔记只存在浏览器本地(localStorage),换设备或清缓存就没了;
头像菜单里的「复制为 Markdown」是目前唯一的带走方式。
- 剖切与分层是通用实现(裁剪平面 / 降低外骨骼不透明度),没有按物种定制解剖层次。
- **生活史只做了八种**,其余 55 种的「生活史」卡片仍只有文字。羽化展翅也只对
有翅骨架的物种成立 —— 甲虫的鞘翅是逐只自写的,这段动画对它们空转。
- 卵是最难的一个阶段,因为它本身没有结构。做得成立的几颗靠的都是**可指认的
表面结构**(帝王蝶的辐辏纵棱)或**真实的语境**(蜂巢六角房、育儿粪梨、
水草茎里的产卵刻痕);没有这两样的那几颗只能算过线。
- **拍翅频率是压过的**,见上文「它们是活的」。相对次序是真的,绝对值不是;
真实频率与压缩函数都在代码里写明,别拿屏幕上数出来的次数当数据。
- 部分物种的近缘种列表是按常见中文名写的,未逐条核对分类学文献。
- 英文版(`/en/`)的全部内容是 AI 从中文翻译的,未经母语者校订,也未请昆虫学者
复核英文术语与常用名。学名(拉丁二名法)两版一致,那是唯一可靠的对照锚点。
## 致谢与许可
**信息架构学自 [Anatomy Atelier](https://anatomy-livid.vercel.app/)**(源码在
[thebuggeddev/anatomy](https://github.com/thebuggeddev/anatomy))—— 三栏工作台、模型上的
彩色标注点、底部扩展卡片、引导讲解的位置,以及一部分交互手感,都来自它。本项目不含它的
任何代码,全部为自写实现。感谢它把这个交互形态做了出来。
本仓库的 3D 建模代码、图鉴文案、讲解与测验内容、两套视觉主题及全部实现均为原创,
以 MIT 许可发布,见 [LICENSE](LICENSE)。
⚠️ 再说一次:**图鉴文案由 AI 撰写,仅 14 种做过部分文献核校、均未经专家复审**,不适合作为学术引用来源;用于教学请以页面列出的原始来源为准。