# Chijin

Chijinとは、OpenCASCADEをRustから利用するためのバインディングライブラリです。以下がChijinの仕様書です。

---

作成: 2026-03-02
目的: このプロジェクトで実際に使用している OCC API を網羅し、バグのない最小限の Rust バインディングを再構築するための仕様書とする。

## 1. 使用機能の全体像

### 1.1 型一覧

| Rust 型 | OCC C++ 型 | 役割 | `Send` |
|---|---|---|---|
| `Shape` | `TopoDS_Shape` | すべての形状の基底型。内部は `UniquePtr<TopoDS_Shape>` | ✅ |
| `Face` | `TopoDS_Face` | 面トポロジ | ✅ |
| `Edge` | `TopoDS_Edge` | 辺トポロジ | ✅ |
| `Solid` | `TopoDS_Solid` | 立体トポロジ（`face.extrude()` の戻り値） | ✅ |
| `BooleanShape（廃止予定）` | — | `{ shape: Shape, new_edges: Vec<Edge> }` のラッパー | — |
| `Mesh` | — | `{ vertices: Vec<DVec3>, uvs, normals, indices: Vec<usize> }` の純 Rust 構造体 | ✅ (自動) |
| `FaceIterator` | `TopExp_Explorer` | Face を順に返すイテレータ | ✅ |
| `EdgeIterator` | `TopExp_Explorer` | Edge を順に返すイテレータ | ✅ |
| `ApproximationSegmentIterator` | `GCPnts_TangentialDeflection` | Edge の折れ線近似点を順に返すイテレータ | ✅ |

### 1.2 スレッド安全性（`Send` / `Sync`）

`Shape`・`Face`・`Edge`・`Solid` はすべて `Send` を実装しており、Tokio の `spawn_blocking` や axum の async ハンドラなどスレッドをまたぐコードで問題なく使用できる。

#### 実装方針

`unsafe impl Send` は各ラッパー構造体ではなく、cxx の opaque 型 (`TopoDS_Shape` 等) に対して `src/ffi.rs` で一括して宣言している。
`UniquePtr<T>: Send` は `T: Send` のときのみ成立するため、この宣言により `UniquePtr<TopoDS_Shape>` が `Send` となり、それを内部に持つ `Shape` が自動的に `Send` になる。

```rust
// src/ffi.rs — cxx opaque 型への Send 宣言
unsafe impl Send for TopoDS_Shape {}
unsafe impl Send for TopoDS_Face {}
unsafe impl Send for TopoDS_Edge {}
unsafe impl Send for TopExp_Explorer {}
```

#### 安全性の根拠

- `UniquePtr` は排他所有権を持つため、同じオブジェクトを複数スレッドが同時に参照することはない
- `Send` はスレッド間の**移動**のみを許可する（読み書きの共有は許可しない）

---

## 2. API 詳細一覧（使用箇所付き）

### 2.1 Shape コンストラクタ

#### `Shape::read_step(reader: impl Read) -> Result<Shape, Error>`
- **使用箇所**: `step_to_brep.rs`
- **OCC 実装**:
  1. Rust の `dyn Read` を `RustReader` でラップし C++ に渡す
  2. C++ 側で `RustReadStreambuf`（`std::streambuf` サブクラス）が `rust_reader_read()` コールバック経由でチャンク読み出し
  3. `RustReadStreambuf` → `std::istream` → `STEPControl_Reader::ReadStream("stream", istream)`
  4. Reader は C++ 側でリーク（Bug 2 回避）。Rust に `mem::forget` は露出しない
- **エラー**: `IFSelect_RetDone` 以外なら `Error::StepReadFailed`

#### `Shape::read_brep_text(reader: impl Read) -> Result<Shape, Error>`
- **使用箇所**: `shape.rs`（テスト含む）
- **OCC 実装**: `RustReadStreambuf` → `std::istream` → `BRepTools::Read(shape, istream, builder)`
- **エラー**: `shape.IsNull()` なら `Error::BrepReadFailed`

