Metadata-Version: 2.2
Name: roadmark-maker
Version: 0.2.8
Summary: Make road mark from road shapefile.
Requires-Python: >=3.11
Requires-Dist: numpy
Requires-Dist: geopandas
Requires-Dist: shapely
Requires-Dist: mapbox-earcut
Requires-Dist: matplotlib
Requires-Dist: pyproj
Description-Content-Type: text/markdown

# roadmark-maker

从 3D 道路 Shapefile 生成沿路排布的立体路名标牌（Wavefront OBJ）或实例化贴图用的 Billboard 位姿（CSV）。核心为 Python（GeoPandas / Shapely / Matplotlib）与 C++（OpenMP、可选 AVX2）混合的 **Maplex 风格** 避让与网格管线。

## 构建

```powershell
chcp 65001 && uv build --wheel --out-dir dist
```

依赖与构建说明见 `pyproject.toml`；C++ 求解器声明在 `src/roadmark_maker/roadmark_core.hpp`，Cython 绑定在 `src/roadmark_maker/roadmark_accel.pyx`。

---

## 对外接口与能力

| 接口 | 作用 | 典型下游 |
|------|------|----------|
| `generate(...)` | 完整管线：读 SHP → 采样与排版 → C++ OBB 碰撞解算 → **逐字或整段** 挤出网格 → 写 OBJ | Cesium、OSG、Blender 等需三角网格的场景 |
| `generate_csv(...)` | 与 `generate` **共用** `_prepare_data`（同样的采样、CRS、`density` / `repeat_mode`、碰撞），但 **不建网格**；在引擎内固定 `curved_text=False`，将每条标注的 **整体** 位姿（平移 + 旋转）写出为 CSV | UE、Cesium 等用 **实例化 Quad + 贴图** 画路名 |

二者均在 `src/roadmark_maker/__init__.py` 中导出；优先加载 `roadmark_accel_avx2`，不可用时回退 `roadmark_accel`。

---

## 核心逻辑（文字从哪来、模型 / CSV 怎么来）

1. **文字内容**：来自 Shapefile 中由 `name_field` 指定的字段（默认 `name`），经 `strip` 后跳过空、`none`、`nan`。
2. **字体与字形**：`_FontManager.get_chinese_font_path()` 依次尝试常见中文字体；失败则下载 Noto Sans SC 到临时目录。`_TextMeshEngine` 用 `matplotlib.textpath.TextPath` 将字符转为多边形，**单字**用 `symmetric_difference` 处理字内孔洞，**整段**用 `union` 合并字间形状；三角化使用 `mapbox_earcut`，再挤出顶/底与侧墙。
3. **三维模型（OBJ）**：对每个未 `hidden` 的标注，`build_tbn_matrix` 在弧长 `distance` 处建立局部 **TBN（切线 / 副法线 / 法线）** 齐次矩阵；`curved_text=True` 时按字符沿线的弧长位置逐字建矩阵并变换顶点；`curved_text=False` 时整段网格用 **一个** 采样点的矩阵整体放置。`separate_objects` 控制是否每个标注单独 `o object_name`。
4. **CSV**：`generate_csv` 在 `_prepare_data` 之后遍历存活标注，取矩阵的平移 `(x,y,z)`，将 `3×3` 旋转经 C++ `MathUtils.matrix_to_rotations` 转为四元数与/或 ZYX 欧拉角，可按 `target_crs` 再投影；列为 `x,y,z` + 角度 + `name`。**不**包含逐字几何，适合整条路名一张纹理的实例化。

---

## 参数如何控制「分布」与外观

