cadrum 0.7.6

Rust CAD library powered by statically linked, headless OpenCASCADE (OCCT 8.0.0-beta1)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
//! Backend-independent trait definitions.
//!
//! Trait hierarchy:
//!
//! ```text
//! Transform  ─┬─  Compound   ──  SolidStruct  (pub(crate))
//!             └─  Wire    ──  EdgeStruct   (pub(crate))
//! ```
//!
//! `Face` 型は `FaceStruct` トレイトに `id` / `project` / `iter_edge` を持つ。
//! `SolidStruct::type Face: FaceStruct` で結合される。
//!
//! `Edge` / `Vec<Edge>` の対称関係は `Solid` / `Vec<Solid>` と同じ:
//!   - 単一エッジ向け constructor は `EdgeStruct` (cube/sphere に対応)
//!   - エッジ列 (= Wire) を含む共通操作は `Wire` (volume/clean に対応)
//!   - `Vec<Edge>` がそのまま Wire — 専用型は無い (`Vec<Solid>` = Compound と同様)
//!
//! - `Transform` (crate-internal): spatial ops (translate/rotate/scale/mirror).
//!   Geometry-agnostic. Implemented for shapes (`Solid`, `Edge`) and collections
//!   (`Vec<T>`, `[T; N]`). Not re-exported from `lib.rs`, so external users cannot
//!   name it — they reach the same methods via `Compound` / `Wire` forwarders.
//!
//! - `Compound: Transform` (pub): solid-specific operations on Solid, Vec<T>, and [T; N]
//!   (clean/volume/contains/color/boolean wrappers). Also exposes 1-line forwarders
//!   for every Transform method so `use cadrum::Compound;` alone enables
//!   `vec.translate(...)` etc. on `Vec<Solid>` / `[Solid; N]`.
//!
//! - `SolidStruct: Sized + Clone + Compound` (pub(crate)): backend implementation trait.
//!   Adds Solid-only operations (constructors, topology accessors, boolean primitives).
//!   examples/codegen.rs parses this and generates pub inherent methods on Solid,
//!   walking the supertrait chain so all `Compound` and `Transform` methods are also
//!   exposed inherently. Trait name follows `<Type>Struct` convention (SolidStruct → Solid).
//!
//! ## 関連型による型ヒエラルキー(バックエンド非依存ルール)
//!
//! このファイルはバックエンド(occt / pure)の具象型を一切名指ししない。具象型への
//! 参照はすべて **関連型** 経由にすること。`use crate::{Edge, Face, Solid};` を
//! このファイルに書いてはいけない(書くと、両バックエンドが同時に存在する将来構成で
//! どちらの型を指すか曖昧になる)。
//!
//! ### 階層と関連型の向き
//!
//! 型同士の依存に **一方向の階層** を導入し、上位が下位を関連型として参照する:
//!
//! ```text
//!   EdgeStruct       ← 単独。下位を一切知らない
//!   FaceStruct       ← type Edge: EdgeStruct;
//!   SolidStruct      ← type Edge: EdgeStruct;  type Face: FaceStruct;
//!                       I/O メソッド (read_step / write_step / mesh など) も SolidStruct に同居
//! ```
//!
//! 下位(Edge/Face)→ 上位(Solid)への参照は持たせない。例えば「Edge を sweep して
//! Solid を作る」操作は `EdgeStruct::sweep` ではなく `SolidStruct::sweep(profile, spine)`
//! として上位側に置き、ヒエラルキーを保つ。逆向き参照を導入する瞬間に associated type
//! の循環や Backend バンドルトレイトが必要になり、examples/codegen.rs のテキスト処理が
//! 追従できなくなる。
//!
//! ### 命名と codegen の対応
//!
//! - `SolidStruct` の `type Edge` / `type Face` という名前は examples/codegen.rs の
//!   `TYPE_MAP` と一致させること。`Self::Edge` / `Self::Face` は生成時に
//!   バックエンドの具象型名(`Edge` / `Face`)へ置換され、`lib.rs` の
//!   `pub use occt::{Solid, Edge, Face};` により実体に解決される。
//! - 戻り型・引数型は `Vec<Self::Edge>`、`impl IntoIterator<Item = &'a Self>` の
//!   ように常に関連型 / Self 経由で書く(具象型名を直接書かない)。
//! - associated type 宣言(`type Foo: Bound;`)はパーサーが行頭でスキップするので、
//!   メソッドと同じインデントで 1 行に収めること。
//!
//! パーサー挙動と制約(examples/codegen.rs — 行ベースのテキスト処理):
//!
//! トレイトヘッダ:
//! - `pub trait Foo: A + B + C {` から名前と supertrait リスト(`+` 区切り)を抽出する
//! - `Foo` が `Struct` サフィックスを持つトレイトの supertrait に出現した場合、
//!   `Foo` のメソッドも親側の inherent impl に取り込まれる(再帰的に祖先まで辿る)
//! - 解析対象トレイト一覧に存在しない名前(`Sized`, `Clone`, ライフタイム束縛 `'a` 等)は
//!   黙って無視される
//! - 同名メソッドは子トレイト優先で重複排除される(親のオーバーライド)
//! - ヘッダ行は1行に収めること(`where` 句を改行して書くと検出されない)
//!
//! メソッドシグネチャ:
//! - fn シグネチャは1行に収めること(`where` 句・ライフタイム・ジェネリクス引数も同じ行)
//! - default impl はサポート。本体が1行に収まる場合はそのまま、複数行の場合も
//!   `{...}` ブロックを brace 深さでスキップする
//! - ライフタイム引数 `<'a, 'b>` および `where Self: 'a` のような句はそのまま保持される。
//!   `Self` は inherent impl 文脈では具象型と等価なので置換せず残す(`Self::Elem` のような
//!   関連型のみ事前に concrete type へ置換される)
//! - `Self::Elem` は impl 対象の具象型へ置換される。`Self::Face` / `Self::Edge` /
//!   `Self::Solid` はそれぞれ `Face` / `Edge` / `Solid` へ置換され、`lib.rs` の
//!   バックエンド再エクスポートで解決される
//!
//! その他:
//! - `#[cfg(...)]` は直前1行のみ認識し、続く fn に付与される
//! - `type Foo;` などの associated type 宣言は無視される(メソッド生成対象外)