#### `Shape::read_brep_bin(reader: impl Read) -> Result<Shape, Error>`
- **使用箇所**: `shape.rs`（テスト含む）
- **OCC 実装**: `RustReadStreambuf` → `std::istream` → `BinTools::Read(shape, istream)`
- **エラー**: `shape.IsNull()` なら `Error::BrepReadFailed`

#### `Shape::write_brep_bin(&self, writer: impl Write) -> Result<(), Error>`
- **使用箇所**: `step_to_brep.rs`
- **OCC 実装**: `BinTools::Write(shape, ostream)` → `RustWriteStreambuf` 経由で `dyn Write` に書き出し
- **エラー**: `ostream.good()` が `false` なら `Error::BrepWriteFailed`

#### `Shape::half_space(plane_origin: DVec3, plane_normal: DVec3) -> Shape`
- **使用箇所**: `shape_stretch.rs`
- **OCC 実装**:
  1. `gp_Pln_ctor(&origin_point, &normal_dir)` で無限平面を構築
  2. `BRepBuilderAPI_MakeFace_plane(&plane)` で平面 Face を作成
  3. `ref_point = plane_origin + plane_normal.normalize()` (法線と**同方向**の参照点)
  4. `BRepPrimAPI_MakeHalfSpace_ctor(face, &ref_point)` で半空間ソリッドを構築
- **意味**: 参照点が法線と同じ側にあるため、ソリッドは **法線が指す方向の半空間**を埋める
- **注意**: `shape.intersect(&half_space)` すると `plane_normal` 側が残る

#### `Shape::box_from_corners(corner_1: DVec3, corner_2: DVec3) -> Shape`
- **使用箇所**: `shape_stretch.rs`（テスト）
- **OCC 実装**: `min/max` でコーナーを正規化 → `BRepPrimAPI_MakeBox_ctor(&min_point, dx, dy, dz)`

#### `Shape::cylinder(p: DVec3, r: f64, dir: DVec3, h: f64) -> Shape`
- **使用箇所**: `shape_stretch.rs`（テスト）
- **OCC 実装**: `make_axis_2(p, dir)` → `BRepPrimAPI_MakeCylinder_ctor(&axis, r, h)`

#### `Shape::empty() -> Shape`
- **使用箇所**: `shape_stretch.rs`
- **OCC 実装**: `TopoDS_Compound_ctor()` → `BRep_Builder::MakeCompound()` で空のコンパウンドを作成
- **重要**: `TopoDS_Shape()` (真のヌル形状) を使うとブール演算が失敗するため、空コンパウンドを使用する

#### `Shape::deep_copy(&self) -> Shape`
- **使用箇所**: `shape_stretch.rs`（ブール演算後に毎回呼ぶ）
- **OCC 実装**: `BRepBuilderAPI_Copy_ctor(&inner)` → `.Shape()` で完全独立コピーを取得
- **目的**: ヒープ破壊バグの回避（後述）

---

### 2.2 Shape メソッド

#### `shape.union(&other) -> BooleanShape`
- **使用箇所**: `shape_stretch.rs`
- **OCC 実装**: `BRepAlgoAPI_Fuse_ctor(&self.inner, &other.inner)` → `.Shape()`, `.SectionEdges()`
- **戻り値**: `BooleanShape { shape: Shape, new_edges: Vec<Edge> }`
- **使い方**: `.union(&other).shape` で `new_edges` は捨てる

#### `shape.subtract(&other) -> BooleanShape`
- **使用箇所**: `shape_stretch.rs`
- **OCC 実装**: `BRepAlgoAPI_Cut_ctor(&self.inner, &other.inner)` → `.Shape()`, `.SectionEdges()`

#### `shape.intersect(&other) -> BooleanShape`
- **使用箇所**: `shape_stretch.rs`
- **OCC 実装**: `BRepAlgoAPI_Common_ctor(&self.inner, &other.inner)` → `.Shape()`, `.SectionEdges()`

#### `shape.clean() -> Shape`
- **使用箇所**: `shape_stretch.rs`（ストレッチ後に一度だけ呼ぶ）
- **OCC 実装**:
  1. `ShapeUpgrade_UnifySameDomain_ctor(&inner, true, true, true)` (faces, edges, vertices を統合)
  2. `.AllowInternalEdges(false)`
  3. `.Build()`
  4. `.Shape()` → `TopoDS_Shape`