| 参数 | 控制点 |
|------|--------|
| `text_count` | 若为正整数，在每条线段上 **等分弧长** 生成该数量的候选（忽略 `distance_interval`）。 |
| `distance_interval` | 未设 `text_count` 时，沿弧长每隔约该距离（与当前 CRS 单位一致；UTM 下为米）放一个候选。 |
| `add_at_endpoints` | 为 true 时在弧长 0 与 `line_len` 处额外各加一个候选。 |
| `density` | 仅当 **`curved_text=True`** 时参与 **`auto_place_labels`**：档位决定 **全局最小间距**、**同组重复最小间距**、**每组最多保留条数**、**全局贪心外层迭代次数**（见下表）；可用 `max_global_iterations` 覆盖迭代次数。 |
| `repeat_mode` / `connect_eps` | `name_connected`（默认）下按 **路名 + 线段端点距离 ≤ eps** 并查集合并连通分量，同组共享 `max_labels_per_group` 配额；`feature` / `name` 为其它分组策略。 |
| `font_size` / `letter_spacing` | 世界空间大致字高与字距；碰撞体宽度用 `effective_text_len` 与 `effective_char_width_est`（自动压缩时变化）。 |
| `curved_text` | **True**：自动适配线长、全局选点、逐字贴合折线；**False**：整段文本一块网格、不做 `auto_place_labels` 筛选。 |
| `auto_fit_text` / `min_font_scale` / `min_letter_spacing` / `overflow_strategy` | 在 **`curved_text=True`** 时，`apply_autofit` 按线长比例缩小字号与字距，仍过长则 **`truncate`** 策略下二分保留长度并加 `…`。 |
| `posture` | `flat`：法线朝上贴地；`billboard`：以世界 Z 为参考的立式朝向。 |
| `use_utm` | **True**：以数据质心经度推算 **UTM** 带并 `to_crs`，XY/Z 比例更接近真实米制；**False**（默认）：工作 CRS 为 **EPSG:3857**。 |
| `weight_field` | 碰撞时 **权重大者不滑动**；权重相同则 **文本更长者** 作为 yielder 被推动。 |
| `separate_objects` | 仅影响 OBJ 是否分 `o` 组。 |

---

## 全局迭代优化（`auto_place_labels`）与 C++ 碰撞

**全局阶段**（仅 `curved_text=True`）：

- 对每个要素在「线长能放下估计跨度」的候选里，选弧长最接近 **线段中点** 的一个作为 **锚点**。
- 外层循环最多 `iterations` 次（由 `density` 决定，或由 `max_global_iterations` 覆盖）；每轮对当前已选集合调用 **`_fast_resolve_collisions(..., 3, ...)`**，去掉被标为 `hidden` 的。
- 在剩余候选中贪心加入下一个：需满足 **全局与其它已选中心的平面距离** ≥ `global_min_sep`、**同 `group_id` 内** ≥ `repeat_min_interval`、且组内个数小于 `max_labels_per_group`；得分优先 **离已选集合最远**。
- 未被选中的候选最终 **`hidden=True`**，不再导出。

**`density` 默认档位（`font_size` 取中位数有效字号参与比例，代码见 `_density_profile`）**

| 档位 | `global_min_sep` | `repeat_min_interval` | `max_labels_per_group` | 外层迭代 `iterations` |
|------|------------------|------------------------|-------------------------|------------------------|
| `very_dense` | 6×fs | 10×fs | 5 | 10 |
| `dense` | 8×fs | 14×fs | 4 | 9 |
| `moderate` | 10×fs | 18×fs | 3 | 8 |
| `sparse` | 14×fs | 26×fs | 2 | 7 |
| `very_sparse` | 18×fs | 34×fs | 1 | 6 |

**C++ 碰撞**（`MaplexSolver::resolve_collisions`，Python 侧 **`max_iterations` 固定为 3**，非用户参数）：

- 根据当前 OBB 最大宽度建 **均匀网格**，格内两两做 **分离轴 SAT**（四个轴：两边 OBB 的方向与法向）。
- 冲突时对 **yielder** 做 **同线级联滑动**：步长约 `2 * char_width_est`，沿切向推进并可能推动后方同线标注；超出 `line_len` 则 `hidden`。
- 固定轮次结束后仍有重叠则 **一方直接 hidden**（权重与字长规则同上）。