#[cfg(feature = "color")]
use crate::common::color::Color;
use crate::common::error::Error;
use crate::common::mesh::Mesh;
use glam::{DMat3, DQuat, DVec3};

// ==================== Transform ====================

/// Spatial-transform operations: translate / rotate / scale / mirror.
///
/// Orthogonal to any specific geometry kind. Implemented for individual
/// shapes (`Solid`, `Edge`) and for collections (`Vec<T>`, `[T; N]`) where the
/// element type is itself `Transform`.
///
/// **Visibility**: this trait is declared `pub` but the enclosing `traits`
/// module is `pub(crate)` in `lib.rs`, and `Transform` is intentionally NOT
/// re-exported at the crate root. External users therefore cannot name it and
/// cannot `use` it. They reach the same methods through `Compound` / `Wire`,
/// which declare 1-line forwarders (`fn translate(self, ...) -> Self {
/// <Self as Transform>::translate(self, ...) }`) as default methods. This
/// keeps transforms a single source of truth inside the crate while letting
/// `use cadrum::Compound;` alone expose them externally (including on
/// collections like `Vec<Solid>` where method resolution would otherwise
/// require an import).
///
/// For `Solid` / `Edge` themselves the forwarders are unnecessary —
/// `examples/codegen.rs` walks the supertrait chain and emits inherent
/// methods, so no trait import is needed on the single types.
///
/// TODO(#90): the per-method forwarders in `Compound` / `Wire` are
/// mechanical and could be generated. A future refactor could extend
/// `examples/codegen.rs` (or introduce a proc-macro) to auto-emit
/// `fn foo(self, ..) -> Self { <Self as Transform>::foo(self, ..) }` for
/// every method of a referenced trait, so that Transform's surface is
/// listed exactly once in this file. Not urgent — see the issue for
/// priority notes.
pub trait Transform: Sized {
	fn translate(self, translation: DVec3) -> Self;
	fn rotate(self, axis_origin: DVec3, axis_direction: DVec3, angle: f64) -> Self;
	fn rotate_x(self, angle: f64) -> Self { self.rotate(DVec3::ZERO, DVec3::X, angle) }
	fn rotate_y(self, angle: f64) -> Self { self.rotate(DVec3::ZERO, DVec3::Y, angle) }
	fn rotate_z(self, angle: f64) -> Self { self.rotate(DVec3::ZERO, DVec3::Z, angle) }
	fn scale(self, center: DVec3, factor: f64) -> Self;
	fn mirror(self, plane_origin: DVec3, plane_normal: DVec3) -> Self;
	/// Rotate so that local +X axis aligns with `new_x`, with local +Y projected toward `y_hint`.
	/// Rotation is around the world origin. Panics on degenerate input (zero or parallel vectors).
	fn align_x(self, new_x: DVec3, y_hint: DVec3) -> Self {
		let x = new_x.try_normalize().expect("align_x: new_x is zero");
		let z = x.cross(y_hint).try_normalize().expect("align_x: y_hint parallel to new_x");
		let (axis, angle) = DQuat::from_mat3(&DMat3::from_cols(x, z.cross(x), z)).to_axis_angle();
		self.rotate(DVec3::ZERO, axis, angle)
	}
	/// Rotate so that local +Y axis aligns with `new_y`, with local +Z projected toward `z_hint`.
	/// Rotation is around the world origin. Panics on degenerate input (zero or parallel vectors).
	fn align_y(self, new_y: DVec3, z_hint: DVec3) -> Self {
		let y = new_y.try_normalize().expect("align_y: new_y is zero");
		let x = y.cross(z_hint).try_normalize().expect("align_y: z_hint parallel to new_y");
		let (axis, angle) = DQuat::from_mat3(&DMat3::from_cols(x, y, x.cross(y))).to_axis_angle();
		self.rotate(DVec3::ZERO, axis, angle)
	}
	/// Rotate so that local +Z axis aligns with `new_z`, with local +X projected toward `x_hint`.
	/// Rotation is around the world origin. Panics on degenerate input (zero or parallel vectors).
	fn align_z(self, new_z: DVec3, x_hint: DVec3) -> Self {
		let z = new_z.try_normalize().expect("align_z: new_z is zero");
		let y = z.cross(x_hint).try_normalize().expect("align_z: x_hint parallel to new_z");
		let (axis, angle) = DQuat::from_mat3(&DMat3::from_cols(y.cross(z), y, z)).to_axis_angle();
		self.rotate(DVec3::ZERO, axis, angle)
	}
}

// ==================== ProfileOrient ====================

/// Controls how the cross-section profile is oriented as it travels along the
/// spine in [`SolidStruct::sweep`] and [`SolidStruct::sweep_sections`].
///
/// **どれを選ぶか:**
///
/// | やりたいこと | 選ぶ variant |
/// |---|---|
/// | 直線押し出し / profile を回したくない | [`Fixed`](Self::Fixed) |
/// | ねじ・バネ・つる (helix 系) | [`Torsion`](Self::Torsion) または [`Up`](Self::Up)`(axis)` |
/// | 道路・線路・パイプ (重力方向を保ちたい) | [`Up`](Self::Up)`(DVec3::Z)` |
/// | 上記に当てはまらない 3D 自由曲線 | [`Torsion`](Self::Torsion) |
/// | 任意の捻り制御 (メビウスの輪等) | [`Auxiliary`](Self::Auxiliary)`(&aux_spine)` |
///
/// **`Torsion` と `Up(axis)` の関係**: helix のような定曲率・定 torsion 曲線では、
/// この 2 つは数学的に等価なトリヘドロンを生成します。`Torsion` は曲線の主法線
/// (= `d²C/dt²` の T 直交成分) に profile を貼り付け、`Up` はユーザが渡した
/// 方向を T 直交平面に射影して binormal にする — helix 上ではこの 2 つが
/// 同じ axis を指すため、結果が一致します。helix 以外の曲線では一致しません。
#[derive(Clone, Copy)]
pub enum ProfileOrient<'a> {
	/// Profile is parallel-transported along the spine **without rotating**.
	/// All cross-sections stay parallel to the starting orientation.
	///
	/// - **適**: 直線 spine (押し出し)
	/// - **不適**: 曲がる spine (profile が tangent と直交しなくなり、見た目が壊れる)
	Fixed,

	/// Profile rotates following the spine's principal normal direction
	/// (= the T-perpendicular component of `d²C/dt²`). Equivalent to OCCT's
	/// raw Frenet–Serret frame.
	///
	/// - **適**: helix, spring, screw thread, twisted ribbon — 定曲率・
	///   定 torsion な曲線、および 3D 自由曲線で「曲線の自然な捻れ」を
	///   profile に反映させたいケース
	/// - **不適**: 変曲点 (curvature → 0) を含む 2D / 3D スプライン。
	///   変曲点で N が不定になり profile が 180° flip しうる。その場合は
	///   `Up` を使う
	Torsion,

	/// Profile keeps the given direction as its "up" axis (binormal).
	///
	/// - **適**: 道路 (`up = DVec3::Z`), 線路, パイプ, 運河 — 重力方向を
	///   保ちたい sweep 全般
	/// - **不適**: 任意の点で `up` が tangent と平行になる spine
	Up(DVec3),

	/// Profile orientation is controlled by an auxiliary spine curve.
	/// The profile's X axis tracks the direction toward the auxiliary spine.
	///
	/// - **適**: メビウスの輪、ステラレーターの断面回転、任意の捻り制御
	Auxiliary(&'a [crate::Edge]),
}

