Files
orisys-sensor-design/docs/design-parameters.md
T
ykcheungab e7618faed4 docs: give the multi-view design and geometry facts one home each
Two workspace documents described the multi-view ownership decision by
overlapping each other and had both gone stale: neither matched the shipped
code, and the architecture document was referenced by nothing at all. They are
archived in the workspace, and this repo now carries the parts that belong to
it.

New: docs/geometry-measurements.md. Measured properties of the r01 sensing
surface, the sensing camera, the UV layouts, the printed cell pitch and the
runtime unroll domain - topology (1032 v / 1974 t), 355.77 mm2 area with 38.1%
near-vertical wall, the off-origin centroid, 94.5% inward-facing normals, the
0.516 x 0.714 mm measured pitch against a wrong 0.205 x 0.3556 mm nominal cell,
75.6% runtime coverage, and incidence over that region (p50 55.6 deg, invariant
to adding views). Merge-mode agreement is included, with an explicit note that
these values are camera-specific and expire when a camera moves - the Xynova
surface already needs 70 deg where r01 ships 62 deg.

Wiring:
- README.md points at the measurements and says they expire.
- design-parameters.md section 4 keeps the key semantics and now links to the
  workspace record for rationale and to the measurements for data, instead of
  restating either.
- design.yaml: replace a self-referential comment pointing back at the same
  document with the keys location and the rationale location.

No behaviour change: the YAML edit is a comment, and no source file is touched.
2026-09-30 16:29:39 +08:00