- **意味**: ブール演算後に生じた冗長なエッジ・面を統合し、トポロジをクリーンにする

#### `shape.set_global_translation(&mut self, translation: DVec3)`
- **使用箇所**: `shape_stretch.rs`
- **OCC 実装**:
  1. `new_transform()` → `gp_Trsf`
  2. `transform.set_translation_vec(&make_vec(translation))`
  3. `TopLoc_Location_from_transform(&transform)` → `TopLoc_Location`
  4. `self.inner.set_global_translation(&location, false)` ← `false` = 再帰的に伝播しない
- **注意**: `false` (propagate=false) なのでコンパウンド内のサブシェイプには適用されない

#### `shape.mesh_with_tolerance(tol: f64) -> Result<Mesh, Error>`
- **使用箇所**: `shape.rs`, `shape_stretch.rs`（テスト）
- **OCC 実装** (`Mesher`):
  1. `BRepMesh_IncrementalMesh_ctor(&shape.inner, tol)` でメッシュを計算
  2. `IsDone()` が false なら `Error::TriangulationFailed`
  3. 全 Face を `TopExp_Explorer` で巡回
  4. 各 Face: `BRep_Tool_Triangulation(&face, location)` で三角形データを取得
  5. 頂点: `Poly_Triangulation_Node(i)` → `.Transform(location)` → `DVec3`
  6. UV: `Poly_Triangulation_UV(i)` → 各 Face で正規化 ([0,1])
  7. 法線: `compute_normals(&face, &triangulation_handle)` → `Poly_Triangulation_Normal(i)`
  8. インデックス: `triangulation.Triangle(i)` → `.Value(1,2,3)` → Face の向きで順序反転
- **戻り値**: `Mesh { vertices: Vec<DVec3>, uvs: Vec<DVec2>, normals: Vec<DVec3>, indices: Vec<usize> }`

#### `shape.edges() -> EdgeIterator`
- **使用箇所**: `shape.rs`, `shape_to_glb.rs`
- **OCC 実装**: `TopExp_Explorer_ctor(&inner, TopAbs_EDGE)`

#### `shape.faces() -> FaceIterator`
- **使用箇所**: `shape_stretch.rs`
- **OCC 実装**: `TopExp_Explorer_ctor(&inner, TopAbs_FACE)`

---

### 2.3 Face メソッド

#### `face.normal_at_center() -> DVec3`
- **使用箇所**: `shape_stretch.rs`
- **OCC 実装** (2ステップ):
  1. **`center_of_mass()`**: `GProp_GProps_ctor()` → `BRepGProp_SurfaceProperties(face_shape, props)` → `props.CentreOfMass()` → `DVec3`
  2. **`normal_at(center)`**: `BRep_Tool_Surface(&face)` → `GeomAPI_ProjectPointOnSurf_ctor(&center_point, &surface)` → `.LowerDistanceParameters(&mut u, &mut v)` → `BRepGProp_Face_ctor(&face)` → `.Normal(u, v, &mut p, &mut normal)` → `DVec3`
- **意味**: 面の重心（サーフェス積分による面積重み付き中心）を求め、そこでの法線ベクトルを返す

#### `face.center_of_mass() -> DVec3`
- **使用箇所**: `shape_stretch.rs`（`normal_at_center()` の内部でも使用）
- **OCC 実装**: `GProp_GProps_ctor()` → `BRepGProp_SurfaceProperties(face_shape, props)` → `props.CentreOfMass()`

#### `face.extrude(dir: DVec3) -> Solid`
- **使用箇所**: `shape_stretch.rs`（`Shape::from(face.extrude(dir))` として使用）
- **OCC 実装**: `BRepPrimAPI_MakePrism_ctor(face_shape, &prism_vec, copy=false, canonize=true)` → `.Shape()` → `TopoDS_cast_to_solid`
- **戻り値**: `Solid`（`Shape::from(Solid)` で `Shape` に変換して使う）

---

### 2.4 Edge メソッド