// ==================== BSplineEnd ====================

/// End-condition selector for [`EdgeStruct::bspline`].
///
/// A cubic B-spline interpolating N data points has 4(N−1) coefficient
/// degrees of freedom. The interpolation conditions plus C¹/C² continuity
/// at internal knots fix all but **2** of those. This enum chooses how
/// the remaining 2 degrees are determined.
///
/// **どれを選ぶか:**
///
/// | やりたいこと | 選ぶ variant |
/// |---|---|
/// | 閉じた断面プロファイル (プラズマ poloidal section, 自由曲線リング) | [`Periodic`](Self::Periodic) |
/// | 開いた自由曲線で端点接線が分からない | [`NotAKnot`](Self::NotAKnot) |
/// | 開いた自由曲線で端点接線が物理的に決まっている | [`Clamped`](Self::Clamped) |
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BSplineEnd {
	/// Build a periodic curve. Start and end coincide with **C² continuity**
	/// (position + tangent + curvature all match at the wrap-around).
	/// The first data point must NOT be repeated at the end — periodicity
	/// is encoded in the basis function structure. Passing a duplicated
	/// endpoint yields [`Error::InvalidEdge`].
	///
	/// Requires ≥ 3 distinct points. The most common choice for closed
	/// profile curves (plasma poloidal sections, screw threads, gear teeth)
	/// where the start/end seam should be invisible.
	Periodic,

	/// Open curve, end conditions chosen so that the cubics on the first
	/// two intervals collapse into a single cubic (and likewise at the
	/// other end). The 2nd and (N−1)th data points behave as plain
	/// interpolation targets that do not act as real knots.
	///
	/// This is the default in MATLAB, SciPy, and OCCT itself. Best when
	/// nothing is known about end behavior — gives the most "natural"
	/// looking boundary because the boundary cubic is fit to 3 data
	/// points instead of being constrained by an artificial derivative
	/// condition. Requires ≥ 2 points.
	NotAKnot,

	/// Open curve with explicit start/end tangent vectors. The magnitude
	/// of each vector controls how strongly the curve is pulled along
	/// that direction near the boundary — a unit vector gives a gentle
	/// hint, a longer vector pulls more aggressively. Requires ≥ 2 points.
	Clamped {
		start: DVec3,
		end: DVec3,
	},
}

// ==================== Wire / EdgeStruct ====================

/// Public trait: container abstraction over `Edge`, `Vec<Edge>` and `[Edge; N]`.
///
/// **コレクション最小契約**: 実装者は要素列挙 (`iter_elem`) と要素全置換 (`map_elem`)
/// の 2 つだけを提供する (`Compound` と同形)。start_point / end_point / start_tangent /
/// end_tangent / is_closed / approximation_segments / project は default で提供され、
/// 内部で `<Self::Elem as EdgeStruct>::xxx(s)` を `iter_elem` 結果から取り出して合成する。
///
/// `Vec<Edge>` plays the role of a wire in this library — there is no dedicated
/// `Wire` type, mirroring how `Compound` is just `Vec<Solid>`. Methods on `Wire`
/// therefore have meaningful semantics for both a single edge and an ordered edge list:
///
/// - `start_point` / `end_tangent` / etc. — for a single `Edge` this is the
///   edge's own endpoint/tangent (via `EdgeStruct`); for a `Vec<Edge>` this is
///   the first edge's start / last edge's end aggregated by the default impl.
/// - `is_closed` — for a single `Edge` defers to the edge's geometry (e.g. a
///   circle is closed); for a `Vec<Edge>` checks `first.start ≈ last.end`.
/// - `approximation_segments` — concatenates sub-edge polylines, dropping
///   duplicate seam points.
/// - `project` — finds the smallest-distance projection across all elements.
///
/// Spatial transforms live on the (crate-private) supertrait `Transform`.
/// Since `Transform` is not re-exported from the crate root, users cannot
/// bring it into scope directly. Instead `Wire` exposes 1-line forwarders
/// for every `Transform` method as default methods, so `use cadrum::Wire;`
/// alone enables `vec_of_edges.translate(...)` etc.
///
/// As with `Compound`, `EdgeStruct: Wire` so users of `Edge` get these
/// methods inherently via `examples/codegen.rs`; the `use` import is only
/// required when chaining on `Vec<Edge>` / `[Edge; N]`.
pub trait Wire: Transform {
	type Elem: Wire;

	/// Borrow each element. For `Edge` itself this yields `std::iter::once(self)`;
	/// for `Vec<T>` / `[T; N]` it yields `self.iter()`.
	fn iter_elem(&self) -> impl Iterator<Item = &Self::Elem> + '_;
	/// Replace every element by mapping through `f`. Length is preserved.
	/// For `Edge` this is `f(self)`; for collections it consumes self and
	/// rebuilds in the same shape.
	fn map_elem(self, f: impl FnMut(Self::Elem) -> Self::Elem) -> Self;

