volt-spatial-assembler 1.0.1__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VoltLabs Research
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,2 @@
1
+ include src/glb_core.h
2
+ include src/glb_python.cpp
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: volt-spatial-assembler
3
+ Version: 1.0.1
4
+ Summary: Native GLB assembler for Volt (shared C++ core with the Node addon).
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ License-File: LICENSE
8
+ Dynamic: license-file
@@ -0,0 +1,13 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "pybind11>=2.12"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "volt-spatial-assembler"
7
+ version = "1.0.1"
8
+ description = "Native GLB assembler for Volt (shared C++ core with the Node addon)."
9
+ requires-python = ">=3.10"
10
+ license = "MIT"
11
+
12
+ [tool.setuptools]
13
+ packages = ["volt_spatial_assembler"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ from pybind11.setup_helpers import Pybind11Extension, build_ext
2
+ from setuptools import setup
3
+
4
+ setup(
5
+ cmdclass={'build_ext': build_ext},
6
+ ext_modules=[
7
+ Pybind11Extension(
8
+ 'volt_spatial_assembler._spatial_assembler',
9
+ ['src/glb_python.cpp'],
10
+ include_dirs=['src'],
11
+ cxx_std=17,
12
+ extra_compile_args=['-O3', '-pthread'],
13
+ ),
14
+ ],
15
+ )
@@ -0,0 +1,353 @@
1
+ #pragma once
2
+ //
3
+ // glb_core.h - Runtime-agnostic GLB assembly core.
4
+ //
5
+ // Single source of truth for GLB generation, shared by the Node (N-API) and
6
+ // Python (pybind11) bindings. Contains NO node_api.h / Python.h: just plain
7
+ // C++ on raw buffers. All functions are `inline` and all file-scope state is
8
+ // `static` so the header can be included in multiple translation units.
9
+ //
10
+ #include <algorithm>
11
+ #include <cstdint>
12
+ #include <cstdio>
13
+ #include <cstdlib>
14
+ #include <cstring>
15
+ #include <thread>
16
+ #include <vector>
17
+
18
+ // Fast SIMD path: GCC/Clang on x86 only (target attributes + __builtin_cpu_*).
19
+ #if (defined(__GNUC__) || defined(__clang__)) && (defined(__x86_64__) || defined(__i386__))
20
+ #define SA_FAST_PATH 1
21
+ #include <immintrin.h>
22
+ #define SA_AVX2_BMI2_TARGET __attribute__((target("avx2,bmi2")))
23
+ #else
24
+ #define SA_FAST_PATH 0
25
+ #define SA_AVX2_BMI2_TARGET
26
+ #endif
27
+
28
+ #if defined(_MSC_VER)
29
+ #define SA_FORCE_INLINE inline
30
+ #else
31
+ #define SA_FORCE_INLINE inline __attribute__((always_inline))
32
+ #endif
33
+ #define SA_RESTRICT __restrict
34
+
35
+ namespace glbcore {
36
+
37
+ static inline void* aligned_malloc(size_t size) {
38
+ void* ptr = malloc(size);
39
+ if (!ptr) abort();
40
+ return ptr;
41
+ }
42
+
43
+ static inline void aligned_free(void* ptr) {
44
+ free(ptr);
45
+ }
46
+
47
+ static inline unsigned int worker_thread_count(unsigned int fallback = 4) {
48
+ unsigned int n = std::thread::hardware_concurrency();
49
+ return n == 0 ? fallback : n;
50
+ }
51
+
52
+ static inline bool has_fast_cpu_features() {
53
+ #if SA_FAST_PATH
54
+ __builtin_cpu_init();
55
+ return __builtin_cpu_supports("avx2") && __builtin_cpu_supports("bmi2");
56
+ #else
57
+ return false;
58
+ #endif
59
+ }
60
+
61
+ // Type colors (SoA), 8 structure types.
62
+ static const float TYPE_COLORS_R[8] = {0.5f, 1.0f, 0.267f, 0.267f, 1.0f, 1.0f, 0.267f, 0.6f};
63
+ static const float TYPE_COLORS_G[8] = {0.5f, 0.267f, 1.0f, 0.267f, 1.0f, 0.267f, 1.0f, 0.6f};
64
+ static const float TYPE_COLORS_B[8] = {0.5f, 0.267f, 0.267f, 1.0f, 0.267f, 1.0f, 1.0f, 0.6f};
65
+
66
+ SA_FORCE_INLINE uint32_t expand_bits_3d(uint32_t v) {
67
+ v &= 0x000003ffu;
68
+ v = (v | (v << 16)) & 0x030000FFu;
69
+ v = (v | (v << 8)) & 0x0300F00Fu;
70
+ v = (v | (v << 4)) & 0x030C30C3u;
71
+ v = (v | (v << 2)) & 0x09249249u;
72
+ return v;
73
+ }
74
+
75
+ SA_FORCE_INLINE uint32_t morton3d_scalar(uint32_t x, uint32_t y, uint32_t z) {
76
+ return expand_bits_3d(x) | (expand_bits_3d(y) << 1) | (expand_bits_3d(z) << 2);
77
+ }
78
+
79
+ #if SA_FAST_PATH
80
+ SA_AVX2_BMI2_TARGET static inline uint32_t morton3d_bmi2(uint32_t x, uint32_t y, uint32_t z) {
81
+ return _pdep_u32(x, 0x92492492) | _pdep_u32(y, 0x24924924) | _pdep_u32(z, 0x49249249);
82
+ }
83
+ #endif
84
+
85
+ struct RadixHist {
86
+ uint32_t count[256];
87
+ };
88
+
89
+ static inline void radix_count_chunk(const uint32_t* SA_RESTRICT keys, size_t start, size_t end, int shift, RadixHist* hist) {
90
+ memset(hist->count, 0, sizeof(hist->count));
91
+ for (size_t i = start; i < end; i++) {
92
+ hist->count[(keys[i] >> shift) & 0xFF]++;
93
+ }
94
+ }
95
+
96
+ static inline void radix_scatter(
97
+ const uint32_t* SA_RESTRICT srcKeys, const uint32_t* SA_RESTRICT srcIndices,
98
+ uint32_t* SA_RESTRICT dstKeys, uint32_t* SA_RESTRICT dstIndices,
99
+ size_t start, size_t end, int shift, uint32_t* localOffsets) {
100
+ for (size_t i = start; i < end; i++) {
101
+ uint8_t bucket = (srcKeys[i] >> shift) & 0xFF;
102
+ uint32_t d = localOffsets[bucket]++;
103
+ dstKeys[d] = srcKeys[i];
104
+ dstIndices[d] = srcIndices[i];
105
+ }
106
+ }
107
+
108
+ static inline void lock_free_radix_sort(uint32_t*& keys, uint32_t*& indices, size_t n, unsigned int numThreads) {
109
+ uint32_t* tmpKeys = (uint32_t*)aligned_malloc(n * sizeof(uint32_t));
110
+ uint32_t* tmpIndices = (uint32_t*)aligned_malloc(n * sizeof(uint32_t));
111
+ uint32_t *srcK = keys, *dstK = tmpKeys;
112
+ uint32_t *srcI = indices, *dstI = tmpIndices;
113
+
114
+ std::vector<RadixHist> hists(numThreads);
115
+ std::vector<std::thread> threads;
116
+ size_t blockSize = (n + numThreads - 1) / numThreads;
117
+ std::vector<std::vector<uint32_t>> threadOffsets(numThreads, std::vector<uint32_t>(256));
118
+
119
+ for (int shift = 0; shift < 32; shift += 8) {
120
+ threads.clear();
121
+ for (unsigned int t = 0; t < numThreads; t++) {
122
+ size_t start = t * blockSize, end = std::min(start + blockSize, n);
123
+ if (start < n) threads.emplace_back(radix_count_chunk, srcK, start, end, shift, &hists[t]);
124
+ }
125
+ for (auto& th : threads) th.join();
126
+
127
+ uint32_t globalOffsets[256];
128
+ uint32_t running = 0;
129
+ for (int b = 0; b < 256; b++) {
130
+ globalOffsets[b] = running;
131
+ for (unsigned int t = 0; t < numThreads; t++) running += hists[t].count[b];
132
+ }
133
+ for (int b = 0; b < 256; b++) {
134
+ uint32_t offset = globalOffsets[b];
135
+ for (unsigned int t = 0; t < numThreads; t++) {
136
+ threadOffsets[t][b] = offset;
137
+ offset += hists[t].count[b];
138
+ }
139
+ }
140
+
141
+ threads.clear();
142
+ for (unsigned int t = 0; t < numThreads; t++) {
143
+ size_t start = t * blockSize, end = std::min(start + blockSize, n);
144
+ if (start < n) threads.emplace_back(radix_scatter, srcK, srcI, dstK, dstI, start, end, shift, threadOffsets[t].data());
145
+ }
146
+ for (auto& th : threads) th.join();
147
+ std::swap(srcK, dstK);
148
+ std::swap(srcI, dstI);
149
+ }
150
+
151
+ if (srcK != keys) {
152
+ memcpy(keys, srcK, n * sizeof(uint32_t));
153
+ memcpy(indices, srcI, n * sizeof(uint32_t));
154
+ }
155
+ aligned_free(tmpKeys);
156
+ aligned_free(tmpIndices);
157
+ }
158
+
159
+ static inline void colorize_by_type(
160
+ const uint32_t* SA_RESTRICT indices, const uint16_t* SA_RESTRICT srcTypes,
161
+ float* SA_RESTRICT dstColors, size_t start, size_t end) {
162
+ for (size_t i = start; i < end; i++) {
163
+ uint16_t t = srcTypes[indices[i]];
164
+ uint32_t c = (t <= 7) ? t : 7;
165
+ size_t out = i * 3;
166
+ dstColors[out] = TYPE_COLORS_R[c];
167
+ dstColors[out + 1] = TYPE_COLORS_G[c];
168
+ dstColors[out + 2] = TYPE_COLORS_B[c];
169
+ }
170
+ }
171
+
172
+ static inline void gather_positions(
173
+ const uint32_t* SA_RESTRICT indices, const float* SA_RESTRICT srcPos,
174
+ float* SA_RESTRICT dstPos, size_t start, size_t end) {
175
+ for (size_t i = start; i < end; i++) {
176
+ size_t s = indices[i] * 3, d = i * 3;
177
+ dstPos[d] = srcPos[s];
178
+ dstPos[d + 1] = srcPos[s + 1];
179
+ dstPos[d + 2] = srcPos[s + 2];
180
+ }
181
+ }
182
+
183
+ // Assemble a GLB container from a JSON chunk and a contiguous BIN blob.
184
+ static inline std::vector<uint8_t> assemble_glb(const char* json, size_t jsonLen, const uint8_t* bin, size_t binLen) {
185
+ size_t jsonPad = (4 - (jsonLen % 4)) % 4;
186
+ size_t binPad = (4 - (binLen % 4)) % 4;
187
+ size_t jsonChunk = jsonLen + jsonPad;
188
+ size_t binChunk = binLen + binPad;
189
+ size_t total = 12 + 8 + jsonChunk + 8 + binChunk;
190
+
191
+ std::vector<uint8_t> glb(total);
192
+ uint8_t* p = glb.data();
193
+ auto put = [&](uint32_t v) { memcpy(p, &v, 4); p += 4; };
194
+ put(0x46546C67); put(2); put((uint32_t)total); // GLB header
195
+ put((uint32_t)jsonChunk); put(0x4E4F534A); // JSON chunk header
196
+ memcpy(p, json, jsonLen); p += jsonLen;
197
+ memset(p, ' ', jsonPad); p += jsonPad;
198
+ put((uint32_t)binChunk); put(0x004E4942); // BIN chunk header
199
+ if (binLen) { memcpy(p, bin, binLen); p += binLen; }
200
+ memset(p, 0, binPad);
201
+ return glb;
202
+ }
203
+
204
+ // ---- Entry points ---------------------------------------------------------
205
+
206
+ // Atom cloud: Morton-sort by position, colorize by structure type, emit GLB.
207
+ static inline std::vector<uint8_t> generate_atom_glb(
208
+ const float* srcPos, size_t n, const uint16_t* srcTypes,
209
+ const double minA[3], const double maxA[3]) {
210
+ if (n == 0) return assemble_glb("{}", 2, nullptr, 0);
211
+
212
+ const bool fast = has_fast_cpu_features();
213
+ float* outPos = (float*)aligned_malloc(n * 3 * sizeof(float));
214
+ float* outCol = (float*)aligned_malloc(n * 3 * sizeof(float));
215
+ uint32_t* keys = (uint32_t*)aligned_malloc(n * sizeof(uint32_t));
216
+ uint32_t* indices = (uint32_t*)aligned_malloc(n * sizeof(uint32_t));
217
+
218
+ unsigned int numThreads = (n < 100000) ? 1 : worker_thread_count();
219
+ size_t blockSize = (n + numThreads - 1) / numThreads;
220
+
221
+ float invX = 1.0f / std::max(1e-10f, (float)(maxA[0] - minA[0]));
222
+ float invY = 1.0f / std::max(1e-10f, (float)(maxA[1] - minA[1]));
223
+ float invZ = 1.0f / std::max(1e-10f, (float)(maxA[2] - minA[2]));
224
+
225
+ auto mortonWorker = [&](size_t start, size_t end) {
226
+ for (size_t i = start; i < end; i++) {
227
+ size_t p = i * 3;
228
+ float x = (srcPos[p] - (float)minA[0]) * invX;
229
+ float y = (srcPos[p + 1] - (float)minA[1]) * invY;
230
+ float z = (srcPos[p + 2] - (float)minA[2]) * invZ;
231
+ uint32_t ux = std::min(1023u, std::max(0u, (uint32_t)(x * 1023.0f)));
232
+ uint32_t uy = std::min(1023u, std::max(0u, (uint32_t)(y * 1023.0f)));
233
+ uint32_t uz = std::min(1023u, std::max(0u, (uint32_t)(z * 1023.0f)));
234
+ #if SA_FAST_PATH
235
+ keys[i] = fast ? morton3d_bmi2(ux, uy, uz) : morton3d_scalar(ux, uy, uz);
236
+ #else
237
+ keys[i] = morton3d_scalar(ux, uy, uz);
238
+ #endif
239
+ indices[i] = (uint32_t)i;
240
+ }
241
+ };
242
+
243
+ std::vector<std::thread> threads;
244
+ for (unsigned int t = 0; t < numThreads; t++) {
245
+ size_t start = t * blockSize, end = std::min(start + blockSize, n);
246
+ if (start < n) threads.emplace_back(mortonWorker, start, end);
247
+ }
248
+ for (auto& th : threads) th.join();
249
+
250
+ lock_free_radix_sort(keys, indices, n, numThreads);
251
+
252
+ threads.clear();
253
+ for (unsigned int t = 0; t < numThreads; t++) {
254
+ size_t start = t * blockSize, end = std::min(start + blockSize, n);
255
+ if (start < n) threads.emplace_back([&, start, end]() {
256
+ gather_positions(indices, srcPos, outPos, start, end);
257
+ colorize_by_type(indices, srcTypes, outCol, start, end);
258
+ });
259
+ }
260
+ for (auto& th : threads) th.join();
261
+
262
+ size_t posBytes = n * 3 * sizeof(float);
263
+ char json[2048];
264
+ int jsonLen = snprintf(json, sizeof(json),
265
+ R"({"asset":{"version":"2.0","generator":"Volt Native"},"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0,"name":"Atoms"}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"COLOR_0":1},"mode":0}],"name":"AtomCloud"}],"accessors":[{"bufferView":0,"componentType":5126,"count":%zu,"type":"VEC3","min":[%.6f,%.6f,%.6f],"max":[%.6f,%.6f,%.6f]},{"bufferView":1,"componentType":5126,"count":%zu,"type":"VEC3"}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":%zu,"target":34962},{"buffer":0,"byteOffset":%zu,"byteLength":%zu,"target":34962}],"buffers":[{"byteLength":%zu}]})",
266
+ n, minA[0], minA[1], minA[2], maxA[0], maxA[1], maxA[2], n, posBytes, posBytes, posBytes, posBytes * 2);
267
+
268
+ std::vector<uint8_t> bin(posBytes * 2);
269
+ memcpy(bin.data(), outPos, posBytes);
270
+ memcpy(bin.data() + posBytes, outCol, posBytes);
271
+
272
+ aligned_free(outPos);
273
+ aligned_free(outCol);
274
+ aligned_free(keys);
275
+ aligned_free(indices);
276
+ return assemble_glb(json, jsonLen, bin.data(), bin.size());
277
+ }
278
+
279
+ // Pre-colored point cloud (colors VEC3 or VEC4).
280
+ static inline std::vector<uint8_t> generate_point_cloud_glb(
281
+ const float* positions, size_t posCount, const float* colors, size_t colCount,
282
+ const double minA[3], const double maxA[3]) {
283
+ size_t atomCount = posCount / 3;
284
+ bool isVec4 = (colCount == atomCount * 4);
285
+ size_t posBytes = posCount * sizeof(float);
286
+ size_t colBytes = colCount * sizeof(float);
287
+
288
+ char json[2048];
289
+ int jsonLen = snprintf(json, sizeof(json),
290
+ R"({"asset":{"version":"2.0","generator":"Volt Native"},"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0,"name":"Atoms"}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"COLOR_0":1},"mode":0}],"name":"AtomCloud"}],"accessors":[{"bufferView":0,"componentType":5126,"count":%zu,"type":"VEC3","min":[%.6f,%.6f,%.6f],"max":[%.6f,%.6f,%.6f]},{"bufferView":1,"componentType":5126,"count":%zu,"type":"%s"}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":%zu,"target":34962},{"buffer":0,"byteOffset":%zu,"byteLength":%zu,"target":34962}],"buffers":[{"byteLength":%zu}]})",
291
+ atomCount, minA[0], minA[1], minA[2], maxA[0], maxA[1], maxA[2],
292
+ atomCount, isVec4 ? "VEC4" : "VEC3", posBytes, posBytes, colBytes, posBytes + colBytes);
293
+
294
+ std::vector<uint8_t> bin(posBytes + colBytes);
295
+ memcpy(bin.data(), positions, posBytes);
296
+ memcpy(bin.data() + posBytes, colors, colBytes);
297
+ return assemble_glb(json, jsonLen, bin.data(), bin.size());
298
+ }
299
+
300
+ struct Material {
301
+ double baseColor[4] = {1, 1, 1, 1};
302
+ double metallic = 0;
303
+ double roughness = 1;
304
+ double emissive[3] = {0, 0, 0};
305
+ bool doubleSided = true;
306
+ };
307
+
308
+ // Indexed triangle mesh with normals, optional VEC4 colors, and a PBR material.
309
+ // Indices are uint32 (SCALAR componentType 5125).
310
+ static inline std::vector<uint8_t> generate_mesh_glb(
311
+ const float* positions, size_t posCount, const float* normals, size_t normCount,
312
+ const uint32_t* indices, size_t idxCount, const float* colors, size_t colCount,
313
+ const double bounds[6], const Material& mat) {
314
+ size_t vertexCount = posCount / 3;
315
+ bool hasColors = (colors != nullptr && colCount > 0);
316
+
317
+ size_t posBytes = posCount * sizeof(float);
318
+ size_t normBytes = normCount * sizeof(float);
319
+ size_t colBytes = hasColors ? colCount * sizeof(float) : 0;
320
+ size_t idxBytes = idxCount * sizeof(uint32_t);
321
+ size_t binTotal = posBytes + normBytes + colBytes + idxBytes;
322
+
323
+ const char* ds = mat.doubleSided ? "true" : "false";
324
+ char json[8192];
325
+ int jsonLen;
326
+ if (hasColors) {
327
+ jsonLen = snprintf(json, sizeof(json),
328
+ R"({"asset":{"version":"2.0","generator":"Volt Native"},"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0,"name":"Mesh"}],"materials":[{"pbrMetallicRoughness":{"baseColorFactor":[%.4f,%.4f,%.4f,%.4f],"metallicFactor":%.4f,"roughnessFactor":%.4f},"emissiveFactor":[%.4f,%.4f,%.4f],"doubleSided":%s}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"NORMAL":1,"COLOR_0":2},"indices":3,"material":0,"mode":4}],"name":"MeshGeometry"}],"accessors":[{"bufferView":0,"componentType":5126,"count":%zu,"type":"VEC3","min":[%.6f,%.6f,%.6f],"max":[%.6f,%.6f,%.6f]},{"bufferView":1,"componentType":5126,"count":%zu,"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":%zu,"type":"VEC4"},{"bufferView":3,"componentType":5125,"count":%zu,"type":"SCALAR"}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":%zu,"target":34962},{"buffer":0,"byteOffset":%zu,"byteLength":%zu,"target":34962},{"buffer":0,"byteOffset":%zu,"byteLength":%zu,"target":34962},{"buffer":0,"byteOffset":%zu,"byteLength":%zu,"target":34963}],"buffers":[{"byteLength":%zu}]})",
329
+ mat.baseColor[0], mat.baseColor[1], mat.baseColor[2], mat.baseColor[3], mat.metallic, mat.roughness,
330
+ mat.emissive[0], mat.emissive[1], mat.emissive[2], ds,
331
+ vertexCount, bounds[0], bounds[1], bounds[2], bounds[3], bounds[4], bounds[5],
332
+ vertexCount, vertexCount, idxCount,
333
+ posBytes, posBytes, normBytes, posBytes + normBytes, colBytes, posBytes + normBytes + colBytes, idxBytes, binTotal);
334
+ } else {
335
+ jsonLen = snprintf(json, sizeof(json),
336
+ R"({"asset":{"version":"2.0","generator":"Volt Native"},"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0,"name":"Mesh"}],"materials":[{"pbrMetallicRoughness":{"baseColorFactor":[%.4f,%.4f,%.4f,%.4f],"metallicFactor":%.4f,"roughnessFactor":%.4f},"emissiveFactor":[%.4f,%.4f,%.4f],"doubleSided":%s}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"NORMAL":1},"indices":2,"material":0,"mode":4}],"name":"MeshGeometry"}],"accessors":[{"bufferView":0,"componentType":5126,"count":%zu,"type":"VEC3","min":[%.6f,%.6f,%.6f],"max":[%.6f,%.6f,%.6f]},{"bufferView":1,"componentType":5126,"count":%zu,"type":"VEC3"},{"bufferView":2,"componentType":5125,"count":%zu,"type":"SCALAR"}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":%zu,"target":34962},{"buffer":0,"byteOffset":%zu,"byteLength":%zu,"target":34962},{"buffer":0,"byteOffset":%zu,"byteLength":%zu,"target":34963}],"buffers":[{"byteLength":%zu}]})",
337
+ mat.baseColor[0], mat.baseColor[1], mat.baseColor[2], mat.baseColor[3], mat.metallic, mat.roughness,
338
+ mat.emissive[0], mat.emissive[1], mat.emissive[2], ds,
339
+ vertexCount, bounds[0], bounds[1], bounds[2], bounds[3], bounds[4], bounds[5],
340
+ vertexCount, idxCount,
341
+ posBytes, posBytes, normBytes, posBytes + normBytes, idxBytes, binTotal);
342
+ }
343
+
344
+ std::vector<uint8_t> bin(binTotal);
345
+ uint8_t* p = bin.data();
346
+ memcpy(p, positions, posBytes); p += posBytes;
347
+ memcpy(p, normals, normBytes); p += normBytes;
348
+ if (hasColors) { memcpy(p, colors, colBytes); p += colBytes; }
349
+ memcpy(p, indices, idxBytes);
350
+ return assemble_glb(json, jsonLen, bin.data(), bin.size());
351
+ }
352
+
353
+ } // namespace glbcore
@@ -0,0 +1,80 @@
1
+ // Python (pybind11) binding over the shared glb_core.h. Mirrors the Node addon
2
+ // using the same C++ encoders, so there is one source of truth for GLB output.
3
+ #include <pybind11/pybind11.h>
4
+
5
+ #include <cstdint>
6
+ #include <vector>
7
+
8
+ #include "glb_core.h"
9
+
10
+ namespace py = pybind11;
11
+
12
+ static const uint8_t* buf_bytes(const py::buffer& b, size_t& nbytes) {
13
+ py::buffer_info info = b.request();
14
+ nbytes = static_cast<size_t>(info.size) * static_cast<size_t>(info.itemsize);
15
+ return reinterpret_cast<const uint8_t*>(info.ptr);
16
+ }
17
+
18
+ static void read_doubles(const py::sequence& s, double* out, size_t n) {
19
+ if (static_cast<size_t>(py::len(s)) < n) throw py::value_error("sequence too short");
20
+ for (size_t i = 0; i < n; i++) out[i] = py::cast<double>(s[i]);
21
+ }
22
+
23
+ static py::bytes to_bytes(const std::vector<uint8_t>& v) {
24
+ return py::bytes(reinterpret_cast<const char*>(v.data()), v.size());
25
+ }
26
+
27
+ static py::bytes atom_glb(py::buffer positions, py::buffer types, py::sequence vmin, py::sequence vmax) {
28
+ size_t pb, tb;
29
+ const float* pos = reinterpret_cast<const float*>(buf_bytes(positions, pb));
30
+ const uint16_t* typ = reinterpret_cast<const uint16_t*>(buf_bytes(types, tb));
31
+ double mn[3], mx[3];
32
+ read_doubles(vmin, mn, 3);
33
+ read_doubles(vmax, mx, 3);
34
+ return to_bytes(glbcore::generate_atom_glb(pos, (pb / 4) / 3, typ, mn, mx));
35
+ }
36
+
37
+ static py::bytes point_cloud_glb(py::buffer positions, py::buffer colors, py::sequence vmin, py::sequence vmax) {
38
+ size_t pb, cb;
39
+ const float* pos = reinterpret_cast<const float*>(buf_bytes(positions, pb));
40
+ const float* col = reinterpret_cast<const float*>(buf_bytes(colors, cb));
41
+ double mn[3], mx[3];
42
+ read_doubles(vmin, mn, 3);
43
+ read_doubles(vmax, mx, 3);
44
+ return to_bytes(glbcore::generate_point_cloud_glb(pos, pb / 4, col, cb / 4, mn, mx));
45
+ }
46
+
47
+ static py::bytes mesh_glb(py::buffer positions, py::buffer normals, py::buffer indices,
48
+ py::sequence bounds, py::sequence base_color, double metallic, double roughness,
49
+ py::sequence emissive, bool double_sided, py::object colors) {
50
+ size_t pb, nb, ib;
51
+ const float* pos = reinterpret_cast<const float*>(buf_bytes(positions, pb));
52
+ const float* nor = reinterpret_cast<const float*>(buf_bytes(normals, nb));
53
+ const uint32_t* idx = reinterpret_cast<const uint32_t*>(buf_bytes(indices, ib));
54
+
55
+ const float* col = nullptr;
56
+ size_t cb = 0;
57
+ if (!colors.is_none()) {
58
+ col = reinterpret_cast<const float*>(buf_bytes(py::cast<py::buffer>(colors), cb));
59
+ }
60
+
61
+ double bnd[6];
62
+ read_doubles(bounds, bnd, 6);
63
+ glbcore::Material mat;
64
+ read_doubles(base_color, mat.baseColor, 4);
65
+ read_doubles(emissive, mat.emissive, 3);
66
+ mat.metallic = metallic;
67
+ mat.roughness = roughness;
68
+ mat.doubleSided = double_sided;
69
+
70
+ return to_bytes(glbcore::generate_mesh_glb(pos, pb / 4, nor, nb / 4, idx, ib / 4, col, cb / 4, bnd, mat));
71
+ }
72
+
73
+ PYBIND11_MODULE(_spatial_assembler, m) {
74
+ m.doc() = "Native GLB assembler (shared C++ core with the Node addon).";
75
+ m.def("atom_glb", &atom_glb, py::arg("positions"), py::arg("types"), py::arg("vmin"), py::arg("vmax"));
76
+ m.def("point_cloud_glb", &point_cloud_glb, py::arg("positions"), py::arg("colors"), py::arg("vmin"), py::arg("vmax"));
77
+ m.def("mesh_glb", &mesh_glb, py::arg("positions"), py::arg("normals"), py::arg("indices"), py::arg("bounds"),
78
+ py::arg("base_color"), py::arg("metallic"), py::arg("roughness"), py::arg("emissive"),
79
+ py::arg("double_sided"), py::arg("colors") = py::none());
80
+ }
@@ -0,0 +1,5 @@
1
+ """Native GLB assembler for Volt — thin Python surface over the shared C++ core."""
2
+
3
+ from ._spatial_assembler import atom_glb, mesh_glb, point_cloud_glb
4
+
5
+ __all__ = ['atom_glb', 'mesh_glb', 'point_cloud_glb']
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: volt-spatial-assembler
3
+ Version: 1.0.1
4
+ Summary: Native GLB assembler for Volt (shared C++ core with the Node addon).
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ License-File: LICENSE
8
+ Dynamic: license-file
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ pyproject.toml
4
+ setup.py
5
+ src/glb_core.h
6
+ src/glb_python.cpp
7
+ volt_spatial_assembler/__init__.py
8
+ volt_spatial_assembler.egg-info/PKG-INFO
9
+ volt_spatial_assembler.egg-info/SOURCES.txt
10
+ volt_spatial_assembler.egg-info/dependency_links.txt
11
+ volt_spatial_assembler.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ volt_spatial_assembler