@francisdb/vpin-wasm 0.28.1 → 0.29.0

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.
package/README.md CHANGED
@@ -11,7 +11,7 @@ npm install @francisdb/vpin-wasm
11
11
  ## Usage
12
12
 
13
13
  ```typescript
14
- import init, { extract, assemble, obj_to_mesh, mesh_to_obj } from '@francisdb/vpin-wasm';
14
+ import init, { extract, assemble, obj_to_mesh, mesh_to_obj, AxisConvention } from '@francisdb/vpin-wasm';
15
15
 
16
16
  await init();
17
17
  ```
@@ -35,7 +35,7 @@ const files = extract(vpxBytes, (message) => {
35
35
  - `data: Uint8Array` - VPX file bytes
36
36
  - `callback?: (message: string) => void` - Optional progress callback
37
37
 
38
- **Returns:** `Record<string, Uint8Array>` - Object mapping file paths to contents
38
+ **Returns:** `VpxFileMap` (`Record<string, Uint8Array>`) - file paths to contents
39
39
 
40
40
  ### assemble(files, callback?)
41
41
 
@@ -57,12 +57,47 @@ const vpxBytes = assemble(files, (message) => {
57
57
 
58
58
  **Parameters:**
59
59
 
60
- - `files: Record<string, Uint8Array>` - Object mapping file paths to contents
60
+ - `files: VpxFileMap` (`Record<string, Uint8Array>`) - file paths to contents
61
61
  - `callback?: (message: string) => void` - Optional progress callback
62
62
 
63
63
  **Returns:** `Uint8Array` - VPX file bytes
64
64
 
65
- ### obj_to_mesh(data) / mesh_to_obj(name, positions, texCoords, normals, indices)
65
+ ### export_glb(files, options?, callback?)
66
+
67
+ Exports a whole table to a GLB (binary glTF 2.0) file. Takes the same
68
+ extracted-file map `assemble` takes; call `extract` first if you only
69
+ have `.vpx` bytes.
70
+
71
+ ```typescript
72
+ const files = extract(vpxBytes);
73
+ const glb = export_glb(files, { exportInvisibleItems: false }, (message) => {
74
+ console.log(message);
75
+ });
76
+ // glb is a Uint8Array with model/gltf-binary content
77
+ ```
78
+
79
+ The output follows the glTF conventions - Y-up right-handed axes and
80
+ meters - so it opens correctly in Blender and other viewers with
81
+ default import settings. It contains generated meshes for every part
82
+ type, PBR materials from the table's material list, embedded textures,
83
+ and the three VPX view cameras.
84
+
85
+ Sound entries (`/vpx/sounds.json`, `/vpx/sounds/*`) are ignored - the
86
+ exporter never reads them, and skipping them keeps peak wasm memory
87
+ down on sound-heavy tables. A map without them works too.
88
+
89
+ **Parameters:**
90
+
91
+ - `files: VpxFileMap` (`Record<string, Uint8Array>`) - file paths to contents
92
+ - `options?: GlbExportOptions` - `{ exportInvisibleItems?: boolean }`;
93
+ when `true`, invisible items are exported with the
94
+ `KHR_node_visibility` extension instead of skipped (needs viewer
95
+ support; leave off for Blender). Default `false`.
96
+ - `callback?: (message: string) => void` - Optional progress callback
97
+
98
+ **Returns:** `Uint8Array` - GLB file bytes
99
+
100
+ ### obj_to_mesh(data, options?) / mesh_to_obj(name, positions, texCoords, normals, indices, options?)
66
101
 
67
102
  Renderer-friendly mesh I/O. `obj_to_mesh` parses any flavor of OBJ
68
103
  (n-gons fan-triangulated, mismatched `v/vt/vn` corners deduplicated)
@@ -71,7 +106,9 @@ JS-side OBJ parser needed.
71
106
 
72
107
  ```typescript
73
108
  const objBytes = files['/vpx/gameitems/Primitive.MyMesh.obj'];
74
- const mesh = obj_to_mesh(objBytes);
109
+ const mesh = obj_to_mesh(objBytes); // defaults: vpinball convention, scale 1.0
110
+ // or e.g. for a Blender-exported OBJ:
111
+ // const mesh = obj_to_mesh(objBytes, { axes: AxisConvention.YUpRightHanded });
75
112
 
76
113
  // mesh.name: string
77
114
  // mesh.positions: Float32Array (length = 3 * vertCount, x,y,z,...)
@@ -95,20 +132,52 @@ const obj = mesh_to_obj(mesh.name, mesh.positions, mesh.texCoords, mesh.normals,
95
132
  files['/vpx/gameitems/Primitive.MyMesh.obj'] = obj;
96
133
  ```
97
134
 
135
+ ### mesh_to_glb(name, positions, texCoords, normals, indices, options?)
136
+
137
+ The GLB sibling of `mesh_to_obj` - serializes a single mesh as a
138
+ binary glTF 2.0 file, for handing one primitive's mesh to a DCC tool.
139
+ There is no axis option: glTF mandates Y-up right-handed, so the
140
+ vpx-internal input is always converted the same way `export_glb`
141
+ converts the whole table (texture coordinates pass through unchanged -
142
+ glTF's UV origin matches vpx's, unlike OBJ's).
143
+
144
+ ```typescript
145
+ const glb = mesh_to_glb(mesh.name, mesh.positions, mesh.texCoords, mesh.normals, mesh.indices);
146
+ // glb is a Uint8Array with model/gltf-binary content
147
+ ```
148
+
149
+ `options.unitScale` (default `1.0`) multiplies positions. A
150
+ primitive's local mesh coordinates are arbitrary units (the table
151
+ scales them by the primitive's Size at render), so no unit conversion
152
+ is applied by default.
153
+
98
154
  The published wasm bundle is built with `wasm-bindgen --weak-refs`, so
99
155
  the Rust-owned memory backing each `mesh` is reclaimed automatically
100
156
  via `FinalizationRegistry` when the JS wrapper is garbage-collected.
101
157
  You may call `mesh.free()` explicitly for deterministic cleanup of
102
158
  large meshes, but it is not required.
103
159
 
104
- **Coordinate convention:** the mesh data is in vpx-internal form -
105
- `obj_to_mesh` applies the same transforms as `assemble`'s read path
106
- (vertex Z negated, normal Z negated, V coordinate flipped, per-triangle
107
- corner order reversed), and `mesh_to_obj` applies the inverse, matching
108
- `extract`'s write path. Round-trip
109
- `obj_to_mesh -> edit -> mesh_to_obj -> assemble` preserves vpx data by
110
- construction. If your renderer uses a different convention than
111
- vpinball's left-handed +Z up, apply a transform matrix on the JS side.
160
+ **Coordinate convention:** the mesh data is always in vpx-internal
161
+ form; `options.axes` names the convention of the OBJ side:
162
+
163
+ - `AxisConvention.ZDownRightHanded` (default) - vpinball's exported OBJ
164
+ convention (vertex/normal Z negated, V coordinate flipped,
165
+ per-triangle corner order reversed). This is what `extract` writes
166
+ and `assemble` reads, so it is the right choice for OBJs from the
167
+ extracted file map. Round-trip
168
+ `obj_to_mesh -> edit -> mesh_to_obj -> assemble` preserves vpx data
169
+ by construction.
170
+ - `AxisConvention.YUpRightHanded` - the Blender / DCC default (Y-Z
171
+ swap, V flip, winding reverse). Use this to hand a Blender-exported
172
+ OBJ straight to `obj_to_mesh`, or to write an OBJ that opens upright
173
+ in Blender with default import settings.
174
+ - `AxisConvention.ZUpLeftHanded` - vpx-internal values verbatim, no
175
+ transforms.
176
+
177
+ `options.unitScale` (default `1.0`) multiplies positions (on the vpx
178
+ side for `obj_to_mesh`, before the axis mapping for `mesh_to_obj`).
179
+ Normals and texture coordinates are never scaled. An OBJ written with
180
+ scale `k` reads back with scale `1 / k`.
112
181
 
113
182
  **Animation frames:** primitives with vertex animation extract as
114
183
  sibling files `Primitive.MyMesh_00000.obj`, `Primitive.MyMesh_00001.obj`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@francisdb/vpin-wasm",
3
- "version": "0.28.1",
3
+ "version": "0.29.0",
4
4
  "description": "WASM bindings for vpin, a rust library for the visual/virtual pinball ecosystem.",
5
5
  "homepage": "https://github.com/francisdb/vpin",
6
6
  "bugs": {
package/vpin.d.ts CHANGED
@@ -1,6 +1,113 @@
1
1
  /* tslint:disable */
2
2
  /* eslint-disable */
3
3
 
4
+ /**
5
+ * An extracted table as a file map: absolute path under `/vpx` to file
6
+ * contents, e.g. `{ "/vpx/images/ball.png": Uint8Array, ... }`.
7
+ * Produced by `extract`, consumed by `assemble` and `export_glb`.
8
+ */
9
+ export type VpxFileMap = Record<string, Uint8Array>;
10
+
11
+
12
+
13
+ /**
14
+ * Options for `export_glb`. Pass a plain object literal; every field
15
+ * is optional.
16
+ */
17
+ export interface GlbExportOptions {
18
+ /**
19
+ * When `true`, items marked invisible in the table are exported
20
+ * with the `KHR_node_visibility` extension set to `visible: false`
21
+ * instead of being skipped. Preserves the full table structure but
22
+ * requires viewer support for the extension (e.g. some game
23
+ * engines); leave off for Blender. Default: `false`.
24
+ */
25
+ exportInvisibleItems?: boolean;
26
+ }
27
+
28
+
29
+
30
+ /**
31
+ * Options for `mesh_to_glb`. Pass a plain object literal; every field
32
+ * is optional.
33
+ */
34
+ export interface MeshGlbOptions {
35
+ /**
36
+ * Multiplier applied to positions before the axis mapping. Normals
37
+ * and texture coordinates are never scaled. Default: `1.0`.
38
+ */
39
+ unitScale?: number;
40
+ }
41
+
42
+
43
+
44
+ /**
45
+ * Options for `obj_to_mesh` / `mesh_to_obj`. Pass a plain object
46
+ * literal; every field is optional.
47
+ */
48
+ export interface MeshIoOptions {
49
+ /**
50
+ * Axis convention of the OBJ side of the conversion (the mesh side
51
+ * is always vpx-internal). Default: `AxisConvention.ZDownRightHanded`,
52
+ * matching what `extract` writes and `assemble` reads.
53
+ */
54
+ axes?: AxisConvention;
55
+ /**
56
+ * Multiplier applied to positions (on the vpx side for
57
+ * `obj_to_mesh`, before the axis mapping for `mesh_to_obj`).
58
+ * Normals and texture coordinates are never scaled. An OBJ written
59
+ * with scale `k` reads back with `1 / k`. Default: `1.0`.
60
+ */
61
+ unitScale?: number;
62
+ }
63
+
64
+
65
+
66
+ /**
67
+ * Axis convention for 3D mesh interchange (OBJ, glTF/GLB), in either
68
+ * direction: exporting VPX geometry or importing meshes back in.
69
+ *
70
+ * VPX space is left-handed with Z up: X right (across the playfield),
71
+ * Y towards the player (down the playfield), Z up (towards the glass).
72
+ * [`Self::ZUpLeftHanded`] is that internal frame verbatim; the other
73
+ * variants negate or swap exactly one axis pair, so each produces a
74
+ * right-handed frame from the left-handed one. A conversion between two
75
+ * frames of different [`Self::handedness`] must pair with a reversed
76
+ * triangle winding (matching vpinball's `ObjLoader`); one between frames
77
+ * of equal handedness keeps the winding, since such frames differ only
78
+ * by a pure rotation (the two right-handed variants: 90 degrees about
79
+ * X).
80
+ */
81
+ export enum AxisConvention {
82
+ /**
83
+ * VPX's internal coordinate system: left-handed with Z up, inherited
84
+ * from DirectX. X runs right across the playfield, Y down the
85
+ * playfield towards the player, Z up towards the glass.
86
+ *
87
+ * Converting to or from this convention leaves coordinates untouched
88
+ * (vertices keep their vpx-internal values). Only meaningful for
89
+ * tooling that works on the raw vpx data; not what any viewer or DCC
90
+ * tool expects.
91
+ */
92
+ ZUpLeftHanded = 0,
93
+ /**
94
+ * VPinball's exported OBJ convention: `(x, y, -z)`.
95
+ *
96
+ * Right-handed like [`Self::YUpRightHanded`], but keeping VPX's axis
97
+ * roles: Y still runs down the playfield and Z, now negated, points
98
+ * down through the table instead of up. This is what vpinball itself
99
+ * writes when exporting an OBJ and expects when importing one, so a
100
+ * table shows up rotated 90 degrees about X in a Y-up viewer.
101
+ */
102
+ ZDownRightHanded = 1,
103
+ /**
104
+ * Y up, right-handed: `(x, z, y)`. The glTF and Wavefront OBJ
105
+ * convention, and what Blender / Maya / 3ds Max assume with their
106
+ * default import settings (`Scale 1.0, Forward -Z, Up Y`).
107
+ */
108
+ YUpRightHanded = 2,
109
+ }
110
+
4
111
  /**
5
112
  * Mesh data for a single primitive: positions, texture coordinates,
6
113
  * normals and triangle indices, packed as flat typed arrays for direct
@@ -45,9 +152,30 @@ export class PrimitiveMesh {
45
152
  readonly texCoords: Float32Array;
46
153
  }
47
154
 
48
- export function assemble(files: object, callback?: Function | null): Uint8Array;
155
+ export function assemble(files: VpxFileMap, callback?: Function | null): Uint8Array;
49
156
 
50
- export function extract(data: Uint8Array, callback?: Function | null): object;
157
+ /**
158
+ * Export a whole table to a GLB (binary glTF 2.0) file.
159
+ *
160
+ * Takes the same extracted-file map `assemble` takes
161
+ * (`{ "/vpx/...": Uint8Array }`), so consumers holding a table as an
162
+ * extracted tree skip a full assemble/parse round trip; consumers with
163
+ * only `.vpx` bytes can call `extract` first.
164
+ *
165
+ * The output follows the glTF conventions: Y-up right-handed axes and
166
+ * meters, so it opens correctly in Blender and other glTF viewers with
167
+ * default settings. It contains the generated meshes for every part
168
+ * type, PBR materials from the VPX material list, embedded textures,
169
+ * and the three VPX view cameras (desktop / fullscreen / FSS).
170
+ *
171
+ * Sound entries (`/vpx/sounds.json` and `/vpx/sounds/*`) are ignored -
172
+ * the exporter never reads them, and skipping them keeps peak wasm
173
+ * memory down on sound-heavy tables. Passing a map without them works
174
+ * too.
175
+ */
176
+ export function export_glb(files: VpxFileMap, options?: GlbExportOptions | null, callback?: Function | null): Uint8Array;
177
+
178
+ export function extract(data: Uint8Array, callback?: Function | null): VpxFileMap;
51
179
 
52
180
  /**
53
181
  * Generate the procedural mesh used by vpinball primitives that
@@ -72,6 +200,26 @@ export function generate_builtin_primitive(sides: number, draw_textures_inside:
72
200
 
73
201
  export function init(): void;
74
202
 
203
+ /**
204
+ * Serialize a single mesh as a GLB (binary glTF 2.0) file.
205
+ *
206
+ * The GLB sibling of [`mesh_to_obj`], for exporting one primitive's
207
+ * mesh to DCC tools. There is no axis option: glTF mandates Y-up
208
+ * right-handed, so the vpx-internal input is always converted the same
209
+ * way the whole-table exporter converts it (Y-Z swap, winding
210
+ * reversed; texture coordinates pass through - glTF's UV origin
211
+ * matches vpx's, unlike OBJ's).
212
+ *
213
+ * `name` becomes the mesh/node name; pass an empty string to use
214
+ * `"object"`. Array requirements are the same as [`mesh_to_obj`].
215
+ *
216
+ * `options.unitScale` (default `1.0`) multiplies positions. A
217
+ * primitive's local mesh coordinates are arbitrary units (the table
218
+ * scales them by the primitive's Size at render), so no unit
219
+ * conversion is applied by default.
220
+ */
221
+ export function mesh_to_glb(name: string, positions: Float32Array, tex_coords: Float32Array, normals: Float32Array, indices: Uint32Array, options?: MeshGlbOptions | null): Uint8Array;
222
+
75
223
  /**
76
224
  * Serialize a mesh as a Wavefront OBJ.
77
225
  *
@@ -81,18 +229,29 @@ export function init(): void;
81
229
  * normals.len() / 3`); index values must be valid 0-based offsets into
82
230
  * that vertex array.
83
231
  *
84
- * `convert_to_left_handed` is the symmetric inverse of the same flag
85
- * on [`obj_to_mesh`]:
232
+ * `options.axes` is the symmetric inverse of the same option on
233
+ * [`obj_to_mesh`]: the input is always vpx-internal data, `axes` names
234
+ * the convention to write the OBJ in.
86
235
  *
87
- * - `true` (matches `extract`'s write path): the input is treated as
88
- * vpx-internal data and converted out: vertex Z is negated, normal Z
89
- * is negated, V is flipped (`obj_v = 1 - vpx_tv`), and per-triangle
90
- * corner order is reversed. The result is a vpinball-format OBJ
91
- * that `assemble` (or `obj_to_mesh(.., true)`) reads back identically.
92
- * - `false`: the input vpx-internal data is written out verbatim, no
93
- * transforms applied. Round-trips with `obj_to_mesh(.., false)`.
236
+ * - [`AxisConvention::ZDownRightHanded`] (the default; matches
237
+ * `extract`'s write path, the old `convert_to_left_handed = true`):
238
+ * vertex and normal Z are negated, V is flipped (`obj_v = 1 -
239
+ * vpx_tv`) and per-triangle corner order is reversed. The result is a
240
+ * vpinball-format OBJ that `assemble` (or `obj_to_mesh` with the same
241
+ * convention) reads back identically.
242
+ * - [`AxisConvention::YUpRightHanded`]: Y and Z are swapped, V is
243
+ * flipped and winding is reversed; the OBJ opens upright in Blender
244
+ * with default import settings.
245
+ * - [`AxisConvention::ZUpLeftHanded`] (the old `convert_to_left_handed
246
+ * = false`): the vpx-internal data is written out verbatim, no
247
+ * transforms applied.
248
+ *
249
+ * `options.unitScale` (default `1.0`) multiplies positions before the
250
+ * axis mapping. Normals and texture coordinates are never scaled.
251
+ * Round-trips through `obj_to_mesh` with the same convention and scale
252
+ * `1.0 / unitScale`.
94
253
  */
95
- export function mesh_to_obj(name: string, positions: Float32Array, tex_coords: Float32Array, normals: Float32Array, indices: Uint32Array, convert_to_left_handed: boolean): Uint8Array;
254
+ export function mesh_to_obj(name: string, positions: Float32Array, tex_coords: Float32Array, normals: Float32Array, indices: Uint32Array, options?: MeshIoOptions | null): Uint8Array;
96
255
 
97
256
  /**
98
257
  * Parse a Wavefront OBJ into a [`PrimitiveMesh`].
@@ -101,21 +260,31 @@ export function mesh_to_obj(name: string, positions: Float32Array, tex_coords: F
101
260
  * anything in between): n-gons are fan-triangulated and `(position, uv,
102
261
  * normal)` corners are deduplicated so the result is renderer-ready.
103
262
  *
104
- * `convert_to_left_handed` mirrors vpinball's `ObjLoader::Load` flag of
105
- * the same name (the "Convert coordinate system" checkbox in the
106
- * vpinball mesh-import dialog):
107
- *
108
- * - `true` (matches `assemble`'s read path and vpinball's dialog
109
- * default): the input is treated as right-handed (Blender / standard
110
- * convention). Vertex Z is negated, normal Z is negated, V is flipped
111
- * (`vpx_tv = 1 - obj_v`), and the per-triangle corner order is
112
- * reversed. The returned mesh data ends up in vpx-internal,
113
- * left-handed convention.
114
- * - `false`: the input is assumed to already be in vpx-internal
115
- * convention (e.g. produced by a previous `mesh_to_obj` with the same
116
- * flag). The transforms are skipped and values pass through verbatim.
263
+ * `options.axes` names the convention the OBJ data is in; the returned
264
+ * mesh is always in vpx-internal convention:
265
+ *
266
+ * - [`AxisConvention::ZDownRightHanded`] (the default; matches
267
+ * `assemble`'s read path and vpinball's mesh-import dialog with
268
+ * "Convert coordinate system" checked, the old `convert_to_left_handed
269
+ * = true`): vertex and normal Z are negated, V is flipped (`vpx_tv =
270
+ * 1 - obj_v`) and the per-triangle corner order is reversed. This path
271
+ * also honors the `# vpx <hex>` byte-preservation comments, so
272
+ * vpinball-format OBJs from `extract` reproduce the original vpx
273
+ * values bit-for-bit.
274
+ * - [`AxisConvention::YUpRightHanded`] (Blender / DCC default export
275
+ * settings): Y and Z are swapped, V is flipped and winding is
276
+ * reversed.
277
+ * - [`AxisConvention::ZUpLeftHanded`] (the old `convert_to_left_handed
278
+ * = false`): the input is assumed to already hold vpx-internal values
279
+ * (e.g. produced by a previous `mesh_to_obj` with the same
280
+ * convention). No transforms; values pass through verbatim.
281
+ *
282
+ * `options.unitScale` (default `1.0`) multiplies positions after the
283
+ * axis mapping. Normals and texture coordinates are never scaled. A
284
+ * mesh exported through `mesh_to_obj` with scale `k` reads back with
285
+ * scale `1.0 / k`.
117
286
  */
118
- export function obj_to_mesh(data: Uint8Array, convert_to_left_handed: boolean): PrimitiveMesh;
287
+ export function obj_to_mesh(data: Uint8Array, options?: MeshIoOptions | null): PrimitiveMesh;
119
288
 
120
289
  export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
121
290
 
@@ -123,8 +292,10 @@ export interface InitOutput {
123
292
  readonly memory: WebAssembly.Memory;
124
293
  readonly __wbg_primitivemesh_free: (a: number, b: number) => void;
125
294
  readonly assemble: (a: any, b: number) => [number, number, number, number];
295
+ readonly export_glb: (a: any, b: number, c: number) => [number, number, number, number];
126
296
  readonly extract: (a: number, b: number, c: number) => [number, number, number];
127
297
  readonly generate_builtin_primitive: (a: number, b: number) => [number, number, number];
298
+ readonly mesh_to_glb: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number) => [number, number, number, number];
128
299
  readonly mesh_to_obj: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number) => [number, number, number, number];
129
300
  readonly obj_to_mesh: (a: number, b: number, c: number) => [number, number, number];
130
301
  readonly primitivemesh_indices: (a: number) => any;
package/vpin.js CHANGED
@@ -1,5 +1,51 @@
1
1
  /* @ts-self-types="./vpin.d.ts" */
2
2
 
3
+ /**
4
+ * Axis convention for 3D mesh interchange (OBJ, glTF/GLB), in either
5
+ * direction: exporting VPX geometry or importing meshes back in.
6
+ *
7
+ * VPX space is left-handed with Z up: X right (across the playfield),
8
+ * Y towards the player (down the playfield), Z up (towards the glass).
9
+ * [`Self::ZUpLeftHanded`] is that internal frame verbatim; the other
10
+ * variants negate or swap exactly one axis pair, so each produces a
11
+ * right-handed frame from the left-handed one. A conversion between two
12
+ * frames of different [`Self::handedness`] must pair with a reversed
13
+ * triangle winding (matching vpinball's `ObjLoader`); one between frames
14
+ * of equal handedness keeps the winding, since such frames differ only
15
+ * by a pure rotation (the two right-handed variants: 90 degrees about
16
+ * X).
17
+ * @enum {0 | 1 | 2}
18
+ */
19
+ export const AxisConvention = Object.freeze({
20
+ /**
21
+ * VPX's internal coordinate system: left-handed with Z up, inherited
22
+ * from DirectX. X runs right across the playfield, Y down the
23
+ * playfield towards the player, Z up towards the glass.
24
+ *
25
+ * Converting to or from this convention leaves coordinates untouched
26
+ * (vertices keep their vpx-internal values). Only meaningful for
27
+ * tooling that works on the raw vpx data; not what any viewer or DCC
28
+ * tool expects.
29
+ */
30
+ ZUpLeftHanded: 0, "0": "ZUpLeftHanded",
31
+ /**
32
+ * VPinball's exported OBJ convention: `(x, y, -z)`.
33
+ *
34
+ * Right-handed like [`Self::YUpRightHanded`], but keeping VPX's axis
35
+ * roles: Y still runs down the playfield and Z, now negated, points
36
+ * down through the table instead of up. This is what vpinball itself
37
+ * writes when exporting an OBJ and expects when importing one, so a
38
+ * table shows up rotated 90 degrees about X in a Y-up viewer.
39
+ */
40
+ ZDownRightHanded: 1, "1": "ZDownRightHanded",
41
+ /**
42
+ * Y up, right-handed: `(x, z, y)`. The glTF and Wavefront OBJ
43
+ * convention, and what Blender / Maya / 3ds Max assume with their
44
+ * default import settings (`Scale 1.0, Forward -Z, Up Y`).
45
+ */
46
+ YUpRightHanded: 2, "2": "YUpRightHanded",
47
+ });
48
+
3
49
  /**
4
50
  * Mesh data for a single primitive: positions, texture coordinates,
5
51
  * normals and triangle indices, packed as flat typed arrays for direct
@@ -101,7 +147,7 @@ export class PrimitiveMesh {
101
147
  if (Symbol.dispose) PrimitiveMesh.prototype[Symbol.dispose] = PrimitiveMesh.prototype.free;
102
148
 
103
149
  /**
104
- * @param {object} files
150
+ * @param {VpxFileMap} files
105
151
  * @param {Function | null} [callback]
106
152
  * @returns {Uint8Array}
107
153
  */
@@ -115,10 +161,43 @@ export function assemble(files, callback) {
115
161
  return v1;
116
162
  }
117
163
 
164
+ /**
165
+ * Export a whole table to a GLB (binary glTF 2.0) file.
166
+ *
167
+ * Takes the same extracted-file map `assemble` takes
168
+ * (`{ "/vpx/...": Uint8Array }`), so consumers holding a table as an
169
+ * extracted tree skip a full assemble/parse round trip; consumers with
170
+ * only `.vpx` bytes can call `extract` first.
171
+ *
172
+ * The output follows the glTF conventions: Y-up right-handed axes and
173
+ * meters, so it opens correctly in Blender and other glTF viewers with
174
+ * default settings. It contains the generated meshes for every part
175
+ * type, PBR materials from the VPX material list, embedded textures,
176
+ * and the three VPX view cameras (desktop / fullscreen / FSS).
177
+ *
178
+ * Sound entries (`/vpx/sounds.json` and `/vpx/sounds/*`) are ignored -
179
+ * the exporter never reads them, and skipping them keeps peak wasm
180
+ * memory down on sound-heavy tables. Passing a map without them works
181
+ * too.
182
+ * @param {VpxFileMap} files
183
+ * @param {GlbExportOptions | null} [options]
184
+ * @param {Function | null} [callback]
185
+ * @returns {Uint8Array}
186
+ */
187
+ export function export_glb(files, options, callback) {
188
+ const ret = wasm.export_glb(files, isLikeNone(options) ? 0 : addToExternrefTable0(options), isLikeNone(callback) ? 0 : addToExternrefTable0(callback));
189
+ if (ret[3]) {
190
+ throw takeFromExternrefTable0(ret[2]);
191
+ }
192
+ var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
193
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
194
+ return v1;
195
+ }
196
+
118
197
  /**
119
198
  * @param {Uint8Array} data
120
199
  * @param {Function | null} [callback]
121
- * @returns {object}
200
+ * @returns {VpxFileMap}
122
201
  */
123
202
  export function extract(data, callback) {
124
203
  const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
@@ -164,6 +243,51 @@ export function init() {
164
243
  wasm.init();
165
244
  }
166
245
 
246
+ /**
247
+ * Serialize a single mesh as a GLB (binary glTF 2.0) file.
248
+ *
249
+ * The GLB sibling of [`mesh_to_obj`], for exporting one primitive's
250
+ * mesh to DCC tools. There is no axis option: glTF mandates Y-up
251
+ * right-handed, so the vpx-internal input is always converted the same
252
+ * way the whole-table exporter converts it (Y-Z swap, winding
253
+ * reversed; texture coordinates pass through - glTF's UV origin
254
+ * matches vpx's, unlike OBJ's).
255
+ *
256
+ * `name` becomes the mesh/node name; pass an empty string to use
257
+ * `"object"`. Array requirements are the same as [`mesh_to_obj`].
258
+ *
259
+ * `options.unitScale` (default `1.0`) multiplies positions. A
260
+ * primitive's local mesh coordinates are arbitrary units (the table
261
+ * scales them by the primitive's Size at render), so no unit
262
+ * conversion is applied by default.
263
+ * @param {string} name
264
+ * @param {Float32Array} positions
265
+ * @param {Float32Array} tex_coords
266
+ * @param {Float32Array} normals
267
+ * @param {Uint32Array} indices
268
+ * @param {MeshGlbOptions | null} [options]
269
+ * @returns {Uint8Array}
270
+ */
271
+ export function mesh_to_glb(name, positions, tex_coords, normals, indices, options) {
272
+ const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
273
+ const len0 = WASM_VECTOR_LEN;
274
+ const ptr1 = passArrayF32ToWasm0(positions, wasm.__wbindgen_malloc);
275
+ const len1 = WASM_VECTOR_LEN;
276
+ const ptr2 = passArrayF32ToWasm0(tex_coords, wasm.__wbindgen_malloc);
277
+ const len2 = WASM_VECTOR_LEN;
278
+ const ptr3 = passArrayF32ToWasm0(normals, wasm.__wbindgen_malloc);
279
+ const len3 = WASM_VECTOR_LEN;
280
+ const ptr4 = passArray32ToWasm0(indices, wasm.__wbindgen_malloc);
281
+ const len4 = WASM_VECTOR_LEN;
282
+ const ret = wasm.mesh_to_glb(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, isLikeNone(options) ? 0 : addToExternrefTable0(options));
283
+ if (ret[3]) {
284
+ throw takeFromExternrefTable0(ret[2]);
285
+ }
286
+ var v6 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
287
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
288
+ return v6;
289
+ }
290
+
167
291
  /**
168
292
  * Serialize a mesh as a Wavefront OBJ.
169
293
  *
@@ -173,25 +297,36 @@ export function init() {
173
297
  * normals.len() / 3`); index values must be valid 0-based offsets into
174
298
  * that vertex array.
175
299
  *
176
- * `convert_to_left_handed` is the symmetric inverse of the same flag
177
- * on [`obj_to_mesh`]:
300
+ * `options.axes` is the symmetric inverse of the same option on
301
+ * [`obj_to_mesh`]: the input is always vpx-internal data, `axes` names
302
+ * the convention to write the OBJ in.
303
+ *
304
+ * - [`AxisConvention::ZDownRightHanded`] (the default; matches
305
+ * `extract`'s write path, the old `convert_to_left_handed = true`):
306
+ * vertex and normal Z are negated, V is flipped (`obj_v = 1 -
307
+ * vpx_tv`) and per-triangle corner order is reversed. The result is a
308
+ * vpinball-format OBJ that `assemble` (or `obj_to_mesh` with the same
309
+ * convention) reads back identically.
310
+ * - [`AxisConvention::YUpRightHanded`]: Y and Z are swapped, V is
311
+ * flipped and winding is reversed; the OBJ opens upright in Blender
312
+ * with default import settings.
313
+ * - [`AxisConvention::ZUpLeftHanded`] (the old `convert_to_left_handed
314
+ * = false`): the vpx-internal data is written out verbatim, no
315
+ * transforms applied.
178
316
  *
179
- * - `true` (matches `extract`'s write path): the input is treated as
180
- * vpx-internal data and converted out: vertex Z is negated, normal Z
181
- * is negated, V is flipped (`obj_v = 1 - vpx_tv`), and per-triangle
182
- * corner order is reversed. The result is a vpinball-format OBJ
183
- * that `assemble` (or `obj_to_mesh(.., true)`) reads back identically.
184
- * - `false`: the input vpx-internal data is written out verbatim, no
185
- * transforms applied. Round-trips with `obj_to_mesh(.., false)`.
317
+ * `options.unitScale` (default `1.0`) multiplies positions before the
318
+ * axis mapping. Normals and texture coordinates are never scaled.
319
+ * Round-trips through `obj_to_mesh` with the same convention and scale
320
+ * `1.0 / unitScale`.
186
321
  * @param {string} name
187
322
  * @param {Float32Array} positions
188
323
  * @param {Float32Array} tex_coords
189
324
  * @param {Float32Array} normals
190
325
  * @param {Uint32Array} indices
191
- * @param {boolean} convert_to_left_handed
326
+ * @param {MeshIoOptions | null} [options]
192
327
  * @returns {Uint8Array}
193
328
  */
194
- export function mesh_to_obj(name, positions, tex_coords, normals, indices, convert_to_left_handed) {
329
+ export function mesh_to_obj(name, positions, tex_coords, normals, indices, options) {
195
330
  const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
196
331
  const len0 = WASM_VECTOR_LEN;
197
332
  const ptr1 = passArrayF32ToWasm0(positions, wasm.__wbindgen_malloc);
@@ -202,7 +337,7 @@ export function mesh_to_obj(name, positions, tex_coords, normals, indices, conve
202
337
  const len3 = WASM_VECTOR_LEN;
203
338
  const ptr4 = passArray32ToWasm0(indices, wasm.__wbindgen_malloc);
204
339
  const len4 = WASM_VECTOR_LEN;
205
- const ret = wasm.mesh_to_obj(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, convert_to_left_handed);
340
+ const ret = wasm.mesh_to_obj(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, isLikeNone(options) ? 0 : addToExternrefTable0(options));
206
341
  if (ret[3]) {
207
342
  throw takeFromExternrefTable0(ret[2]);
208
343
  }
@@ -218,27 +353,37 @@ export function mesh_to_obj(name, positions, tex_coords, normals, indices, conve
218
353
  * anything in between): n-gons are fan-triangulated and `(position, uv,
219
354
  * normal)` corners are deduplicated so the result is renderer-ready.
220
355
  *
221
- * `convert_to_left_handed` mirrors vpinball's `ObjLoader::Load` flag of
222
- * the same name (the "Convert coordinate system" checkbox in the
223
- * vpinball mesh-import dialog):
356
+ * `options.axes` names the convention the OBJ data is in; the returned
357
+ * mesh is always in vpx-internal convention:
358
+ *
359
+ * - [`AxisConvention::ZDownRightHanded`] (the default; matches
360
+ * `assemble`'s read path and vpinball's mesh-import dialog with
361
+ * "Convert coordinate system" checked, the old `convert_to_left_handed
362
+ * = true`): vertex and normal Z are negated, V is flipped (`vpx_tv =
363
+ * 1 - obj_v`) and the per-triangle corner order is reversed. This path
364
+ * also honors the `# vpx <hex>` byte-preservation comments, so
365
+ * vpinball-format OBJs from `extract` reproduce the original vpx
366
+ * values bit-for-bit.
367
+ * - [`AxisConvention::YUpRightHanded`] (Blender / DCC default export
368
+ * settings): Y and Z are swapped, V is flipped and winding is
369
+ * reversed.
370
+ * - [`AxisConvention::ZUpLeftHanded`] (the old `convert_to_left_handed
371
+ * = false`): the input is assumed to already hold vpx-internal values
372
+ * (e.g. produced by a previous `mesh_to_obj` with the same
373
+ * convention). No transforms; values pass through verbatim.
224
374
  *
225
- * - `true` (matches `assemble`'s read path and vpinball's dialog
226
- * default): the input is treated as right-handed (Blender / standard
227
- * convention). Vertex Z is negated, normal Z is negated, V is flipped
228
- * (`vpx_tv = 1 - obj_v`), and the per-triangle corner order is
229
- * reversed. The returned mesh data ends up in vpx-internal,
230
- * left-handed convention.
231
- * - `false`: the input is assumed to already be in vpx-internal
232
- * convention (e.g. produced by a previous `mesh_to_obj` with the same
233
- * flag). The transforms are skipped and values pass through verbatim.
375
+ * `options.unitScale` (default `1.0`) multiplies positions after the
376
+ * axis mapping. Normals and texture coordinates are never scaled. A
377
+ * mesh exported through `mesh_to_obj` with scale `k` reads back with
378
+ * scale `1.0 / k`.
234
379
  * @param {Uint8Array} data
235
- * @param {boolean} convert_to_left_handed
380
+ * @param {MeshIoOptions | null} [options]
236
381
  * @returns {PrimitiveMesh}
237
382
  */
238
- export function obj_to_mesh(data, convert_to_left_handed) {
383
+ export function obj_to_mesh(data, options) {
239
384
  const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
240
385
  const len0 = WASM_VECTOR_LEN;
241
- const ret = wasm.obj_to_mesh(ptr0, len0, convert_to_left_handed);
386
+ const ret = wasm.obj_to_mesh(ptr0, len0, isLikeNone(options) ? 0 : addToExternrefTable0(options));
242
387
  if (ret[2]) {
243
388
  throw takeFromExternrefTable0(ret[1]);
244
389
  }
@@ -251,6 +396,22 @@ function __wbg_get_imports() {
251
396
  const ret = Error(getStringFromWasm0(arg0, arg1));
252
397
  return ret;
253
398
  },
399
+ __wbg_Number_3890faa6d3ff057d: function(arg0) {
400
+ const ret = Number(arg0);
401
+ return ret;
402
+ },
403
+ __wbg_String_8564e559799eccda: function(arg0, arg1) {
404
+ const ret = String(arg1);
405
+ const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
406
+ const len1 = WASM_VECTOR_LEN;
407
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
408
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
409
+ },
410
+ __wbg___wbindgen_boolean_get_c9c83ebd41b34df3: function(arg0) {
411
+ const v = arg0;
412
+ const ret = typeof(v) === 'boolean' ? v : undefined;
413
+ return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0;
414
+ },
254
415
  __wbg___wbindgen_debug_string_a57024b9c6e4a48b: function(arg0, arg1) {
255
416
  const ret = debugString(arg1);
256
417
  const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
@@ -258,6 +419,33 @@ function __wbg_get_imports() {
258
419
  getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
259
420
  getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
260
421
  },
422
+ __wbg___wbindgen_in_ac983077f137f2e6: function(arg0, arg1) {
423
+ const ret = arg0 in arg1;
424
+ return ret;
425
+ },
426
+ __wbg___wbindgen_is_null_7d13f41e1a2d5140: function(arg0) {
427
+ const ret = arg0 === null;
428
+ return ret;
429
+ },
430
+ __wbg___wbindgen_is_object_a2790eb24c211ea0: function(arg0) {
431
+ const val = arg0;
432
+ const ret = typeof(val) === 'object' && val !== null;
433
+ return ret;
434
+ },
435
+ __wbg___wbindgen_is_undefined_6cff064c44e0d823: function(arg0) {
436
+ const ret = arg0 === undefined;
437
+ return ret;
438
+ },
439
+ __wbg___wbindgen_jsval_loose_eq_acf2776254a8d832: function(arg0, arg1) {
440
+ const ret = arg0 == arg1;
441
+ return ret;
442
+ },
443
+ __wbg___wbindgen_number_get_136b9679cab35cfb: function(arg0, arg1) {
444
+ const obj = arg1;
445
+ const ret = typeof(obj) === 'number' ? obj : undefined;
446
+ getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
447
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
448
+ },
261
449
  __wbg___wbindgen_string_get_d154f1e671052120: function(arg0, arg1) {
262
450
  const obj = arg1;
263
451
  const ret = typeof(obj) === 'string' ? obj : undefined;
@@ -292,6 +480,34 @@ function __wbg_get_imports() {
292
480
  const ret = arg0[arg1 >>> 0];
293
481
  return ret;
294
482
  },
483
+ __wbg_get_with_ref_key_6412cf3094599694: function(arg0, arg1) {
484
+ const ret = arg0[arg1];
485
+ return ret;
486
+ },
487
+ __wbg_instanceof_ArrayBuffer_993d02d2d254cad1: function(arg0) {
488
+ let result;
489
+ try {
490
+ result = arg0 instanceof ArrayBuffer;
491
+ } catch (_) {
492
+ result = false;
493
+ }
494
+ const ret = result;
495
+ return ret;
496
+ },
497
+ __wbg_instanceof_Uint8Array_f935dbb0aa7cdeed: function(arg0) {
498
+ let result;
499
+ try {
500
+ result = arg0 instanceof Uint8Array;
501
+ } catch (_) {
502
+ result = false;
503
+ }
504
+ const ret = result;
505
+ return ret;
506
+ },
507
+ __wbg_isSafeInteger_f3d6cd19ccfe4512: function(arg0) {
508
+ const ret = Number.isSafeInteger(arg0);
509
+ return ret;
510
+ },
295
511
  __wbg_keys_ec7f8c0c2370d91d: function(arg0) {
296
512
  const ret = Object.keys(arg0);
297
513
  return ret;
@@ -308,6 +524,10 @@ function __wbg_get_imports() {
308
524
  const ret = new Error();
309
525
  return ret;
310
526
  },
527
+ __wbg_new_77cc4f4f472aeb81: function(arg0) {
528
+ const ret = new Uint8Array(arg0);
529
+ return ret;
530
+ },
311
531
  __wbg_new_ebe3e0f6837f0879: function() {
312
532
  const ret = new Object();
313
533
  return ret;
package/vpin_bg.wasm CHANGED
Binary file
package/vpin_bg.wasm.d.ts CHANGED
@@ -3,8 +3,10 @@
3
3
  export const memory: WebAssembly.Memory;
4
4
  export const __wbg_primitivemesh_free: (a: number, b: number) => void;
5
5
  export const assemble: (a: any, b: number) => [number, number, number, number];
6
+ export const export_glb: (a: any, b: number, c: number) => [number, number, number, number];
6
7
  export const extract: (a: number, b: number, c: number) => [number, number, number];
7
8
  export const generate_builtin_primitive: (a: number, b: number) => [number, number, number];
9
+ export const mesh_to_glb: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number) => [number, number, number, number];
8
10
  export const mesh_to_obj: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number) => [number, number, number, number];
9
11
  export const obj_to_mesh: (a: number, b: number, c: number) => [number, number, number];
10
12
  export const primitivemesh_indices: (a: number) => any;