	// --- Endpoints / tangents (default — first / last element) ---
	fn start_point(&self) -> DVec3 {
		self.iter_elem().next().map(|e| <Self::Elem as Wire>::start_point(e)).unwrap_or(DVec3::ZERO)
	}
	fn end_point(&self) -> DVec3 {
		self.iter_elem().last().map(|e| <Self::Elem as Wire>::end_point(e)).unwrap_or(DVec3::ZERO)
	}
	fn start_tangent(&self) -> DVec3 {
		self.iter_elem().next().map(|e| <Self::Elem as Wire>::start_tangent(e)).unwrap_or(DVec3::ZERO)
	}
	fn end_tangent(&self) -> DVec3 {
		self.iter_elem().last().map(|e| <Self::Elem as Wire>::end_tangent(e)).unwrap_or(DVec3::ZERO)
	}
	/// Empty wire: false. Single-edge wire: defer to that edge's geometry
	/// (a circle is closed). Multi-edge wire: first edge's start ≈ last edge's
	/// end. 1e-6 はモデル単位 (mm) を想定したハードコード — 引数化は API が
	/// 増えるため後回し。極小/極大スケールのモデルで誤判定したら直す。
	fn is_closed(&self) -> bool {
		let mut iter = self.iter_elem();
		let Some(first) = iter.next() else { return false; };
		match iter.last() {
			None => <Self::Elem as Wire>::is_closed(first),
			Some(last) => (<Self::Elem as Wire>::start_point(first) - <Self::Elem as Wire>::end_point(last)).length() < 1e-6,
		}
	}
	fn approximation_segments(&self, tolerance: f64) -> Vec<DVec3> {
		let mut out: Vec<DVec3> = Vec::new();
		for e in self.iter_elem() {
			let pts = <Self::Elem as Wire>::approximation_segments(e, tolerance);
			if let Some((first, rest)) = pts.split_first() {
				if out.last().map(|p| (*p - *first).length() < 1e-9).unwrap_or(false) {
					out.extend_from_slice(rest);
				} else {
					out.push(*first);
					out.extend_from_slice(rest);
				}
			}
		}
		out
	}
	/// Project `p` onto the wire and return `(closest_point, unit_tangent)`.
	/// The tangent follows the curve's native parameter direction.
	///
	/// An empty wire returns `(DVec3::ZERO, DVec3::ZERO)`, matching the
	/// silent-zero convention of `start_point` / `start_tangent`. A single
	/// `Edge` that lacks a 3D geometric curve (i.e. FFI-level failure,
	/// which cadrum-built edges never produce) panics — that case
	/// indicates a bug, not a degenerate user input.
	fn project(&self, p: DVec3) -> (DVec3, DVec3) {
		self.iter_elem()
			.map(|e| <Self::Elem as Wire>::project(e, p))
			.min_by(|(a, _), (b, _)| (a - p).length_squared().partial_cmp(&(b - p).length_squared()).unwrap_or(std::cmp::Ordering::Equal))
			.unwrap_or((DVec3::ZERO, DVec3::ZERO))
	}

	////////// codegen.rs
	fn translate(self, translation: DVec3) -> Self { <Self as Transform>::translate(self, translation) }
	fn rotate(self, axis_origin: DVec3, axis_direction: DVec3, angle: f64) -> Self { <Self as Transform>::rotate(self, axis_origin, axis_direction, angle) }
	fn rotate_x(self, angle: f64) -> Self { <Self as Transform>::rotate_x(self, angle) }
	fn rotate_y(self, angle: f64) -> Self { <Self as Transform>::rotate_y(self, angle) }
	fn rotate_z(self, angle: f64) -> Self { <Self as Transform>::rotate_z(self, angle) }
	fn scale(self, center: DVec3, factor: f64) -> Self { <Self as Transform>::scale(self, center, factor) }
	fn mirror(self, plane_origin: DVec3, plane_normal: DVec3) -> Self { <Self as Transform>::mirror(self, plane_origin, plane_normal) }
	fn align_x(self, new_x: DVec3, y_hint: DVec3) -> Self { <Self as Transform>::align_x(self, new_x, y_hint) }
	fn align_y(self, new_y: DVec3, z_hint: DVec3) -> Self { <Self as Transform>::align_y(self, new_y, z_hint) }
	fn align_z(self, new_z: DVec3, x_hint: DVec3) -> Self { <Self as Transform>::align_z(self, new_z, x_hint) }
}

/// Backend-independent edge trait (pub(crate) — not exposed to users).
///
/// Single-edge constructors only. Wire/edge-list operations live on `Wire`
/// and are inherited via the supertrait bound, in symmetry with `SolidStruct`.
///
/// All constructors return `Result<..., Error>`. Invalid inputs (degenerate
/// geometry, zero/negative radius, collinear arc points, etc.) yield
/// `Error::InvalidEdge(String)` with a message that identifies the failing
/// constructor and the offending parameters.
pub trait EdgeStruct: Sized + Clone + Wire {
	/// Stable, backend-defined identity for this edge. Two `Edge` values
	/// returning the same `id()` refer to the same topology element.
	/// Use to compare edges across `Solid::iter_edge()` / `Face::iter_edge()`
	/// (e.g. `face.iter_edge().any(|e| e.id() == edge.id())`).
	fn id(&self) -> u64;

	/// Construct a single helical edge on a cylindrical surface centered at
	/// the world origin.
	///
	/// - `radius`: cylinder radius
	/// - `pitch`: rise per full revolution
	/// - `height`: total rise (number of turns = `height / pitch`)
	/// - `axis`: cylinder axis direction (must be non-zero)
	/// - `x_ref`: reference direction that anchors the local +X axis of the
	///   cylindrical frame. The helix start point is
	///   `radius * normalize(component of x_ref orthogonal to axis)`.
	///   `x_ref` must not be parallel to `axis`.
	///
	/// Making `x_ref` explicit guarantees the start point is deterministic
	/// rather than depending on whatever orthogonal direction OCCT picks
	/// from `axis` alone.
	fn helix(radius: f64, pitch: f64, height: f64, axis: DVec3, x_ref: DVec3) -> Result<Self, Error>;