#### `edge.approximation_segments() -> ApproximationSegmentIterator`
- **使用箇所**: `shape_to_glb.rs`
- **OCC 実装**: `BRepAdaptor_Curve_ctor(&edge)` → `GCPnts_TangentialDeflection_ctor(&curve, angular=0.1, chord=0.1)`
- **戻り値**: 点列のイテレータ。`NbPoints()` の数だけ `GCPnts_TangentialDeflection_Value(i)` で `DVec3` を取得
- **注意**: angular deflection = 0.1 rad、chord deflection = 0.1 mm が**ハードコード**されている

---

## 3. 現バインディングの既知バグと合格基準

各バグについて「症状・原因・現在の回避策」と、再構築後のバインディングが通過しなければならない **合格基準（テスト条件）** を定義する。実装手段は問わない。

---

### Bug 1: STATUS_HEAP_CORRUPTION（最重要）

**症状**: `shape_stretch.rs` でブール演算 (union/subtract/intersect) の結果を `drop` するとヒープ破壊。テストランナーごとクラッシュする。

**原因**: OCC のブール演算結果 `TopoDS_Shape` は入力と同じ `Handle<Geom_XXX>` を共有する。`TopoDS_Shape_to_owned()` のコピーもこれを引き継ぐため、Rust 側の `UniquePtr` の `drop` 順次第で Handle の参照カウントが壊れ、ヒープ破壊が起きる。

**現在の回避策**: ブール演算直後に呼び出し側が手動で `shape.deep_copy()` を呼ぶ（`BRepBuilderAPI_Copy` による完全複製）。

**合格基準**:

```rust
// ① 結果を入力より先に drop しても落ちない
let result = a.union(&b);
drop(result);
drop(a);
drop(b);

// ② 入力より後に drop しても落ちない
let result = a.union(&b);
drop(a);
drop(b);
drop(result);

// ③ subtract / intersect でも同様
let result = a.subtract(&b);
drop(a);
drop(b);
drop(result);

// ④ チェーンしても落ちない
let r1 = a.union(&b);
let r2 = r1.subtract(&c);
drop(r1);
drop(r2);
drop(a); drop(b); drop(c);
```

これらすべてが STATUS_HEAP_CORRUPTION を起こさずに完走すること。なお `copyGeom=false` でトポロジのみコピーしても合格するなら、幾何データのコピーは不要と判断できる（コスト削減の余地）。

---

### Bug 2: STATUS_ACCESS_VIOLATION（プロセス終了時）

**症状**: `read_step` を使ったプロセスが終了時に STATUS_ACCESS_VIOLATION でクラッシュする。

**原因**: `STEPControl_Reader` のデストラクタが OCCT グローバル状態を変更し、終了フェーズの未定義なデストラクタ順序と衝突する。

**現在の回避策**: `std::mem::forget(reader)` でリーダーをリークしてデストラクタを抑制する。

**合格基準**:

```rust
// read_step を複数回呼んだ後もプロセスが正常終了する
// （通常の #[test] はプロセス終了を観測できないため、
//   この関数が完走するテストバイナリを cargo test で実行し
//   exit code 0 で終了することを確認する）
for _ in 0..5 {
    let _shape = Shape::read_step(&STEP_BYTES[..]).unwrap();
}
// ここに到達すること、かつテストプロセスの exit code が 0 であること
```

---

### Bug 3: メッシュ法線の off-by-one

**症状**: `mesh.normals` の要素数が `mesh.vertices` より 1 少ない（末尾が欠落）。

**原因**: 法線ループで `normal_array.Length()`（キャパシティ = 頂点数 + 1）を上限に使っており、最後の要素が取得されない。

**現在の状況**: `normals` フィールドが GLB 出力で未使用のため実害なし。

**合格基準**:

```rust
let mesh = shape.mesh_with_tolerance(0.1).unwrap();
assert_eq!(mesh.normals.len(), mesh.vertices.len());
```

---

### Bug 4: `approximation_segments` の tolerance ハードコード

**症状**: Edge の折れ線近似の angular deflection（0.1 rad）と chord deflection（0.1 mm）が固定で変更できない。

**合格基準**:

```rust
// tolerance が小さいほど点列が密になること
let arc_edge = /* 円弧エッジ */;
let coarse = arc_edge.approximation_segments(1.0).count();
let fine   = arc_edge.approximation_segments(0.01).count();
assert!(fine > coarse);

// tolerance が大きいほど点列が粗くなること（上の逆）
assert!(coarse < fine);
```

---

### Bug 5: 平行移動がコンパウンド形状に正しく反映されない

**症状**: `union` 等で生成したコンパウンド形状への平行移動が、サブシェイプに伝播しないことがある。

**原因**: `set_global_translation(propagate=false)` はルートシェイプの Location のみ更新し、内部シェイプの Location は変更しない。現状は `deep_copy()` 後の単純化された形状に使っているため実害は出ていない。

**合格基準**:

```rust
// union 後のコンパウンド形状に平行移動を適用しても
// すべての頂点が正確に v だけシフトすること
let compound = a.union(&b);
let v = dvec3(100.0, 0.0, 0.0);
let shifted = compound.translated(v);

let orig_mesh    = compound.mesh_with_tolerance(0.1).unwrap();
let shifted_mesh = shifted.mesh_with_tolerance(0.1).unwrap();

assert_eq!(orig_mesh.vertices.len(), shifted_mesh.vertices.len());
for (o, s) in orig_mesh.vertices.iter().zip(shifted_mesh.vertices.iter()) {
    assert!((s.x - o.x - v.x).abs() < 1e-6);
    assert!((s.y - o.y - v.y).abs() < 1e-6);
    assert!((s.z - o.z - v.z).abs() < 1e-6);
}
```

---

## 4. 最小バインディング仕様

再構築するバインディングに必要な OCC クラスと関数のみを抽出する。

### 4.1 必要な OCC クラス

| OCC クラス | 用途 |
|---|---|
| `TopoDS_Shape` | 全形状の基底型 |
| `TopoDS_Face` | Face トポロジ |
| `TopoDS_Edge` | Edge トポロジ |
| `TopoDS_Solid` | Solid トポロジ（extrude の戻り値） |
| `TopoDS_Compound` | 空形状の実装用 |
| `TopExp_Explorer` | Face/Edge イテレータ |
| `BRepMesh_IncrementalMesh` | メッシュ化 |
| `Poly_Triangulation` | メッシュデータアクセス |
| `BRep_Tool` | Face の Surface 取得など |
| `BRepAdaptor_Curve` | Edge の曲線アダプタ |
| `GCPnts_TangentialDeflection` | Edge の折れ線近似 |
| `GProp_GProps` + `BRepGProp` | Face の重心・面積計算 |
| `BRepGProp_Face` | Face の法線計算 |
| `GeomAPI_ProjectPointOnSurf` | 点のサーフェスへの投影 |
| `BRepAlgoAPI_Fuse` | union |
| `BRepAlgoAPI_Cut` | subtract |
| `BRepAlgoAPI_Common` | intersect |
| `BRepPrimAPI_MakeBox` | box プリミティブ |
| `BRepPrimAPI_MakeCylinder` | cylinder プリミティブ |
| `BRepPrimAPI_MakeHalfSpace` | 半空間プリミティブ |
| `BRepPrimAPI_MakePrism` | extrude |
| `BRepBuilderAPI_MakeFace` | 平面→Face（半空間で使用） |
| `ShapeUpgrade_UnifySameDomain` | clean |
| `STEPControl_Reader` | STEP 読み込み |
| `BRep_Builder` | 空形状の実装用 |
| `gp_Pln` | 半空間の平面定義 |

### 4.2 不要な OCC クラス（削除可能）

現プロジェクトでは使用していないが `opencascade-rs` が持つもの:
- `IGESControl_Reader/Writer`（IGES）
- `BRepFilletAPI_MakeFillet/MakeChamfer`（フィレット・チャンファー）
- `BRepOffsetAPI_MakeOffset`（オフセット）
- `BRepOffsetAPI_MakePipe`（スイープ）
- `BRepFeat_MakeCylindricalHole`（穴あけ）
- `BRepFeat_MakeDPrism`（押し出し特徴）
- `BRepIntCurveSurface_Inter`（ライン-サーフェス交差）
- `BRepOffsetAPI_MakeThickSolid`（中空化）
- `GeomAPI_Interpolate`（スプライン補間）
- `Workplane` 系（2D ワークプレーン）