**推荐**：城市密集路网可先用 `moderate` 或 `dense`；希望同一路名少重复、留白大可 `sparse` / `very_sparse`。若仍过密，可增大 `distance_interval` 或设置较小的 `text_count`，或略减小 `font_size`。

**注意**：`generate_csv` 将 `curved_text` 置为 **False**，因此 **不会** 执行上述全局 `auto_place_labels` 筛选；CSV 与 OBJ（`curved_text=True`）在「候选数量」上可能不一致。

---

## 处理流程图（与实现一致）

```mermaid
flowchart TB
    subgraph API["入口"]
        G["generate() → OBJ"]
        C["generate_csv() → CSV<br/>内部固定 curved_text=False"]
    end

    subgraph S1["1. 数据与坐标系"]
        A[读取 Shapefile<br/>GeoPandas] --> B{name_field 存在?}
        B -->|否| W[警告并返回空结果]
        B -->|是| CRS{use_utm}
        CRS -->|true| U[质心经纬度 → 推算 UTM EPSG]
        CRS -->|false 默认| M[EPSG:3857]
        U --> T[to_crs 工作 CRS]
        M --> T
    end

    subgraph S2["2. 沿几何采样候选 Label"]
        T --> L[拆分 LineString / MultiLineString]
        L --> S[text_count 或 distance_interval<br/>+ 可选端点 add_at_endpoints]
        S --> K[每条记录: text / weight≥1 / line_len / distance 等]
    end

    subgraph S3["3. 曲线模式预处理"]
        K --> CT{curved_text?}
        CT -->|False| SK[跳过 autofit / auto_place]
        CT -->|True| AF{auto_fit_text?}
        AF -->|是| AF2[apply_autofit:<br/>按线长缩放 + truncate …]
        AF -->|否| AP
        AF2 --> AP[auto_place_labels:<br/>锚点 + 全局贪心 + 组约束<br/>每轮子集 _fast_resolve ×3]
        AP --> SK
    end

    subgraph S4["4. 局部坐标架与 C++ 碰撞"]
        SK --> MX[为各候选 build_tbn_matrix<br/>差分弧长得 T; auto_flip 可读性]
        MX --> COL["_fast_resolve_collisions<br/>C++ 网格 + SAT-OBB<br/>固定 max_iterations=3"]
        COL --> MD[distance 变化则 matrix_dirty]
    end

    subgraph S5["5a. OBJ 导出 generate"]
        MD --> OBJ{导出分支}
        OBJ -->|curved_text=True| CV[逐字 TextPath + earcut 挤出<br/>沿线分布弧长]
        OBJ -->|curved_text=False| FL[整段 generate_mesh 一次<br/>单矩阵变换]
        CV --> WOBJ[写 v/f; separate_objects 控制 o 分组]
        FL --> WOBJ
    end

    subgraph S5b["5b. CSV 导出 generate_csv"]
        MD --> CSVM[矩阵平移 + matrix_to_rotations<br/>可选 target_crs 变换]
        CSVM --> WCSV[UTF-8-SIG CSV<br/>x,y,z + 四元数/欧拉 + name]
    end

    G --> A
    C --> A
```

一次运行只调用 `generate` **或** `generate_csv` 之一；上图中 5a / 5b 为两种出口，不会在同一次执行里同时发生。

---

## 逻辑关系简图（阶段依赖）

```mermaid
graph LR
    IO[SHP + CRS] --> SP[弧长采样]
    SP --> GF{curved_text?}
    GF -->|是| AP[autofit + 全局选点]
    GF -->|否| MX[建 TBN]
    AP --> MX
    MX --> CPP[C++ 碰撞 3 轮]
    CPP --> OUT{输出}
    OUT -->|generate| MESH[网格 + OBJ]
    OUT -->|generate_csv| TBL[位姿 + CSV]
```

以上流程与 `src/roadmark_maker/roadmark_accel.pyx` 中 `_RoadMaplexEngine._prepare_data`、`export_obj`、`export_csv` 的实现顺序一致，便于对照阅读源码。