	/// Build a closed polygon from a sequence of points and return its
	/// constituent edges in order. The polygon is **always closed**: the
	/// last point is automatically connected back to the first.
	// 非平面の点列も受理する (検証しない) — `Solid::sweep` で face 化に失敗
	// したとき `Error::SweepFailed` で気付ける想定なので、入力側での事前検査は省略。
	fn polygon<'a>(points: impl IntoIterator<Item = &'a DVec3>) -> Result<Vec<Self>, Error>;

	/// Closed circle of radius `r` centered at the world origin, lying in
	/// the plane normal to `axis`. Returns a single edge (one Geom_Circle
	/// curve — not a polygon approximation).
	///
	/// The circle's start/end point (at which `start_point()` /
	/// `start_tangent()` are evaluated) is chosen by the backend from an
	/// arbitrary orthogonal direction to `axis`. Callers that need a
	/// deterministic start point should translate/rotate the resulting
	/// edge into place rather than relying on the implicit choice.
	fn circle(radius: f64, axis: DVec3) -> Result<Self, Error>;

	/// Straight line segment from `a` to `b`. Fails with `InvalidEdge` if
	/// `a == b` (zero-length segment).
	fn line(a: DVec3, b: DVec3) -> Result<Self, Error>;

	/// Circular arc through three points: start, mid, end. The unique circle
	/// passing through the three points defines the arc; `mid` disambiguates
	/// which of the two possible arcs is returned (the one passing through
	/// `mid`). Fails with `InvalidEdge` if `mid` is collinear with `start`
	/// and `end`, or if any pair of points coincides.
	fn arc_3pts(start: DVec3, mid: DVec3, end: DVec3) -> Result<Self, Error>;

	/// Cubic B-spline curve interpolating the given data points.
	///
	/// **The points are interpolation targets, not control points.** OCCT's
	/// `GeomAPI_Interpolate` solves a linear system so the resulting curve
	/// passes through every input point exactly. The internal control points
	/// and knots are computed automatically and not exposed.
	///
	/// - Degree: 3 (cubic)
	/// - Parameterization: chord-length
	/// - End behavior: chosen by `end` (see [`BSplineEnd`])
	///
	/// Returns one `Edge` wrapping a single `Geom_BSplineCurve`. Use as a
	/// sweep/loft section by wrapping in `vec![...]` or `&[edge]`.
	///
	/// # Errors
	///
	/// Returns [`Error::InvalidEdge`] if:
	/// - point count is below the minimum (≥3 for `Periodic`, ≥2 otherwise)
	/// - `BSplineEnd::Periodic` is requested but the first and last points
	///   coincide (periodicity is encoded in the basis; do not duplicate)
	/// - OCCT's interpolation fails (degenerate point distribution, etc.)
	fn bspline<'a>(points: impl IntoIterator<Item = &'a DVec3>, end: BSplineEnd) -> Result<Self, Error>;
}

/// Backend-independent face trait (pub(crate) — not exposed to users).
///
/// `Face` is a query handle for surfaces in a solid. Used to read identity
/// (for colormap / boolean history matching) and to project external 3D
/// points onto the face for snap-to-surface workflows.
///
/// examples/codegen.rs generates `impl Face { pub fn ... }` from this trait
/// so callers reach the methods inherently as `face.id()` / `face.project(p)`.
pub trait FaceStruct: Sized {
	type Edge: EdgeStruct;

	/// Stable, backend-defined identity for this face. Two `Face` values
	/// returning the same `id()` refer to the same topology element. Used
	/// to look up entries in `Solid::colormap` or to match faces against
	/// boolean / clean operation history. The numeric value itself has no
	/// meaning beyond equality / hash use.
	fn id(&self) -> u64;

	/// Project a 3D point onto this face. Returns `(closest_point,
	/// outward_normal)`. Sister of `Wire::project` which returns `(closest,
	/// tangent)` on a 1D curve.
	///
	/// The closest hit respects the face's trim — projection lands on the
	/// actual face area, not its underlying infinite surface. To project
	/// onto a full solid, iterate `Solid::iter_face()` and call `project`
	/// on each face; the caller picks the smallest-distance face and keeps
	/// the face object for follow-up queries (e.g. `face.id()` for
	/// colormap lookup).
	///
	/// `outward_normal` is the zero vector when the surface evaluator
	/// cannot define a normal at the closest hit (degenerate surface
	/// point); callers can detect this case via `normal.length() == 0`.
	fn project(&self, p: DVec3) -> (DVec3, DVec3);

	/// Iterate this face's boundary edges (outer wire and any inner wires).
	/// Each edge appears once even when shared between wires. Backends may
	/// cache the result internally; re-calls are expected to be cheap.
	///
	/// Use with `Edge::id()` to test face/edge incidence:
	/// `face.iter_edge().any(|e| e.id() == edge.id())`.
	fn iter_edge(&self) -> impl Iterator<Item = &Self::Edge> + '_;
}

/// Backend-independent solid trait (pub(crate) — not exposed to users).
///
/// `Solid`-specific operations only. The shared methods (transforms, queries,
/// color, boolean wrappers) live on `Compound` and are inherited via the
/// supertrait bound.
///

/// examples/codegen.rs generates `impl Solid { pub fn ... }` from this trait
/// and walks the supertrait chain to expose `Compound` methods inherently as well.
///
/// Associated types `Edge`/`Face` keep this trait backend-independent: each
/// backend (occt / pure) binds them to its own concrete types in the impl.
pub trait SolidStruct: Sized + Clone + Compound {
	type Edge: EdgeStruct;
	type Face: FaceStruct;

	// --- Identity ---
	/// Stable, backend-defined identity for this solid. Two `Solid` values
	/// returning the same `id()` refer to the same topology element.
	/// translate / rotate / color preserve this id; scale / mirror / Clone
	/// rebuild topology and produce a fresh id. Distinct from the ids of
	/// the solid's contained faces / edges (each sub-shape has its own).
	fn id(&self) -> u64;

	// --- Constructors ---
	fn cube(x: f64, y: f64, z: f64) -> Self;
	fn sphere(radius: f64) -> Self;
	fn cylinder(r: f64, axis: DVec3, h: f64) -> Self;
	fn cone(r1: f64, r2: f64, axis: DVec3, h: f64) -> Self;
	fn torus(r1: f64, r2: f64, axis: DVec3) -> Self;
	fn half_space(plane_origin: DVec3, plane_normal: DVec3) -> Self;