### 4.3 合格基準一覧

実装手段を問わず、再構築したバインディングがすべて通過しなければならない条件。

| # | 合格基準 | 関連バグ |
|---|---|---|
| T-01 | ブール演算結果を任意の順序で drop しても STATUS_HEAP_CORRUPTION が起きない | Bug 1 |
| T-02 | `read_step` を同一プロセスで複数回呼んだ後、プロセスが exit code 0 で終了する | Bug 2 |
| T-03 | `mesh.normals.len() == mesh.vertices.len()` | Bug 3 |
| T-04 | `approximation_segments(fine) > approximation_segments(coarse)` (点列密度が tolerance に追従) | Bug 4 |
| T-05 | `union` 後のコンパウンドへの平行移動が全頂点に正確に反映される | Bug 5 |
| T-06 | BRep バイナリの write → read がラウンドトリップする（頂点数・座標が一致） | I/O |
| T-07 | STEP の read/write 中に一時ファイルが生成されない | I/O |
| T-08 | `union/subtract/intersect` の戻り値型が `Shape` であり `BooleanShape` 等の中間型を要求しない | API 設計 |

## 5. I/O のファイルシステム非依存化

### 方針

BRep（バイナリ・テキスト）および STEP の読み書きは、ファイルパスではなく Rust の `std::io::Read` / `std::io::Write` トレイトを受け取る API にする。これにより呼び出し側は `&[u8]`、`Cursor<Vec<u8>>`、`File`、ネットワークストリームなど任意のソース/シンクを使えるようになり、一時ファイルへの依存を完全に除去できる。

### streambuf コールバック方式

`cxx` は `std::istream`/`std::ostream` を直接渡せない。そこで、Rust の `dyn Read`/`dyn Write` を C++ 側の `std::streambuf` サブクラスにブリッジする。データは中間バッファにコピーされず、チャンク単位（8KB）で Rust ↔ C++ 間をストリーミングする。

```
Read:  Rust dyn Read ←[callback]← RustReadStreambuf : std::streambuf → std::istream → OCC API
Write: Rust dyn Write →[callback]→ RustWriteStreambuf : std::streambuf ← std::ostream ← OCC API
```

#### FFI 構造

| 層 | 型 | 役割 |
|---|---|---|
| Rust | `RustReader(Box<dyn Read>)` | opaque extern Rust type。C++ から `rust_reader_read()` で呼び出される |
| Rust | `RustWriter(Box<dyn Write>)` | 同上。`rust_writer_write()` / `rust_writer_flush()` |
| C++ | `RustReadStreambuf : std::streambuf` | `underflow()` で `rust_reader_read()` を呼び、8KB バッファに読み込む |
| C++ | `RustWriteStreambuf : std::streambuf` | `overflow()`/`xsputn()`/`sync()` で `rust_writer_write()` に書き出す |

### OCC で使用するストリーム API

| 操作 | OCC API |
|---|---|
| BRep バイナリ 書き込み | `BinTools::Write(shape, std::ostream&)` |
| BRep バイナリ 読み込み | `BinTools::Read(shape, std::istream&)` |
| BRep テキスト 書き込み | `BRepTools::Write(shape, std::ostream&)` |
| BRep テキスト 読み込み | `BRepTools::Read(shape, std::istream&, builder)` |
| STEP 読み込み | `STEPControl_Reader::ReadStream(name, std::istream&)` |
| STEP 書き込み | `STEPControl_Writer::WriteStream(std::ostream&)` |

### 注意点

- `BinTools::Read` / `BRepTools::Read` のストリーム版は戻り値が `void`。成否は読み込み後の `shape.IsNull()` で判定する。
- `STEPControl_Reader` のリーク（Bug 2 参照）は C++ ラッパー層に封じ込める。
- ピークメモリは streambuf バッファサイズ（~8KB）のみ。データ全体のコピーは発生しない。
