-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathwem_bindings.cpp
More file actions
1326 lines (1176 loc) · 117 KB
/
Copy pathwem_bindings.cpp
File metadata and controls
1326 lines (1176 loc) · 117 KB
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
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 Fernando Sahmkow
//
// AUTOGENERATED by tools/codegen — do not edit by hand.
// Regenerate via: python -m tools.codegen.codegen wem --backend pybind11
//
// Source headers and `@bind` annotations live under include/whiteout/.
//
// Include order matters here:
// 1. pybind11/pybind11.h sets up the base library
// 2. project headers define whiteout::u32 et al used inside MAKE_OPAQUE
// 3. PYBIND11_MAKE_OPAQUE opts out of stl.h's auto-conversion for our vectors
// 4. pybind11/stl.h, stl_bind.h honor the opaque declarations
#include <pybind11/pybind11.h>
#include <array>
#include <cstdint>
#include <optional>
#include <sstream>
#include <string>
#include <vector>
#include <whiteout/vector_types.h>
#include <whiteout/models/wem/profile.h>
#include <whiteout/models/wem/diagnostics.h>
#include <whiteout/models/wem/bounds.h>
#include <whiteout/models/wem/asset_key.h>
#include <whiteout/models/wem/native_bag.h>
#include <whiteout/models/wem/materials/texture.h>
#include <whiteout/models/wem/materials/common.h>
#include <whiteout/models/wem/materials/features.h>
#include <whiteout/models/wem/materials/looks.h>
#include <whiteout/models/wem/materials/material.h>
#include <whiteout/models/wem/geometry/attributes.h>
#include <whiteout/models/wem/geometry/skin.h>
#include <whiteout/models/wem/geometry/mesh.h>
#include <whiteout/models/wem/nodes/emitters.h>
#include <whiteout/models/wem/nodes/fields.h>
#include <whiteout/models/wem/nodes/node.h>
#include <whiteout/models/wem/nodes/tree.h>
#include <whiteout/models/wem/anim/channel.h>
#include <whiteout/models/wem/anim/clip.h>
#include <whiteout/models/wem/physics/physics.h>
#include <whiteout/models/wem/model.h>
#include <whiteout/models/wem/document.h>
#include <whiteout/models/wem/parser.h>
#include <whiteout/models/wem/writer.h>
#include <whiteout/models/wem/validate.h>
#include <whiteout/interfaces.h>
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::f32>);
PYBIND11_MAKE_OPAQUE(std::vector<std::string>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::u8>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::u32>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::Vector2f>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::Vector3f>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::AnimChannel>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::AnimSet>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::AnimTag>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::AssetKey>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::Clip>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::ClipEvent>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::ClipTrackSet>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::Cloth>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::ClothBinding>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::ClothCollider>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::CombinerStage>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::CompositeLayer>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::Diagnostic>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::Diagnostics::CodeCount>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::Look>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::Material>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::MaterialFeature>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::Mesh>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::MeshSection>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::Model>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::NativeKind>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::Node>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::PhysicsBody>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::PhysicsJoint>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::PhysicsRig>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::PhysicsShape>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::PoseSchema>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::ProfileId>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::ProfileMaterialSet>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::Sc2Property<whiteout::Vector3f>>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::Sc2RibbonSplinePoint>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::SlotBinding>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::SubTrack>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::SubTrackContainer>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::TextureRef>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::TrackSet>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::Transform>);
PYBIND11_MAKE_OPAQUE(std::vector<whiteout::models::wem::UnknownChunk>);
#include <pybind11/stl.h>
#include <pybind11/stl_bind.h>
#include <pybind11/operators.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
namespace {
// Buffer-protocol vector wrapper for std::vector<Elem> where Elem is laid
// out as `Components` contiguous Scalars (Vector3f, Quaternion, ColorBGRA…).
//
// pybind11's bind_vector<> wires up py::buffer_protocol() automatically,
// but stl_bind.h's auto-buffer-info path static-asserts on element types
// lacking a format_descriptor — Vector3f/Quaternion don't have one.
// We provide the same Python surface (append, extend, clear, __getitem__,
// __setitem__, __iter__, __len__, __bool__) plus a 2D buffer view.
template <typename Elem, typename Scalar, py::ssize_t Components>
auto bindBufferVector(py::module_& m, const char* name) {
using Vec = std::vector<Elem>;
py::class_<Vec> cls(m, name, py::buffer_protocol());
cls.def(py::init<>());
cls.def("__len__", [](const Vec& v) { return v.size(); });
cls.def("__bool__", [](const Vec& v) { return !v.empty(); });
cls.def("__getitem__", [](const Vec& v, std::size_t i) -> Elem {
if (i >= v.size()) throw py::index_error();
return v[i];
});
cls.def("__setitem__", [](Vec& v, std::size_t i, const Elem& val) {
if (i >= v.size()) throw py::index_error();
v[i] = val;
});
cls.def("__iter__", [](Vec& v) {
return py::make_iterator(v.begin(), v.end());
}, py::keep_alive<0, 1>());
cls.def("append", [](Vec& v, const Elem& val) { v.push_back(val); });
cls.def("extend", [](Vec& v, const Vec& o) {
v.insert(v.end(), o.begin(), o.end());
});
cls.def("clear", &Vec::clear);
cls.def_buffer([](Vec& v) -> py::buffer_info {
if constexpr (Components == 1) {
return py::buffer_info(
v.data(),
static_cast<py::ssize_t>(sizeof(Scalar)),
py::format_descriptor<Scalar>::format(),
1,
{ static_cast<py::ssize_t>(v.size()) },
{ static_cast<py::ssize_t>(sizeof(Scalar)) });
} else {
return py::buffer_info(
v.data(),
static_cast<py::ssize_t>(sizeof(Scalar)),
py::format_descriptor<Scalar>::format(),
2,
{ static_cast<py::ssize_t>(v.size()), Components },
{ static_cast<py::ssize_t>(sizeof(Elem)),
static_cast<py::ssize_t>(sizeof(Scalar)) });
}
});
return cls;
}
} // namespace
void bind_wem_1(py::module_& m);
void bind_wem_2(py::module_& m);
void bind_wem_3(py::module_& m);
void bind_wem(py::module_& m) {
py::enum_<whiteout::models::wem::ProfileId>(m, "ProfileId")
.value("GENERIC", whiteout::models::wem::ProfileId::Generic, R"doc(No native blocks: authored, retargeted, or neutrally imported.)doc")
.value("WC3_CLASSIC", whiteout::models::wem::ProfileId::Wc3Classic, R"doc(Warcraft III, SD shading (`.mdx`, `is_hd == false` layers).)doc")
.value("WC3_REFORGED", whiteout::models::wem::ProfileId::Wc3Reforged, R"doc(Warcraft III, HD shading (`.mdx`, `is_hd == true` layers).)doc")
.value("WOW", whiteout::models::wem::ProfileId::Wow, R"doc(World of Warcraft (`.m2`).)doc")
.value("SC2", whiteout::models::wem::ProfileId::Sc2, R"doc(StarCraft II (`.m3`, MODL v23–v29).)doc")
.value("HEROES", whiteout::models::wem::ProfileId::Heroes, R"doc(Heroes of the Storm (`.m3`, MODL v30+ — MADD available).)doc")
.value("DIABLO3", whiteout::models::wem::ProfileId::Diablo3, R"doc(Diablo III (`.app` / `.acr`).)doc")
.value("COUNT", whiteout::models::wem::ProfileId::Count)
;
py::enum_<whiteout::models::wem::Game>(m, "Game", R"doc(The game a profile belongs to. Classic, Reforged and Generic are one game — Generic is drawn as Reforged — and so are StarCraft II and Heroes: what depends on the game (a clip's read rule, the storage policies) follows this, never a profile compared by hand.)doc")
.value("WARCRAFT", whiteout::models::wem::Game::Warcraft)
.value("STAR_CRAFT", whiteout::models::wem::Game::StarCraft)
.value("WOW", whiteout::models::wem::Game::Wow)
.value("DIABLO", whiteout::models::wem::Game::Diablo)
;
py::enum_<whiteout::models::wem::CoordSpace>(m, "CoordSpace", R"doc(The authoring space a format's geometry arrives in.
WEM's canonical space is `Blizzard` (+X forward, +Y left, +Z up, right-handed, CCW front faces). Every importer rebases into it and records `sourceSpace` so the exporter can rebase back. For `Sc2` that rebase is a 90° yaw — `new = (-old.y, old.x, old.z)`, determinant +1, so a rotation and not a mirror: winding is preserved and no index reversal is needed. It is an axis permutation with sign flips, so it is bit-exact in floating point.)doc")
.value("BLIZZARD", whiteout::models::wem::CoordSpace::Blizzard, R"doc(+X forward, +Y left, +Z up.)doc")
.value("SC2", whiteout::models::wem::CoordSpace::Sc2, R"doc(-Y forward, +X left, +Z up (Max's basis verbatim).)doc")
;
py::enum_<whiteout::models::wem::Handedness>(m, "Handedness")
.value("RIGHT", whiteout::models::wem::Handedness::Right)
.value("LEFT", whiteout::models::wem::Handedness::Left)
;
py::enum_<whiteout::models::wem::RigConvention>(m, "RigConvention", R"doc(How a format's rig states the bind pose, and therefore what a node's TRS animation channels mean.
The two are one fact, not two: every format that ships a per-bone inverse bind matrix keys the bone's local transform absolutely, and every format that does not folds the bind into the node's own composition and keys an offset from it. All four agree, so naming them separately would only create a fifth combination nothing writes.
This is the fact `RetargetSkeleton` needs and could not previously ask for, and it is why `.m3 -> .mdx` used to write a skeleton of zero pivots.)doc")
.value("PIVOT_RELATIVE", whiteout::models::wem::RigConvention::PivotRelative, R"doc(MDX, `.m2`. The inverse bind is the identity: the mesh is stored in the bind pose and a node at rest contributes nothing.
A node composes `T(-pivot) * S * R * T(pivot + t)`, so a TRS channel is an **offset** and the pivot is a parameter of the composition rather than a transform of its own. The linear part is therefore always `diag(s) * R` — the format cannot hold shear, which is what makes the retarget into one of these the interesting direction.)doc")
.value("EXPLICIT_BIND", whiteout::models::wem::RigConvention::ExplicitBind, R"doc(`.m3` (IREF), D3 (`tTransform4`). One inverse model-space bind matrix per bone, shipped beside the rest chain and **not** derivable from it — on `Marine.m3` the two disagree by 2.2 units.
A node composes `S * R * T` from its channels directly, and `pivot` is zero.)doc")
;
py::enum_<whiteout::models::wem::WindingOrder>(m, "WindingOrder")
.value("COUNTER_CLOCKWISE", whiteout::models::wem::WindingOrder::CounterClockwise)
.value("CLOCKWISE", whiteout::models::wem::WindingOrder::Clockwise)
;
py::enum_<whiteout::models::wem::IndexWidth>(m, "IndexWidth")
.value("U16", whiteout::models::wem::IndexWidth::U16)
.value("U32", whiteout::models::wem::IndexWidth::U32)
;
py::enum_<whiteout::models::wem::BlendMode>(m, "BlendMode", R"doc(Rendering blend mode — the superset of MDX filterMode, M2 blendingMode and M3 BlendMode.
Enumerator order is load-bearing: it is the on-disk value in both the v2 format and v3's `CommonMaterial::blend`, so entries are appended, never reordered.)doc")
.value("OPAQUE", whiteout::models::wem::BlendMode::Opaque, R"doc(MDX None(0), M2 Opaque(0), M3 Opaque(0))doc")
.value("ALPHA_KEY", whiteout::models::wem::BlendMode::AlphaKey, R"doc(M2 AlphaKey(1))doc")
.value("ALPHA_BLEND", whiteout::models::wem::BlendMode::AlphaBlend, R"doc(MDX Blend(2), M2 Alpha(2), M3 AlphaBlend(1))doc")
.value("ADDITIVE", whiteout::models::wem::BlendMode::Additive, R"doc(MDX Additive(3), M2 NoAlphaAdd(3), M3 Add(2) — ONE,ONE)doc")
.value("ADDITIVE_ALPHA", whiteout::models::wem::BlendMode::AdditiveAlpha, R"doc(MDX AddAlpha(4), M2 Add(4), M3 AlphaAdd(3) — SRC_ALPHA,ONE)doc")
.value("MODULATE", whiteout::models::wem::BlendMode::Modulate, R"doc(MDX Modulate(5), M2 Mod(5), M3 Mod(4))doc")
.value("MODULATE2X", whiteout::models::wem::BlendMode::Modulate2x, R"doc(MDX Modulate2x(6), M2 Mod2x(6), M3 Mod2x(5))doc")
.value("BLEND_ADD", whiteout::models::wem::BlendMode::BlendAdd, R"doc(M2 BlendAdd(7))doc")
.value("TRANSPARENT", whiteout::models::wem::BlendMode::Transparent, R"doc(MDX Transparent(1))doc")
.value("PREMULTIPLIED_ALPHA", whiteout::models::wem::BlendMode::PremultipliedAlpha, R"doc(D3's premultiplied family (SrcBlend One) — see P6)doc")
;
py::enum_<whiteout::models::wem::CullMode>(m, "CullMode")
.value("BACK", whiteout::models::wem::CullMode::Back, R"doc(The default everywhere; front faces are CCW.)doc")
.value("FRONT", whiteout::models::wem::CullMode::Front, R"doc(WoW's mirrored-actor case.)doc")
.value("NONE", whiteout::models::wem::CullMode::None, R"doc(Two-sided.)doc")
;
py::enum_<whiteout::models::wem::MaterialKind>(m, "MaterialKind", R"doc(The shading model a common material contracts to (§7.2).
The kind *is* the contract a generic consumer renders by, which is why it discriminates the body rather than being an advisory tag.)doc")
.value("COMPOSITE", whiteout::models::wem::MaterialKind::Composite, R"doc(Ordered layer stack over named surface channels, lit forward.)doc")
.value("COMBINERS", whiteout::models::wem::MaterialKind::Combiners, R"doc(Fixed-function stage chain in one draw.)doc")
.value("LEGACY_DEFERRED", whiteout::models::wem::MaterialKind::LegacyDeferred, R"doc(Named-slot G-buffer, legacy spec/gloss lighting.)doc")
.value("PBR_DEFERRED", whiteout::models::wem::MaterialKind::PBRDeferred, R"doc(Named-slot G-buffer, PBR lighting.)doc")
.value("COUNT", whiteout::models::wem::MaterialKind::Count)
;
py::enum_<whiteout::models::wem::NativeKind>(m, "NativeKind", R"doc(Which native material record a profile's materials may carry (§6.1).
Named for the **format**, because that is what a native block is — a record of what the file held. The profile is named for the game. Same split as `D3ModelAdapter` vs `Diablo3Profile`.)doc")
.value("NONE", whiteout::models::wem::NativeKind::None, R"doc(`Generic` only; the variant must be `monostate`.)doc")
.value("MDX", whiteout::models::wem::NativeKind::Mdx, R"doc(`Wc3Classic`, `Wc3Reforged`.)doc")
.value("M2", whiteout::models::wem::NativeKind::M2, R"doc(`Wow`.)doc")
.value("M3", whiteout::models::wem::NativeKind::M3, R"doc(`Sc2`, `Heroes`.)doc")
.value("D3", whiteout::models::wem::NativeKind::D3, R"doc(`Diablo3`.)doc")
;
py::enum_<whiteout::models::wem::Severity>(m, "Severity")
.value("INFO", whiteout::models::wem::Severity::Info, R"doc(Something worth recording that cost nothing.)doc")
.value("WARNING", whiteout::models::wem::Severity::Warning, R"doc(Data was lost or approximated; the result is still usable.)doc")
.value("ERROR", whiteout::models::wem::Severity::Error, R"doc(The operation did not produce a valid result.)doc")
;
py::enum_<whiteout::models::wem::DiagCode>(m, "DiagCode", R"doc(What kind of thing happened — the groupable, assertable half of a report.
Codes are grouped by area and never renumbered once shipped: the recorded expected-loss lists under `tests/data/wem/` are keyed by name, and the histogram of a corpus sweep is a reviewable diff. Add new codes at the end of their group.)doc")
.value("UNSPECIFIED", whiteout::models::wem::DiagCode::Unspecified)
.value("NON_MANIFOLD_EDGE_SPLIT", whiteout::models::wem::DiagCode::NonManifoldEdgeSplit, R"doc(An edge with >2 incident faces; the vertex was split.)doc")
.value("NON_MANIFOLD_VERTEX_SPLIT", whiteout::models::wem::DiagCode::NonManifoldVertexSplit, R"doc(A bowtie vertex; one copy per fan.)doc")
.value("DEGENERATE_FACE_DROPPED", whiteout::models::wem::DiagCode::DegenerateFaceDropped, R"doc(A face with repeated or collinear-to-zero-area corners.)doc")
.value("DUPLICATE_FACE_DROPPED", whiteout::models::wem::DiagCode::DuplicateFaceDropped, R"doc(The same corner set already had a face.)doc")
.value("INCONSISTENT_WINDING_FLIPPED", whiteout::models::wem::DiagCode::InconsistentWindingFlipped, R"doc(A face was reversed to agree with its neighbours.)doc")
.value("NGON_TRIANGULATED", whiteout::models::wem::DiagCode::NgonTriangulated, R"doc(An n-gon was cut into the triangles it is drawn as.)doc")
.value("ISOLATED_VERTEX_DROPPED", whiteout::models::wem::DiagCode::IsolatedVertexDropped, R"doc(A vertex no face referenced.)doc")
.value("CONNECTIVITY_CORRUPT", whiteout::models::wem::DiagCode::ConnectivityCorrupt, R"doc(opposite/next/prev/face relations disagree.)doc")
.value("INDEX_OUT_OF_RANGE", whiteout::models::wem::DiagCode::IndexOutOfRange, R"doc(A section, material slot or attribute index is out of range.)doc")
.value("ATTRIBUTE_COUNT_MISMATCH", whiteout::models::wem::DiagCode::AttributeCountMismatch, R"doc(A layer's element count disagrees with its domain.)doc")
.value("SKIN_BINDING_MALFORMED", whiteout::models::wem::DiagCode::SkinBindingMalformed, R"doc(`offsets` not monotonic, or not sized vertexCount + 1.)doc")
.value("NON_CANONICAL_NUMBERING", whiteout::models::wem::DiagCode::NonCanonicalNumbering, R"doc(Halfedges or edges numbered as no build of the face set would.)doc")
.value("TRIANGULATION_MALFORMED", whiteout::models::wem::DiagCode::TriangulationMalformed, R"doc(The stored triangulation's table is mis-shaped, or a triangle has a row.)doc")
.value("STALE_TRIANGULATION", whiteout::models::wem::DiagCode::StaleTriangulation, R"doc(A face's stored row is not a valid cut of it; the automatic rule draws it.)doc")
.value("DUPLICATE_EDGE", whiteout::models::wem::DiagCode::DuplicateEdge, R"doc(Two edges join one pair of vertices; a rebuild cannot hold them.)doc")
.value("RESERVED_LAYER_MISTYPED", whiteout::models::wem::DiagCode::ReservedLayerMistyped, R"doc(A reserved layer name on the wrong domain or with the wrong type.)doc")
.value("LOSSY_BLEND_MODE", whiteout::models::wem::DiagCode::LossyBlendMode, R"doc(The target has no equivalent; the nearest was written.)doc")
.value("DROPPED_NATIVE_BLOCK", whiteout::models::wem::DiagCode::DroppedNativeBlock, R"doc(A native material block did not survive an operation.)doc")
.value("SLOT_NOT_BOUND", whiteout::models::wem::DiagCode::SlotNotBound, R"doc(A material slot has no binding in a set that needs one.)doc")
.value("MIXED_SHADED_UNSHADED_STACK", whiteout::models::wem::DiagCode::MixedShadedUnshadedStack, R"doc(A WC3 stack mixes lit and unlit layers (§7.2.1).)doc")
.value("SCENE_READING_MULTI_LAYER_STACK", whiteout::models::wem::DiagCode::SceneReadingMultiLayerStack, R"doc(Multi-layer stack over a scene-reading fold (§7.2.1).)doc")
.value("COLLAPSE_REFUSED", whiteout::models::wem::DiagCode::CollapseRefused, R"doc(The §7.2.2 collapse was ineligible (first layer not opaque).)doc")
.value("UNKNOWN_SHADER_COMBO", whiteout::models::wem::DiagCode::UnknownShaderCombo, R"doc(An `.m2` shaderId past the transcribed rows (never row 0).)doc")
.value("UNSUPPORTED_MATERIAL_KIND", whiteout::models::wem::DiagCode::UnsupportedMaterialKind, R"doc(The target profile's `commonKinds` excludes this kind.)doc")
.value("LOSSY_KIND_CONVERSION", whiteout::models::wem::DiagCode::LossyKindConversion, R"doc(A kind change dropped state (§7.2.2 collapse, `Flatten`).)doc")
.value("FEATURE_DROPPED", whiteout::models::wem::DiagCode::FeatureDropped, R"doc(A §7.2.5 feature the target cannot express.)doc")
.value("LAYER_DROPPED", whiteout::models::wem::DiagCode::LayerDropped, R"doc(A layer past the target's capacity or vocabulary.)doc")
.value("TEXTURE_UNRESOLVED", whiteout::models::wem::DiagCode::TextureUnresolved, R"doc(A texture reference names nothing the document holds.)doc")
.value("MATERIAL_SLOT_UNUSED", whiteout::models::wem::DiagCode::MaterialSlotUnused, R"doc(A slot no section binds — informational on import.)doc")
.value("MATERIAL_BODY_INVALID", whiteout::models::wem::DiagCode::MaterialBodyInvalid, R"doc(A §7.2 body invariant: repeated slot, Orm with the unpacked three, two features of a kind on one layer.)doc")
.value("LOOK_BINDING_MALFORMED", whiteout::models::wem::DiagCode::LookBindingMalformed, R"doc(`SlotBinding::byLook` is not sized to the `LookTable`.)doc")
.value("LOOK_DROPPED", whiteout::models::wem::DiagCode::LookDropped, R"doc(A look the target profile has no table for (§6.6).)doc")
.value("TEAM_PLATE_COVERED", whiteout::models::wem::DiagCode::TeamPlateCovered, R"doc(A team plate under an opaque-alpha texture never showed; dropped.)doc")
.value("TEAM_PLATE_UNLIT", whiteout::models::wem::DiagCode::TeamPlateUnlit, R"doc(The unlit team plate is lit with the texture over it (R2).)doc")
.value("SHADED_ADDITIVE_FOLDED", whiteout::models::wem::DiagCode::ShadedAdditiveFolded, R"doc(A lit additive pass went to an emissive slot, which is unlit.)doc")
.value("PASS_FLAGS_FOLDED", whiteout::models::wem::DiagCode::PassFlagsFolded, R"doc(A pass's two-sided / fog flag differs from the material's.)doc")
.value("COVERAGE_CLIPPED", whiteout::models::wem::DiagCode::CoverageClipped, R"doc(A later pass is confined to a keyed or blended base's coverage.)doc")
.value("BASE_FADE_SHARED", whiteout::models::wem::DiagCode::BaseFadeShared, R"doc(The base pass's alpha fades every pass folded onto it.)doc")
.value("LIT_FOLD", whiteout::models::wem::DiagCode::LitFold, R"doc(An unlit blend pass went to the decal, which is lit.)doc")
.value("UNLIT_FOLD", whiteout::models::wem::DiagCode::UnlitFold, R"doc(A lit blend pass went to an emissive slot, which is unlit.)doc")
.value("DOUBLE_LIT", whiteout::models::wem::DiagCode::DoubleLit, R"doc(A lit modulate pass (lit twice in WC3) folded once.)doc")
.value("LIT_ENV_FOLDED", whiteout::models::wem::DiagCode::LitEnvFolded, R"doc(A lit sphere-map pass went to the env layer, which is unlit.)doc")
.value("SOFT_KEY", whiteout::models::wem::DiagCode::SoftKey, R"doc(A keyed pass over a base became a lerp by its gradient alpha.)doc")
.value("MOD_ALPHA_FOLDED", whiteout::models::wem::DiagCode::ModAlphaFolded, R"doc(A modulate pass's animated alpha has no exact layer home.)doc")
.value("DEPTH_FLAGS_DROPPED", whiteout::models::wem::DiagCode::DepthFlagsDropped, R"doc(NoDepthTest / NoDepthSet: no StarCraft II material says them.)doc")
.value("FLIPBOOK_DROPPED", whiteout::models::wem::DiagCode::FlipbookDropped, R"doc(A texture-id track (KMTF) kept only its first frame.)doc")
.value("COMPOSITE_EMITTED", whiteout::models::wem::DiagCode::CompositeEmitted, R"doc(The stack needed N > 1 materials; a CMP_ carries them.)doc")
.value("PASS_ORDER_FOLDED", whiteout::models::wem::DiagCode::PassOrderFolded, R"doc(A blend or modulate pass folded BEFORE an additive one it followed.)doc")
.value("FRESNEL_FOLDED", whiteout::models::wem::DiagCode::FresnelFolded, R"doc(A rim overlay folded approximately: one lerp, or an undimmed base.)doc")
.value("PROFILE_NOT_CARRIED", whiteout::models::wem::DiagCode::ProfileNotCarried, R"doc(Export asked for a profile the document does not have.)doc")
.value("PROFILE_COVERAGE_INCOMPLETE", whiteout::models::wem::DiagCode::ProfileCoverageIncomplete, R"doc(A carried profile has no material for some slot (§6.3).)doc")
.value("BONE_INFLUENCE_LIMIT", whiteout::models::wem::DiagCode::BoneInfluenceLimit, R"doc(More influences than `maxBoneInfluences`; the tail was cut.)doc")
.value("UV_SET_LIMIT", whiteout::models::wem::DiagCode::UvSetLimit, R"doc(More UV sets than `maxUvSets`.)doc")
.value("INDEX_WIDTH_EXCEEDED", whiteout::models::wem::DiagCode::IndexWidthExceeded, R"doc(Vertex count exceeds `indexWidth`.)doc")
.value("NGON_UNSUPPORTED", whiteout::models::wem::DiagCode::NgonUnsupported, R"doc(Retired: never raised. Every exporter cuts n-gons into triangles.)doc")
.value("VERTEX_COLOR_UNSUPPORTED", whiteout::models::wem::DiagCode::VertexColorUnsupported, R"doc(The profile has no vertex colour and a layer carries one.)doc")
.value("NATIVE_KIND_PROFILE_MISMATCH", whiteout::models::wem::DiagCode::NativeKindProfileMismatch, R"doc(A native block's kind disagrees with its set's profile (§6.5).)doc")
.value("BONE_PALETTE_LIMIT", whiteout::models::wem::DiagCode::BonePaletteLimit, R"doc(A section needs more bones than `maxBonesPerPalette`.)doc")
.value("GEOMETRY_MISMATCH", whiteout::models::wem::DiagCode::GeometryMismatch, R"doc(`AddProfileFromImport` geometry did not match (§6.6).)doc")
.value("SECTION_UNDRAWN", whiteout::models::wem::DiagCode::SectionUndrawn, R"doc(A section whose profile mask is empty — nothing draws it.)doc")
.value("NODE_REMOVAL_REFUSED", whiteout::models::wem::DiagCode::NodeRemovalRefused, R"doc(`SkinPolicy::Refuse` and the node had influences.)doc")
.value("SKIN_INFLUENCE_REASSIGNED", whiteout::models::wem::DiagCode::SkinInfluenceReassigned, R"doc(`SkinPolicy::ReassignToParent` moved weights up (§10.6).)doc")
.value("DANGLING_NODE_REFERENCE", whiteout::models::wem::DiagCode::DanglingNodeReference, R"doc(A referencer pointed at a node that no longer exists.)doc")
.value("BIND_POSE_RECOMPOSED", whiteout::models::wem::DiagCode::BindPoseRecomposed, R"doc(`preserveWorld` rebuilt a survivor's local transform.)doc")
.value("RIG_CONVENTION_CHANGED", whiteout::models::wem::DiagCode::RigConventionChanged, R"doc(`RetargetSkeleton` restated the tree's bind convention.)doc")
.value("BONE_SHEAR_SPLIT", whiteout::models::wem::DiagCode::BoneShearSplit, R"doc(A bind frame sheared; a helper parent carries the stretch.)doc")
.value("BONE_SHEAR_PROJECTED", whiteout::models::wem::DiagCode::BoneShearProjected, R"doc(The shear was projected away — the pose is approximate.)doc")
.value("NON_UNIFORM_SCALE_FLATTENED", whiteout::models::wem::DiagCode::NonUniformScaleFlattened, R"doc(A target holding one scale float got the x component.)doc")
.value("NODE_KIND_NOT_CARRIED", whiteout::models::wem::DiagCode::NodeKindNotCarried, R"doc(A node kind the profile does not carry (§10.9).)doc")
.value("MIXED_INTERPOLATION_IN_TRACK", whiteout::models::wem::DiagCode::MixedInterpolationInTrack, R"doc(A sub-track mixes interpolation modes (§10.8.2).)doc")
.value("ANIM_CHANNEL_INVALIDATED", whiteout::models::wem::DiagCode::AnimChannelInvalidated, R"doc(A channel's target was removed.)doc")
.value("CLIP_TARGET_MISSING", whiteout::models::wem::DiagCode::ClipTargetMissing, R"doc(A clip references a model or set that is absent.)doc")
.value("ANIM_TRACK_DROPPED", whiteout::models::wem::DiagCode::AnimTrackDropped, R"doc(A track the target format cannot express.)doc")
.value("ANIM_TRACK_APPROXIMATED", whiteout::models::wem::DiagCode::AnimTrackApproximated, R"doc(A track was written, but not as it was held.)doc")
.value("ANIM_CLIP_RETIMED", whiteout::models::wem::DiagCode::AnimClipRetimed, R"doc(A clip was placed on a timeline it did not come from.)doc")
.value("ANIM_TCB_BAKED", whiteout::models::wem::DiagCode::AnimTcbBaked, R"doc(TCB parameters written as the Hermite tangents they produce.)doc")
.value("TRACK_SET_BELOW_DEFAULT", whiteout::models::wem::DiagCode::TrackSetBelowDefault, R"doc(A track set's layer does not outrank the layers it splits from.)doc")
.value("GLOBAL_AND_CLIP_CHANNEL", whiteout::models::wem::DiagCode::GlobalAndClipChannel, R"doc(A channel a global loop keys is keyed by another clip too.)doc")
.value("ANIM_TRACK_RESAMPLED", whiteout::models::wem::DiagCode::AnimTrackResampled, R"doc(A track crossing to another read rule got keys to play the same.)doc")
.value("ANIM_LAYERS_FLATTENED", whiteout::models::wem::DiagCode::AnimLayersFlattened, R"doc(A clip's layers were baked into one for a format that plays one.)doc")
.value("ANIM_STAGE_BAKED", whiteout::models::wem::DiagCode::AnimStageBaked, R"doc(A pose stage was baked into keys on the nodes it drives.)doc")
.value("ANIM_STAGE_NOT_BAKED", whiteout::models::wem::DiagCode::AnimStageNotBaked, R"doc(A pose stage needs a host's physics and was not baked.)doc")
.value("CLIP_RULE_CONVERTED", whiteout::models::wem::DiagCode::ClipRuleConverted, R"doc(A clip's keys were rewritten for another game's read rule.)doc")
.value("ANIM_STAGE_INVALID", whiteout::models::wem::DiagCode::AnimStageInvalid, R"doc(A pose stage out of order, or with sources it cannot tell apart.)doc")
.value("EVENT_PAYLOAD_MISMATCH", whiteout::models::wem::DiagCode::EventPayloadMismatch, R"doc(`ActorEvent` kind and payload group disagree (§9.5).)doc")
.value("ASSET_UNRESOLVED", whiteout::models::wem::DiagCode::AssetUnresolved, R"doc(An `AssetKey` the `AssetSource` could not load.)doc")
.value("HARDPOINT_UNRESOLVED", whiteout::models::wem::DiagCode::HardpointUnresolved, R"doc(A hardpoint names a node the model does not have.)doc")
.value("UNKNOWN_CHUNK_PRESERVED", whiteout::models::wem::DiagCode::UnknownChunkPreserved, R"doc(An unrecognised chunk was carried through unchanged.)doc")
.value("ORPHAN_CHUNK", whiteout::models::wem::DiagCode::OrphanChunk, R"doc(A preserved chunk's owner is gone (§11.4).)doc")
.value("UNSUPPORTED_VERSION", whiteout::models::wem::DiagCode::UnsupportedVersion, R"doc(A file version this build cannot read.)doc")
.value("LEGACY_DOCUMENT_UPGRADED", whiteout::models::wem::DiagCode::LegacyDocumentUpgraded, R"doc(A v2 file was read through the compatibility path (§11.5).)doc")
.value("OPERATION_UNSUPPORTED", whiteout::models::wem::DiagCode::OperationUnsupported, R"doc(The converter does not implement this direction at all.)doc")
.value("GEOMETRY_RESCALED", whiteout::models::wem::DiagCode::GeometryRescaled, R"doc(Every length in the document was restated at another scale.)doc")
.value("LEVEL_OF_DETAIL_DROPPED", whiteout::models::wem::DiagCode::LevelOfDetailDropped, R"doc(A mesh above the base level of detail was not carried.)doc")
.value("SKIN_PROFILE_NOT_CARRIED", whiteout::models::wem::DiagCode::SkinProfileNotCarried, R"doc(Another game's file got one skin profile or geoset set of several.)doc")
.value("SKIN_INFLUENCE_DUPLICATED", whiteout::models::wem::DiagCode::SkinInfluenceDuplicated, R"doc(One vertex names a bone twice.)doc")
.value("SKIN_WEIGHT_INVALID", whiteout::models::wem::DiagCode::SkinWeightInvalid, R"doc(A weight is negative, NaN or infinite.)doc")
.value("SKIN_INFLUENCES_UNSORTED", whiteout::models::wem::DiagCode::SkinInfluencesUnsorted, R"doc(A vertex's influences are not heaviest first.)doc")
.value("SKIN_SETUP_INVALID", whiteout::models::wem::DiagCode::SkinSetupInvalid, R"doc(The saved skin setup disagrees with the document (§13.4).)doc")
.value("PHYSICS_CONSTRAINT_DROPPED", whiteout::models::wem::DiagCode::PhysicsConstraintDropped, R"doc(A `PHCT` on import: none ships and the client never reads it.)doc")
.value("PHYSICS_VERSION_REFUSED", whiteout::models::wem::DiagCode::PhysicsVersionRefused, R"doc(A chunk version the client refuses (`WRP_` v0); dropped.)doc")
.value("PHYSICS_MATERIAL_MERGED", whiteout::models::wem::DiagCode::PhysicsMaterialMerged, R"doc(A body's shapes disagree on material; the first shape's wins.)doc")
.value("PHYSICS_GRAVITY_SCALE_DROPPED", whiteout::models::wem::DiagCode::PhysicsGravityScaleDropped, R"doc(A body's gravity scale is not 1, which StarCraft II cannot say.)doc")
.value("PHYSICS_JOINT_KIND_UNSUPPORTED", whiteout::models::wem::DiagCode::PhysicsJointKindUnsupported, R"doc(A joint kind the target has no record for; not written.)doc")
.value("PHYSICS_JOINT_FIELD_DROPPED", whiteout::models::wem::DiagCode::PhysicsJointFieldDropped, R"doc(A joint field the target cannot say (springs, breaking, rest).)doc")
.value("PHYSICS_JOINT_BODY_AMBIGUOUS", whiteout::models::wem::DiagCode::PhysicsJointBodyAmbiguous, R"doc(A joint body is not the first on its node, which the target binds.)doc")
.value("PHYSICS_HULL_SIMPLIFIED", whiteout::models::wem::DiagCode::PhysicsHullSimplified, R"doc(A hull merged faces to fit the cooked tables' limits.)doc")
.value("PHYSICS_REFERENCE_INVALID", whiteout::models::wem::DiagCode::PhysicsReferenceInvalid, R"doc(A record names a node, body, section or collider that is not there.)doc")
.value("PHYSICS_JOINT_SNAPS", whiteout::models::wem::DiagCode::PhysicsJointSnaps, R"doc(A joint's two frames disagree at rest; it snaps on the first step.)doc")
.value("PHYSICS_SHAPE_DEGENERATE", whiteout::models::wem::DiagCode::PhysicsShapeDegenerate, R"doc(A hull with no volume, or a mesh with no triangle.)doc")
.value("PHYSICS_UNSUPPORTED", whiteout::models::wem::DiagCode::PhysicsUnsupported, R"doc(A shape kind, joint kind or cloth the target profile does not carry.)doc")
.value("CLOTH_PARTICLE_LIMIT", whiteout::models::wem::DiagCode::ClothParticleLimit, R"doc(A cage outside the target's particle range.)doc")
.value("CLOTH_ANCHOR_BONE_OUT_OF_RANGE", whiteout::models::wem::DiagCode::ClothAnchorBoneOutOfRange, R"doc(A cloth anchor names a bone past the byte the target stores.)doc")
.value("CLOTH_SECTION_SPLIT", whiteout::models::wem::DiagCode::ClothSectionSplit, R"doc(A cloth section needs more bones than one region's palette.)doc")
.value("CLOTH_TOPOLOGY_INVALID", whiteout::models::wem::DiagCode::ClothTopologyInvalid, R"doc(A cage not flagged, a binding in another mesh, a lane past the cage.)doc")
.value("COUNT", whiteout::models::wem::DiagCode::Count)
;
py::enum_<whiteout::models::wem::ElementKind>(m, "ElementKind")
.value("NONE", whiteout::models::wem::ElementKind::None)
.value("DOCUMENT", whiteout::models::wem::ElementKind::Document)
.value("MESH", whiteout::models::wem::ElementKind::Mesh)
.value("FACE", whiteout::models::wem::ElementKind::Face)
.value("VERTEX", whiteout::models::wem::ElementKind::Vertex)
.value("HALFEDGE", whiteout::models::wem::ElementKind::Halfedge)
.value("EDGE", whiteout::models::wem::ElementKind::Edge)
.value("SECTION", whiteout::models::wem::ElementKind::Section)
.value("NODE", whiteout::models::wem::ElementKind::Node)
.value("MATERIAL", whiteout::models::wem::ElementKind::Material)
.value("LAYER", whiteout::models::wem::ElementKind::Layer)
.value("SLOT", whiteout::models::wem::ElementKind::Slot)
.value("TEXTURE", whiteout::models::wem::ElementKind::Texture)
.value("LOOK", whiteout::models::wem::ElementKind::Look)
.value("FEATURE", whiteout::models::wem::ElementKind::Feature)
.value("ACTOR", whiteout::models::wem::ElementKind::Actor)
.value("EVENT", whiteout::models::wem::ElementKind::Event)
.value("CLIP", whiteout::models::wem::ElementKind::Clip)
.value("CHANNEL", whiteout::models::wem::ElementKind::Channel)
.value("TRACK", whiteout::models::wem::ElementKind::Track)
.value("CHUNK", whiteout::models::wem::ElementKind::Chunk)
.value("PHYSICS_RECORD", whiteout::models::wem::ElementKind::PhysicsRecord, R"doc(A `PhysicsSet` record, by id.)doc")
;
py::enum_<whiteout::models::wem::ColorSpace>(m, "ColorSpace", R"doc(How a texture's samples are to be interpreted.
`Auto` is resolved from the **slot** the texture is used in, not from its filename: the filename guess misses 22.7% of `.m3` normal maps, and an sRGB view on a DXT5nm map mirrors the lighting. `AutoColorSpaceFor` in common.h is the resolution, one overload per slot vocabulary.)doc")
.value("AUTO", whiteout::models::wem::ColorSpace::Auto, R"doc(Decide from the slot (`AutoColorSpaceFor`).)doc")
.value("LINEAR", whiteout::models::wem::ColorSpace::Linear, R"doc(Data: normals, masks, ORM, roughness.)doc")
.value("SRGB", whiteout::models::wem::ColorSpace::Srgb, R"doc(Colour: base colour, emissive, diffuse.)doc")
;
py::enum_<whiteout::models::wem::WrapMode>(m, "WrapMode")
.value("REPEAT", whiteout::models::wem::WrapMode::Repeat)
.value("CLAMP", whiteout::models::wem::WrapMode::Clamp)
.value("MIRROR", whiteout::models::wem::WrapMode::Mirror)
;
py::enum_<whiteout::models::wem::TextureKeyKind>(m, "TextureKeyKind")
.value("NONE", whiteout::models::wem::TextureKeyKind::None, R"doc(The reference names nothing yet.)doc")
.value("PATH", whiteout::models::wem::TextureKeyKind::Path)
.value("FILE_DATA_ID", whiteout::models::wem::TextureKeyKind::FileDataId)
.value("SNO_ID", whiteout::models::wem::TextureKeyKind::SnoId)
;
py::enum_<whiteout::models::wem::FeatureKind>(m, "FeatureKind")
.value("FRESNEL", whiteout::models::wem::FeatureKind::Fresnel)
.value("UV_ANIMATION", whiteout::models::wem::FeatureKind::UvAnimation)
.value("LAYER_SHADING", whiteout::models::wem::FeatureKind::LayerShading)
.value("COUNT", whiteout::models::wem::FeatureKind::Count)
;
py::enum_<whiteout::models::wem::MaterialFlags>(m, "MaterialFlags")
.value("NONE", whiteout::models::wem::MaterialFlags::None)
.value("UNLIT", whiteout::models::wem::MaterialFlags::Unlit, R"doc(MDX Unshaded, M2 Unlit, M3; D3's tag 0xA000F.)doc")
.value("UNFOGGED", whiteout::models::wem::MaterialFlags::Unfogged, R"doc(MDX, M2.)doc")
.value("NO_SHADOW_CAST", whiteout::models::wem::MaterialFlags::NoShadowCast)
.value("NO_SHADOW_RECEIVE", whiteout::models::wem::MaterialFlags::NoShadowReceive)
.value("SORT_NEAR_Z", whiteout::models::wem::MaterialFlags::SortNearZ, R"doc(MDX SortPrimsNearZ — a draw-order hint, not a depth mode.)doc")
.value("SORT_FAR_Z", whiteout::models::wem::MaterialFlags::SortFarZ, R"doc(MDX SortPrimsFarZ.)doc")
.value("INVISIBLE", whiteout::models::wem::MaterialFlags::Invisible, R"doc(The surface exists and does not draw.
Not the same thing as a hidden *section* (`SectionFlags::Hidden`), which is why it is here: Diablo III's visibility bit is per (sub-object, look) and a material is per (slot, look), so the material is the only record with the right scope to hold it. Tyrael's two bodies and the Skeleton King's four are each other's alternates, and every one of them drew at once for as long as this lived only in the native block.)doc")
;
py::enum_<whiteout::models::wem::UVMappingMode>(m, "UVMappingMode", R"doc(How a texture's coordinates are produced. Revision 2's `UVMappingMode` had one enumerator per explicit UV set *and* the projections; the set is `TextureInput::uvSet` now, so this is only the projection family.)doc")
.value("EXPLICIT_UV", whiteout::models::wem::UVMappingMode::ExplicitUV, R"doc(Read `uvSet` from the mesh.)doc")
.value("ENV_SPHERE", whiteout::models::wem::UVMappingMode::EnvSphere, R"doc(MDX SphereEnvMap, M3 spherical environment.)doc")
.value("ENV_CUBE", whiteout::models::wem::UVMappingMode::EnvCube, R"doc(M3 cubic environment reflection.)doc")
;
py::enum_<whiteout::models::wem::SurfaceChannel>(m, "SurfaceChannel")
.value("COLOR", whiteout::models::wem::SurfaceChannel::Color)
.value("EMISSIVE", whiteout::models::wem::SurfaceChannel::Emissive)
.value("SPECULAR", whiteout::models::wem::SurfaceChannel::Specular)
.value("NORMAL", whiteout::models::wem::SurfaceChannel::Normal)
.value("AMBIENT_OCCLUSION", whiteout::models::wem::SurfaceChannel::AmbientOcclusion)
.value("ENVIRONMENT", whiteout::models::wem::SurfaceChannel::Environment, R"doc(M3's env layer family. Sampled per `TextureInput::mapping` (`EnvSphere` / `EnvCube`), scaled by `CompositeBody::environmentFactor`.)doc")
.value("COVERAGE", whiteout::models::wem::SurfaceChannel::Coverage, R"doc(Per-texel opacity — M3's alpha-mask layers, which are what StarCraft II blends and alpha-tests by (`cFinal.a = mask1.a * mask2.a * alphaFactor`; the diffuse alpha is the team mask there, never coverage). The fold reads each sample's **alpha** and multiplies; a channel-select mask is the native block's detail, like every other layer's select.)doc")
.value("GLOSS", whiteout::models::wem::SurfaceChannel::Gloss, R"doc(A specularity scale: M3's gloss (SpecularExponent) layer, whose sample's ALPHA squared scales `CompositeBody::specularExponent` per texel (psmaterial.fx MaterialSpecularity) and, under `simulateRoughness`, sets how blurred the environment reflection reads. Diablo III's `LegacySlot::Gloss`. Appended: a byte on disk.)doc")
.value("COUNT", whiteout::models::wem::SurfaceChannel::Count, R"doc(A specularity scale: M3's gloss (SpecularExponent) layer, whose sample's ALPHA squared scales `CompositeBody::specularExponent` per texel (psmaterial.fx MaterialSpecularity) and, under `simulateRoughness`, sets how blurred the environment reflection reads. Diablo III's `LegacySlot::Gloss`. Appended: a byte on disk.)doc")
;
py::enum_<whiteout::models::wem::CompositeOp>(m, "CompositeOp", R"doc(WC3's filter modes, verbatim — the superset in real use.
M3's own layer op field is near-signal-free in shipped content (mod and add cover it), so the richer vocabulary is the one worth keeping.)doc")
.value("SET", whiteout::models::wem::CompositeOp::Set, R"doc(Replace. WC3 None; the first layer of a channel.)doc")
.value("ALPHA_KEY", whiteout::models::wem::CompositeOp::AlphaKey, R"doc(Replace where `sample.a >= alphaTestThreshold`. WC3 Transparent.)doc")
.value("ALPHA_BLEND", whiteout::models::wem::CompositeOp::AlphaBlend, R"doc(Lerp by source alpha. WC3 Blend.)doc")
.value("ADD", whiteout::models::wem::CompositeOp::Add, R"doc(WC3 Additive; M3 add.)doc")
.value("ADD_ALPHA", whiteout::models::wem::CompositeOp::AddAlpha, R"doc(Add, scaled by source alpha. WC3 AddAlpha.)doc")
.value("MODULATE", whiteout::models::wem::CompositeOp::Modulate, R"doc(WC3 Modulate; M3 mod.)doc")
.value("MODULATE2X", whiteout::models::wem::CompositeOp::Modulate2x, R"doc(WC3 Modulate2x; M3 mod2x.)doc")
.value("COUNT", whiteout::models::wem::CompositeOp::Count)
;
py::enum_<whiteout::models::wem::CombinerOp>(m, "CombinerOp", R"doc(M2's vocabulary. The pixel shaders are literally named for it: `Combiners_Opaque_Mod2x` is stage 0 `Opaque`, stage 1 `Mod2x`.
`Pass` is the one addition WoW did not need: D3's Legacy stage block encodes a per-channel **zero** meaning "this stage does not touch that channel" — a glow stage sampled `.xyz` and a mask stage sampled `.w` are mirror images of each other — and without an identity the chain would have to invent a modulate by white.
`AddAlpha` is appended rather than filed next to `Add` because these values are a byte on disk and inserting one would renumber every `.wem` already written. It is `Add` scaled by the sample's own alpha — five of World of Warcraft's shipped combiners spell it (`Combiners_Opaque_AddAlpha` and its family), `CompositeOp` has carried the twin since revision 3, and Warcraft III draws it as `FilterMode::AddAlpha`. Folding it onto `Add` added a glow mask at full strength over the whole surface.
The `Masked` pair (appended for the same on-disk reason) is the `_NA_Alpha` masked fold, `rgb * lerp(sample * N, 1, seedAlpha)`: the stage modulates only where the SEED stage's alpha opens the mask, which is how WoW paints an env sheen over armor whose base map saturates its alpha everywhere else. The op contributes no alpha of its own. Targets that cannot read the seed's alpha (an MDX pass, a composite layer) drop the stage — the identity is what the old collapse onto `Pass` got right and keeping the modulate got measurably wrong.)doc")
.value("OPAQUE", whiteout::models::wem::CombinerOp::Opaque)
.value("MOD", whiteout::models::wem::CombinerOp::Mod)
.value("MOD2X", whiteout::models::wem::CombinerOp::Mod2x)
.value("ADD", whiteout::models::wem::CombinerOp::Add)
.value("DECAL", whiteout::models::wem::CombinerOp::Decal)
.value("FADE", whiteout::models::wem::CombinerOp::Fade)
.value("PASS", whiteout::models::wem::CombinerOp::Pass)
.value("ADD_ALPHA", whiteout::models::wem::CombinerOp::AddAlpha)
.value("MASKED_MOD", whiteout::models::wem::CombinerOp::MaskedMod)
.value("MASKED_MOD2X", whiteout::models::wem::CombinerOp::MaskedMod2x)
.value("COUNT", whiteout::models::wem::CombinerOp::Count)
;
py::enum_<whiteout::models::wem::LegacySlot>(m, "LegacySlot", R"doc(SC2's slot vocabulary — that generation's lingua franca.
The kind is named for the **lighting family** (a legacy spec/gloss G-buffer), not for a claim about SC2's pass structure: SC2 itself runs forward with a G-buffer sidecar and defers only local lights.)doc")
.value("DIFFUSE", whiteout::models::wem::LegacySlot::Diffuse)
.value("NORMAL", whiteout::models::wem::LegacySlot::Normal)
.value("SPECULAR", whiteout::models::wem::LegacySlot::Specular)
.value("GLOSS", whiteout::models::wem::LegacySlot::Gloss)
.value("EMISSIVE", whiteout::models::wem::LegacySlot::Emissive)
.value("ENVIRONMENT", whiteout::models::wem::LegacySlot::Environment)
.value("AMBIENT_OCCLUSION", whiteout::models::wem::LegacySlot::AmbientOcclusion)
.value("HEIGHT", whiteout::models::wem::LegacySlot::Height)
.value("LIGHTMAP", whiteout::models::wem::LegacySlot::Lightmap)
.value("DETAIL", whiteout::models::wem::LegacySlot::Detail, R"doc(A second base map lerped over the diffuse by its own alpha — Diablo III's texture type 25 (`actor2_opaque_glow_skin`: `lerp(diffuse, t25, t25.a·k)`, on 3,992 of 6,920 shipped variants) and M3's decal layer, which folds the same way. Appended because the enum is a byte on disk.)doc")
.value("COUNT", whiteout::models::wem::LegacySlot::Count, R"doc(A second base map lerped over the diffuse by its own alpha — Diablo III's texture type 25 (`actor2_opaque_glow_skin`: `lerp(diffuse, t25, t25.a·k)`, on 3,992 of 6,920 shipped variants) and M3's decal layer, which folds the same way. Appended because the enum is a byte on disk.)doc")
;
py::enum_<whiteout::models::wem::PbrSlot>(m, "PbrSlot", R"doc(Reforged's `SlotType` enum, nearly verbatim.
Deliberate: Reforged HD is the only shipped PBR content among the six games, so its vocabulary is the ground truth and the MDX HD import is a rename, not a mapping. `Orm` and the unpacked `Metallic`/`Roughness`/`AmbientOcclusion` are **alternatives, not composable** — `Validate` rejects a body carrying both — and DCC-sourced authoring (glTF-style split maps) uses the unpacked form.)doc")
.value("BASE_COLOR", whiteout::models::wem::PbrSlot::BaseColor)
.value("NORMAL", whiteout::models::wem::PbrSlot::Normal)
.value("ORM", whiteout::models::wem::PbrSlot::Orm, R"doc(Occlusion-roughness-metalness, packed.)doc")
.value("METALLIC", whiteout::models::wem::PbrSlot::Metallic)
.value("ROUGHNESS", whiteout::models::wem::PbrSlot::Roughness)
.value("AMBIENT_OCCLUSION", whiteout::models::wem::PbrSlot::AmbientOcclusion)
.value("EMISSIVE", whiteout::models::wem::PbrSlot::Emissive)
.value("ENVIRONMENT", whiteout::models::wem::PbrSlot::Environment)
.value("TEAM_COLOR_MASK", whiteout::models::wem::PbrSlot::TeamColorMask)
.value("COUNT", whiteout::models::wem::PbrSlot::Count)
;
py::enum_<whiteout::models::wem::NativeSync>(m, "NativeSync")
.value("ABSENT", whiteout::models::wem::NativeSync::Absent, R"doc(No native block: `Generic`, or authored in WEM.)doc")
.value("IN_SYNC", whiteout::models::wem::NativeSync::InSync, R"doc(Common was derived from native and neither has been edited.)doc")
.value("NATIVE_AUTHORITATIVE", whiteout::models::wem::NativeSync::NativeAuthoritative, R"doc(Native is the truth; common is a lossy view of it.)doc")
.value("COMMON_EDITED", whiteout::models::wem::NativeSync::CommonEdited, R"doc(Common was edited after import; native is now stale.)doc")
;
py::enum_<whiteout::models::wem::SectionFlags>(m, "SectionFlags")
.value("NONE", whiteout::models::wem::SectionFlags::None)
.value("HIDDEN", whiteout::models::wem::SectionFlags::Hidden, R"doc(M3's per-batch visibility bit; the section is not submitted.)doc")
.value("CLOTH_SIMULATED", whiteout::models::wem::SectionFlags::ClothSimulated, R"doc(The section's vertices are driven by cloth.)doc")
.value("CLOTH_INFLUENCED", whiteout::models::wem::SectionFlags::ClothInfluenced, R"doc(The section is deformed by cloth it does not own.)doc")
.value("BILLBOARD", whiteout::models::wem::SectionFlags::Billboard, R"doc(The section is oriented per view rather than by its node.)doc")
.value("PROJECTED_SHADOW", whiteout::models::wem::SectionFlags::ProjectedShadow, R"doc(MDX's GEOA `DropShadow`: the bones linked to this mesh (`BonePayload::gateMesh`) are placed as a drop shadow.)doc")
;
py::enum_<whiteout::models::wem::EmitterLink>(m, "EmitterLink", R"doc(What one of a payload's node links points at, so `Validate` can hold it to the kind it has to be and say which link is wrong.)doc")
.value("COPY_SOURCE", whiteout::models::wem::EmitterLink::CopySource, R"doc(`copyOf`: a `Sc2ParticleEmitter` that is not itself a copy.)doc")
.value("COLLISION_SPAWN", whiteout::models::wem::EmitterLink::CollisionSpawn, R"doc(A `Sc2ParticleEmitter`.)doc")
.value("TRAIL", whiteout::models::wem::EmitterLink::Trail, R"doc(A `Sc2ParticleEmitter`.)doc")
.value("BOUNCE_RIBBON", whiteout::models::wem::EmitterLink::BounceRibbon, R"doc(A `Sc2RibbonEmitter`.)doc")
.value("SPLINE_BONE", whiteout::models::wem::EmitterLink::SplineBone, R"doc(Any node; a bone in every shipped file.)doc")
.value("SKIN_MIRROR", whiteout::models::wem::EmitterLink::SkinMirror, R"doc(Not an emitter link: `NodeSkinSetup::mirror`, the skin setup's override of §7.5's mirror map. It rides this walk because the referencer table is one walk, and it must name a `Bone`.)doc")
.value("RIG_RIDES_WITH", whiteout::models::wem::EmitterLink::RigRidesWith, R"doc(Not an emitter link either: `NodeRig::ridesWith`, the node a rig record rides (EDIT_MODE_AUTO_IK_DESIGN.md §3.3). A bone or a helper, never the node itself.)doc")
;
py::enum_<whiteout::models::wem::Wc3Particle1Property>(m, "Wc3Particle1Property", R"doc(MDX `PREM` tracks (KPEE, KPEG, KPLN, KPLT, KPEL, KPES).)doc")
.value("EMISSION_RATE", whiteout::models::wem::Wc3Particle1Property::EmissionRate)
.value("GRAVITY", whiteout::models::wem::Wc3Particle1Property::Gravity)
.value("LONGITUDE", whiteout::models::wem::Wc3Particle1Property::Longitude)
.value("LATITUDE", whiteout::models::wem::Wc3Particle1Property::Latitude)
.value("LIFESPAN", whiteout::models::wem::Wc3Particle1Property::Lifespan)
.value("SPEED", whiteout::models::wem::Wc3Particle1Property::Speed)
.value("COUNT", whiteout::models::wem::Wc3Particle1Property::Count)
;
py::enum_<whiteout::models::wem::Wc3Particle2Property>(m, "Wc3Particle2Property", R"doc(MDX `PRE2` tracks (KP2S, KP2R, KP2L, KP2G, KP2E, KP2N, KP2W).)doc")
.value("SPEED", whiteout::models::wem::Wc3Particle2Property::Speed)
.value("VARIATION", whiteout::models::wem::Wc3Particle2Property::Variation)
.value("LATITUDE", whiteout::models::wem::Wc3Particle2Property::Latitude)
.value("GRAVITY", whiteout::models::wem::Wc3Particle2Property::Gravity)
.value("EMISSION_RATE", whiteout::models::wem::Wc3Particle2Property::EmissionRate)
.value("WIDTH", whiteout::models::wem::Wc3Particle2Property::Width)
.value("LENGTH", whiteout::models::wem::Wc3Particle2Property::Length)
.value("COUNT", whiteout::models::wem::Wc3Particle2Property::Count)
;
py::enum_<whiteout::models::wem::Wc3RibbonProperty>(m, "Wc3RibbonProperty", R"doc(MDX `RIBB` tracks beyond the shared channels (KRHA, KRHB).)doc")
.value("HEIGHT_ABOVE", whiteout::models::wem::Wc3RibbonProperty::HeightAbove)
.value("HEIGHT_BELOW", whiteout::models::wem::Wc3RibbonProperty::HeightBelow)
.value("COUNT", whiteout::models::wem::Wc3RibbonProperty::Count)
;
py::enum_<whiteout::models::wem::Wc3CornProperty>(m, "Wc3CornProperty", R"doc(MDX `CORN` tracks beyond the shared channels (KPPL, KPPE, KPPS).)doc")
.value("LIFESPAN", whiteout::models::wem::Wc3CornProperty::Lifespan)
.value("EMISSION_RATE", whiteout::models::wem::Wc3CornProperty::EmissionRate)
.value("SPEED", whiteout::models::wem::Wc3CornProperty::Speed)
.value("COUNT", whiteout::models::wem::Wc3CornProperty::Count)
;
py::enum_<whiteout::models::wem::Sc2ParticleProperty>(m, "Sc2ParticleProperty", R"doc(M3 `PAR_` AnimRefs, and a `PARC` copy's two. `SplinePoint` is per element.)doc")
.value("INITIAL_SPEED", whiteout::models::wem::Sc2ParticleProperty::InitialSpeed)
.value("INITIAL_SPEED_RANDOM", whiteout::models::wem::Sc2ParticleProperty::InitialSpeedRandom)
.value("INITIAL_YAW", whiteout::models::wem::Sc2ParticleProperty::InitialYaw)
.value("INITIAL_PITCH", whiteout::models::wem::Sc2ParticleProperty::InitialPitch)
.value("INITIAL_HORIZONTAL", whiteout::models::wem::Sc2ParticleProperty::InitialHorizontal)
.value("INITIAL_VERTICAL", whiteout::models::wem::Sc2ParticleProperty::InitialVertical)
.value("LIFETIME", whiteout::models::wem::Sc2ParticleProperty::Lifetime)
.value("LIFETIME_RANDOM", whiteout::models::wem::Sc2ParticleProperty::LifetimeRandom)
.value("SIZE_ANIMATION", whiteout::models::wem::Sc2ParticleProperty::SizeAnimation)
.value("ROTATION_ANIMATION", whiteout::models::wem::Sc2ParticleProperty::RotationAnimation)
.value("COLOR_START", whiteout::models::wem::Sc2ParticleProperty::ColorStart)
.value("COLOR_MID", whiteout::models::wem::Sc2ParticleProperty::ColorMid)
.value("COLOR_END", whiteout::models::wem::Sc2ParticleProperty::ColorEnd)
.value("EMISSION_RATE", whiteout::models::wem::Sc2ParticleProperty::EmissionRate)
.value("SHAPE_OUTER", whiteout::models::wem::Sc2ParticleProperty::ShapeOuter)
.value("SHAPE_INNER", whiteout::models::wem::Sc2ParticleProperty::ShapeInner)
.value("OUTER_RADIUS", whiteout::models::wem::Sc2ParticleProperty::OuterRadius)
.value("INNER_RADIUS", whiteout::models::wem::Sc2ParticleProperty::InnerRadius)
.value("SIZE_RANDOM_ANIMATION", whiteout::models::wem::Sc2ParticleProperty::SizeRandomAnimation)
.value("ROTATION_RANDOM_ANIMATION", whiteout::models::wem::Sc2ParticleProperty::RotationRandomAnimation)
.value("COLOR_START_RANDOM", whiteout::models::wem::Sc2ParticleProperty::ColorStartRandom)
.value("COLOR_MID_RANDOM", whiteout::models::wem::Sc2ParticleProperty::ColorMidRandom)
.value("COLOR_END_RANDOM", whiteout::models::wem::Sc2ParticleProperty::ColorEndRandom)
.value("SQUIRT_AMOUNT", whiteout::models::wem::Sc2ParticleProperty::SquirtAmount)
.value("PITCH_AMPLITUDE", whiteout::models::wem::Sc2ParticleProperty::PitchAmplitude)
.value("PITCH_FREQUENCY", whiteout::models::wem::Sc2ParticleProperty::PitchFrequency)
.value("YAW_AMPLITUDE", whiteout::models::wem::Sc2ParticleProperty::YawAmplitude)
.value("YAW_FREQUENCY", whiteout::models::wem::Sc2ParticleProperty::YawFrequency)
.value("SPEED_AMPLITUDE", whiteout::models::wem::Sc2ParticleProperty::SpeedAmplitude)
.value("SPEED_FREQUENCY", whiteout::models::wem::Sc2ParticleProperty::SpeedFrequency)
.value("SIZE_AMPLITUDE", whiteout::models::wem::Sc2ParticleProperty::SizeAmplitude)
.value("SIZE_FREQUENCY", whiteout::models::wem::Sc2ParticleProperty::SizeFrequency)
.value("ALPHA_AMPLITUDE", whiteout::models::wem::Sc2ParticleProperty::AlphaAmplitude)
.value("ALPHA_FREQUENCY", whiteout::models::wem::Sc2ParticleProperty::AlphaFrequency)
.value("COLOR_AMPLITUDE", whiteout::models::wem::Sc2ParticleProperty::ColorAmplitude)
.value("COLOR_FREQUENCY", whiteout::models::wem::Sc2ParticleProperty::ColorFrequency)
.value("ROTATION_AMPLITUDE", whiteout::models::wem::Sc2ParticleProperty::RotationAmplitude)
.value("ROTATION_FREQUENCY", whiteout::models::wem::Sc2ParticleProperty::RotationFrequency)
.value("HORIZONTAL_AMPLITUDE", whiteout::models::wem::Sc2ParticleProperty::HorizontalAmplitude)
.value("HORIZONTAL_FREQUENCY", whiteout::models::wem::Sc2ParticleProperty::HorizontalFrequency)
.value("VERTICAL_AMPLITUDE", whiteout::models::wem::Sc2ParticleProperty::VerticalAmplitude)
.value("VERTICAL_FREQUENCY", whiteout::models::wem::Sc2ParticleProperty::VerticalFrequency)
.value("PARTICLE_VELOCITY", whiteout::models::wem::Sc2ParticleProperty::ParticleVelocity)
.value("PHASE_SHIFT", whiteout::models::wem::Sc2ParticleProperty::PhaseShift)
.value("ALPHA_THRESHOLD", whiteout::models::wem::Sc2ParticleProperty::AlphaThreshold)
.value("UV_OFFSET", whiteout::models::wem::Sc2ParticleProperty::UvOffset)
.value("UV_ANGLE", whiteout::models::wem::Sc2ParticleProperty::UvAngle)
.value("UV_TILING", whiteout::models::wem::Sc2ParticleProperty::UvTiling)
.value("SPLINE_POINT", whiteout::models::wem::Sc2ParticleProperty::SplinePoint)
.value("LOWER_BOUND", whiteout::models::wem::Sc2ParticleProperty::LowerBound)
.value("UPPER_BOUND", whiteout::models::wem::Sc2ParticleProperty::UpperBound)
.value("TRAIL_EMISSION_RATE", whiteout::models::wem::Sc2ParticleProperty::TrailEmissionRate)
.value("COUNT", whiteout::models::wem::Sc2ParticleProperty::Count)
;
py::enum_<whiteout::models::wem::Sc2RibbonProperty>(m, "Sc2RibbonProperty", R"doc(M3 `RIB_` AnimRefs; the `Spline*` ones are per `SRIB` element.)doc")
.value("INITIAL_SPEED", whiteout::models::wem::Sc2RibbonProperty::InitialSpeed)
.value("INITIAL_SPEED_RANDOM", whiteout::models::wem::Sc2RibbonProperty::InitialSpeedRandom)
.value("INITIAL_YAW", whiteout::models::wem::Sc2RibbonProperty::InitialYaw)
.value("INITIAL_PITCH", whiteout::models::wem::Sc2RibbonProperty::InitialPitch)
.value("INITIAL_HORIZONTAL", whiteout::models::wem::Sc2RibbonProperty::InitialHorizontal)
.value("INITIAL_VERTICAL", whiteout::models::wem::Sc2RibbonProperty::InitialVertical)
.value("LIFETIME", whiteout::models::wem::Sc2RibbonProperty::Lifetime)
.value("LIFETIME_RANDOM", whiteout::models::wem::Sc2RibbonProperty::LifetimeRandom)
.value("SIZE_ANIMATION", whiteout::models::wem::Sc2RibbonProperty::SizeAnimation)
.value("ROTATION_ANIMATION", whiteout::models::wem::Sc2RibbonProperty::RotationAnimation)
.value("COLOR_START", whiteout::models::wem::Sc2RibbonProperty::ColorStart)
.value("COLOR_MID", whiteout::models::wem::Sc2RibbonProperty::ColorMid)
.value("COLOR_END", whiteout::models::wem::Sc2RibbonProperty::ColorEnd)
.value("MAX_LENGTH", whiteout::models::wem::Sc2RibbonProperty::MaxLength)
.value("ACTIVE", whiteout::models::wem::Sc2RibbonProperty::Active)
.value("YAW_AMPLITUDE", whiteout::models::wem::Sc2RibbonProperty::YawAmplitude)
.value("YAW_FREQUENCY", whiteout::models::wem::Sc2RibbonProperty::YawFrequency)
.value("PITCH_AMPLITUDE", whiteout::models::wem::Sc2RibbonProperty::PitchAmplitude)
.value("PITCH_FREQUENCY", whiteout::models::wem::Sc2RibbonProperty::PitchFrequency)
.value("SPEED_AMPLITUDE", whiteout::models::wem::Sc2RibbonProperty::SpeedAmplitude)
.value("SPEED_FREQUENCY", whiteout::models::wem::Sc2RibbonProperty::SpeedFrequency)
.value("SIZE_AMPLITUDE", whiteout::models::wem::Sc2RibbonProperty::SizeAmplitude)
.value("SIZE_FREQUENCY", whiteout::models::wem::Sc2RibbonProperty::SizeFrequency)
.value("ALPHA_AMPLITUDE", whiteout::models::wem::Sc2RibbonProperty::AlphaAmplitude)
.value("ALPHA_FREQUENCY", whiteout::models::wem::Sc2RibbonProperty::AlphaFrequency)
.value("PARTICLE_VELOCITY", whiteout::models::wem::Sc2RibbonProperty::ParticleVelocity)
.value("OVERLAY", whiteout::models::wem::Sc2RibbonProperty::Overlay)
.value("SPLINE_VELOCITY", whiteout::models::wem::Sc2RibbonProperty::SplineVelocity)
.value("SPLINE_VELOCITY_BASE_FACTOR", whiteout::models::wem::Sc2RibbonProperty::SplineVelocityBaseFactor)
.value("SPLINE_VELOCITY_END_FACTOR", whiteout::models::wem::Sc2RibbonProperty::SplineVelocityEndFactor)
.value("SPLINE_YAW_AMPLITUDE", whiteout::models::wem::Sc2RibbonProperty::SplineYawAmplitude)
.value("SPLINE_YAW_FREQUENCY", whiteout::models::wem::Sc2RibbonProperty::SplineYawFrequency)
.value("SPLINE_PITCH_AMPLITUDE", whiteout::models::wem::Sc2RibbonProperty::SplinePitchAmplitude)
.value("SPLINE_PITCH_FREQUENCY", whiteout::models::wem::Sc2RibbonProperty::SplinePitchFrequency)
.value("SPLINE_VELOCITY_AMPLITUDE", whiteout::models::wem::Sc2RibbonProperty::SplineVelocityAmplitude)
.value("SPLINE_VELOCITY_FREQUENCY", whiteout::models::wem::Sc2RibbonProperty::SplineVelocityFrequency)
.value("SPLINE_YAW", whiteout::models::wem::Sc2RibbonProperty::SplineYaw)
.value("SPLINE_PITCH", whiteout::models::wem::Sc2RibbonProperty::SplinePitch)
.value("COUNT", whiteout::models::wem::Sc2RibbonProperty::Count)
;
py::enum_<whiteout::models::wem::Wc3ParticleFilter>(m, "Wc3ParticleFilter", R"doc(`PRE2`'s own blend vocabulary — not a material's `filterMode`.)doc")
.value("BLEND", whiteout::models::wem::Wc3ParticleFilter::Blend)
.value("ADDITIVE", whiteout::models::wem::Wc3ParticleFilter::Additive)
.value("MODULATE", whiteout::models::wem::Wc3ParticleFilter::Modulate)
.value("MODULATE2X", whiteout::models::wem::Wc3ParticleFilter::Modulate2x)
.value("ALPHA_KEY", whiteout::models::wem::Wc3ParticleFilter::AlphaKey)
;
py::enum_<whiteout::models::wem::Wc3ParticleHeadOrTail>(m, "Wc3ParticleHeadOrTail")
.value("HEAD", whiteout::models::wem::Wc3ParticleHeadOrTail::Head)
.value("TAIL", whiteout::models::wem::Wc3ParticleHeadOrTail::Tail)
.value("BOTH", whiteout::models::wem::Wc3ParticleHeadOrTail::Both)
;
py::enum_<whiteout::models::wem::M2ParticleProperty>(m, "M2ParticleProperty", R"doc(`M2Particle` tracks. `Gravity` is a vector: a record flagged with compressed gravity keys a direction and a magnitude, and a plain one keys -Z.)doc")
.value("SPEED", whiteout::models::wem::M2ParticleProperty::Speed)
.value("SPEED_VARIATION", whiteout::models::wem::M2ParticleProperty::SpeedVariation)
.value("VERTICAL_RANGE", whiteout::models::wem::M2ParticleProperty::VerticalRange, R"doc(Radians, the cone about +Z.)doc")
.value("HORIZONTAL_RANGE", whiteout::models::wem::M2ParticleProperty::HorizontalRange, R"doc(Radians, the azimuth the cone sweeps.)doc")
.value("GRAVITY", whiteout::models::wem::M2ParticleProperty::Gravity, R"doc(F32x3, model units per second squared.)doc")
.value("LIFESPAN", whiteout::models::wem::M2ParticleProperty::Lifespan, R"doc(Seconds.)doc")
.value("EMISSION_RATE", whiteout::models::wem::M2ParticleProperty::EmissionRate)
.value("WIDTH", whiteout::models::wem::M2ParticleProperty::Width, R"doc(The emission area along the record's X.)doc")
.value("LENGTH", whiteout::models::wem::M2ParticleProperty::Length, R"doc(...and its Y. A sphere reads the two as its min and max radius.)doc")
.value("Z_SOURCE", whiteout::models::wem::M2ParticleProperty::ZSource)
.value("COUNT", whiteout::models::wem::M2ParticleProperty::Count)
;
py::enum_<whiteout::models::wem::Sc2EmitterShape>(m, "Sc2EmitterShape", R"doc(`PAR_` emission shapes, by the file's own value.)doc")
.value("POINT", whiteout::models::wem::Sc2EmitterShape::Point)
.value("PLANE", whiteout::models::wem::Sc2EmitterShape::Plane)
.value("SPHERE", whiteout::models::wem::Sc2EmitterShape::Sphere)
.value("BOX", whiteout::models::wem::Sc2EmitterShape::Box)
.value("CYLINDER", whiteout::models::wem::Sc2EmitterShape::Cylinder)
.value("DISC", whiteout::models::wem::Sc2EmitterShape::Disc)
.value("SPLINE", whiteout::models::wem::Sc2EmitterShape::Spline, R"doc(Along `splinePoints`.)doc")
.value("MESH", whiteout::models::wem::Sc2EmitterShape::Mesh, R"doc(Off `shapeSections`.)doc")
;
py::enum_<whiteout::models::wem::Sc2ParticleInstance>(m, "Sc2ParticleInstance", R"doc(`PAR_` instance types — what one particle is drawn as.)doc")
.value("BILLBOARD", whiteout::models::wem::Sc2ParticleInstance::Billboard)
.value("TAIL", whiteout::models::wem::Sc2ParticleInstance::Tail)
.value("FACE_TRAVEL_DIR", whiteout::models::wem::Sc2ParticleInstance::FaceTravelDir)
.value("FACE_WORLD_DIR", whiteout::models::wem::Sc2ParticleInstance::FaceWorldDir)
.value("SINGLE_AXIS", whiteout::models::wem::Sc2ParticleInstance::SingleAxis)
.value("TERRAIN_ORIENTED", whiteout::models::wem::Sc2ParticleInstance::TerrainOriented)
.value("TERRAIN_DIR_ORIENTED", whiteout::models::wem::Sc2ParticleInstance::TerrainDirOriented)
.value("EMITTER_ORIENTED", whiteout::models::wem::Sc2ParticleInstance::EmitterOriented)
.value("PHYSICS_ORIENTED", whiteout::models::wem::Sc2ParticleInstance::PhysicsOriented)
.value("PINNED", whiteout::models::wem::Sc2ParticleInstance::Pinned)
.value("TRAIL", whiteout::models::wem::Sc2ParticleInstance::Trail)
;
py::enum_<whiteout::models::wem::Sc2Smoothing>(m, "Sc2Smoothing", R"doc(How a birth-middle-death curve is blended.)doc")
.value("LINEAR", whiteout::models::wem::Sc2Smoothing::Linear)
.value("LINEAR_SMOOTH", whiteout::models::wem::Sc2Smoothing::LinearSmooth)
.value("BEZIER", whiteout::models::wem::Sc2Smoothing::Bezier)
.value("LINEAR_WITH_HOLD", whiteout::models::wem::Sc2Smoothing::LinearWithHold)
.value("BEZIER_WITH_HOLD", whiteout::models::wem::Sc2Smoothing::BezierWithHold)
;
py::enum_<whiteout::models::wem::Sc2RibbonType>(m, "Sc2RibbonType")
.value("BILLBOARD", whiteout::models::wem::Sc2RibbonType::Billboard)
.value("PLANAR", whiteout::models::wem::Sc2RibbonType::Planar)
.value("CYLINDER", whiteout::models::wem::Sc2RibbonType::Cylinder)
.value("STAR", whiteout::models::wem::Sc2RibbonType::Star)
;
py::enum_<whiteout::models::wem::ForceKind>(m, "ForceKind", R"doc(Appended, never reordered.)doc")
.value("DIRECTIONAL", whiteout::models::wem::ForceKind::Directional)
.value("RADIAL", whiteout::models::wem::ForceKind::Radial)
.value("DRAG", whiteout::models::wem::ForceKind::Drag)
.value("VORTEX", whiteout::models::wem::ForceKind::Vortex)
.value("COUNT", whiteout::models::wem::ForceKind::Count)
;
py::enum_<whiteout::models::wem::ForceVolume>(m, "ForceVolume", R"doc(Appended, never reordered.)doc")
.value("SPHERE", whiteout::models::wem::ForceVolume::Sphere)
.value("CYLINDER", whiteout::models::wem::ForceVolume::Cylinder)
.value("BOX", whiteout::models::wem::ForceVolume::Box)
.value("HEMISPHERE", whiteout::models::wem::ForceVolume::Hemisphere)
.value("CONE", whiteout::models::wem::ForceVolume::Cone)
.value("COUNT", whiteout::models::wem::ForceVolume::Count)
;
py::enum_<whiteout::models::wem::ForceFieldProperty>(m, "ForceFieldProperty", R"doc(A force field's animatable properties, in `EmitterProperty` sub order.)doc")
.value("STRENGTH", whiteout::models::wem::ForceFieldProperty::Strength)
.value("WIDTH", whiteout::models::wem::ForceFieldProperty::Width)
.value("HEIGHT", whiteout::models::wem::ForceFieldProperty::Height)
.value("LENGTH", whiteout::models::wem::ForceFieldProperty::Length)
.value("COUNT", whiteout::models::wem::ForceFieldProperty::Count)
;
py::enum_<whiteout::models::wem::VertexWarpProperty>(m, "VertexWarpProperty", R"doc(A vertex warp's animatable properties, in `EmitterProperty` sub order.)doc")
.value("RADIUS", whiteout::models::wem::VertexWarpProperty::Radius)
.value("HEIGHT", whiteout::models::wem::VertexWarpProperty::Height)
.value("STRENGTH", whiteout::models::wem::VertexWarpProperty::Strength)
.value("ANGULAR", whiteout::models::wem::VertexWarpProperty::Angular)
.value("AXIAL", whiteout::models::wem::VertexWarpProperty::Axial)
.value("RADIAL", whiteout::models::wem::VertexWarpProperty::Radial)
.value("COUNT", whiteout::models::wem::VertexWarpProperty::Count)
;
py::enum_<whiteout::models::wem::NodeKind>(m, "NodeKind", R"doc(Appended, never reordered: the value is what a `NODE` chunk stores.)doc")
.value("HELPER", whiteout::models::wem::NodeKind::Helper, R"doc(Transform only (MDX Helper).)doc")
.value("BONE", whiteout::models::wem::NodeKind::Bone, R"doc(Skinnable.)doc")
.value("ATTACHMENT", whiteout::models::wem::NodeKind::Attachment, R"doc(MDX/M2/M3 attachment, D3 hardpoint.)doc")
.value("LIGHT", whiteout::models::wem::NodeKind::Light)
.value("CAMERA", whiteout::models::wem::NodeKind::Camera)
.value("PARTICLE_EMITTER", whiteout::models::wem::NodeKind::ParticleEmitter, R"doc(Placement + a reference to a system WEM does not hold (§18).)doc")
.value("RIBBON_EMITTER", whiteout::models::wem::NodeKind::RibbonEmitter, R"doc(The same, for a trail.)doc")
.value("EVENT", whiteout::models::wem::NodeKind::Event, R"doc(A named anchor clips fire at (MDX EventObject, M2 Event).)doc")
.value("COLLISION_SHAPE", whiteout::models::wem::NodeKind::CollisionShape)
.value("WC3_PARTICLE_EMITTER1", whiteout::models::wem::NodeKind::Wc3ParticleEmitter1, R"doc(MDX `PREM`: every particle is a copy of a model.)doc")
.value("WC3_PARTICLE_EMITTER2", whiteout::models::wem::NodeKind::Wc3ParticleEmitter2, R"doc(MDX `PRE2`: textured quads with a head and a tail.)doc")
.value("WC3_RIBBON_EMITTER", whiteout::models::wem::NodeKind::Wc3RibbonEmitter, R"doc(MDX `RIBB`.)doc")
.value("SC2_PARTICLE_EMITTER", whiteout::models::wem::NodeKind::Sc2ParticleEmitter, R"doc(M3 `PAR_`, and each `PARC` copy of one.)doc")
.value("SC2_RIBBON_EMITTER", whiteout::models::wem::NodeKind::Sc2RibbonEmitter, R"doc(M3 `RIB_` with its `SRIB` spline.)doc")
.value("WC3_CORN_EMITTER", whiteout::models::wem::NodeKind::Wc3CornEmitter, R"doc(MDX `CORN`: a PopcornFX effect (Reforged). `NODE` v7.)doc")
.value("M2_PARTICLE_EMITTER", whiteout::models::wem::NodeKind::M2ParticleEmitter, R"doc(M2 `M2Particle`. `NODE` v8.)doc")
.value("FORCE_FIELD", whiteout::models::wem::NodeKind::ForceField, R"doc(M3 `FOR_` (WEM_PHYSICS_DESIGN.md §3.8). `NODE` v14.)doc")
.value("VERTEX_WARP", whiteout::models::wem::NodeKind::VertexWarp, R"doc(M3 `WRP_`. `NODE` v14.)doc")
.value("COUNT", whiteout::models::wem::NodeKind::Count)
;
py::enum_<whiteout::models::wem::NodeFlags>(m, "NodeFlags", R"doc(What at least two formats mean the same way.
The inherit bits and the billboard family are MDX's vocabulary, and M3's `BoneFlag` carries the same concepts. Everything else rides in `native`.)doc")
.value("NONE", whiteout::models::wem::NodeFlags::None)
.value("HIDDEN", whiteout::models::wem::NodeFlags::Hidden)
.value("DONT_INHERIT_TRANSLATION", whiteout::models::wem::NodeFlags::DontInheritTranslation)
.value("DONT_INHERIT_ROTATION", whiteout::models::wem::NodeFlags::DontInheritRotation)
.value("DONT_INHERIT_SCALE", whiteout::models::wem::NodeFlags::DontInheritScale)
.value("BILLBOARDED", whiteout::models::wem::NodeFlags::Billboarded)
.value("BILLBOARD_LOCK_X", whiteout::models::wem::NodeFlags::BillboardLockX)
.value("BILLBOARD_LOCK_Y", whiteout::models::wem::NodeFlags::BillboardLockY)
.value("BILLBOARD_LOCK_Z", whiteout::models::wem::NodeFlags::BillboardLockZ)
.value("MODEL_SPACE", whiteout::models::wem::NodeFlags::ModelSpace, R"doc(The node's transform is authored in model space.)doc")
;
py::enum_<whiteout::models::wem::LightKind>(m, "LightKind")
.value("OMNI", whiteout::models::wem::LightKind::Omni)
.value("DIRECTIONAL", whiteout::models::wem::LightKind::Directional)
.value("SPOT", whiteout::models::wem::LightKind::Spot)
.value("AMBIENT", whiteout::models::wem::LightKind::Ambient)
;
py::enum_<whiteout::models::wem::CollisionShapeKind>(m, "CollisionShapeKind")
.value("BOX", whiteout::models::wem::CollisionShapeKind::Box)
.value("SPHERE", whiteout::models::wem::CollisionShapeKind::Sphere)
.value("CAPSULE", whiteout::models::wem::CollisionShapeKind::Capsule)
.value("PLANE", whiteout::models::wem::CollisionShapeKind::Plane)
.value("CYLINDER", whiteout::models::wem::CollisionShapeKind::Cylinder)
.value("HULL", whiteout::models::wem::CollisionShapeKind::Hull)
;
py::enum_<whiteout::models::wem::PoseSpace>(m, "PoseSpace")
.value("MODEL", whiteout::models::wem::PoseSpace::Model, R"doc(Relative to the model root.)doc")
.value("PARENT_RELATIVE", whiteout::models::wem::PoseSpace::ParentRelative, R"doc(Relative to the node's parent — the same space as `local`.)doc")
;
py::enum_<whiteout::models::wem::PoseStorage>(m, "PoseStorage", R"doc(Where one schema entry's authoritative value lives.
`Trs` is the shape every format authors in and the one every consumer wants, so it is the default and `Node::poses` is the storage.
`Matrix` exists because M3's `IREF` is not a TRS. It ships one 4x4 inverse model-space bind matrix per bone, and across 250 corpus `.m3` files 23 of them (9.2%) hold shear that no translation/rotation/scale can reproduce — the error reaches 30.7 units on a character-sized model, and allowing a negative scale component (which would cover a mirrored bone) fixes none of them. A TRS-only pose would therefore have been silently wrong on a tenth of StarCraft II, so the entry says so and `Node::poseMatrices` carries the matrix as shipped.)doc")
.value("TRS", whiteout::models::wem::PoseStorage::Trs, R"doc(`Node::poses[i]` is the value.)doc")
.value("MATRIX", whiteout::models::wem::PoseStorage::Matrix, R"doc(`Node::poseMatrices[i]` is the value; `poses[i]` is its decomposition.)doc")
;
py::enum_<whiteout::models::wem::Channel>(m, "Channel", R"doc(Which property, in the vocabulary all four formats share.
Closed on purpose. A source property with no entry here is **dropped with an `AnimTrackDropped` diagnostic**, not smuggled through under a free-text name: a consumer that cannot enumerate what it might be handed cannot implement the table, and M3 alone would contribute hundreds of AnimRefs on structures WEM does not store at all (§18).
`EmitterProperty` is the one entry whose meaning the target's node picks: an emitter system's own properties (§10.9) are shared by no other format, so rather than a hundred entries here they are one, with `TrackTarget::sub` naming the property in that kind's closed enum — still enumerable, through `FindEmitterProperty`.)doc")
.value("TRANSLATION", whiteout::models::wem::Channel::Translation, R"doc(F32x3. MDX KGTR, M2 `Bone::translation`, M3 BONE, D3 `TranslationCurve`.)doc")
.value("ROTATION", whiteout::models::wem::Channel::Rotation, R"doc(Quat. Slerp by default; the source's interpolation still decides.)doc")
.value("SCALE", whiteout::models::wem::Channel::Scale, R"doc(F32x3, or F32 where the source ships one float (D3 `ScaleCurve`).)doc")
.value("VISIBILITY", whiteout::models::wem::Channel::Visibility, R"doc(F32 or Bool. MDX KATV/KLAV/KRVS, M2 attachment/light visibility, M3's SDFG-keyed dynamic state (§10.8.2).)doc")
.value("COLOR", whiteout::models::wem::Channel::Color, R"doc(F32x3. A light's colour, a geoset animation's tint, a fresnel colour.)doc")
.value("ALPHA", whiteout::models::wem::Channel::Alpha, R"doc(F32. MDX KMTA/KGAO, M2 texture weights, M3 layer alpha.)doc")
.value("INTENSITY", whiteout::models::wem::Channel::Intensity, R"doc(F32. Light.)doc")
.value("ATTENUATION_START", whiteout::models::wem::Channel::AttenuationStart, R"doc(F32. MDX KLAS, M2 `attenuationStart`.)doc")
.value("ATTENUATION_END", whiteout::models::wem::Channel::AttenuationEnd, R"doc(F32. MDX KLAE, M2 `attenuationEnd`.)doc")
.value("UV_TRANSLATE", whiteout::models::wem::Channel::UvTranslate, R"doc(F32x3 (MDX/M2 key three components even for a 2D transform).)doc")
.value("UV_ROTATE", whiteout::models::wem::Channel::UvRotate, R"doc(Quat on MDX/M2, F32 where the source keys an angle.)doc")
.value("UV_SCALE", whiteout::models::wem::Channel::UvScale, R"doc(F32x3.)doc")
.value("WEIGHT", whiteout::models::wem::Channel::Weight, R"doc(F32. A blend factor with no better name: MDX's fresnel team-colour amount, M3's layer blend weights.)doc")
.value("TEXTURE_INDEX", whiteout::models::wem::Channel::TextureIndex, R"doc(U32. MDX KMTF's flipbook frame and KRTX's ribbon slot.)doc")
.value("EMISSIVE", whiteout::models::wem::Channel::Emissive, R"doc(F32. MDX KMTE.)doc")
.value("EMITTER_PROPERTY", whiteout::models::wem::Channel::EmitterProperty, R"doc(A node system's own property -- an emitter system's (§10.9), a force field's or a vertex warp's; `sub` says which. The type is the property's, not this entry's.)doc")
.value("SHADOW_CASTING_START", whiteout::models::wem::Channel::ShadowCastingStart, R"doc(F32. MDX KLSS (v1300).)doc")
.value("SHADOW_CASTING_END", whiteout::models::wem::Channel::ShadowCastingEnd, R"doc(F32. MDX KLSE (v1300).)doc")
.value("QUADRATIC_FALLOFF", whiteout::models::wem::Channel::QuadraticFalloff, R"doc(F32. MDX KLQF (v1600).)doc")
.value("LINEAR_FALLOFF", whiteout::models::wem::Channel::LinearFalloff, R"doc(F32. MDX KLLF (v1600).)doc")
.value("DAMPING", whiteout::models::wem::Channel::Damping, R"doc(F32. MDX KLDA (v1600).)doc")
.value("STAGE_WEIGHT", whiteout::models::wem::Channel::StageWeight, R"doc(F32. A pose stage's weight, on its first driven node with `sub` the stage's id (WEM_ANIMATION_RUNTIME_DESIGN.md §5.1). The editor's own: every exporter skips it, and an export's bake consumes it.)doc")
.value("STAGE_SOURCE_WEIGHT", whiteout::models::wem::Channel::StageSourceWeight, R"doc(F32. A constraint source's weight, on the stage's driven node with `sub` `StageSub(stage, source)`. The editor's own, as `StageWeight` is.)doc")
.value("STAGE_SOURCE_ENABLED", whiteout::models::wem::Channel::StageSourceEnabled, R"doc(F32. A Link source's share of the carry (`LinkShares`), held from key to key as a visibility is; placed as `StageSourceWeight` is.)doc")
.value("FOCUS_DISTANCE", whiteout::models::wem::Channel::FocusDistance, R"doc(F32. MDX IDUF, in scene units; 0 turns depth of field off.)doc")
.value("FOCAL_LENGTH", whiteout::models::wem::Channel::FocalLength, R"doc(F32. MDX ELAF, in millimetres.)doc")
.value("F_STOP", whiteout::models::wem::Channel::FStop, R"doc(F32. MDX PTSF.)doc")
.value("TARGET", whiteout::models::wem::Channel::Target, R"doc(F32x3. An offset from `CameraPayload::target`: MDX KTTR, M2 `targetPositions`.)doc")
.value("ROLL", whiteout::models::wem::Channel::Roll, R"doc(F32. Radians about the line of sight: MDX KCRL, M2 `roll`.)doc")
.value("PHYSICS_DYNAMIC", whiteout::models::wem::Channel::PhysicsDynamic, R"doc(F32. Whether a body simulates now; M3 `PHRB.dynamicState`.)doc")
.value("CLOTH_ACTIVE", whiteout::models::wem::Channel::ClothActive, R"doc(F32. Whether a cloth writes back; M3 `PHCL.active`.)doc")
.value("COUNT", whiteout::models::wem::Channel::Count)
;
py::enum_<whiteout::models::wem::Interpolation>(m, "Interpolation")
.value("STEP", whiteout::models::wem::Interpolation::Step, R"doc(Hold the previous key. M3's AnimRef flags **bit 4** (§10.8.2).)doc")
.value("LINEAR", whiteout::models::wem::Interpolation::Linear, R"doc(Componentwise lerp.)doc")
.value("HERMITE", whiteout::models::wem::Interpolation::Hermite, R"doc(Two tangents per key; see `ValuesPerKey`.)doc")
.value("BEZIER", whiteout::models::wem::Interpolation::Bezier, R"doc(Two control values per key; stored the same way.)doc")
.value("SLERP", whiteout::models::wem::Interpolation::Slerp, R"doc(Quaternion shortest-arc. The default for `Channel::Rotation`.)doc")
.value("COUNT", whiteout::models::wem::Interpolation::Count)
;
py::enum_<whiteout::models::wem::ClipFlags>(m, "ClipFlags", R"doc(What starts this clip, beyond a host asking for it by name.
**`AutoPlay | WorldClocked` is one concept across three formats**: M3's SEQS flag 0x2, M2's global sequences, MDX's `globalSeqId` tracks. All three are a loop the model runs on its own, off a clock that is not the host play's — and all three become this.)doc")
.value("NONE", whiteout::models::wem::ClipFlags::None)
.value("AUTO_PLAY", whiteout::models::wem::ClipFlags::AutoPlay, R"doc(Started at anim-state init, not by a play request.)doc")
.value("PERSISTENT", whiteout::models::wem::ClipFlags::Persistent, R"doc(Survives an anim-state change.)doc")
.value("WORLD_CLOCKED", whiteout::models::wem::ClipFlags::WorldClocked, R"doc(Timed off the world clock, not the play's own bracket.)doc")
;
py::enum_<whiteout::models::wem::ReadRule>(m, "ReadRule", R"doc(How one of a clip's tracks is read: the interpolation, the window and the wrap of the game the clip was made for (WEM_ANIMATION_RUNTIME_DESIGN.md §3.1).
Stored, because a conversion rewrites the keys for another game and the rule has to say which arithmetic the keys now in the clip were written for.)doc")
.value("WC3", whiteout::models::wem::ReadRule::Wc3, R"doc(Warcraft III: keys inside the window, the MDX curves, the window's wrap.)doc")
.value("SC2", whiteout::models::wem::ReadRule::Sc2, R"doc(StarCraft II: a raw quaternion lerp; a looping track wraps at its own last key.)doc")
.value("WOW", whiteout::models::wem::ReadRule::Wow, R"doc(World of Warcraft: a quaternion nlerp without a sign flip; the ends hold.)doc")
;
py::enum_<whiteout::models::wem::PhysicsShapeKind>(m, "PhysicsShapeKind", R"doc(Appended, never reordered: the value is what a `PSHP` chunk stores.)doc")
.value("BOX", whiteout::models::wem::PhysicsShapeKind::Box)
.value("SPHERE", whiteout::models::wem::PhysicsShapeKind::Sphere)
.value("CAPSULE", whiteout::models::wem::PhysicsShapeKind::Capsule)
.value("CYLINDER", whiteout::models::wem::PhysicsShapeKind::Cylinder)
.value("CONVEX_HULL", whiteout::models::wem::PhysicsShapeKind::ConvexHull)
.value("TRIANGLE_MESH", whiteout::models::wem::PhysicsShapeKind::TriangleMesh)
.value("COUNT", whiteout::models::wem::PhysicsShapeKind::Count)
;
py::enum_<whiteout::models::wem::BodyMotion>(m, "BodyMotion", R"doc(How a body is created, in `dmBodyType` order. Whether it simulates at a moment is `PhysicsBody::simulates` and its channel.)doc")
.value("DYNAMIC", whiteout::models::wem::BodyMotion::Dynamic)
.value("KINEMATIC", whiteout::models::wem::BodyMotion::Kinematic)
.value("STATIC", whiteout::models::wem::BodyMotion::Static)
.value("COUNT", whiteout::models::wem::BodyMotion::Count)
;
py::enum_<whiteout::models::wem::JointKind>(m, "JointKind", R"doc(Appended, never reordered. StarCraft II has the first four.)doc")
.value("SPHERICAL", whiteout::models::wem::JointKind::Spherical)
.value("REVOLUTE", whiteout::models::wem::JointKind::Revolute)
.value("CONE_TWIST", whiteout::models::wem::JointKind::ConeTwist)
.value("WELD", whiteout::models::wem::JointKind::Weld)
.value("PRISMATIC", whiteout::models::wem::JointKind::Prismatic)
.value("DISTANCE", whiteout::models::wem::JointKind::Distance)
.value("COUNT", whiteout::models::wem::JointKind::Count)
;
py::enum_<whiteout::models::wem::JointFriction>(m, "JointFriction")
.value("NONE", whiteout::models::wem::JointFriction::None)
.value("TORQUE", whiteout::models::wem::JointFriction::Torque, R"doc(A holding torque (World of Warcraft).)doc")
.value("GRAVITY_HOLD", whiteout::models::wem::JointFriction::GravityHold, R"doc(A multiplier on an estimated gravity-holding torque (StarCraft II).)doc")
.value("COUNT", whiteout::models::wem::JointFriction::Count)
;
py::enum_<whiteout::models::wem::ClothColliderKind>(m, "ClothColliderKind")
.value("CAPSULE", whiteout::models::wem::ClothColliderKind::Capsule)
.value("PLANE", whiteout::models::wem::ClothColliderKind::Plane)
.value("COUNT", whiteout::models::wem::ClothColliderKind::Count)
;
py::enum_<whiteout::models::wem::RigStart>(m, "RigStart", R"doc(When a rig starts simulating.)doc")
.value("ANIMATED", whiteout::models::wem::RigStart::Animated)
.value("ON_DEATH", whiteout::models::wem::RigStart::OnDeath)
.value("ALWAYS", whiteout::models::wem::RigStart::Always)
.value("NEVER", whiteout::models::wem::RigStart::Never)
.value("COUNT", whiteout::models::wem::RigStart::Count)
;
py::enum_<whiteout::models::wem::ValidateLevel>(m, "ValidateLevel")
.value("STRUCTURAL", whiteout::models::wem::ValidateLevel::Structural, R"doc(Connectivity and index ranges.)doc")
.value("MANIFOLD", whiteout::models::wem::ValidateLevel::Manifold, R"doc(Structural, plus the §5.10 contract.)doc")
.value("PROFILE", whiteout::models::wem::ValidateLevel::Profile, R"doc(Manifold, plus per-profile limits and coverage.)doc")
;
py::class_<whiteout::models::wem::PhysicsCaps>(m, "PhysicsCaps", R"doc(The physics a profile's own format carries (WEM_PHYSICS_DESIGN.md §3.11), read by `Validate` and the exporters.
Empty for a profile whose format has none: its exports bake physics into keys instead. The masks are a bit per `PhysicsShapeKind` / `JointKind` (`physics/physics.h`), plain words for the same reason `NodeKindMask` is.)doc")
.def(py::init<>())
.def_readwrite("shape_kinds", &whiteout::models::wem::PhysicsCaps::shapeKinds)
.def_readwrite("joint_kinds", &whiteout::models::wem::PhysicsCaps::jointKinds)
.def_readwrite("cloth", &whiteout::models::wem::PhysicsCaps::cloth)
.def_readwrite("max_cloth_particles", &whiteout::models::wem::PhysicsCaps::maxClothParticles)
.def_readwrite("max_hull_vertices", &whiteout::models::wem::PhysicsCaps::maxHullVertices)
.def_readwrite("max_hull_faces", &whiteout::models::wem::PhysicsCaps::maxHullFaces)
.def_readwrite("max_hull_half_edges", &whiteout::models::wem::PhysicsCaps::maxHullHalfEdges)
.def_readwrite("max_cloth_anchor_bone", &whiteout::models::wem::PhysicsCaps::maxClothAnchorBone, R"doc(The highest bone index a cloth anchor may name.)doc")
;
py::class_<whiteout::models::wem::ProfileDesc>(m, "ProfileDesc", R"doc(Everything WEM knows about a profile, as data.)doc")
.def(py::init<>())
.def_readwrite("id", &whiteout::models::wem::ProfileDesc::id)