455 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `design.yaml` 参数说明
本文是 `sensors/<sensor>/r01/design.yaml` 的详细说明,主要面向传感器研发工程师。
本文重点解释两个命令:
- `texture_patch`:在普通 UV 展开图上生成一整张标记图案;
- `texture_patch_views`:把同一套图案拆成多个相机投影视图,并决定每个视图负责印刷哪一部分。
例子:`orisys-design texture-views .\sensors\finger\r01 --grayscale`
## 1. 总览
传感器设计工具依赖开源3维绘图软件blender以及自带的Python API 开发,需要传感器3维CAD模型、镜头FOV参数、cmos的尺寸。目前主要用于摄像头位置和标记点的设计,后续会加入光源优化。
摄像头位置的设计主要是确保在镜头FOV限制下,可以完全覆盖传感的表面,分为感知面积最大化和死区清除两部分。
标记点设计目的是:
* 算法上要导出unwarp conformal的平面图,把3维变形映射到2维计算同时降低畸变。
* 生产上要在不可展曲面上印刷/雕刻标记点,程序需要导出曲面在加工平面上投影的平面图,格式包括 PNG、SVG 或 PDF。
导出的图片类型一览:
```text
Blender 三维传感表面 (sensing surface)
├─ `export` conformal UV 展开 → 计算用
│
├─ `texture` 单相机投影(projected from view) UV 展开 → 生产用,一张图覆盖整个表面
│
└─ `texture-views` 多个相机投影视图 + 分割 → 生产用,每张图只印自己负责的区域
```
设计参数放置在`sensors/<sensor>/r01/design.yaml`里面,具体设计资源参考现有传感器的文件树。
### 1.1 基础原理
叠动视触觉传感器基于单色光技术,标记图案要具备高密度纹理信息,用于光流追踪,目前采用随机彩色方格矩阵纹理设计和UV打印生产,对于平面传感器可以简单密铺,对于Finger曲面传感器需要考虑曲面畸变、镜头透视误差、可视区域、可生产性等等,要在3维层面进行设计,与三维绘图的呈现方式类似。考虑到代码兼容性、价格、功能等因素,采用开源的三维绘图软件Blender和配套的python进行程序开发,可以从相机、模型、图案、光源4个部分独立和耦合设计。
设计流程是:1. 结构传感器曲面设计;2.模型导入Blender摄像头位置摆放;3.曲面不可视区域去除;4.结构曲面修改;5.模型导入Blender和标记图案设计;6.生产摄像头位置摆放和图案生产文件导出。
曲面结构除了标品外,要根据客户灵巧手可释放的空间来设计,但一般有一个非触觉版本的模型,可以作为基准来修改,考虑塑料壳、硅胶层的厚度,得出一个初步的感知曲面形状和位置。然后导入Blender进入详细设计,在结构和摄像头FOV的约束下,选择合适的摄像头摆放位置来达到可视区域的最大化,再根据摄像头FOV去除曲面的不可视区域,然后反馈结构进行修改。最后根据修改后的摄像头位置、曲面结构,生成计算用和生产用标记点图案。
计算用标记点图案作用是把在三维空间的曲面投影到二维的平面上,方便通过过去用于平面的算法计算,投影方法目前采用conformal投影,未来可以尝试其他方法。三维到二维的投影通过Blender里面在实际摄像头位置的透视投影相机,和感知曲面的模型获取,并通过`export`指令导出图案和投影矩阵。生产用图案是通过Blender的正交投影相机模拟在传感器治具正上方,由加工设备(UV打印机或激光雕刻机)看到的形状/投影,从而把我们在曲面上的目标图案,从三维空间投影到生产的二维平面上,支持多角度生产和重复部分去除功能。
通过Bleder Python API,在具备视触觉传感器结构和材料特性下可以进行物理准确的仿真,是本设计和模拟工具的核心支撑。
### 1.2 Blender 使用
以下文档基于Blender 5.0 版本 (5.0.1)编写,如非必要不进行版本更新。
![blender_layout](.\img\design_parameters\blender_layout.png)
Blender的资源树在右上方,不同集合(Collection)分别代表不同的资源:
| 集合 | 种类 | 作用 | 导出 |
| --- | --- | --- |--- |
| ASSEMBLY| mesh、摄像头 | 外壳模型、生产视角摄像头 |所有网格物体,cam_uv_**x** |
| EXCLUDE | 非导出物体 | 文件暂存 |无|
| EXPORT_COMPUTE| mesh | 感知曲面 |sensing_surface|
| FRAMES| empty | 原点定义 |origin_sensor |
| DESIGN_SIM| 摄像头、光源 | 视场设计仿真 |cam_sensor |
---
# 下方文档是AI生成,待整理
---
### 1.1 初学者需要知道的几个词
|术语|简单解释|
|---|---|
|UV|把三维表面的点映射到二维平面的坐标。`U`、`V` 类似图像中的 `x`、`y`|
|UV 展开(unwrap)|像展开纸盒一样,把弯曲表面摊到二维平面;允许人为安排接缝和布局|
|Project from View (Bounds)|从指定相机看向物体,把画面投影到 UV,并缩放到 `[0,1]²`|
|pattern camera|定义标记图案从哪个方向投到表面,即“图案来源”|
|region camera|判断一个区域由哪个印刷视图负责,即“区域分工”|
|ownership|某个三角形最终归哪一个视图打印|
|hole|没有任何视图负责的表面区域,会形成漏印或“秃斑”|
|seam|Blender 中标记的接缝,用于把表面分区|
|page|输出 SVG/PDF 对应的实际物理尺寸,单位为毫米|
|raster|PNG 的像素尺寸,例如 `3700 × 3280 px`|
### 1.2 阅读参数时遵守三条规则
1. **每一个测量值都必须说明失效条件。** 例如相机移动后,以旧相机位置测出的覆盖率就不能
继续使用。
2. **测量条件是结果的一部分。** 涉及区域相机时,必须同时记录相机位姿,因为位姿就是算法
输入。
3. **文件哈希只代表测量当时的文件。** `geometry.blend` 会持续修改。引用旧哈希前必须重新
计算,不能把历史哈希当成当前状态。
本文最重要的目的,是防止一个过期数值继续显得“很合理”。只把说明写在 YAML 注释里,没有
自动失败路径;参数失效后程序仍可能正常运行,却生成错误结果。因此本文会明确写出每个关键
数值的来源、失效条件和重测方法。
## 2. 两种纹理命令的区别
|项目|`texture`|`texture-views`|
|---|---|---|
|YAML 配置块|`texture_patch`|`texture_patch_views`|
|输出定义|`uv_outputs`:名称到文件名的映射|`pairs[]`:视图 ID、相机、UV map|
|二维布局|普通 UV 展开,不是相机投影|由相机拟合到边界的投影视图|
|物理页面|`fixed`:读取 `texture_patch.vector` 中声明的毫米尺寸|`auto`:从相机图像平面推导,并验证它确实是投影视图|
|区域归属|不需要,一张图覆盖整个表面|必须,每个 pair 都包含完整表面,但只打印归自己负责的部分|
|任务格式|`texture-job-v3`|`texture-job-multiview-v1`|
|Blender 驱动|`blender/scripts/random_patch_dd02c.py`|`blender/scripts/random_patch_multiview.py`|
`texture_patch_views` 会从 `texture_patch` 继承以下“图案本身”的设置:
- 栅格尺寸 `width`、`height`;
- 网格数量 `nx`、`ny`;
- 随机种子 `seed`;
- 固定页面备用值 `vector`;
- 颜色配置;
- `edge_sample_grid`。
代码入口是 `texture_views.resolve_views_settings(...)`,内部调用
`resolve_texture_settings(...)`。它不会继承 `uv_outputs`、`merge`、`seam` 或 `print`,
因为这些字段分别属于单图输出或多视图分区。
继承方向只有一个:`texture_patch` 是页面、颜色和图案随机性的基础配置;
`texture_patch_views` 在此基础上增加投影视图和区域分工。
> 为什么不合并两个配置块?在 `texture_patch` 中,页面是人工声明的输入,例如
> `18.5 × 16.4 mm`;在 `texture_patch_views` 中,页面是经过验证后从相机投影推导出的
> 结果。合并后,`page.mode` 的含义会依赖旁边是否出现某个键,还会削弱当前严格的未知键
> 检查,使拼错的配置可能被静默忽略。如果未来出现第三个消费者,或者 `uv_1`、`uv_2`
> 不再是边界投影,再重新评估公共配置结构。
## 3. `texture_patch`:在普通 UV 展开图上生成图案
命令会为 `uv_outputs` 中的每个 UV map 各生成一套 PNG、SVG 和 PDF。所有输出共用相同的
随机颜色和页面设置,仅 UV 布局不同。
UV 展开必须预先在 Blender GUI 中完成并保存在 `source_blend`。无界面运行只读取已有 UV
map,不会自动创建或重新展开 UV。
### 3.1 `uv_outputs` 的顺序不能随意调整
程序通过下面的方式构造输出列表:
```python
block["uv_outputs"].items()
```
因此 YAML mapping 的顺序会进入 `texture-job-v3` 的 `outputs[]`。目前四个测试固定要求顺序
为:
```text
["produce", "compute"]
```
仅从生成像素来看,调换顺序不会改变颜色,因为 `patch_colors` 在遍历各 UV 前只计算一次。
但任务文件数组顺序会改变并导致测试失败,所以不要为了“整理”而重排。
### 3.2 为什么有的配置是 `nx == ny`,有的是 `80 × 45`
`nx`、`ny` 表示相机空间中的图案网格数量。`finger-xynova/r01` 的 `cam_sensor` 是正方形
画幅,所以 `nx == ny` 可以让相机空间中的单元接近正方形。
`finger/r01` 使用 `80 × 45`,来自 16:9 相机。这个比例对它自己的相机是合理的,不能因为
另一个型号采用方形网格就统一修改。
### 3.3 `width`、`height` 与 `vector` 的关系
- `width`、`height`:PNG 栅格尺寸,单位是像素;
- `vector.width_mm`、`vector.height_mm`:SVG/PDF 对应的物理页面尺寸,单位是毫米。
当前两个 revision 都使用 200 px/mm:
```text
3280 / 16.4 = 200
3200 / 16.0 = 200
3700 / 18.5 = 200
```
维护时应满足:
```text
raster pixels = px_per_mm × physical millimetres
```
`_warn_if_page_aspect_differs` 会比较栅格长宽比与物理页面长宽比。如果不一致,SVG/PDF
填满页面时会沿某一方向被拉伸。保持上述关系,警告就不会出现。
### 3.4 `seed` 是整个物理图案的随机种子
固定 `seed` 是为了让同一配置反复运行得到相同的图案。同一次生成中的 PNG、SVG、PDF 和
所有 UV map 共用一套颜色。
`texture-views` 里的每个 pair 也必须使用相同 seed,因为它们打印的是同一个物理表面的
不同部分。如果每个视图使用 `seed + i`,两个印刷文件会在同一个表面上产生不同图案。
> 已知文档缺陷:`orisys-design texture-views --seed` 的帮助文字声称“第 i 个 pair 使用
> `seed + i`”,但实际代码让所有 pair 使用 `settings.seed`。实际代码行为是正确的,CLI
> 帮助文字需要另行修正。
## 4. `texture_patch_views`:多个投影视图和区域分工
本节说明**每个键的语义**。为什么这样设计——为什么所有权在生成之前决定、为什么单位是三角形、
以及两种 seam 模式各自的适用条件——见
`D:\orisys_ws\docs\workflow\multiview-ownership-decision.md`。测量数据(拓扑、相机、入射角、
cell pitch)见 [`geometry-measurements.md`](geometry-measurements.md)。
这个流程较容易混淆,因为它同时使用三类信息。
### 4.1 三种角色
1. **图案相机 `pattern_camera`**:决定标记图案从哪个方向投到传感表面。所有视图共用一个
图案来源,默认是 `cam_sensor`。
2. **区域相机 `pairs[].camera`**:决定该视图适合负责哪些三角形。它只参与区域分工,不是
图案来源。
3. **`pairs[].uv_map`**:决定该视图最终写入哪个二维布局。
可以把它们理解成:pattern camera 决定“印什么”,region camera 决定“谁来印”,UV map
决定“印刷文件中的坐标”。区分这三者,也能解释为什么片段数量和三角形数量不是同一个概念。
### 4.2 `merge.incidence_limit_deg`
区域归属以 loop triangle 为单位。一个三角形只有同时满足以下条件,才可能归某个视图负责:
- 区域相机能看到它;
- 相机视线与表面法向的夹角不超过 `incidence_limit_deg`;
- 投影面积达到要求。
没有任何视图能够负责的三角形就是 hole,会造成漏印。
在 `finger-xynova/r01` 上,使用真正的 `assign_ownership` 和本次运行生成的 ground truth,
测得:
|`incidence_limit_deg`|hole fraction|相对 `max_hole_fraction: 0.02`|
|---|---|---|
|62°|**0.0480**|**失败**|
|65°|0.0130|通过|
|**70°**|**0.0001**|通过|
这里 `min_triangle_coverage` 不是限制因素:把它从 0.60 放宽到 0,在 62° 和 70° 下都不改变
结果。
这组测量对应以下区域相机位姿:
|相机|方位角|仰角|距离|
|---|---:|---:|---:|
|`cam_uv_1`|308.26°|−24.46°|32.62 mm|
|`cam_uv_2`|231.74°|−24.46°|32.62 mm|
**只要移动区域相机,这张表就立即失效。** `incidence_limit_deg` 描述的是“特定相机位姿与
特定表面”的组合,不是传感器型号自身永远不变的属性。重新调整相机后必须重跑扫描。
> 已撤回的旧结论:早期 `design.yaml` 曾写过“默认 62° 时 hole 面积为 0.100 mm²、比例
> 0.0002”,以及“4960/5007 个三角形有归属,输出 20454 + 21483 个片段”。这些数据在
> 相机移动前成立,现在已经失效。保留这段记录,是为了说明为什么相机相关数据必须写明
> 重测触发条件。
不要删除 YAML 中显式设置的 `incidence_limit_deg: 70.0`。代码中
`texture_views.MERGE_DEFAULTS` 和 `blender/scripts/texture_merge.py` 的默认值都是 62.0,
并有测试要求两处一致。但对当前 Xynova 表面,62° 会产生 4.8% hole;删除覆盖值会静默恢复
错误默认值。
### 4.3 `max_hole_fraction` 是阻断门槛
当 hole fraction 超过 `max_hole_fraction` 时,命令会失败并报告:
- 实际 hole fraction;
- 未覆盖的三角形数量;
- 可能原因。
它不是普通警告。这项检查是在一次 4.8% 漏印被静默输出之后增加的,目的是让“表面有秃斑”
从肉眼发现的问题变成自动失败。
### 4.4 `page.mode: auto` 与 `fixed`
`auto` 只适合使用 Blender **Project from View (Bounds)** 生成的布局。这种布局把相机图像
平面映射到 `[0,1]²`,程序可通过最小二乘仿射拟合反推出页面的实际尺寸。
程序不会因为写了 `auto` 就盲目信任 UV。它会验证:
- 最大拟合残差不超过 `1e-5`;
- UV 边界位于 `[0,1]²`。
真实投影视图的拟合误差大约为 `1.2e-7`;普通 UV 展开的误差约为 0.17–0.70,因此会被拒绝。
`auto` 不会在失败时悄悄退回固定页面。
`fixed` 使用 `texture_patch.vector` 中的页面尺寸。`produce` 和 `compute` 是真正的普通 UV
展开,必须使用 `fixed`;从相机反推它们的页面没有物理意义。
自动页面最终按整数像素确定:
```text
page size = rounded raster pixels / px_per_mm
```
这样页面和栅格的长宽比天然一致。200 px/mm 下,舍入导致的尺寸误差最多为 0.0025 mm,
远低于当前 0.1 mm 对位公差。
已经测得的页面尺寸:
|revision|view|对应测量位姿下的页面|
|---|---|---|
|`finger/r01`|`uv_1`|17.510 × 15.975 mm|
|`finger/r01`|`uv_2`|17.510 × 15.980 mm|
|`finger-xynova/r01`|`uv_1` / `uv_2`|17.670 × 23.565 mm|
这些数值会随相机位置改变,正是使用 `auto` 的原因。Xynova 相机重新定位后,页面从
21.200 × 23.200 mm 自动更新为 17.670 × 23.565 mm。引入自动推导前,固定共享页面在
`finger/r01` 上误差为 6.9%,在 `finger-xynova/r01` 上误差达到 23%,而且横竖方向相反。
### 4.5 `seam.boundary`、`pins` 与 `gap`
`boundary: use_seam` 表示使用 Blender 中人工标记的 seam 作为区域边界。图案生成阶段不会
读取 seam;`patch_projection.py` 只读取 `loop_triangles` 和 `uv_layers`。seam 由后续
ownership 步骤使用。
`pins` 用于在自动分配不符合设计意图时,强制某个连通区域归指定视图。默认留空,让覆盖率
指标决定归属。每次增加 pin 都应写清原因,避免把几何或相机错误隐藏成“人工设置”。
如果启用 gap,总间隙宽度是 `alignment_tolerance_mm` 的两倍。原因是两次独立印刷都可能
产生最大对位误差;每一侧退让一个公差,最坏情况下仍不会重叠。
- `apply: post_process`:将 UV 多边形裁剪到 seam 偏移后的半平面,生成物理宽度准确的直边;
- `apply: geometry_trim`:丢弃整个片段,会产生锯齿边,仅保留作比较。
> 当前仍可看到的阶梯形状来自按三角形分配的 ownership 边界,而不是 gap 自身。gap 只是
> 从这条阶梯边界向内退让。要得到平滑边界,需要支持三角形内部的更细粒度分区。
### 4.6 `print.base_fill`
`base_fill: owned` 会把本视图负责的三角形填成白色,其余区域填黑。这样白色底层也已经沿
seam 分开,两张印刷文件不会在同一位置重复打印白底。
## 5. 常用参数速查
|参数|当前值|来源/测量条件|什么时候失效|怎样重测|
|---|---:|---|---|---|
|Xynova `incidence_limit_deg`|70.0|第 4.2 节记录的相机位姿|移动区域相机或修改表面|对新 ground truth 运行 `assign_ownership`,扫描 62/65/70/75°|
|自动页面|见第 4.4 节|对应区域相机位姿|移动相机或修改网格|重跑 `texture-views`,读取 `_print_views.json`|
|`px_per_mm`|200|设计选择|修改栅格或页面尺寸|计算 `width / vector.width_mm` 和 `height / vector.height_mm`|
|`alignment_tolerance_mm`|0.1 mm|当前印刷与对位能力假设|更换打印、夹具或对位工艺|通过实际计量重新评估,不由本仓库推断|
### 内置默认值不适合当前表面的键
|键|内置默认值|为什么必须保留在 YAML 中|
|---|---:|---|
|Xynova `incidence_limit_deg`|62.0|当前表面会产生 4.8% hole;显式 70.0 是必要配置|
## 6. `expected.*`:目前还没有真正起到保护作用
`expected.sensing_vertices` 和 `expected.sensing_triangles` 看起来像拓扑变化门槛,但当前
`src/`、`blender/`、`tools/`、`tests/` 中没有代码读取它们。
Xynova YAML 当前写的是 2595 vertices / 5005 triangles,实际测得为 2584 vertices /
5007 triangles。早期注释曾声称 `expected.*` 能发现这类变化,这个说法不成立,因为字段尚未
接入导出 manifest 或测试。
在它们真正接入检查之前:
- 把这些数值视为“未验证”;
- 不要在报告中引用它们作为自动验收结果;
- 不要因为字段存在,就假设拓扑变化已经受到保护。
理想的后续实现是:导出时测量实际拓扑,与 `expected.*` 比较;不一致时明确失败并打印差异。
## 7. 已知几何漂移
仓库中的历史资料对 Xynova sensing surface 的描述互不一致:
|来源|记录值|
|---|---|
|`probe-pipeline.md`|2508 vertices / 4834 polygons|
|`xynova-texture-investigation.md`|4818 triangles + 16 quads|
|当前实际测量|2584 vertices / 5007 triangles|
`build/probe/finger-xynova/r01/probe_frames.npz` 来自 2508 顶点的
`sensing_surface_918_s`,而当前带 seam、`uv_1` 和 `uv_2` 的表面有 2584 顶点。因此
`tests/test_render.py::test_end_to_end_render_manifest` 会在顶点数量检查处失败。
这是过期 fixture,不是纹理生成错误。修复方式需要先决定哪个表面才是该 revision 的唯一权威
表面,然后选择其中一种方案:
1. 为当前权威表面重新生成 probe frames;或
2. 把 seam 和 UV 布局制作到已经生成 probe frames 的那个表面上。
当前 revision 包含六个 sensing-surface 变体:
```text
sensing_surface
sensing_surface_0
sensing_surface_0918
sensing_surface_0928
sensing_surface_45
sensing_surface_918_s
```
其中 `_0928` 与 `sensing_surface` 位于相同位置,是重合副本。`EXCLUDE` collection 的
`exclude=False` 表明它只是名称叫 EXCLUDE,并没有在 view layer 中真正排除。射线检测会先
击中这个副本,随后把所有片段判为不可用。
目前 `_0928` 已隐藏;`texture-views` 现在也会在发生遮挡时失败,并指出拦截射线的对象,
不会继续生成空白纹理。
## 8. 两个 revision 的差异
|项目|`finger/r01`|`finger-xynova/r01`|
|---|---|---|
|栅格尺寸|3280 × 3200|3700 × 3280|
|固定页面|16.4 × 16.0 mm|18.5 × 16.4 mm|
|`nx`, `ny`|80, 45(16:9 相机)|80, 80(方形相机)|
|seam 边数|32|45|
|自身 hole|0.049 mm²(0.0001)|0.092 mm²(0.00015)|
|`incidence_limit_deg`|62.0;对应旧相机位姿,移动后必须重测|70.0|
|`optics` 配置|无|有|
不能为了让两个 revision 的 YAML 看起来一致,就统一这些值。分辨率、页面、网格比例、相机
位姿和入射角门槛都是各自设计与测量条件的一部分。
## 9. 新同事修改参数时的建议步骤
1. 先确认正在修改哪个 sensor 和 revision,不要把 `finger` 的参数复制到
`finger-xynova` 后直接使用。
2. 判断改动属于“图案”“UV 布局”“相机位姿”“印刷工艺”还是“几何”。
3. 根据第 5 节查找被改动失效的测量值。
4. 如果移动区域相机,重测 ownership、hole fraction 和自动页面。
5. 如果修改网格,核对拓扑、UV、seam、probe fixture 和 field-domain 相关输出。
6. 如果修改栅格或物理页面,重新计算两个方向的 `px_per_mm`,不能只检查宽度。
7. 运行对应测试并查看生成任务中的 manifest,不要只凭最终图片看起来正常。
8. 将新数值、测量条件、结果和下次失效条件一起更新到本文。
遇到结果异常时,可以按以下顺序排查:
```text
输出为空
→ 是否有重合/遮挡对象
→ region camera 是否能看到表面
→ UV map 名称和对象是否正确
表面有漏印
→ 查看 hole fraction
→ 检查 incidence_limit_deg
→ 检查相机位姿和 footprint 条件
页面比例不对
→ 确认 UV 是 unwrap 还是 Project from View (Bounds)
→ unwrap 使用 fixed;投影视图才使用 auto
→ 检查 raster / px_per_mm / page 的关系
接缝呈阶梯形
→ 先区分 ownership 边界与 gap
→ gap 使用 post_process 只能让退让边变直
→ 平滑 ownership 需要更细的分区算法
```
最后,不要删除“已知缺陷”“撤回结论”和失效条件。这些内容不是历史包袱,而是防止同类问题
再次以合理外观进入生产输出的工程约束。