tmb-codec 0.2.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tmb_codec/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """Python bindings for the TMB1 triangle mesh codec."""
2
+
3
+ from .codec import (
4
+ CORNER_ROTATION,
5
+ FACE_IDS,
6
+ VERTEX_IDS,
7
+ MeshInfo,
8
+ decode,
9
+ encode,
10
+ inspect,
11
+ )
12
+
13
+ __all__ = [
14
+ "CORNER_ROTATION",
15
+ "FACE_IDS",
16
+ "VERTEX_IDS",
17
+ "MeshInfo",
18
+ "decode",
19
+ "encode",
20
+ "inspect",
21
+ ]
22
+
23
+ __version__ = "0.2.0"
Binary file
tmb_codec/codec.py ADDED
@@ -0,0 +1,230 @@
1
+ """NumPy API for the native TMB1 codec."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes as ct
6
+ import os
7
+ import struct
8
+ import sys
9
+ from dataclasses import dataclass
10
+ from importlib.resources import files
11
+ from pathlib import Path
12
+
13
+ import numpy as np
14
+ import numpy.typing as npt
15
+ import zstandard as zstd
16
+
17
+ VERTEX_IDS = 1 << 0
18
+ FACE_IDS = 1 << 1
19
+ CORNER_ROTATION = 1 << 2
20
+
21
+ _MAGIC = 0x31424D54
22
+ _VERSION = 1
23
+ _QUANTIZED = 1 << 8
24
+ _BITS_SHIFT = 16
25
+ _HEADER = struct.Struct("<10I")
26
+ _MAX_RAW = 128 * 1024 * 1024
27
+ _LIBRARY: ct.CDLL | None = None
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class MeshInfo:
32
+ vertex_count: int
33
+ face_count: int
34
+ bits: int | None
35
+ flags: int
36
+ compressed: bool
37
+
38
+ @property
39
+ def lossless(self) -> bool:
40
+ return self.bits is None
41
+
42
+
43
+ def _library_name() -> str:
44
+ if os.name == "nt":
45
+ return "tmb_codec.dll"
46
+ if sys.platform == "darwin":
47
+ return "libtmb_codec.dylib"
48
+ return "libtmb_codec.so"
49
+
50
+
51
+ def _library() -> ct.CDLL:
52
+ global _LIBRARY
53
+ if _LIBRARY is not None:
54
+ return _LIBRARY
55
+ override = os.environ.get("TMB_CODEC_LIBRARY")
56
+ path = Path(override) if override else Path(str(files("tmb_codec") / "_native" / _library_name()))
57
+ if not path.is_file():
58
+ raise ModuleNotFoundError(f"TMB native library is not installed: {path}")
59
+ library = ct.CDLL(str(path.resolve()))
60
+ encode_base = [ct.c_void_p, ct.c_uint32, ct.c_void_p, ct.c_uint32, ct.c_uint32]
61
+ decode_base = [
62
+ ct.c_void_p,
63
+ ct.c_size_t,
64
+ ct.c_void_p,
65
+ ct.c_void_p,
66
+ ct.c_uint32,
67
+ ct.c_uint32,
68
+ ct.c_uint32,
69
+ ]
70
+ library.tmb_encode_lossless.argtypes = encode_base + [
71
+ ct.POINTER(ct.c_void_p),
72
+ ct.POINTER(ct.c_size_t),
73
+ ct.c_void_p,
74
+ ]
75
+ library.tmb_encode_lossless.restype = ct.c_int
76
+ library.tmb_encode_quantized.argtypes = encode_base + [
77
+ ct.c_uint32,
78
+ ct.POINTER(ct.c_void_p),
79
+ ct.POINTER(ct.c_size_t),
80
+ ct.c_void_p,
81
+ ]
82
+ library.tmb_encode_quantized.restype = ct.c_int
83
+ library.tmb_decode_lossless.argtypes = decode_base + [ct.c_void_p]
84
+ library.tmb_decode_lossless.restype = ct.c_int
85
+ library.tmb_decode_quantized.argtypes = decode_base + [ct.c_uint32, ct.c_void_p]
86
+ library.tmb_decode_quantized.restype = ct.c_int
87
+ library.tmb_free.argtypes = [ct.c_void_p]
88
+ library.tmb_free.restype = None
89
+ _LIBRARY = library
90
+ return library
91
+
92
+
93
+ def _arrays(
94
+ vertices: npt.ArrayLike, faces: npt.ArrayLike
95
+ ) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.uint32]]:
96
+ vertex_array = np.ascontiguousarray(vertices, dtype=np.float32)
97
+ original_faces = np.asarray(faces)
98
+ if (
99
+ vertex_array.ndim != 2
100
+ or vertex_array.shape[1] != 3
101
+ or not len(vertex_array)
102
+ or not np.isfinite(vertex_array).all()
103
+ ):
104
+ raise ValueError("vertices must be a finite float32 array with shape [V,3]")
105
+ if (
106
+ original_faces.ndim != 2
107
+ or original_faces.shape[1] != 3
108
+ or not len(original_faces)
109
+ or original_faces.dtype.kind not in "iu"
110
+ or original_faces.min() < 0
111
+ or original_faces.max() >= len(vertex_array)
112
+ ):
113
+ raise ValueError("faces must contain valid triangle vertex indices")
114
+ return vertex_array, np.ascontiguousarray(original_faces, dtype=np.uint32)
115
+
116
+
117
+ def encode(
118
+ vertices: npt.ArrayLike,
119
+ faces: npt.ArrayLike,
120
+ *,
121
+ bits: int | None = 14,
122
+ flags: int = VERTEX_IDS,
123
+ compress: bool = True,
124
+ compression_level: int = 2,
125
+ ) -> bytes:
126
+ """Encode a triangle mesh; ``bits=None`` preserves float32 positions exactly."""
127
+ if flags not in range(8):
128
+ raise ValueError("flags must be in [0,7]")
129
+ if bits is not None and not 1 <= bits <= 31:
130
+ raise ValueError("bits must be None or in [1,31]")
131
+ vertex_array, face_array = _arrays(vertices, faces)
132
+ library = _library()
133
+ output = ct.c_void_p()
134
+ output_size = ct.c_size_t()
135
+ timings = np.empty(3, dtype=np.float64)
136
+ common = (
137
+ vertex_array.ctypes.data,
138
+ len(vertex_array),
139
+ face_array.ctypes.data,
140
+ len(face_array),
141
+ flags,
142
+ )
143
+ code = (
144
+ library.tmb_encode_lossless(
145
+ *common, ct.byref(output), ct.byref(output_size), timings.ctypes.data
146
+ )
147
+ if bits is None
148
+ else library.tmb_encode_quantized(
149
+ *common, bits, ct.byref(output), ct.byref(output_size), timings.ctypes.data
150
+ )
151
+ )
152
+ if code:
153
+ raise ValueError(f"TMB1 encode failed ({code})")
154
+ try:
155
+ raw = ct.string_at(output, output_size.value)
156
+ finally:
157
+ library.tmb_free(output)
158
+ return zstd.ZstdCompressor(level=compression_level).compress(raw) if compress else raw
159
+
160
+
161
+ def _raw(data: bytes | bytearray | memoryview, compressed: bool) -> bytes:
162
+ body = bytes(data)
163
+ if not compressed:
164
+ return body
165
+ try:
166
+ size = zstd.frame_content_size(body)
167
+ if size < 0 or size == zstd.CONTENTSIZE_UNKNOWN or size > _MAX_RAW:
168
+ raise ValueError("invalid decompressed size")
169
+ return zstd.ZstdDecompressor(max_window_size=128 * 1024).decompress(
170
+ body, max_output_size=_MAX_RAW, allow_extra_data=False
171
+ )
172
+ except zstd.ZstdError as exc:
173
+ raise ValueError("invalid Zstd frame") from exc
174
+
175
+
176
+ def _info(raw: bytes, compressed: bool) -> MeshInfo:
177
+ if len(raw) < _HEADER.size:
178
+ raise ValueError("truncated TMB1 header")
179
+ header = _HEADER.unpack_from(raw)
180
+ magic, version, wire_flags, vertex_count, face_count = header[:5]
181
+ flags = wire_flags & 7
182
+ quantized = bool(wire_flags & _QUANTIZED)
183
+ bits = (wire_flags >> _BITS_SHIFT) & 31
184
+ expected = flags | (_QUANTIZED if quantized else 0) | (bits << _BITS_SHIFT)
185
+ if magic != _MAGIC or version != _VERSION or wire_flags != expected:
186
+ raise ValueError("unsupported TMB1 version or flags")
187
+ if not 0 < vertex_count <= 2_000_000 or not 0 < face_count <= 4_000_000:
188
+ raise ValueError("TMB1 mesh exceeds supported limits")
189
+ if quantized != bool(bits):
190
+ raise ValueError("invalid TMB1 precision")
191
+ if sum(header[5:]) != len(raw) - _HEADER.size:
192
+ raise ValueError("invalid TMB1 section lengths")
193
+ return MeshInfo(vertex_count, face_count, bits if quantized else None, flags, compressed)
194
+
195
+
196
+ def inspect(data: bytes | bytearray | memoryview, *, compressed: bool = True) -> MeshInfo:
197
+ """Read validated public metadata without decoding the mesh."""
198
+ raw = _raw(data, compressed)
199
+ return _info(raw, compressed)
200
+
201
+
202
+ def decode(
203
+ data: bytes | bytearray | memoryview, *, compressed: bool = True
204
+ ) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.uint32]]:
205
+ """Decode TMB1 bytes into contiguous ``vertices`` and ``faces`` arrays."""
206
+ raw = _raw(data, compressed)
207
+ info = _info(raw, compressed)
208
+ vertices = np.empty((info.vertex_count, 3), dtype=np.float32)
209
+ faces = np.empty((info.face_count, 3), dtype=np.uint32)
210
+ timings = np.empty(2, dtype=np.float64)
211
+ common = (
212
+ raw,
213
+ len(raw),
214
+ vertices.ctypes.data,
215
+ faces.ctypes.data,
216
+ info.vertex_count,
217
+ info.face_count,
218
+ info.flags,
219
+ )
220
+ library = _library()
221
+ code = (
222
+ library.tmb_decode_lossless(*common, timings.ctypes.data)
223
+ if info.lossless
224
+ else library.tmb_decode_quantized(*common, info.bits, timings.ctypes.data)
225
+ )
226
+ if code:
227
+ raise ValueError(f"TMB1 decode failed ({code})")
228
+ if not np.isfinite(vertices).all() or faces.max() >= info.vertex_count:
229
+ raise ValueError("invalid decoded mesh")
230
+ return vertices, faces
@@ -0,0 +1,21 @@
1
+ Corto
2
+ Copyright (c) 2015-2022, Visual Computing Lab, ISTI - CNR
3
+ All rights reserved.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
tmb_codec/py.typed ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: tmb-codec
3
+ Version: 0.2.0
4
+ Summary: Fast Triangle Mesh Binary encoding and decoding
5
+ Author: Tyndall-log
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ License-File: licenses/CORTO_LICENSE.txt
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: C++
12
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
13
+ Project-URL: Documentation, https://github.com/Tyndall-log/tmb-codec#readme
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: numpy>=1.26
16
+ Requires-Dist: zstandard>=0.23
17
+ Description-Content-Type: text/markdown
18
+
19
+ # TMB Codec
20
+
21
+ TMB1 (Triangle Mesh Binary) is a compact general-purpose triangle-mesh codec. The
22
+ library provides one C ABI across Windows, macOS, Linux, and WebAssembly.
23
+ Native CI builds use Clang (`clang-cl` on Windows and `clang++` elsewhere).
24
+
25
+ ## Features
26
+
27
+ - Float32 bit-exact lossless positions
28
+ - Quantized positions from 1 through 31 bits
29
+ - Optional restoration of vertex IDs, face IDs, and triangle corner rotation
30
+ - Single-file C++17 implementation
31
+ - No runtime dependency on Zstd; compress the completed TMB1 bundle once outside
32
+ the codec when using `tmb1-zstd-v1`
33
+
34
+ Applications that require stable per-vertex data use `TMB_VERTEX_IDS` (`flags=1`). This restores
35
+ vertex rows and triangle references to original vertex IDs while allowing face
36
+ row order and cyclic starting corners to remain in traversal order.
37
+
38
+ ## Python
39
+
40
+ Install a prebuilt native wheel:
41
+
42
+ ```bash
43
+ pip install tmb-codec
44
+ ```
45
+
46
+ ```python
47
+ from tmb_codec import decode, encode
48
+
49
+ data = encode(vertices, faces, bits=14)
50
+ decoded_vertices, decoded_faces = decode(data)
51
+
52
+ lossless = encode(vertices, faces, bits=None)
53
+ ```
54
+
55
+ Wheels contain the Python API, the platform-native shared library, and license
56
+ notices. They do not contain `src/tmb_codec.cpp` or an sdist.
57
+
58
+ ## Native build
59
+
60
+ Windows:
61
+
62
+ ```powershell
63
+ cmake -S . -B build -G Ninja -DCMAKE_CXX_COMPILER=clang-cl -DTMB_BUILD_TESTS=ON
64
+ cmake --build build --parallel
65
+ ctest --test-dir build --output-on-failure
66
+ ```
67
+
68
+ macOS and Linux:
69
+
70
+ ```bash
71
+ cmake -S . -B build -G Ninja -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Release -DTMB_BUILD_TESTS=ON
72
+ cmake --build build --parallel
73
+ ctest --test-dir build --output-on-failure
74
+ ```
75
+
76
+ WebAssembly with Emscripten:
77
+
78
+ ```bash
79
+ emcmake cmake -S . -B build-wasm -DCMAKE_BUILD_TYPE=Release -DTMB_BUILD_TESTS=OFF
80
+ cmake --build build-wasm --parallel
81
+ node tests/wasm_smoke.mjs build-wasm/tmb_codec.js
82
+ ```
83
+
84
+ The WASM build emits `tmb_codec.js` and `tmb_codec.wasm` with
85
+ memory growth enabled and exports the five public TMB functions plus malloc/free.
86
+
87
+ Every push and pull request builds and installs native wheels on Windows, macOS,
88
+ and Linux and runs the Python tests against those installed wheels. WASM has a
89
+ separate Node.js round-trip test. Pushing a `v*` tag publishes wheels to PyPI and
90
+ then creates a GitHub Release only after every required job succeeds.
91
+
92
+ ## C API
93
+
94
+ ```cpp
95
+ #include <tmb_codec.h>
96
+
97
+ unsigned char* bytes = nullptr;
98
+ size_t byte_count = 0;
99
+ double timings[3] = {};
100
+ int result = tmb_encode_quantized(
101
+ vertices, vertex_count,
102
+ faces, face_count,
103
+ TMB_VERTEX_IDS, 14,
104
+ &bytes, &byte_count, timings);
105
+ if (result == TMB_OK) {
106
+ // Send zstd(bytes[0:byte_count]) or store the raw TMB1 bundle.
107
+ tmb_free(bytes);
108
+ }
109
+ ```
110
+
111
+ Inputs are contiguous `float[V][3]` positions and `uint32_t[F][3]` triangle
112
+ indices. Encoder output is allocated by the codec and must be released with
113
+ `tmb_free`. Decoder output arrays are allocated by the caller.
114
+
115
+ ## Format
116
+
117
+ TMB1 starts with ten little-endian uint32 values:
118
+
119
+ 1. Magic `TMB1`
120
+ 2. Format version `1`
121
+ 3. Flags
122
+ 4. Vertex count
123
+ 5. Face count
124
+ 6. Topology section size
125
+ 7. Position section size
126
+ 8. Vertex-ID section size
127
+ 9. Face-ID section size
128
+ 10. Corner-rotation section size
129
+
130
+ Quantized streams set flag bit 8 and store the coordinate bit count in flag bits
131
+ 16 through 20. Their position section begins with four little-endian float64
132
+ values: origin x/y/z and a common extent. Coordinates from 1 through 16 bits use
133
+ one integer component per axis. Coordinates from 17 through 31 bits use low/high
134
+ 16-bit components to keep prediction residuals inside signed 32-bit range.
135
+
136
+ The topology and position entropy streams are defined by the reference source in
137
+ `src/tmb_codec.cpp`. Consumers should use the provided encoder and decoder
138
+ unless they also maintain compatibility tests against the reference implementation.
139
+
140
+ ## License
141
+
142
+ The codec incorporates and modifies MIT-licensed Corto code. Preserve
143
+ `licenses/CORTO_LICENSE.txt` with source and binary distributions. Project-level
144
+ terms are in `LICENSE`; third-party attribution is in `THIRD_PARTY_NOTICES.md`.
@@ -0,0 +1,10 @@
1
+ tmb_codec/__init__.py,sha256=E5_bzkk2CHCqBtpNyEsj-YmUAku82AMxqKZhuH7IAUk,359
2
+ tmb_codec/_native/tmb_codec.dll,sha256=pmrdN5Pz7oktD_cOzCMfAMIk4sDGFDpVECdPqXsUn4o,211968
3
+ tmb_codec/codec.py,sha256=ig7lmaUve7BpgSaOS8XgxBtuObF9rZiV6sIc9gJjv1o,7840
4
+ tmb_codec/licenses/CORTO_LICENSE.txt,sha256=pO8euBMldLQyYyiVVpDkqp9wRdOd4fJzbyB9OtrobIo,1130
5
+ tmb_codec/py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
6
+ tmb_codec-0.2.0.dist-info/METADATA,sha256=moKzgWEHJ4O80UszveDqi68Tg9E_Px4MQ-FBpOwfnRc,4669
7
+ tmb_codec-0.2.0.dist-info/WHEEL,sha256=7cuQBrKlgrMjywT6nmSZIIhuOog1xNCTkMyVVbw2aow,103
8
+ tmb_codec-0.2.0.dist-info/licenses/LICENSE,sha256=cQwMShNEo-Ae_Kix_mPphGFiM4tPRJe5fxVbnIa_lC0,1089
9
+ tmb_codec-0.2.0.dist-info/licenses/licenses/CORTO_LICENSE.txt,sha256=pO8euBMldLQyYyiVVpDkqp9wRdOd4fJzbyB9OtrobIo,1130
10
+ tmb_codec-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: scikit-build-core 0.11.6
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tyndall-log
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ Corto
2
+ Copyright (c) 2015-2022, Visual Computing Lab, ISTI - CNR
3
+ All rights reserved.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.