	// --- Topology iteration ---
	/// Iterate this solid's unique edges. Each OCCT edge appears once even
	/// when shared between faces. Backends may cache the result internally;
	/// re-calls are expected to be cheap.
	fn iter_edge(&self) -> impl Iterator<Item = &Self::Edge> + '_;
	/// Iterate this solid's faces. Backends may cache the result internally.
	fn iter_face(&self) -> impl Iterator<Item = &Self::Face> + '_;
	/// Iterate face-derivation pairs `[post_id, src_id]` from the most recent
	/// boolean operation that produced this Solid (or its source chain, while
	/// it stays through translate/rotate/color). Empty after primitive/builder
	/// construction, I/O read, scale/mirror, or Clone.
	fn iter_history(&self) -> impl Iterator<Item = [u64; 2]> + '_;

	// --- Per-element atomic ops ---
	// `Compound` の default メソッド (volume / area / ... / color) はこれらを
	// `<Self::Elem as SolidStruct>::volume(s)` 形式の UFCS で呼ぶ。Solid 単体は
	// ここで FFI を直接叩き、Vec<T> / [T; N] は Compound default 経由で集約される。

	/// Heal/regularize this solid (fuse coplanar faces, drop micro-edges,
	/// repair small inconsistencies). Wraps `ShapeUpgrade_UnifySameDomain`
	/// + cleanup. Failure is reported as `Error::CleanFailed`.
	fn clean(&self) -> Result<Self, Error>;
	/// Extrude a closed profile wire along a direction vector to form a solid.
	///
	/// Internally builds a face from the wire and uses `BRepPrimAPI_MakePrism`.
	/// Fails if the profile is empty, not closed, or the direction is zero-length.
	fn extrude<'a>(profile: impl IntoIterator<Item = &'a Self::Edge>, dir: DVec3) -> Result<Self, Error> where Self::Edge: 'a;

	/// Hollow this solid into a thin-walled shell by removing `open_faces`
	/// (they become openings) and building a wall of signed `thickness` along
	/// each remaining face. Wraps OCCT's `BRepOffsetAPI_MakeThickSolid`.
	///
	/// `thickness` is the wall thickness with direction encoded in its sign:
	/// negative → wall grows inward (carve cavity inside the original volume),
	/// positive → wall grows outward (shell sits outside the original surface,
	/// enclosing the original as its inner boundary).
	///
	/// `open_faces` must be faces of `self` (e.g. selected via `self.iter_face()`).
	/// When `open_faces` is empty, `BRepOffsetAPI_MakeThickSolid` degenerates to
	/// a plain offset shape (no cavity) because it needs at least one removed
	/// face to build the inner wall. The wrapper detects this and falls back to
	/// `BRepOffsetAPI_MakeOffsetShape` + `BRepBuilderAPI_MakeSolid`, assembling
	/// an outer shell and a reversed inner shell into a sealed multi-shell
	/// solid with an internal void (the void is inaccessible from outside).
	/// Fails on OCCT rejection (self-intersecting offset at sharp corners, etc).
	fn shell<'a>(&self, thickness: f64, open_faces: impl IntoIterator<Item = &'a Self::Face>) -> Result<Self, Error> where Self::Face: 'a;

	/// Round the given edges of `self` with a uniform radius. Edges are
	/// typically selected via `self.iter_edge().filter(...)`.
	///
	/// Wraps `BRepFilletAPI_MakeFillet`. Fails (`Error::FilletFailed`) if
	/// the radius is too large for the local geometry, if tangent
	/// discontinuity prevents OCCT from building the fillet surface, or
	/// if an edge not belonging to `self` is passed.
	///
	/// Empty `edges` is a no-op and returns a clone of `self` — handy when
	/// a selector chain legitimately yields zero edges.
	fn fillet_edges<'a>(&self, radius: f64, edges: impl IntoIterator<Item = &'a Self::Edge>) -> Result<Self, Error> where Self::Edge: 'a;

	/// Chamfer (bevel) the given edges of `self` with a uniform distance.
	/// Edges are typically selected via `self.iter_edge().filter(...)`.
	///
	/// Wraps `BRepFilletAPI_MakeChamfer`. The chamfer plane is symmetric —
	/// the same `distance` is taken off along each of the two faces
	/// adjacent to the edge. Fails (`Error::ChamferFailed`) under the same
	/// conditions as `fillet_edges`.
	///
	/// Empty `edges` is a no-op and returns a clone of `self`.
	fn chamfer_edges<'a>(&self, distance: f64, edges: impl IntoIterator<Item = &'a Self::Edge>) -> Result<Self, Error> where Self::Edge: 'a;

	// --- Sweep ---
	/// Sweep a closed profile wire (= ordered edge list) along a spine wire
	/// to create a solid. Both inputs are accepted as `IntoIterator` of edge
	/// references so a single `&Edge` (via `std::slice::from_ref`) and a
	/// `&Vec<Edge>` work uniformly.
	///
	/// The profile must be closed; otherwise the underlying pipe operation
	/// produces a shell rather than a solid and an error is returned.
	///
	/// `orient` selects how the profile is oriented along the spine. See
	/// [`ProfileOrient`] for the trade-offs between [`Fixed`](ProfileOrient::Fixed),
	/// [`Torsion`](ProfileOrient::Torsion), and [`Up`](ProfileOrient::Up).
	// 戻り型は単一 `Self` 固定。MakePipeShell が compound を返すことは closed
	// face 入力に対しては実質起きないため、`Vec<Self>` に拡張する手間を省いた。
	// 想定外ケースに当たったら `Solid::new` の debug_assert で気付ける。
	fn sweep<'a, 'b, 'c>(profile: impl IntoIterator<Item = &'a Self::Edge>, spine: impl IntoIterator<Item = &'b Self::Edge>, orient: ProfileOrient<'c>) -> Result<Self, Error> where Self::Edge: 'a + 'b;

	/// Loft (skin) a smooth solid through a sequence of cross-section wires.
	///
	/// Each `section` is an ordered list of edges forming a closed wire (a
	/// "rib"). The lofter interpolates a B-spline surface through all sections
	/// in order, then caps the ends to form a `Solid`.
	///
	/// OCCT caps the first/last sections with planar faces to form a closed
	/// solid (the standard "trunk" / "frustum" shape).
	///
	/// Internally uses `BRepOffsetAPI_ThruSections(isSolid=true, isRuled=false)`.
	fn loft<'a, S, I>(sections: S) -> Result<Self, Error> where S: IntoIterator<Item = I>, I: IntoIterator<Item = &'a Self::Edge>, Self::Edge: 'a;

	/// Build a B-spline surface solid from a 2D control-point grid.
	///
	/// `grid[i][j]` — index `i` (0..M) runs along the longitudinal (U) direction,
	/// index `j` (0..N) runs along the cross-section (V) direction. V is always
	/// periodic (the cross-section is a closed loop); U is periodic iff
	/// `periodic=true`, producing a torus. When `periodic=false` the U-ends are
	/// capped with planar faces, producing a pipe.
	///
	/// Internally builds a `Geom_BSplineSurface` via tensor-product periodic
	/// curve interpolation: per-V-column then per-U-row `GeomAPI_Interpolate`
	/// with explicit uniform parameters. Yields C^(degree-1) continuity at
	/// both seams. The closure `point(i, j)` is called for `i ∈ 0..u`,
	/// `j ∈ 0..v` to produce the M×N control grid, where `u` is the
	/// toroidal direction (closed iff `u_periodic`) and `v` is the
	/// cross-section direction (always closed).
	fn bspline(u: usize, v: usize, u_periodic: bool, point: impl Fn(usize, usize) -> DVec3) -> Result<Self, Error>;

	// --- Boolean primitives (consumed by Compound::union/subtract/intersect wrappers) ---
	// Per-result-Solid face derivation history is attached to each Solid via
	// `Solid::iter_history()`; no separate metadata channel.
	fn boolean_union<'a, 'b>(a: impl IntoIterator<Item = &'a Self>, b: impl IntoIterator<Item = &'b Self>) -> Result<Vec<Self>, Error> where Self: 'a + 'b;
	fn boolean_subtract<'a, 'b>(a: impl IntoIterator<Item = &'a Self>, b: impl IntoIterator<Item = &'b Self>) -> Result<Vec<Self>, Error> where Self: 'a + 'b;
	fn boolean_intersect<'a, 'b>(a: impl IntoIterator<Item = &'a Self>, b: impl IntoIterator<Item = &'b Self>) -> Result<Vec<Self>, Error> where Self: 'a + 'b;

	// --- I/O ---
	// Co-located with constructors: STEP / BRep readers return `Vec<Self>` (a
	// build path symmetrical with `Solid::cube` etc.), writers/`mesh` consume
	// solids. Putting them on Solid concentrates the type's surface and keeps
	// the crate root free of generic names like `mesh` / `write_step`.
	fn read_step<R: std::io::Read>(reader: &mut R) -> Result<Vec<Self>, Error>;
	fn read_brep_binary<R: std::io::Read>(reader: &mut R) -> Result<Vec<Self>, Error>;
	fn read_brep_text<R: std::io::Read>(reader: &mut R) -> Result<Vec<Self>, Error>;
	fn write_step<'a, W: std::io::Write>(solids: impl IntoIterator<Item = &'a Self>, writer: &mut W) -> Result<(), Error> where Self: 'a;
	fn write_brep_binary<'a, W: std::io::Write>(solids: impl IntoIterator<Item = &'a Self>, writer: &mut W) -> Result<(), Error> where Self: 'a;
	fn write_brep_text<'a, W: std::io::Write>(solids: impl IntoIterator<Item = &'a Self>, writer: &mut W) -> Result<(), Error> where Self: 'a;
	fn mesh<'a>(solids: impl IntoIterator<Item = &'a Self>, tolerance: f64) -> Result<Mesh, Error> where Self: 'a;
}

