From cc1f9e6734347a7e5db86de0026c49b97612408c Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Wed, 26 Aug 2026 09:31:22 +0000 Subject: [PATCH] re: decode CollisionSet_*.bin -- the per-object collision-mesh library All 18 blobs are byte-identical: the per-stage naming is nominal, and every stage points at one shared 1675148-byte library stored eighteen times. That identical size was the reason to open the item, and it turned out to be the answer to it. Record layout: {u32 size, u32 name_len, char name[name_len], u32 nv, u32 nt, f32[3] x nv, u32[3] x nt}, next record at off + 8 + size. The indices are u32 here where MCOL uses u16 -- two different serialisers in one archive. What makes this a decode rather than a plausible reading: the walk consumes the file to the byte over 158 variable-length records, with the size word predicted from the two counts 158/158. A wrong field would desynchronise within a few records and could not land exactly on the end. All indices in range 158/158; 98.24% of edges shared by exactly two triangles; 147/158 fully manifold. 158 meshes, 90 836 triangles: per-part ship proxies (_bdy/_brg/_eng/_wep/_sld, the XBG7 sub-part vocabulary) plus 46 stage asteroid meshes whose prefixes are exactly the stages that have an _AsteroidVolume_wp MCOL. Two things this file makes me walk back: * The "1 unit = 1 metre" reading from mapmesh_box_500km is downgraded to amber. The 500000 arithmetic stands, but it implies that a craft the game's own tables call "small" is 133 m and that rob_f002 is 447 km -- 89% of the arena width. The format check survives; the interpretation has no independent support. * The _cmesh <-> render-model name link is recorded as UNTESTED, not confirmed: only one .xbg build manifest survives on the disc, so matching stems against object names covers 4 of 158, which is no coverage at all. --- docs/re/BACKLOG.md | 21 +++- docs/re/data/collisionset.txt | 171 +++++++++++++++++++++++++++ docs/re/structures/collisionset.md | 109 +++++++++++++++++ docs/re/structures/mcol-collision.md | 13 +- tools/re-capture/collisionset.py | 121 +++++++++++++++++++ 5 files changed, 430 insertions(+), 5 deletions(-) create mode 100644 docs/re/data/collisionset.txt create mode 100644 docs/re/structures/collisionset.md create mode 100755 tools/re-capture/collisionset.py diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index c0fe3963..6784c58c 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -425,8 +425,25 @@ search cannot find a *schedule*. use that bare box; `_AsteroidVolume_` names the rest. `tools/re-capture/miscbin_names.py [--pairs]` ([`data/miscbin-names.txt`](data/miscbin-names.txt)). - ❔ Still open: the 18 `CollisionSet_*.bin` are named but **not decoded** (magic - `0x00000810`, all exactly 1 675 148 bytes). The runtime consumer now has a + ✅ **`CollisionSet_*.bin` decoded (2026-08-26) — the per-*object* library.** + All 18 are **byte-identical**, so the per-stage naming is nominal: one shared + 1 675 148-byte library stored eighteen times. Record = + `{u32 size, u32 name_len, char name[], u32 nv, u32 nt, f32[3]×nv, u32[3]×nt}` + (note `u32` indices, where `MCOL` uses `u16`), next at `off + 8 + size`. The + walk **consumes the file exactly** over 158 variable-length records with the + size word predicted from the counts **158/158**, all indices in range + **158/158**, and 98.24 % of edges shared by two triangles (**147/158** fully + manifold). 158 meshes / 90 836 triangles: `rou_`/`mob_`/`rob_` per-part ship + proxies (`_bdy`/`_brg`/`_eng`/`_wep`/`_sld`) plus 46 stage asteroids. + ⚠️ **This downgraded the "1 unit = 1 m" claim to 🟡** — it implies a craft the + tables call *small* is 133 m and the largest object 447 km (89 % of the arena). + 🟡 The `_cmesh`↔render-model name link is **untested, not confirmed**: only one + `.xbg` build manifest survives, so the check covers 4/158. + `tools/re-capture/collisionset.py verify|list|obj` + ([`data/collisionset.txt`](data/collisionset.txt)). + ❔ Still open: no material/surface-type field exists in the record, and the + library is not indexed — name lookup from the unit tables is untraced. + The runtime consumer now has a name — `CMapColliderBridge` in the `.pe` RTTI at 9 044 264 — but has not been followed into the code. See [`structures/mcol-collision.md`](structures/mcol-collision.md). diff --git a/docs/re/data/collisionset.txt b/docs/re/data/collisionset.txt new file mode 100644 index 00000000..e0649179 --- /dev/null +++ b/docs/re/data/collisionset.txt @@ -0,0 +1,171 @@ +CollisionSet blobs: 18 distinct contents: 1 +records: 158 bytes consumed: 1675148/1675148 (EXACT) +size word == 16 + name_len + 12*nv + 12*nt - 8 : 158/158 +every triangle index < vertex count : 158/158 +edges shared by exactly two triangles : 132167/134529 = 98.24% +fully manifold meshes : 147/158 + +PASS + name verts tris bbox diagonal + mob_n040_cmesh 58 112 6820.3 + mob_n041_cmesh 39 74 2564.5 + mob_n042_cmesh 14 24 1276.0 + mob_n043_cmesh 14 24 738.9 + mob_n050_cmesh 58 112 6820.3 + mob_n051_cmesh 49 94 3247.5 + mob_n052_cmesh 18 32 1861.9 + mob_n053_cmesh 20 36 1628.5 + mob_n054_cmesh 15 26 778.6 + mob_n055_cmesh 19 34 768.1 + mob_n056_cmesh 21 38 751.4 + mob_n060_cmesh 95 186 9211.9 + mob_n061_cmesh 27 50 4361.3 + mob_n062_cmesh 18 32 1523.0 + mob_n063_cmesh 18 32 1531.0 + mob_n064_cmesh 15 26 835.3 + mob_n065_cmesh 17 30 882.6 + mob_n066_cmesh 15 26 961.3 + mob_n110_bdy_cmesh 29 54 5463.7 + mob_n111_bdy_cmesh 28 52 3602.2 + mob_n112_bdy_cmesh 21 38 1093.2 + mob_n113_bdy_cmesh 20 36 1160.5 + mob_n114_bdy_cmesh 14 24 765.3 + mob_n115_bdy_cmesh 18 32 888.3 + mob_n116_bdy_cmesh 19 34 901.4 + mob_n500_cmesh 26 48 363.2 + mob_n900_b01_cmesh 561 1082 17288.3 + mob_n900_b02_cmesh 545 1050 17288.3 + mob_n900_cmesh 569 1098 17288.3 + mob_n900_gear_cmesh 700 1556 2858.9 + rob_e001_cmesh 1605 4402 44378.7 + rob_e005_bdy_cmesh 3802 7004 50179.3 + rob_f001_bdy_cmesh 3459 6356 50179.3 + rob_f002_cmesh 3682 6968 447521.9 + rou_e010_cmesh 76 148 133.2 + rou_e011_cmesh 94 184 235.8 + rou_e017_cmesh 76 148 133.2 + rou_e018_cmesh 94 184 235.8 + rou_e101_bdy_cmesh 380 756 9371.1 + rou_e101_brg_01_cmesh 71 130 2264.1 + rou_e101_brg_02_cmesh 78 152 2502.4 + rou_e101_eng_01_cmesh 92 156 3518.8 + rou_e101_eng_02_cmesh 92 156 3518.8 + rou_e101_wep_01_cmesh 18 32 2674.5 + rou_e102_bdy_cmesh 283 550 6933.2 + rou_e102_brg_cmesh 12 20 1153.8 + rou_e102_eng_cmesh 66 94 2493.4 + rou_e102_wep_cmesh 8 12 537.4 + rou_e104_bdy_cmesh 280 548 3946.0 + rou_e104_brg_01_cmesh 26 48 897.8 + rou_e104_eng_cmesh 42 64 1297.7 + rou_e104_ext_01_cmesh 32 60 1230.7 + rou_e104_ext_02_cmesh 32 60 1230.7 + rou_e105_bdy_cmesh 262 522 3757.1 + rou_e105_brg_cmesh 48 88 553.7 + rou_e105_eng_01_cmesh 56 96 1332.5 + rou_e105_wep_01_cmesh 12 20 251.6 + rou_e106_bdy_cmesh 412 836 2368.1 + rou_e106_brg_01_cmesh 22 40 356.8 + rou_e106_eng_cmesh 90 176 921.3 + rou_e106_wep_02_01_cmesh 25 46 539.0 + rou_e107_all_cmesh 78 152 419.9 + rou_e107_eng_01_cmesh 36 68 241.2 + rou_e108_all_cmesh 41 78 534.7 + rou_e108_eng_01_cmesh 8 12 266.0 + rou_e201_all_cmesh 121 238 2450.6 + rou_e201_eng_01_cmesh 25 46 937.3 + rou_e301_base_cmesh 12 20 328.0 + rou_e302_base_cmesh 12 20 193.4 + rou_e303_wep_cmesh 8 12 82.1 + rou_e401_cmesh 12 20 107.3 + rou_e402_cmesh 8 12 68.6 + rou_e501_101_cmesh 8 12 405.5 + rou_e501_102_106_cmesh 8 12 202.7 + rou_e501_104_cmesh 8 12 304.1 + rou_e501_105_cmesh 8 12 141.9 + rou_e901_barrier_cmesh 58 112 9534.9 + rou_e901_body_01_c 55 106 840.1 + rou_e901_body_02_c 16 28 258.3 + rou_e901_body_03_c 306 608 1224.2 + rou_e901_tail_L_c 24 44 861.9 + rou_e901_tail_R_c 24 44 861.9 + rou_e901_wing_01_L_c 8 12 181.2 + rou_e901_wing_01_R_c 8 12 181.2 + rou_e901_wing_02_L_c 15 26 382.3 + rou_e901_wing_02_R_c 15 26 382.3 + rou_e901_wing_03_L_c 32 60 895.9 + rou_e901_wing_03_R_c 32 60 895.9 + rou_e901_wing_04_L_c 14 24 209.5 + rou_e901_wing_04_R_c 14 24 209.5 + rou_e901_wing_05_L_c 38 72 1145.5 + rou_e901_wing_05_R_c 38 72 1145.5 + rou_e910_core_cmesh 1410 2816 2058.3 + rou_f101_bdy_cmesh 128 252 1943.1 + rou_f101_wep_01_cmesh 12 20 266.8 + rou_f102_bdy_cmesh 128 252 1943.1 + rou_f102_wep_01_cmesh 12 20 266.8 + rou_f104_bdy_cmesh 270 536 6907.9 + rou_f104_sld_01_cmesh 8 12 1021.4 + rou_f104_sld_02_cmesh 8 12 698.1 + rou_f105_bdy_cmesh 201 398 4345.7 + rou_f105_sld_01_cmesh 8 12 983.8 + rou_f106_bdy_cmesh 136 268 2212.3 + rou_f106_sld_01_cmesh 24 44 636.2 + rou_f106_sld_02_cmesh 24 44 636.2 + rou_f201_bdy_01_cmesh 96 180 421.1 + rou_f202_cmesh 159 314 2572.4 + rou_f301_body_cmesh 12 20 539.7 + rou_f302_cmesh 8 12 183.8 + rou_f303_body_cmesh 8 12 96.0 + rou_f401_cmesh 12 20 77.1 + rou_f402_cmesh 8 12 51.8 + S01_Asteroid_cmesh_01a 363 658 34254.3 + S01_Asteroid_cmesh_01b 212 376 30974.1 + S01_Asteroid_cmesh_02a 619 1138 39752.2 + S01_Asteroid_cmesh_02b 747 1370 39995.1 + S01_Asteroid_cmesh_03a 619 1158 40217.2 + S01_Asteroid_cmesh_03b 736 1360 46540.4 + S01_Asteroid_cmesh_04a 782 1448 44984.2 + S01_Asteroid_cmesh_04b 535 978 40160.5 + S04_Asteroid_cmesh_01 895 1622 73822.3 + S04_Asteroid_cmesh_02a 1204 2176 69436.0 + S04_Asteroid_cmesh_02b 1282 2356 74150.4 + S04_Asteroid_cmesh_03 2003 3626 74048.6 + S04_Asteroid_cmesh_04 1862 3404 84713.5 + S04_Asteroid_cmesh_05 1397 2550 79901.0 + S04_Asteroid_cmesh_06a 442 788 52369.2 + S04_Asteroid_cmesh_06b 330 604 30301.4 + S04_Asteroid_cmesh_07a 746 1368 72327.1 + S04_Asteroid_cmesh_07b 134 244 48014.9 + S05_Asteroid_cmesh_01 219 406 38168.9 + S05_Asteroid_cmesh_02a 205 386 18885.4 + S05_Asteroid_cmesh_02b 204 384 20502.8 + S05_Asteroid_cmesh_03 164 300 25043.3 + S05_Asteroid_cmesh_04 188 344 34732.1 + S08_p1_Asteroid_cmesh_01a 722 1308 29753.7 + S08_p1_Asteroid_cmesh_01b 546 972 26043.0 + S08_p1_Asteroid_cmesh_02a 754 1348 26627.9 + S08_p1_Asteroid_cmesh_02b 404 736 24309.1 + S08_p1_Asteroid_cmesh_03a 430 772 21897.1 + S08_p1_Asteroid_cmesh_03b 509 918 25737.3 + S13_Asteroid_cmesh_01a 1201 2134 22779.1 + S13_Asteroid_cmesh_01b 706 1256 18998.4 + S13_Asteroid_cmesh_02a 398 708 13927.6 + S13_Asteroid_cmesh_02b 514 916 13470.3 + S14_p1_Asteroid_cmesh_01a 328 600 20990.0 + S14_p1_Asteroid_cmesh_01b 257 466 15479.2 + S14_p1_Asteroid_cmesh_02a 570 1040 24344.0 + S14_p1_Asteroid_cmesh_02b 379 698 16168.5 + S14_p1_Asteroid_cmesh_03a 247 442 18536.1 + S14_p1_Asteroid_cmesh_03b 584 1064 23480.9 + S14_p1_Asteroid_cmesh_04a 123 218 13485.6 + S14_p1_Asteroid_cmesh_04b 169 302 14212.4 + S14_p1_Asteroid_cmesh_05 229 410 16777.9 + S14_p2_Asteroid_cmesh_01a 386 696 19582.7 + S14_p2_Asteroid_cmesh_01b 442 812 21074.7 + S14_p2_Asteroid_cmesh_02a 113 202 10183.9 + S14_p2_Asteroid_cmesh_02b 173 314 10685.8 + +158 meshes, 90836 triangles + +PASS diff --git a/docs/re/structures/collisionset.md b/docs/re/structures/collisionset.md new file mode 100644 index 00000000..db269876 --- /dev/null +++ b/docs/re/structures/collisionset.md @@ -0,0 +1,109 @@ +# `CollisionSet_*.bin` — the per-object collision-mesh library + +**✅ Decoded 2026-08-26.** The 18 remaining `hidden/MiscBin.pak` entries, named +in the previous iteration as `CollisionSet_S01…S16.bin`, +`CollisionSet_Tutorial.bin` and `CollisionSet_test.bin` from the `StageResource` +objects' `CollisionMeshes` field. See +[`mcol-collision.md`](mcol-collision.md) for the naming and for `MCOL`, which is +the *map* hull; this is the per-*object* library. + +## ✅ All 18 are byte-identical + + CollisionSet blobs: 18 distinct contents: 1 + +The per-stage naming is nominal. Every stage points at the same 1 675 148-byte +library, stored eighteen times — which is why the sizes were identical, the +observation that made this worth opening. + +## ✅ The record layout + + u32 size bytes following this word's own 8-byte prefix + u32 name_len 16, 24 or 32 — the name field, padded to a multiple of 8 + char name[name_len] + u32 vertex_count + u32 triangle_count + f32 vertex[3] × vertex_count + u32 index[3] × triangle_count + +and the next record starts at `offset + 8 + size`. Note the indices are **`u32`** +here, where `MCOL` uses `u16` — these are two different serialisers. + +| | | +|---|---| +| records | **158** | +| bytes consumed | **1 675 148 / 1 675 148 — exact** | +| `size == 16 + name_len + 12·nv + 12·nt − 8` | **158 / 158** | +| every triangle index `<` vertex count | **158 / 158** | + +The walk consuming the file to the byte over 158 variable-length records, with +the size word predicted from the two counts every time, is what makes this a +decode rather than a plausible reading — a wrong field would desynchronise the +walk within a few records and could not land exactly on the end. + +## ✅ And they are meshes + +| | | +|---|---| +| edges shared by exactly two triangles | **132 167 / 134 529 = 98.24 %** | +| fully manifold meshes (every edge shared by two) | **147 / 158** | + +92 of the 158 have Euler characteristic **2** — a single closed surface. The +remainder are multi-component (χ = 4, 6, 8) or open shells, which is expected +for parts like `rou_e901_wing_01_L` that are one piece of a larger hull. + +## ✅ What is in it + +158 meshes, **48 241 vertices and 90 836 triangles**, in four families: + +| prefix | count | what the names look like | +|---|---|---| +| `rou_` | 78 | `rou_e101_bdy`, `rou_e101_brg_01`, `rou_e106_eng`, `rou_f104_sld_02` | +| `mob_` | 30 | `mob_n040`, `mob_n110_bdy`, `mob_n900_gear` | +| `rob_` | 4 | `rob_e001`, `rob_e005_bdy`, `rob_f001_bdy`, `rob_f002` | +| stage asteroids | 46 | `S01_Asteroid_cmesh_01a`, `S14_p1_Asteroid_cmesh_05` | + +The `_bdy` / `_brg` / `_eng` / `_wep` / `_sld` suffixes are body, bridge, engine, +weapon and shield — the same sub-part vocabulary as the `XBG7` render models, so +these are per-part collision proxies. The asteroid meshes carry exactly the +stage prefixes that have an `_AsteroidVolume_wp` `MCOL`. + +🟡 The name correspondence to the render models is **not demonstrated**. Only one +`XBG2` build manifest survives on the disc (`machines\rou_e104\…`), so matching +`_cmesh` stems against `.xbg` object names covers 4 of 158 — no coverage, and +therefore no evidence either way. It is not a refutation; it is an untested claim. + +## 🟡 The world unit — the metre reading does not survive this file + +[`mcol-collision.md`](mcol-collision.md) inferred **1 unit = 1 metre** from +`mapmesh_box_500km.col` spanning exactly 500 000 units. The arithmetic is exact, +but this library is the first chance to check what that scale *implies*, and the +implications are large: + +| mesh | bbox diagonal, if 1 unit = 1 m | +|---|---| +| `rou_e010` (`e010_ADAN_Attacker_S` — an "attacker, small") | 133 m | +| `rou_e106_bdy` | 2 368 m | +| `rou_e101_bdy` | 9 371 m | +| `rob_f001_bdy` | 50 179 m | +| `S04_Asteroid_cmesh_04` | 84 714 m | +| `rob_f002` | 447 522 m — 89 % of the arena's width | + +A craft the game's own tables call *small* would be 133 m, and the largest object +would nearly span the arena. That is not impossible for the genre, but it is not +the corroboration I claimed. **Downgrading the scale claim to 🟡**: either the +game's units really are metres and its ships are kilometre-scale, or `500km` in +that filename names something other than the box's span. The 500 000 arithmetic +stands; the *interpretation* does not have independent support. + +An independent scale reference — the flight model's speed values against a +measured traversal time in the running game — would settle it, and has not been +run. + +Reproduce with `tools/re-capture/collisionset.py verify` / `list` +([`../data/collisionset.txt`](../data/collisionset.txt)); `collisionset.py obj` +exports any named mesh. + +❔ Still open: the meshes carry no material, flag or surface-type field — the +record is exactly counts, positions and indices — so whatever distinguishes a +hull hit from a shield hit is not stored here. And the library is not indexed: +lookup is presumably by name from the unit tables, which has not been traced. diff --git a/docs/re/structures/mcol-collision.md b/docs/re/structures/mcol-collision.md index 653f1509..5404d73b 100644 --- a/docs/re/structures/mcol-collision.md +++ b/docs/re/structures/mcol-collision.md @@ -540,9 +540,16 @@ triangles spanning exactly ±250 000** — a cube. Its name says that cube is > **one world unit is one metre.** -This is a name-based inference, but the arithmetic is exact and it runs the -other way as a check on the format work: a wrong stride or index width could not -have produced a box whose measured size matches its own filename. +The arithmetic is exact and it runs the other way as a check on the format work: +a wrong stride or index width could not have produced a box whose measured size +matches its own filename. + +> 🟡 **Downgraded 2026-08-26, same day.** The *format* check stands. The +> *interpretation* does not: decoding `CollisionSet_*.bin` gave the first chance +> to see what a metre implies, and it makes a craft the game's own tables call +> "small" 133 m long and the largest object 447 km — 89 % of the arena. Either +> the ships really are kilometre-scale or `500km` names something other than the +> span. See [`collisionset.md`](collisionset.md). Reproduce with `tools/re-capture/miscbin_names.py [--pairs]` (recorded in [`../data/miscbin-names.txt`](../data/miscbin-names.txt)). diff --git a/tools/re-capture/collisionset.py b/tools/re-capture/collisionset.py new file mode 100755 index 00000000..aa28519f --- /dev/null +++ b/tools/re-capture/collisionset.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Decode `CollisionSet_*.bin` from `hidden/MiscBin.pak`. + +All 18 of them are byte-identical, so the per-stage naming is nominal: there is +one shared library of 158 named per-object collision meshes (ships, stations, +asteroids), separate from the per-map `MCOL` hull. + + ./collisionset.py list # the 158 meshes + ./collisionset.py verify # the checks in the doc + ./collisionset.py obj + +A record is + + u32 size bytes that follow this word's own 8-byte prefix + u32 name_len 16, 24 or 32 -- the name field, padded to a multiple of 8 + char name[name_len] + u32 vertex_count + u32 triangle_count + f32 vertex[3] x vertex_count + u32 index[3] x triangle_count -- note u32, not the u16 MCOL uses + +and the next record begins at `offset + 8 + size`. +""" + +import collections +import math +import os +import struct +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from regn_decode import pak_entries +from unitgroup import name_hash + + +def meshes(pak_path, which="CollisionSet_S01.bin"): + """[(name, vertices, triangles)] for one CollisionSet blob.""" + blob = dict(pak_entries(pak_path))[name_hash(which)] + u32 = lambda o: struct.unpack_from(">I", blob, o)[0] + out, p = [], 0 + while p < len(blob): + size, nlen = u32(p), u32(p + 4) + name = blob[p + 8:p + 8 + nlen].split(b"\0")[0].decode("latin-1") + nv, nt = u32(p + 8 + nlen), u32(p + 12 + nlen) + vb = p + 16 + nlen + tb = vb + 12 * nv + verts = [struct.unpack_from(">fff", blob, vb + 12 * i) for i in range(nv)] + tris = [struct.unpack_from(">III", blob, tb + 12 * i) for i in range(nt)] + expect = 16 + nlen + 12 * nv + 12 * nt - 8 + out.append((name, verts, tris, size == expect)) + p += 8 + size + return out, len(blob), p + + +def cmd_list(pak): + ms, _, _ = meshes(pak) + print(f"{'name':>28} {'verts':>6} {'tris':>6} {'bbox diagonal':>14}") + for name, v, t, _ in ms: + lo = [min(q[k] for q in v) for k in range(3)] + hi = [max(q[k] for q in v) for k in range(3)] + print(f"{name:>28} {len(v):6d} {len(t):6d} {math.dist(lo, hi):14.1f}") + print(f"\n{len(ms)} meshes, {sum(len(t) for _, _, t, _ in ms)} triangles") + return True + + +def cmd_verify(pak): + blobs = dict(pak_entries(pak)) + tags = ["test", "Tutorial"] + ["S%02d" % i for i in range(1, 17)] + distinct = {blobs[name_hash(f"CollisionSet_{t}.bin")] for t in tags} + print(f"CollisionSet blobs: {len(tags)} distinct contents: {len(distinct)}") + + ms, total, consumed = meshes(pak) + print(f"records: {len(ms)} bytes consumed: {consumed}/{total}" + f" ({'EXACT' if consumed == total else 'MISMATCH'})") + print(f"size word == 16 + name_len + 12*nv + 12*nt - 8 : " + f"{sum(ok for _, _, _, ok in ms)}/{len(ms)}") + + in_range = sum(all(max(t) < len(v) for t in tris) if tris else 1 + for _, v, tris, _ in ms) + print(f"every triangle index < vertex count : {in_range}/{len(ms)}") + + closed = e2 = etot = 0 + for _, v, tris, _ in ms: + ec = collections.Counter() + for t in tris: + for k in range(3): + a, b = t[k], t[(k + 1) % 3] + ec[(min(a, b), max(a, b))] += 1 + etot += len(ec) + e2 += sum(1 for n in ec.values() if n == 2) + closed += all(n == 2 for n in ec.values()) + print(f"edges shared by exactly two triangles : " + f"{e2}/{etot} = {100*e2/etot:.2f}%") + print(f"fully manifold meshes : {closed}/{len(ms)}") + return (len(distinct) == 1 and consumed == total + and all(ok for _, _, _, ok in ms) and in_range == len(ms)) + + +def cmd_obj(pak, want, out): + for name, v, tris, _ in meshes(pak)[0]: + if name != want: + continue + with open(out, "w") as fh: + fh.write(f"# CollisionSet {name} -- {len(v)} vertices, {len(tris)} triangles\n") + for q in v: + fh.write("v %.4f %.4f %.4f\n" % q) + for t in tris: + fh.write(f"f {t[0]+1} {t[1]+1} {t[2]+1}\n") + print(f"{out}: {len(v)} vertices, {len(tris)} triangles") + return True + raise SystemExit(f"no mesh named {want!r}") + + +if __name__ == "__main__": + if len(sys.argv) < 3: + raise SystemExit(__doc__) + cmd, pak = sys.argv[1], sys.argv[2] + fn = {"list": cmd_list, "verify": cmd_verify}.get(cmd) + ok = cmd_obj(pak, sys.argv[3], sys.argv[4]) if cmd == "obj" else fn(pak) + print("\nPASS" if ok else "\nFAIL") + sys.exit(0 if ok else 1)