// ==================== Compound ====================

/// Public trait: container abstraction over `Solid`, `Vec<Solid>`, and `[Solid; N]`.
///
/// **コレクション最小契約**: 実装者は要素列挙 (`iter_elem`) と要素全置換 (`map_elem`)
/// の 2 つだけを提供する。volume / area / bounding_box / center / inertia / contains /
/// color / color_clear / union / subtract / intersect は default で提供され、
/// 内部で `<Self::Elem as SolidStruct>::xxx(s)` を `iter_elem` 結果に対して集約する。
///
/// **fallible op の意図的な不在**: `clean` は `SolidStruct` のみに置き、`Compound` には
/// 載せない。fallible メソッドを default 化すると `try_map_elem` 相当の追加要求が必要
/// になり container 契約が肥大化するため。コレクションに対して clean したい場合は
/// `vec.into_iter().map(|s| s.clean()).collect::<Result<Vec<_>, _>>()?` と書く。
///
/// Spatial transforms (translate/rotate/scale/mirror) live on the crate-private
/// supertrait `Transform`. `Compound` re-exposes them through 1-line forwarders
/// as default methods, so `use cadrum::Compound;` alone is enough to call
/// `vec.translate(...)` / `[a,b].rotate_z(...)` on collections — no separate
/// `Transform` import is needed (and none is possible from outside the crate).
pub trait Compound: Transform {
	type Elem: Compound+SolidStruct;

	/// Borrow each element. For `Solid` itself this yields `std::iter::once(self)`;
	/// for `Vec<T>` / `[T; N]` it yields `self.iter()`.
	fn iter_elem(&self) -> impl Iterator<Item = &Self::Elem> + '_;
	/// Replace every element by mapping through `f`. Length is preserved.
	/// For `Solid` this is `f(self)`; for collections it consumes self and
	/// rebuilds in the same shape.
	fn map_elem(self, f: impl FnMut(Self::Elem) -> Self::Elem) -> Self;

	// --- Queries (default — aggregate over iter_elem) ---
	fn volume(&self) -> f64 {
		self.iter_elem().map(|s| <Self::Elem as Compound>::volume(s)).sum()
	}
	fn area(&self) -> f64 {
		self.iter_elem().map(|s| <Self::Elem as Compound>::area(s)).sum()
	}
	fn contains(&self, point: DVec3) -> bool {
		self.iter_elem().any(|s| <Self::Elem as Compound>::contains(s, point))
	}
	fn bounding_box(&self) -> [DVec3; 2] {
		self.iter_elem()
			.map(|s| <Self::Elem as Compound>::bounding_box(s))
			.reduce(|[amin, amax], [bmin, bmax]| [amin.min(bmin), amax.max(bmax)])
			.unwrap_or([DVec3::ZERO, DVec3::ZERO])
	}
	/// Center of mass (uniform density). Volume-weighted average of per-element
	/// centers: `Σ(vol_i · center_i) / Σ vol_i`. volume=0 ガードは Vec/[T;N]
	/// 空集合と Solid 単要素 (degenerate) の両方を `DVec3::ZERO` で救済する。
	fn center(&self) -> DVec3 {
		let total: f64 = self.iter_elem().map(|s| <Self::Elem as Compound>::volume(s)).sum();
		if total == 0.0 { return DVec3::ZERO; }
		self.iter_elem()
			.map(|s| <Self::Elem as Compound>::center(s) * <Self::Elem as Compound>::volume(s))
			.sum::<DVec3>() / total
	}
	/// Inertia tensor about the **world origin** (uniform density). Aggregates
	/// as a straight matrix sum across elements (parallel-axis theorem is
	/// already folded in by world-origin referencing).
	fn inertia(&self) -> DMat3 {
		self.iter_elem().map(|s| <Self::Elem as Compound>::inertia(s)).fold(DMat3::ZERO, |a, b| a + b)
	}

	// --- Color (default — map over elements) ---
	#[cfg(feature = "color")]
	fn color(self, color: impl Into<Color>) -> Self {
		let c: Color = color.into();
		self.map_elem(|s| <Self::Elem as Compound>::color(s, c))
	}
	#[cfg(feature = "color")]
	fn color_clear(self) -> Self {
		self.map_elem(|s| <Self::Elem as Compound>::color_clear(s))
	}

	// --- Boolean (default — feed iter_elem to SolidStruct::boolean_*) ---
	// Each result Solid carries its face-derivation history; access via
	// `Solid::iter_history()`.
	fn union<'a>(&self, tool: impl IntoIterator<Item = &'a Self::Elem>) -> Result<Vec<Self::Elem>, Error> where Self::Elem: 'a {
		Self::Elem::boolean_union(self.iter_elem(), tool)
	}
	fn subtract<'a>(&self, tool: impl IntoIterator<Item = &'a Self::Elem>) -> Result<Vec<Self::Elem>, Error> where Self::Elem: 'a {
		Self::Elem::boolean_subtract(self.iter_elem(), tool)
	}
	fn intersect<'a>(&self, tool: impl IntoIterator<Item = &'a Self::Elem>) -> Result<Vec<Self::Elem>, Error> where Self::Elem: 'a {
		Self::Elem::boolean_intersect(self.iter_elem(), tool)
	}
	////////// codegen.rs
	fn translate(self, translation: DVec3) -> Self { <Self as Transform>::translate(self, translation) }
	fn rotate(self, axis_origin: DVec3, axis_direction: DVec3, angle: f64) -> Self { <Self as Transform>::rotate(self, axis_origin, axis_direction, angle) }
	fn rotate_x(self, angle: f64) -> Self { <Self as Transform>::rotate_x(self, angle) }
	fn rotate_y(self, angle: f64) -> Self { <Self as Transform>::rotate_y(self, angle) }
	fn rotate_z(self, angle: f64) -> Self { <Self as Transform>::rotate_z(self, angle) }
	fn scale(self, center: DVec3, factor: f64) -> Self { <Self as Transform>::scale(self, center, factor) }
	fn mirror(self, plane_origin: DVec3, plane_normal: DVec3) -> Self { <Self as Transform>::mirror(self, plane_origin, plane_normal) }
	fn align_x(self, new_x: DVec3, y_hint: DVec3) -> Self { <Self as Transform>::align_x(self, new_x, y_hint) }
	fn align_y(self, new_y: DVec3, z_hint: DVec3) -> Self { <Self as Transform>::align_y(self, new_y, z_hint) }
	fn align_z(self, new_z: DVec3, x_hint: DVec3) -> Self { <Self as Transform>::align_z(self, new_z, x_hint) }
}

// `impl Compound for Solid` lives in the backend module (e.g. src/occt/solid.rs)
// because it needs direct access to the backend FFI for the per-element operations.

// ==================== impl Transform / Compound for Vec<T> ====================

impl<T: Transform> Transform for Vec<T> {
	fn translate(self, v: DVec3) -> Self { self.into_iter().map(|s| s.translate(v)).collect() }
	fn rotate(self, o: DVec3, d: DVec3, a: f64) -> Self { self.into_iter().map(|s| s.rotate(o, d, a)).collect() }
	fn scale(self, c: DVec3, f: f64) -> Self { self.into_iter().map(|s| s.scale(c, f)).collect() }
	fn mirror(self, o: DVec3, n: DVec3) -> Self { self.into_iter().map(|s| s.mirror(o, n)).collect() }
}

impl<T: SolidStruct> Compound for Vec<T> {
	type Elem = T;
	fn iter_elem(&self) -> impl Iterator<Item = &T> + '_ { self.iter() }
	fn map_elem(self, f: impl FnMut(T) -> T) -> Self { self.into_iter().map(f).collect() }
}

// ==================== impl Transform / Compound for [T; N] ====================

impl<T: Transform, const N: usize> Transform for [T; N] {
	fn translate(self, v: DVec3) -> Self { self.map(|s| s.translate(v)) }
	fn rotate(self, o: DVec3, d: DVec3, a: f64) -> Self { self.map(|s| s.rotate(o, d, a)) }
	fn scale(self, c: DVec3, f: f64) -> Self { self.map(|s| s.scale(c, f)) }
	fn mirror(self, o: DVec3, n: DVec3) -> Self { self.map(|s| s.mirror(o, n)) }
}

impl<T: SolidStruct, const N: usize> Compound for [T; N] {
	type Elem = T;
	fn iter_elem(&self) -> impl Iterator<Item = &T> + '_ { self.iter() }
	fn map_elem(self, f: impl FnMut(T) -> T) -> Self { self.map(f) }
}

// ==================== impl Wire for Vec<T> / [T; N] ====================
//
// Vec<Edge> is the wire representation in this library — these impls give
// `Vec<Edge>` and `[Edge; N]` the same Wire methods that single Edge has.

impl<T: EdgeStruct> Wire for Vec<T> {
	type Elem = T;
	fn iter_elem(&self) -> impl Iterator<Item = &T> + '_ { self.iter() }
	fn map_elem(self, f: impl FnMut(T) -> T) -> Self { self.into_iter().map(f).collect() }
}

impl<T: EdgeStruct, const N: usize> Wire for [T; N] {
	type Elem = T;
	fn iter_elem(&self) -> impl Iterator<Item = &T> + '_ { self.iter() }
	fn map_elem(self, f: impl FnMut(T) -> T) -> Self { self.map(f) }
}