From 161944b27b10a1633eee2c2ea1ea70adb8edeb6d Mon Sep 17 00:00:00 2001 From: AlexZ005 Date: Wed, 7 Oct 2026 03:57:32 +0300 Subject: [PATCH] [feat] smart-unwrap 1.0.0: "Smart (xatlas)" UV unwrap backend from a packaged wasm (37 R11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new installable module that registers an xatlas unwrap through api.registerUnwrapBackend('xatlas', 'Smart (xatlas)', async run). xatlas cuts the mesh into charts, flattens them and packs them into a single 0..1 atlas where no charts overlap. - The build is xatlas-wasm 0.1.3 (MIT declared, xatlas MIT). Its wasm is 177 kB with a 6-import C API and a release glue that honours Module.instantiateWasm. watlas 1.0.1 was rejected: its 225 kB embind build ships an ASSERTIONS glue that aborts on instantiateWasm/wasmBinary and resolves the wasm against import.meta.url. - vendor.mjs splits the npm package's SINGLE_FILE bundle into assets/xatlas.wasm and the glue spliced into module.js. It parses the package as text and never runs it. - The wasm is read through api.assetUrl on the first unwrap and cached. onUnload drops the cache, and the SDK journals the backend registration itself. - Welding by exact position gives xatlas the mesh connectivity. Output corners are mapped back through xref. - rotateCharts is off because xatlas's packer rotates a chart by transposing it, which mirrors it. With fixWinding on, every chart comes out unmirrored. - On failure the module shows a toast and returns null, so core commits nothing. Tests: npm run test:smart-unwrap (node, no app) covers a cube, a UV sphere, a cylinder, a scoped half-sphere and a 12k-triangle sphere. It checks properties: 3 corners per face, every uv in [0,1], no zero-area triangle, islands that partition the faces, no overlap between islands on a 512² raster, CCW winding and at most 6 islands on the cube. Counterfactuals prove each check catches a broken result. 55 pass. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 1 + index.json | 16 + modules/smart-unwrap/README.md | 127 ++ modules/smart-unwrap/THIRD_PARTY_LICENSES.txt | 82 + modules/smart-unwrap/assets/xatlas.wasm | Bin 0 -> 176703 bytes modules/smart-unwrap/manifest.json | 9 + modules/smart-unwrap/module.js | 1356 +++++++++++++++++ modules/smart-unwrap/test/run.mjs | 284 ++++ modules/smart-unwrap/vendor.mjs | 97 ++ package.json | 3 +- 10 files changed, 1974 insertions(+), 1 deletion(-) create mode 100644 modules/smart-unwrap/README.md create mode 100644 modules/smart-unwrap/THIRD_PARTY_LICENSES.txt create mode 100644 modules/smart-unwrap/assets/xatlas.wasm create mode 100644 modules/smart-unwrap/manifest.json create mode 100644 modules/smart-unwrap/module.js create mode 100644 modules/smart-unwrap/test/run.mjs create mode 100644 modules/smart-unwrap/vendor.mjs diff --git a/README.md b/README.md index 01eb38d..4d3ce55 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ npm run pack -- my-module # -> my-module.zip, ready to install | [health](modules/health/) | Hit points for objects and players. An object's hp is DERIVED from core Counters fed by `damage`/`heal` pulses (one trigger-log entry per pulse, the count riding the DEVX #18 handshake, so two peers and a late joiner read ONE number with no authority); a player's own hp rides their own `peerVars` row with regen derived from the clock. Sources: a wired event, click, touch, a zone, the knock feed (speed-scaled); death hides or respawns; a manager toolbox; HUD actions. Needs the R3a game seams. | | [waves](modules/waves/) | 2.0 (30b): a VR wave shooter — a gun in hand (Blaster / Scatter / Beam), an ability on the grip (Shield / Slow-mo / Pulse), five levels of grunts, runners and tanks, the crystal breached, a menu reachable in VR. Wave survival composed on `health`: pre-placed enemies with health chains, the wave derived from their hit counters (each wave heals them back with local pulses), enemies walking spawn point → goal on the synced clock, `over` → the game shell, kills on each player's own row, the run log in `gameState.vars`. A late joiner lands on the same wave. Needs `health`. | | [untangle](modules/untangle/) | The 190 test-flight GAME: drag the dots until no edges cross. Procedural guaranteed-solvable puzzles (seed-deterministic — determinism IS the netcode), desktop+VR drag via `api.pointerRay()`, replicated moves, lockstep win/level advance, generative WebAudio pad + SFX. Needs app ≥ the `api.pointerRay` SDK (core PR #37). | +| [smart-unwrap](modules/smart-unwrap/) | **Smart (xatlas)** in the UV editor's Unwrap menu: automatic seams, flattened charts and an overlap-free packed atlas from [xatlas](https://github.com/jpcy/xatlas) compiled to WebAssembly. The 177 kB `.wasm` ships inside the zip and loads through `api.assetUrl` on the first unwrap — no network, no CSP change. Charts come out unmirrored; peers without the module still receive the unwrapped mesh. Needs core ≥ 1.3 (`api.registerUnwrapBackend`). | ## Module format diff --git a/index.json b/index.json index 0897e7a..3715920 100644 --- a/index.json +++ b/index.json @@ -247,6 +247,22 @@ "rhythm" ] }, + { + "id": "smart-unwrap", + "name": "Smart unwrap (xatlas)", + "version": "1.0.0", + "description": "Adds \"Smart (xatlas)\" to the UV editor's Unwrap menu: automatic seams, flattened charts and an overlap-free packed atlas, from xatlas compiled to WebAssembly (the .wasm ships in the zip).", + "author": "theprototype-app", + "source": "modules/smart-unwrap", + "zip": "smart-unwrap.zip", + "category": "tool", + "tags": [ + "uv", + "unwrap", + "texturing", + "wasm" + ] + }, { "id": "tutorial-room", "name": "Tutorial Room", diff --git a/modules/smart-unwrap/README.md b/modules/smart-unwrap/README.md new file mode 100644 index 0000000..628c4b9 --- /dev/null +++ b/modules/smart-unwrap/README.md @@ -0,0 +1,127 @@ +# smart-unwrap + +**Smart unwrap (xatlas)**: an automatic UV unwrapper for the UV editor. Core's +Unwrap menu ships projections (box, planar, cylindrical, spherical). This module +adds **Smart (xatlas)**, which cuts the mesh into charts along creases and seams, +flattens each chart (planar, ortho or LSCM), and packs them into one atlas with +no overlap. Other tools call this "smart UV project" or "lightmap pack". Under +the hood it is [xatlas](https://github.com/jpcy/xatlas) compiled to WebAssembly. +The `.wasm` travels inside the zip, so it needs no network, no CDN and no CSP +change. + +## Use it + +1. Burger menu ▸ **Modules ▸ Install** (from zip: `npm run pack -- smart-unwrap` + writes `smart-unwrap.zip`). +2. Select a mesh and open the **UV editor**. +3. **Unwrap ▾** ▸ **Smart (xatlas)**. With faces picked in Edit Mesh, only those + faces are unwrapped and the rest of the mesh keeps its UVs. That part is core's + scoped-unwrap path. + +The first unwrap instantiates the wasm (about 177 kB) and later ones reuse it. +The result is committed by core (`uvEditor.unwrapObject`) as one undo entry and +replicated as a `meshgeo` snapshot. Peers **without** the module still get the +unwrapped mesh, because only the peer who presses the button runs xatlas. + +If the menu was already open in a UV editor when you installed the module, close +and reopen the editor. The editor reads the backend list when it opens. + +## Options + +The UV editor calls the backend with `{margin: 0.02}`. A script can pass more +through `uvEditor.unwrapObject(uuid, 'mod-smart-unwrap-xatlas', options)`: + +| option | default | meaning | +|---|---|---| +| `margin` | `0.01` | gap between charts as a fraction of the texture, the same meaning as for the built-in packer. It is converted to xatlas padding of `margin × resolution / 2` texels per chart. | +| `resolution` | `512` | the texture size the padding is computed for. Going higher is not reliably tighter: at 1024 the 216-tri sphere covered 46% of the square, against 49% at 512, and took about 10× longer. The 12k-tri sphere went from 50% to 54% coverage. | +| `padding` | (from `margin`) | xatlas padding in texels, which overrides `margin` | +| `chartOptions` | `{fixWinding: true}` | raw xatlas `ChartOptions` (`maxChartArea`, `maxCost`, `normalSeamWeight`, …), merged last | +| `packOptions` | see below | raw xatlas `PackOptions`, merged last | + +The pack defaults are `bilinear: true`, `rotateCharts: false`, and `bruteForce` +for meshes of 4000 triangles or fewer. + +**Why `rotateCharts` is off:** xatlas's packer turns a chart 90° by +*transposing* it (it swaps u and v), and a transpose is a mirror. With it on, a +216-triangle UV sphere came back with 2 of its 6 charts mirrored, so painted text +would read backwards on those faces. With it off and `fixWinding` on, every +triangle in the tests winds counter-clockwise (unmirrored for three's v-up UVs). +The cost is a slightly looser pack. `rotateChartsToAxis` is a true rotation and +stays on. + +## Limits + +- **Main thread.** A few hundred triangles take tens of ms. A 12k-triangle sphere + takes about 1.5 s and a 9.6k-triangle torus knot about 1.5 s, and the UI waits + for it. (These times are from node on a Steam Deck. A worker would keep the UI + responsive and is a possible next step.) +- **One atlas, 0..1.** If xatlas spills into several atlases at the chosen + resolution, the backend re-packs into one so that nothing overlaps once + normalised. +- **Degenerate faces** (zero area or NaN) are ignored by xatlas. They get + `[0,0]` UVs and ride one extra island, so every face is still accounted for. +- **Welding is exact.** Corners are joined only when their positions are + bit-identical, which is true of every mesh the editor builds. A mesh whose + "shared" vertices differ by float noise unwraps with more seams. +- **Failure** (missing asset or a wasm abort) shows a toast and returns + nothing, so core commits nothing. The next unwrap starts a fresh runtime. + +## Lifecycle + +`api.registerUnwrapBackend` is journaled by the SDK (`sdk/backends.js` +`onDispose`). Unloading or live-updating the module removes **Smart (xatlas)** +from the menu with no code here. The module's own `api.onUnload` drops the +cached wasm runtime so its memory can be collected. + +## Which wasm build, and why + +**xatlas-wasm 0.1.3** (mode777, MIT declared in package.json; xatlas itself is +MIT, © Jonathan Young), not **watlas 1.0.1** (toji, MIT). The reasons: + +- **Size.** The xatlas-wasm `.wasm` is 177 kB, a plain C API with 6 imports and + no embind or C++ exceptions. watlas is 225 kB with 56 embind and exception + imports. +- **Glue.** xatlas-wasm has a 39 kB release glue, and its emscripten + `Module.instantiateWasm` hook takes our blob-URL bytes with no patching. + watlas has an 87 kB ASSERTIONS build that *aborts* if `instantiateWasm` or + `wasmBinary` is supplied, and it resolves `watlas.wasm` against + `import.meta.url`, which is meaningless for a blob-URL module. It would need + its minified glue patched. +- **Threads.** Both run single-threaded on the main thread (no pthreads or + SharedArrayBuffer), and both expose the same xatlas API (positions + indices + in, then per-vertex uv / xref / chartIndex and per-chart face lists out). +- **The cost.** xatlas-wasm is young (one maintainer, a few stars), and its repo + has no LICENSE file of its own, only the MIT declaration. The text and + attributions are in [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt), + which ships in the zip. + +### Refreshing the vendored build + +`module.js` carries the xatlas-wasm glue between the `VENDORED` markers, and +`assets/xatlas.wasm` is the binary. Both come from the npm tarball through +`vendor.mjs`, which reads the package as **text** and never runs it: + +```bash +mkdir /tmp/xw && cd /tmp/xw && npm pack xatlas-wasm@0.1.3 --ignore-scripts && tar xzf *.tgz +node modules/smart-unwrap/vendor.mjs /tmp/xw/package/dist/index.mjs +npm run test:smart-unwrap +``` + +## Tests + +`npm run test:smart-unwrap` runs in node with no app. It loads the real +`module.js` and the real `assets/xatlas.wasm` through a fake `api` whose +`assetUrl` returns blob URLs, as the app does. It then unwraps a cube (12 tris), +a UV sphere (216), a cylinder (240), a scoped half-sphere and a 12k-tri sphere. +It checks properties rather than exact numbers: + +- 3 corners per face, every uv in [0,1], no zero-area uv triangle and a total + area of at most 1 +- the islands partition the faces, and no two islands overlap on a 512² raster +- every triangle is counter-clockwise, and the cube has 6 or fewer islands + +Counterfactuals show that each check catches a broken result: stacked islands, +a duplicated island, collapsed UVs, shifted UVs, a dropped face, unwelded input +and xatlas's mirroring `rotateCharts`. The suite also covers the lazy and +cached instantiation, the unload hook and the failure path. diff --git a/modules/smart-unwrap/THIRD_PARTY_LICENSES.txt b/modules/smart-unwrap/THIRD_PARTY_LICENSES.txt new file mode 100644 index 0000000..0177293 --- /dev/null +++ b/modules/smart-unwrap/THIRD_PARTY_LICENSES.txt @@ -0,0 +1,82 @@ +Smart unwrap (xatlas) — third-party software +============================================= + +This module ships, unmodified except as noted, a WebAssembly build of xatlas and the +JavaScript glue that drives it: + + assets/xatlas.wasm the xatlas-wasm 0.1.3 binary, decoded byte-for-byte from the + package's inline (SINGLE_FILE) copy + module.js the xatlas-wasm 0.1.3 emscripten glue + wrapper, between the + "VENDORED xatlas-wasm glue" markers; two edits: the inline binary + is dropped (the .wasm above is loaded through + Module.instantiateWasm instead) and the ES `export {...}` block is + removed + +Source package: https://www.npmjs.com/package/xatlas-wasm (version 0.1.3, + sha512-LGWvSyHryDda9+a3+le2bd+QWPTDMFjfu3CPlVauTm8Liqd+S9wIjQScBzlOChFTyL8OpzZ4AI0tE0zoF3kc6g==) +Repository: https://github.com/mode777/xatlas-wasm + + +------------------------------------------------------------------------------- +xatlas-wasm +------------------------------------------------------------------------------- +Copyright (c) Alexander Klingenbeck (alexander.klingenbeck@gmx.de) + +xatlas-wasm declares the MIT license (package.json "license": "MIT"); the package and +repository carry no separate license text, so the standard MIT terms are reproduced +here: + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------------------- +xatlas — https://github.com/jpcy/xatlas +------------------------------------------------------------------------------- +MIT License + +Copyright (c) 2018-2020 Jonathan Young + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------------------- +Code inside xatlas (notices from source/xatlas/xatlas.cpp, all MIT, terms as above) +------------------------------------------------------------------------------- +thekla_atlas — https://github.com/Thekla/thekla_atlas + Copyright (c) 2013 Thekla, Inc + Copyright NVIDIA Corporation 2006 -- Ignacio Castano + +Fast-BVH — https://github.com/brandonpelfrey/Fast-BVH + Copyright (c) 2012 Brandon Pelfrey diff --git a/modules/smart-unwrap/assets/xatlas.wasm b/modules/smart-unwrap/assets/xatlas.wasm new file mode 100644 index 0000000000000000000000000000000000000000..358a89b3372b497fdfaed3744fefc06648e6e13d GIT binary patch literal 176703 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 (v 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 KD{z+XKNkRQjkDJP literal 0 HcmV?d00001 diff --git a/modules/smart-unwrap/manifest.json b/modules/smart-unwrap/manifest.json new file mode 100644 index 0000000..67c056b --- /dev/null +++ b/modules/smart-unwrap/manifest.json @@ -0,0 +1,9 @@ +{ + "id": "smart-unwrap", + "name": "Smart unwrap (xatlas)", + "version": "1.0.0", + "format": 1, + "description": "Adds \"Smart (xatlas)\" to the UV editor's Unwrap menu: automatic seams, flattened charts and an overlap-free packed atlas, from xatlas compiled to WebAssembly.", + "entry": "module.js", + "files": ["module.js", "assets/xatlas.wasm", "THIRD_PARTY_LICENSES.txt"] +} diff --git a/modules/smart-unwrap/module.js b/modules/smart-unwrap/module.js new file mode 100644 index 0000000..fc64c61 --- /dev/null +++ b/modules/smart-unwrap/module.js @@ -0,0 +1,1356 @@ +// smart-unwrap — an automatic UV unwrapper for the UV editor, backed by xatlas +// (https://github.com/jpcy/xatlas) compiled to WebAssembly (the xatlas-wasm build). +// +// Core ships projection unwraps (box / planar / cylindrical / spherical) and a registry; +// this module adds "Smart (xatlas)" to that registry through api.registerUnwrapBackend. +// xatlas segments the mesh into charts along creases and seams, flattens each chart +// (planar / ortho / LSCM), and packs them into one atlas without overlap — the unwrap a +// DCC calls "smart UV project" / "lightmap pack". +// +// HOW THE WASM LOADS: the .wasm travels INSIDE this module's zip (assets/xatlas.wasm) and +// is reached through api.assetUrl — no CDN, no network, no CSP surface. The emscripten glue +// below is vendored verbatim from xatlas-wasm 0.1.3 except that its inline copy of the +// binary is dropped: the binary is handed over through Module.instantiateWasm, the +// standard emscripten hook. It is instantiated on the FIRST unwrap, not at install, and +// cached until the module unloads. +// +// Runs on the main thread: a few hundred triangles take tens of milliseconds, a 12k +// triangle sphere about 1.5 s (the UI waits for it; measured in node on a Steam Deck). +// The UV result is committed by core (uvEditor.unwrapObject: snapshot, undo entry, meshgeo +// broadcast), so peers WITHOUT this module still receive the unwrapped mesh — only the +// peer pressing the button runs xatlas. +// +// License: xatlas (MIT, Jonathan Young) + xatlas-wasm (MIT, Alexander Klingenbeck) — see +// THIRD_PARTY_LICENSES.txt, packed into the zip alongside this file. + +// ---- BEGIN VENDORED xatlas-wasm glue (0.1.3, MIT) — regenerate with vendor.mjs, do not edit ---- +// dist/xatlas.mjs +async function createXAtlasModule(moduleArg = {}) { + var moduleRtn; + var Module = moduleArg; + var ENVIRONMENT_IS_WEB = !!globalThis.window; + var ENVIRONMENT_IS_WORKER = !!globalThis.WorkerGlobalScope; + var ENVIRONMENT_IS_NODE = globalThis.process?.versions?.node && globalThis.process?.type != "renderer"; + if (ENVIRONMENT_IS_NODE) { + const { createRequire } = await import("node:module"); + var require2 = createRequire(import.meta.url); + } + var arguments_ = []; + var thisProgram = "./this.program"; + var quit_ = (status, toThrow) => { + throw toThrow; + }; + var _scriptName = import.meta.url; + var scriptDirectory = ""; + var readAsync, readBinary; + if (ENVIRONMENT_IS_NODE) { + var fs = require2("node:fs"); + if (_scriptName.startsWith("file:")) { + scriptDirectory = require2("node:path").dirname(require2("node:url").fileURLToPath(_scriptName)) + "/"; + } + readBinary = (filename) => { + filename = isFileURI(filename) ? new URL(filename) : filename; + var ret = fs.readFileSync(filename); + return ret; + }; + readAsync = async (filename, binary = true) => { + filename = isFileURI(filename) ? new URL(filename) : filename; + var ret = fs.readFileSync(filename, binary ? void 0 : "utf8"); + return ret; + }; + if (process.argv.length > 1) { + thisProgram = process.argv[1].replace(/\\/g, "/"); + } + arguments_ = process.argv.slice(2); + quit_ = (status, toThrow) => { + process.exitCode = status; + throw toThrow; + }; + } else if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { + try { + scriptDirectory = new URL(".", _scriptName).href; + } catch { + } + { + readAsync = async (url) => { + var response = await fetch(url, { credentials: "same-origin" }); + if (response.ok) { + return response.arrayBuffer(); + } + throw new Error(response.status + " : " + response.url); + }; + } + } else { + } + var out = console.log.bind(console); + var err = console.error.bind(console); + var wasmBinary; + var ABORT = false; + var EXITSTATUS; + var isFileURI = (filename) => filename.startsWith("file://"); + class EmscriptenEH { + } + class EmscriptenSjLj extends EmscriptenEH { + } + function binaryDecode(bin) { + for (var i = 0, l = bin.length, o = new Uint8Array(l), c; i < l; ++i) { + c = bin.charCodeAt(i); + o[i] = ~c >> 8 & c; + } + return o; + } + var readyPromiseResolve, readyPromiseReject; + var runtimeInitialized = false; + function updateMemoryViews() { + var b = wasmMemory.buffer; + HEAP8 = new Int8Array(b); + HEAP16 = new Int16Array(b); + Module["HEAPU8"] = HEAPU8 = new Uint8Array(b); + Module["HEAPU16"] = HEAPU16 = new Uint16Array(b); + Module["HEAP32"] = HEAP32 = new Int32Array(b); + Module["HEAPU32"] = HEAPU32 = new Uint32Array(b); + Module["HEAPF32"] = HEAPF32 = new Float32Array(b); + HEAPF64 = new Float64Array(b); + HEAP64 = new BigInt64Array(b); + HEAPU64 = new BigUint64Array(b); + } + function preRun() { + if (Module["preRun"]) { + if (typeof Module["preRun"] == "function") Module["preRun"] = [Module["preRun"]]; + while (Module["preRun"].length) { + addOnPreRun(Module["preRun"].shift()); + } + } + callRuntimeCallbacks(onPreRuns); + } + function initRuntime() { + runtimeInitialized = true; + wasmExports["__wasm_call_ctors"](); + } + function postRun() { + if (Module["postRun"]) { + if (typeof Module["postRun"] == "function") Module["postRun"] = [Module["postRun"]]; + while (Module["postRun"].length) { + addOnPostRun(Module["postRun"].shift()); + } + } + callRuntimeCallbacks(onPostRuns); + } + function abort(what) { + Module["onAbort"]?.(what); + what = `Aborted(${what})`; + err(what); + ABORT = true; + what += ". Build with -sASSERTIONS for more info."; + var e = new WebAssembly.RuntimeError(what); + readyPromiseReject?.(e); + throw e; + } + var wasmBinaryFile; + function findWasmBinary() { + return void 0; // smart-unwrap: wasm supplied via Module.instantiateWasm + } + function getBinarySync(file) { + return file; + } + async function getWasmBinary(binaryFile) { + return getBinarySync(binaryFile); + } + async function instantiateArrayBuffer(binaryFile, imports) { + try { + var binary = await getWasmBinary(binaryFile); + var instance = await WebAssembly.instantiate(binary, imports); + return instance; + } catch (reason) { + err(`failed to asynchronously prepare wasm: ${reason}`); + abort(reason); + } + } + async function instantiateAsync(binary, binaryFile, imports) { + return instantiateArrayBuffer(binaryFile, imports); + } + function getWasmImports() { + var imports = { env: wasmImports, wasi_snapshot_preview1: wasmImports }; + return imports; + } + async function createWasm() { + function receiveInstance(instance, module) { + wasmExports = instance.exports; + assignWasmExports(wasmExports); + updateMemoryViews(); + return wasmExports; + } + function receiveInstantiationResult(result2) { + return receiveInstance(result2["instance"]); + } + var info = getWasmImports(); + if (Module["instantiateWasm"]) { + return new Promise((resolve, reject) => { + Module["instantiateWasm"](info, (inst, mod) => { + resolve(receiveInstance(inst, mod)); + }); + }); + } + wasmBinaryFile ??= findWasmBinary(); + var result = await instantiateAsync(wasmBinary, wasmBinaryFile, info); + var exports = receiveInstantiationResult(result); + return exports; + } + class ExitStatus { + name = "ExitStatus"; + constructor(status) { + this.message = `Program terminated with exit(${status})`; + this.status = status; + } + } + var HEAP16; + var HEAP32; + var HEAP64; + var HEAP8; + var HEAPF32; + var HEAPF64; + var HEAPU16; + var HEAPU32; + var HEAPU64; + var HEAPU8; + var callRuntimeCallbacks = (callbacks) => { + while (callbacks.length > 0) { + callbacks.shift()(Module); + } + }; + var onPostRuns = []; + var addOnPostRun = (cb) => onPostRuns.push(cb); + var onPreRuns = []; + var addOnPreRun = (cb) => onPreRuns.push(cb); + function getValue(ptr, type = "i8") { + if (type.endsWith("*")) type = "*"; + switch (type) { + case "i1": + return HEAP8[ptr]; + case "i8": + return HEAP8[ptr]; + case "i16": + return HEAP16[ptr >> 1]; + case "i32": + return HEAP32[ptr >> 2]; + case "i64": + return HEAP64[ptr >> 3]; + case "float": + return HEAPF32[ptr >> 2]; + case "double": + return HEAPF64[ptr >> 3]; + case "*": + return HEAPU32[ptr >> 2]; + default: + abort(`invalid type for getValue: ${type}`); + } + } + var noExitRuntime = true; + function setValue(ptr, value, type = "i8") { + if (type.endsWith("*")) type = "*"; + switch (type) { + case "i1": + HEAP8[ptr] = value; + break; + case "i8": + HEAP8[ptr] = value; + break; + case "i16": + HEAP16[ptr >> 1] = value; + break; + case "i32": + HEAP32[ptr >> 2] = value; + break; + case "i64": + HEAP64[ptr >> 3] = BigInt(value); + break; + case "float": + HEAPF32[ptr >> 2] = value; + break; + case "double": + HEAPF64[ptr >> 3] = value; + break; + case "*": + HEAPU32[ptr >> 2] = value; + break; + default: + abort(`invalid type for setValue: ${type}`); + } + } + var stackRestore = (val) => __emscripten_stack_restore(val); + var stackSave = () => _emscripten_stack_get_current(); + var __abort_js = () => abort(""); + var runtimeKeepaliveCounter = 0; + var __emscripten_runtime_keepalive_clear = () => { + noExitRuntime = false; + runtimeKeepaliveCounter = 0; + }; + var timers = {}; + var handleException = (e) => { + if (e instanceof ExitStatus || e == "unwind") { + return EXITSTATUS; + } + quit_(1, e); + }; + var keepRuntimeAlive = () => noExitRuntime || runtimeKeepaliveCounter > 0; + var _proc_exit = (code) => { + EXITSTATUS = code; + if (!keepRuntimeAlive()) { + Module["onExit"]?.(code); + ABORT = true; + } + quit_(code, new ExitStatus(code)); + }; + var exitJS = (status, implicit) => { + EXITSTATUS = status; + _proc_exit(status); + }; + var _exit = exitJS; + var maybeExit = () => { + if (!keepRuntimeAlive()) { + try { + _exit(EXITSTATUS); + } catch (e) { + handleException(e); + } + } + }; + var callUserCallback = (func) => { + if (ABORT) { + return; + } + try { + return func(); + } catch (e) { + handleException(e); + } finally { + maybeExit(); + } + }; + var _emscripten_get_now = () => performance.now(); + var __setitimer_js = (which, timeout_ms) => { + if (timers[which]) { + clearTimeout(timers[which].id); + delete timers[which]; + } + if (!timeout_ms) return 0; + var id = setTimeout(() => { + delete timers[which]; + callUserCallback(() => __emscripten_timeout(which, _emscripten_get_now())); + }, timeout_ms); + timers[which] = { id, timeout_ms }; + return 0; + }; + var getHeapMax = () => 2147483648; + var alignMemory = (size, alignment) => Math.ceil(size / alignment) * alignment; + var growMemory = (size) => { + var oldHeapSize = wasmMemory.buffer.byteLength; + var pages = (size - oldHeapSize + 65535) / 65536 | 0; + try { + wasmMemory.grow(pages); + updateMemoryViews(); + return 1; + } catch (e) { + } + }; + var _emscripten_resize_heap = (requestedSize) => { + var oldSize = HEAPU8.length; + requestedSize >>>= 0; + var maxHeapSize = getHeapMax(); + if (requestedSize > maxHeapSize) { + return false; + } + for (var cutDown = 1; cutDown <= 4; cutDown *= 2) { + var overGrownHeapSize = oldSize * (1 + 0.2 / cutDown); + overGrownHeapSize = Math.min(overGrownHeapSize, requestedSize + 100663296); + var newSize = Math.min(maxHeapSize, alignMemory(Math.max(requestedSize, overGrownHeapSize), 65536)); + var replacement = growMemory(newSize); + if (replacement) { + return true; + } + } + return false; + }; + var printCharBuffers = [null, [], []]; + var UTF8Decoder = globalThis.TextDecoder && new TextDecoder(); + var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => { + var maxIdx = idx + maxBytesToRead; + if (ignoreNul) return maxIdx; + while (heapOrArray[idx] && !(idx >= maxIdx)) ++idx; + return idx; + }; + var UTF8ArrayToString = (heapOrArray, idx = 0, maxBytesToRead, ignoreNul) => { + var endPtr = findStringEnd(heapOrArray, idx, maxBytesToRead, ignoreNul); + if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) { + return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr)); + } + var str = ""; + while (idx < endPtr) { + var u0 = heapOrArray[idx++]; + if (!(u0 & 128)) { + str += String.fromCharCode(u0); + continue; + } + var u1 = heapOrArray[idx++] & 63; + if ((u0 & 224) == 192) { + str += String.fromCharCode((u0 & 31) << 6 | u1); + continue; + } + var u2 = heapOrArray[idx++] & 63; + if ((u0 & 240) == 224) { + u0 = (u0 & 15) << 12 | u1 << 6 | u2; + } else { + u0 = (u0 & 7) << 18 | u1 << 12 | u2 << 6 | heapOrArray[idx++] & 63; + } + if (u0 < 65536) { + str += String.fromCharCode(u0); + } else { + var ch = u0 - 65536; + str += String.fromCharCode(55296 | ch >> 10, 56320 | ch & 1023); + } + } + return str; + }; + var printChar = (stream, curr) => { + var buffer = printCharBuffers[stream]; + if (curr === 0 || curr === 10) { + (stream === 1 ? out : err)(UTF8ArrayToString(buffer)); + buffer.length = 0; + } else { + buffer.push(curr); + } + }; + var UTF8ToString = (ptr, maxBytesToRead, ignoreNul) => ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead, ignoreNul) : ""; + var _fd_write = (fd, iov, iovcnt, pnum) => { + var num = 0; + for (var i = 0; i < iovcnt; i++) { + var ptr = HEAPU32[iov >> 2]; + var len = HEAPU32[iov + 4 >> 2]; + iov += 8; + for (var j = 0; j < len; j++) { + printChar(fd, HEAPU8[ptr + j]); + } + num += len; + } + HEAPU32[pnum >> 2] = num; + return 0; + }; + var getCFunc = (ident) => { + var func = Module["_" + ident]; + return func; + }; + var writeArrayToMemory = (array, buffer) => { + HEAP8.set(array, buffer); + }; + var lengthBytesUTF8 = (str) => { + var len = 0; + for (var i = 0; i < str.length; ++i) { + var c = str.charCodeAt(i); + if (c <= 127) { + len++; + } else if (c <= 2047) { + len += 2; + } else if (c >= 55296 && c <= 57343) { + len += 4; + ++i; + } else { + len += 3; + } + } + return len; + }; + var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { + if (!(maxBytesToWrite > 0)) return 0; + var startIdx = outIdx; + var endIdx = outIdx + maxBytesToWrite - 1; + for (var i = 0; i < str.length; ++i) { + var u = str.codePointAt(i); + if (u <= 127) { + if (outIdx >= endIdx) break; + heap[outIdx++] = u; + } else if (u <= 2047) { + if (outIdx + 1 >= endIdx) break; + heap[outIdx++] = 192 | u >> 6; + heap[outIdx++] = 128 | u & 63; + } else if (u <= 65535) { + if (outIdx + 2 >= endIdx) break; + heap[outIdx++] = 224 | u >> 12; + heap[outIdx++] = 128 | u >> 6 & 63; + heap[outIdx++] = 128 | u & 63; + } else { + if (outIdx + 3 >= endIdx) break; + heap[outIdx++] = 240 | u >> 18; + heap[outIdx++] = 128 | u >> 12 & 63; + heap[outIdx++] = 128 | u >> 6 & 63; + heap[outIdx++] = 128 | u & 63; + i++; + } + } + heap[outIdx] = 0; + return outIdx - startIdx; + }; + var stringToUTF8 = (str, outPtr, maxBytesToWrite) => stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite); + var stackAlloc = (sz) => __emscripten_stack_alloc(sz); + var stringToUTF8OnStack = (str) => { + var size = lengthBytesUTF8(str) + 1; + var ret = stackAlloc(size); + stringToUTF8(str, ret, size); + return ret; + }; + var ccall = (ident, returnType, argTypes, args, opts) => { + var toC = { string: (str) => { + var ret2 = 0; + if (str !== null && str !== void 0 && str !== 0) { + ret2 = stringToUTF8OnStack(str); + } + return ret2; + }, array: (arr) => { + var ret2 = stackAlloc(arr.length); + writeArrayToMemory(arr, ret2); + return ret2; + } }; + function convertReturnValue(ret2) { + if (returnType === "string") { + return UTF8ToString(ret2); + } + if (returnType === "boolean") return Boolean(ret2); + return ret2; + } + var func = getCFunc(ident); + var cArgs = []; + var stack = 0; + if (args) { + for (var i = 0; i < args.length; i++) { + var converter = toC[argTypes[i]]; + if (converter) { + if (stack === 0) stack = stackSave(); + cArgs[i] = converter(args[i]); + } else { + cArgs[i] = args[i]; + } + } + } + var ret = func(...cArgs); + function onDone(ret2) { + if (stack !== 0) stackRestore(stack); + return convertReturnValue(ret2); + } + ret = onDone(ret); + return ret; + }; + var cwrap = (ident, returnType, argTypes, opts) => { + var numericArgs = !argTypes || argTypes.every((type) => type === "number" || type === "boolean"); + var numericRet = returnType !== "string"; + if (numericRet && numericArgs && !opts) { + return getCFunc(ident); + } + return (...args) => ccall(ident, returnType, argTypes, args, opts); + }; + var wasmTableMirror = []; + var getWasmTableEntry = (funcPtr) => { + var func = wasmTableMirror[funcPtr]; + if (!func) { + wasmTableMirror[funcPtr] = func = wasmTable.get(funcPtr); + } + return func; + }; + var updateTableMap = (offset, count) => { + if (functionsInTableMap) { + for (var i = offset; i < offset + count; i++) { + var item = getWasmTableEntry(i); + if (item) { + functionsInTableMap.set(item, i); + } + } + } + }; + var functionsInTableMap; + var getFunctionAddress = (func) => { + if (!functionsInTableMap) { + functionsInTableMap = /* @__PURE__ */ new WeakMap(); + updateTableMap(0, wasmTable.length); + } + return functionsInTableMap.get(func) || 0; + }; + var freeTableIndexes = []; + var getEmptyTableSlot = () => { + if (freeTableIndexes.length) { + return freeTableIndexes.pop(); + } + return wasmTable["grow"](1); + }; + var setWasmTableEntry = (idx, func) => { + wasmTable.set(idx, func); + wasmTableMirror[idx] = wasmTable.get(idx); + }; + var uleb128EncodeWithLen = (arr) => { + const n = arr.length; + return [n % 128 | 128, n >> 7, ...arr]; + }; + var wasmTypeCodes = { i: 127, p: 127, j: 126, f: 125, d: 124, e: 111 }; + var generateTypePack = (types) => uleb128EncodeWithLen(Array.from(types, (type) => { + var code = wasmTypeCodes[type]; + return code; + })); + var convertJsFunctionToWasm = (func, sig) => { + var bytes = Uint8Array.of(0, 97, 115, 109, 1, 0, 0, 0, 1, ...uleb128EncodeWithLen([1, 96, ...generateTypePack(sig.slice(1)), ...generateTypePack(sig[0] === "v" ? "" : sig[0])]), 2, 7, 1, 1, 101, 1, 102, 0, 0, 7, 5, 1, 1, 102, 0, 0); + var module = new WebAssembly.Module(bytes); + var instance = new WebAssembly.Instance(module, { e: { f: func } }); + var wrappedFunc = instance.exports["f"]; + return wrappedFunc; + }; + var addFunction = (func, sig) => { + var rtn = getFunctionAddress(func); + if (rtn) { + return rtn; + } + var ret = getEmptyTableSlot(); + try { + setWasmTableEntry(ret, func); + } catch (err2) { + if (!(err2 instanceof TypeError)) { + throw err2; + } + var wrapped = convertJsFunctionToWasm(func, sig); + setWasmTableEntry(ret, wrapped); + } + functionsInTableMap.set(func, ret); + return ret; + }; + var removeFunction = (index) => { + functionsInTableMap.delete(getWasmTableEntry(index)); + setWasmTableEntry(index, null); + freeTableIndexes.push(index); + }; + { + if (Module["noExitRuntime"]) noExitRuntime = Module["noExitRuntime"]; + if (Module["print"]) out = Module["print"]; + if (Module["printErr"]) err = Module["printErr"]; + if (Module["wasmBinary"]) wasmBinary = Module["wasmBinary"]; + if (Module["arguments"]) arguments_ = Module["arguments"]; + if (Module["thisProgram"]) thisProgram = Module["thisProgram"]; + if (Module["preInit"]) { + if (typeof Module["preInit"] == "function") Module["preInit"] = [Module["preInit"]]; + while (Module["preInit"].length > 0) { + Module["preInit"].shift()(); + } + } + } + Module["ccall"] = ccall; + Module["cwrap"] = cwrap; + Module["addFunction"] = addFunction; + Module["removeFunction"] = removeFunction; + Module["setValue"] = setValue; + Module["getValue"] = getValue; + Module["UTF8ToString"] = UTF8ToString; + var _xatlasCreate, _xatlasDestroy, _xatlasAddMesh, _xatlasAddMeshJoin, _xatlasAddUvMesh, _xatlasComputeCharts, _xatlasPackCharts, _xatlasGenerate, _xatlasSetProgressCallback, _xatlasSetAlloc, _xatlasSetPrint, _xatlasAddMeshErrorString, _xatlasProgressCategoryString, _xatlasMeshDeclInit, _xatlasUvMeshDeclInit, _xatlasChartOptionsInit, _xatlasPackOptionsInit, _free, _xatlasAtlas_getWidth, _xatlasAtlas_getHeight, _xatlasAtlas_getAtlasCount, _xatlasAtlas_getChartCount, _xatlasAtlas_getMeshCount, _xatlasAtlas_getTexelsPerUnit, _xatlasAtlas_getUtilization, _xatlasAtlas_getMesh, _xatlasAtlas_getImage, _xatlasMesh_getVertexCount, _xatlasMesh_getIndexCount, _xatlasMesh_getChartCount, _xatlasMesh_getVertexArray, _xatlasMesh_getIndexArray, _xatlasMesh_getChartArray, _xatlasVertex_getAtlasIndex, _xatlasVertex_getChartIndex, _xatlasVertex_getUV0, _xatlasVertex_getUV1, _xatlasVertex_getXref, _xatlasChart_getAtlasIndex, _xatlasChart_getFaceCount, _xatlasChart_getType, _xatlasChart_getMaterial, _xatlasChart_getFaceArray, _xatlasMeshDecl_size, _xatlasUvMeshDecl_size, _xatlasChartOptions_size, _xatlasPackOptions_size, __emscripten_timeout, _malloc, __emscripten_stack_restore, __emscripten_stack_alloc, _emscripten_stack_get_current, memory, __indirect_function_table, wasmMemory, wasmTable; + function assignWasmExports(wasmExports2) { + _xatlasCreate = Module["_xatlasCreate"] = wasmExports2["xatlasCreate"]; + _xatlasDestroy = Module["_xatlasDestroy"] = wasmExports2["xatlasDestroy"]; + _xatlasAddMesh = Module["_xatlasAddMesh"] = wasmExports2["xatlasAddMesh"]; + _xatlasAddMeshJoin = Module["_xatlasAddMeshJoin"] = wasmExports2["xatlasAddMeshJoin"]; + _xatlasAddUvMesh = Module["_xatlasAddUvMesh"] = wasmExports2["xatlasAddUvMesh"]; + _xatlasComputeCharts = Module["_xatlasComputeCharts"] = wasmExports2["xatlasComputeCharts"]; + _xatlasPackCharts = Module["_xatlasPackCharts"] = wasmExports2["xatlasPackCharts"]; + _xatlasGenerate = Module["_xatlasGenerate"] = wasmExports2["xatlasGenerate"]; + _xatlasSetProgressCallback = Module["_xatlasSetProgressCallback"] = wasmExports2["xatlasSetProgressCallback"]; + _xatlasSetAlloc = Module["_xatlasSetAlloc"] = wasmExports2["xatlasSetAlloc"]; + _xatlasSetPrint = Module["_xatlasSetPrint"] = wasmExports2["xatlasSetPrint"]; + _xatlasAddMeshErrorString = Module["_xatlasAddMeshErrorString"] = wasmExports2["xatlasAddMeshErrorString"]; + _xatlasProgressCategoryString = Module["_xatlasProgressCategoryString"] = wasmExports2["xatlasProgressCategoryString"]; + _xatlasMeshDeclInit = Module["_xatlasMeshDeclInit"] = wasmExports2["xatlasMeshDeclInit"]; + _xatlasUvMeshDeclInit = Module["_xatlasUvMeshDeclInit"] = wasmExports2["xatlasUvMeshDeclInit"]; + _xatlasChartOptionsInit = Module["_xatlasChartOptionsInit"] = wasmExports2["xatlasChartOptionsInit"]; + _xatlasPackOptionsInit = Module["_xatlasPackOptionsInit"] = wasmExports2["xatlasPackOptionsInit"]; + _free = Module["_free"] = wasmExports2["free"]; + _xatlasAtlas_getWidth = Module["_xatlasAtlas_getWidth"] = wasmExports2["xatlasAtlas_getWidth"]; + _xatlasAtlas_getHeight = Module["_xatlasAtlas_getHeight"] = wasmExports2["xatlasAtlas_getHeight"]; + _xatlasAtlas_getAtlasCount = Module["_xatlasAtlas_getAtlasCount"] = wasmExports2["xatlasAtlas_getAtlasCount"]; + _xatlasAtlas_getChartCount = Module["_xatlasAtlas_getChartCount"] = wasmExports2["xatlasAtlas_getChartCount"]; + _xatlasAtlas_getMeshCount = Module["_xatlasAtlas_getMeshCount"] = wasmExports2["xatlasAtlas_getMeshCount"]; + _xatlasAtlas_getTexelsPerUnit = Module["_xatlasAtlas_getTexelsPerUnit"] = wasmExports2["xatlasAtlas_getTexelsPerUnit"]; + _xatlasAtlas_getUtilization = Module["_xatlasAtlas_getUtilization"] = wasmExports2["xatlasAtlas_getUtilization"]; + _xatlasAtlas_getMesh = Module["_xatlasAtlas_getMesh"] = wasmExports2["xatlasAtlas_getMesh"]; + _xatlasAtlas_getImage = Module["_xatlasAtlas_getImage"] = wasmExports2["xatlasAtlas_getImage"]; + _xatlasMesh_getVertexCount = Module["_xatlasMesh_getVertexCount"] = wasmExports2["xatlasMesh_getVertexCount"]; + _xatlasMesh_getIndexCount = Module["_xatlasMesh_getIndexCount"] = wasmExports2["xatlasMesh_getIndexCount"]; + _xatlasMesh_getChartCount = Module["_xatlasMesh_getChartCount"] = wasmExports2["xatlasMesh_getChartCount"]; + _xatlasMesh_getVertexArray = Module["_xatlasMesh_getVertexArray"] = wasmExports2["xatlasMesh_getVertexArray"]; + _xatlasMesh_getIndexArray = Module["_xatlasMesh_getIndexArray"] = wasmExports2["xatlasMesh_getIndexArray"]; + _xatlasMesh_getChartArray = Module["_xatlasMesh_getChartArray"] = wasmExports2["xatlasMesh_getChartArray"]; + _xatlasVertex_getAtlasIndex = Module["_xatlasVertex_getAtlasIndex"] = wasmExports2["xatlasVertex_getAtlasIndex"]; + _xatlasVertex_getChartIndex = Module["_xatlasVertex_getChartIndex"] = wasmExports2["xatlasVertex_getChartIndex"]; + _xatlasVertex_getUV0 = Module["_xatlasVertex_getUV0"] = wasmExports2["xatlasVertex_getUV0"]; + _xatlasVertex_getUV1 = Module["_xatlasVertex_getUV1"] = wasmExports2["xatlasVertex_getUV1"]; + _xatlasVertex_getXref = Module["_xatlasVertex_getXref"] = wasmExports2["xatlasVertex_getXref"]; + _xatlasChart_getAtlasIndex = Module["_xatlasChart_getAtlasIndex"] = wasmExports2["xatlasChart_getAtlasIndex"]; + _xatlasChart_getFaceCount = Module["_xatlasChart_getFaceCount"] = wasmExports2["xatlasChart_getFaceCount"]; + _xatlasChart_getType = Module["_xatlasChart_getType"] = wasmExports2["xatlasChart_getType"]; + _xatlasChart_getMaterial = Module["_xatlasChart_getMaterial"] = wasmExports2["xatlasChart_getMaterial"]; + _xatlasChart_getFaceArray = Module["_xatlasChart_getFaceArray"] = wasmExports2["xatlasChart_getFaceArray"]; + _xatlasMeshDecl_size = Module["_xatlasMeshDecl_size"] = wasmExports2["xatlasMeshDecl_size"]; + _xatlasUvMeshDecl_size = Module["_xatlasUvMeshDecl_size"] = wasmExports2["xatlasUvMeshDecl_size"]; + _xatlasChartOptions_size = Module["_xatlasChartOptions_size"] = wasmExports2["xatlasChartOptions_size"]; + _xatlasPackOptions_size = Module["_xatlasPackOptions_size"] = wasmExports2["xatlasPackOptions_size"]; + __emscripten_timeout = wasmExports2["_emscripten_timeout"]; + _malloc = Module["_malloc"] = wasmExports2["malloc"]; + __emscripten_stack_restore = wasmExports2["_emscripten_stack_restore"]; + __emscripten_stack_alloc = wasmExports2["_emscripten_stack_alloc"]; + _emscripten_stack_get_current = wasmExports2["emscripten_stack_get_current"]; + memory = wasmMemory = wasmExports2["memory"]; + __indirect_function_table = wasmTable = wasmExports2["__indirect_function_table"]; + } + var wasmImports = { _abort_js: __abort_js, _emscripten_runtime_keepalive_clear: __emscripten_runtime_keepalive_clear, _setitimer_js: __setitimer_js, emscripten_resize_heap: _emscripten_resize_heap, fd_write: _fd_write, proc_exit: _proc_exit }; + function run() { + preRun(); + function doRun() { + Module["calledRun"] = true; + if (ABORT) return; + initRuntime(); + readyPromiseResolve?.(Module); + Module["onRuntimeInitialized"]?.(); + postRun(); + } + if (Module["setStatus"]) { + Module["setStatus"]("Running..."); + setTimeout(() => { + setTimeout(() => Module["setStatus"](""), 1); + doRun(); + }, 1); + } else { + doRun(); + } + } + var wasmExports; + wasmExports = await createWasm(); + run(); + if (runtimeInitialized) { + moduleRtn = Module; + } else { + moduleRtn = new Promise((resolve, reject) => { + readyPromiseResolve = resolve; + readyPromiseReject = reject; + }); + } + ; + return moduleRtn; +} +var xatlas_default = createXAtlasModule; + +// lib/xatlas.mjs +var ChartType = Object.freeze({ + Planar: 0, + Ortho: 1, + LSCM: 2, + Piecewise: 3, + Invalid: 4 +}); +var IndexFormat = Object.freeze({ + UInt16: 0, + UInt32: 1 +}); +var AddMeshError = Object.freeze({ + Success: 0, + Error: 1, + IndexOutOfRange: 2, + InvalidFaceVertexCount: 3, + InvalidIndexCount: 4 +}); +var ProgressCategory = Object.freeze({ + AddMesh: 0, + ComputeCharts: 1, + PackCharts: 2, + BuildOutputMeshes: 3 +}); +function copyToHeap(module, typedArray, heapType) { + const numBytes = typedArray.byteLength; + const ptr = module._malloc(numBytes); + if (!ptr) throw new Error("xatlas-wasm: malloc failed"); + const dst = new Uint8Array(module.HEAPU8.buffer, ptr, numBytes); + dst.set(new Uint8Array(typedArray.buffer, typedArray.byteOffset, numBytes)); + return ptr; +} +var XAtlas = class { + /** @internal */ + constructor(module) { + this._m = module; + this._ptr = module._xatlasCreate(); + this._progressFnPtr = 0; + if (!this._ptr) throw new Error("xatlas-wasm: xatlasCreate returned null"); + } + destroy() { + if (this._progressFnPtr) { + this._m.removeFunction(this._progressFnPtr); + this._progressFnPtr = 0; + } + if (this._ptr) { + this._m._xatlasDestroy(this._ptr); + this._ptr = 0; + } + } + /** + * Add a triangle mesh to the atlas. + * @param {object} opts + * @param {Float32Array} opts.positions - Vertex positions (flat xyz, length = vertexCount * 3). + * @param {Float32Array} [opts.normals] - Vertex normals (flat xyz). + * @param {Float32Array} [opts.uvs] - Vertex UVs (flat xy). + * @param {Uint16Array|Uint32Array} [opts.indices] - Triangle indices. + * @param {Uint32Array} [opts.faceMaterialData] - Per-face material IDs. + * @param {number} [opts.meshCountHint=0] + * @returns {number} AddMeshError code. + */ + addMesh(opts) { + const m = this._m; + const { + positions, + normals = null, + uvs = null, + indices = null, + faceMaterialData = null, + meshCountHint = 0 + } = opts; + const vertexCount = positions.length / 3 | 0; + const declSize = m._xatlasMeshDecl_size(); + const declPtr = m._malloc(declSize); + m.HEAPU8.fill(0, declPtr, declPtr + declSize); + m._xatlasMeshDeclInit(declPtr); + const allocs = [declPtr]; + const posPtr = copyToHeap(m, positions); + allocs.push(posPtr); + let normPtr = 0; + if (normals) { + normPtr = copyToHeap(m, normals); + allocs.push(normPtr); + } + let uvPtr = 0; + if (uvs) { + uvPtr = copyToHeap(m, uvs); + allocs.push(uvPtr); + } + let idxPtr = 0; + let indexCount = 0; + let indexFormat = IndexFormat.UInt16; + if (indices) { + idxPtr = copyToHeap(m, indices); + allocs.push(idxPtr); + indexCount = indices.length; + indexFormat = indices instanceof Uint32Array ? IndexFormat.UInt32 : IndexFormat.UInt16; + } + let faceMatPtr = 0; + if (faceMaterialData) { + faceMatPtr = copyToHeap(m, faceMaterialData); + allocs.push(faceMatPtr); + } + m.setValue(declPtr + 0, posPtr, "i32"); + m.setValue(declPtr + 4, normPtr, "i32"); + m.setValue(declPtr + 8, uvPtr, "i32"); + m.setValue(declPtr + 12, idxPtr, "i32"); + m.setValue(declPtr + 16, 0, "i32"); + m.setValue(declPtr + 20, faceMatPtr, "i32"); + m.setValue(declPtr + 24, 0, "i32"); + m.setValue(declPtr + 28, vertexCount, "i32"); + m.setValue(declPtr + 32, 3 * 4, "i32"); + m.setValue(declPtr + 36, normals ? 3 * 4 : 0, "i32"); + m.setValue(declPtr + 40, uvs ? 2 * 4 : 0, "i32"); + m.setValue(declPtr + 44, indexCount, "i32"); + m.setValue(declPtr + 48, 0, "i32"); + m.setValue(declPtr + 52, 0, "i32"); + m.setValue(declPtr + 56, indexFormat, "i32"); + const error = m._xatlasAddMesh(this._ptr, declPtr, meshCountHint); + for (const p of allocs) m._free(p); + return error; + } + /** + * Add a UV mesh for repacking. + * @param {object} opts + * @param {Float32Array} opts.uvs - Vertex UVs (flat xy). + * @param {Uint16Array|Uint32Array} [opts.indices] + * @param {Uint32Array} [opts.faceMaterialData] + * @returns {number} AddMeshError code. + */ + addUvMesh(opts) { + const m = this._m; + const { uvs, indices = null, faceMaterialData = null } = opts; + const vertexCount = uvs.length / 2 | 0; + const declSize = m._xatlasUvMeshDecl_size(); + const declPtr = m._malloc(declSize); + m.HEAPU8.fill(0, declPtr, declPtr + declSize); + m._xatlasUvMeshDeclInit(declPtr); + const allocs = [declPtr]; + const uvPtr = copyToHeap(m, uvs); + allocs.push(uvPtr); + let idxPtr = 0, indexCount = 0, indexFormat = IndexFormat.UInt16; + if (indices) { + idxPtr = copyToHeap(m, indices); + allocs.push(idxPtr); + indexCount = indices.length; + indexFormat = indices instanceof Uint32Array ? IndexFormat.UInt32 : IndexFormat.UInt16; + } + let faceMatPtr = 0; + if (faceMaterialData) { + faceMatPtr = copyToHeap(m, faceMaterialData); + allocs.push(faceMatPtr); + } + m.setValue(declPtr + 0, uvPtr, "i32"); + m.setValue(declPtr + 4, idxPtr, "i32"); + m.setValue(declPtr + 8, faceMatPtr, "i32"); + m.setValue(declPtr + 12, vertexCount, "i32"); + m.setValue(declPtr + 16, 2 * 4, "i32"); + m.setValue(declPtr + 20, indexCount, "i32"); + m.setValue(declPtr + 24, 0, "i32"); + m.setValue(declPtr + 28, indexFormat, "i32"); + const error = m._xatlasAddUvMesh(this._ptr, declPtr); + for (const p of allocs) m._free(p); + return error; + } + addMeshJoin() { + this._m._xatlasAddMeshJoin(this._ptr); + } + /** + * Compute charts for all added meshes. + * @param {object} [opts] + */ + computeCharts(opts) { + const m = this._m; + if (!opts) { + m._xatlasComputeCharts(this._ptr, 0); + return; + } + const size = m._xatlasChartOptions_size(); + const ptr = m._malloc(size); + m._xatlasChartOptionsInit(ptr); + this._writeChartOptions(ptr, opts); + m._xatlasComputeCharts(this._ptr, ptr); + m._free(ptr); + } + /** + * Pack charts into atlas(es). + * @param {object} [opts] + */ + packCharts(opts) { + const m = this._m; + if (!opts) { + m._xatlasPackCharts(this._ptr, 0); + return; + } + const size = m._xatlasPackOptions_size(); + const ptr = m._malloc(size); + m._xatlasPackOptionsInit(ptr); + this._writePackOptions(ptr, opts); + m._xatlasPackCharts(this._ptr, ptr); + m._free(ptr); + } + /** + * Generate atlas (computeCharts + packCharts in one call). + * @param {object} [chartOptions] + * @param {object} [packOptions] + */ + generate(chartOptions, packOptions) { + const m = this._m; + let chartPtr = 0, packPtr = 0; + if (chartOptions) { + const size = m._xatlasChartOptions_size(); + chartPtr = m._malloc(size); + m._xatlasChartOptionsInit(chartPtr); + this._writeChartOptions(chartPtr, chartOptions); + } + if (packOptions) { + const size = m._xatlasPackOptions_size(); + packPtr = m._malloc(size); + m._xatlasPackOptionsInit(packPtr); + this._writePackOptions(packPtr, packOptions); + } + m._xatlasGenerate(this._ptr, chartPtr, packPtr); + if (chartPtr) m._free(chartPtr); + if (packPtr) m._free(packPtr); + } + /** + * Set progress callback. Pass null to clear. + * @param {function|null} fn - (category: number, progress: number) => boolean + */ + setProgressCallback(fn) { + const m = this._m; + if (this._progressFnPtr) { + m.removeFunction(this._progressFnPtr); + this._progressFnPtr = 0; + } + if (fn) { + this._progressFnPtr = m.addFunction((category, progress, _userData) => { + return fn(category, progress) ? 1 : 0; + }, "iiii"); + m._xatlasSetProgressCallback(this._ptr, this._progressFnPtr, 0); + } else { + m._xatlasSetProgressCallback(this._ptr, 0, 0); + } + } + /* ── Result getters ──────────────────────────────────────────── */ + get width() { + return this._m._xatlasAtlas_getWidth(this._ptr); + } + get height() { + return this._m._xatlasAtlas_getHeight(this._ptr); + } + get atlasCount() { + return this._m._xatlasAtlas_getAtlasCount(this._ptr); + } + get chartCount() { + return this._m._xatlasAtlas_getChartCount(this._ptr); + } + get meshCount() { + return this._m._xatlasAtlas_getMeshCount(this._ptr); + } + get texelsPerUnit() { + return this._m._xatlasAtlas_getTexelsPerUnit(this._ptr); + } + getUtilization(atlasIndex) { + return this._m._xatlasAtlas_getUtilization(this._ptr, atlasIndex); + } + /** + * Retrieve output mesh data for a given input mesh index. + * @param {number} meshIndex + * @returns {{ vertexCount: number, indexCount: number, chartCount: number, vertices: Array, indices: Uint32Array, charts: Array }} + */ + getMesh(meshIndex) { + const m = this._m; + const meshPtr = m._xatlasAtlas_getMesh(this._ptr, meshIndex); + const vertexCount = m._xatlasMesh_getVertexCount(meshPtr); + const indexCount = m._xatlasMesh_getIndexCount(meshPtr); + const chartCount = m._xatlasMesh_getChartCount(meshPtr); + const vertexArrayPtr = m._xatlasMesh_getVertexArray(meshPtr); + const vertices = new Array(vertexCount); + for (let i = 0; i < vertexCount; i++) { + vertices[i] = { + atlasIndex: m._xatlasVertex_getAtlasIndex(vertexArrayPtr, i), + chartIndex: m._xatlasVertex_getChartIndex(vertexArrayPtr, i), + uv: [ + m._xatlasVertex_getUV0(vertexArrayPtr, i), + m._xatlasVertex_getUV1(vertexArrayPtr, i) + ], + xref: m._xatlasVertex_getXref(vertexArrayPtr, i) + }; + } + const indexArrayPtr = m._xatlasMesh_getIndexArray(meshPtr); + const indices = new Uint32Array( + m.HEAPU32.buffer.slice(indexArrayPtr, indexArrayPtr + indexCount * 4) + ); + const chartArrayPtr = m._xatlasMesh_getChartArray(meshPtr); + const charts = new Array(chartCount); + for (let i = 0; i < chartCount; i++) { + const faceCount = m._xatlasChart_getFaceCount(chartArrayPtr, i); + const faceArrayPtr = m._xatlasChart_getFaceArray(chartArrayPtr, i); + charts[i] = { + atlasIndex: m._xatlasChart_getAtlasIndex(chartArrayPtr, i), + faceCount, + type: m._xatlasChart_getType(chartArrayPtr, i), + material: m._xatlasChart_getMaterial(chartArrayPtr, i), + faces: new Uint32Array( + m.HEAPU32.buffer.slice(faceArrayPtr, faceArrayPtr + faceCount * 4) + ) + }; + } + return { vertexCount, indexCount, chartCount, vertices, indices, charts }; + } + /* ── Private: write option structs ─────────────────────────── */ + _writeChartOptions(ptr, opts) { + const m = this._m; + const fields = [ + ["maxChartArea", 4, "float"], + ["maxBoundaryLength", 8, "float"], + ["normalDeviationWeight", 12, "float"], + ["roundnessWeight", 16, "float"], + ["straightnessWeight", 20, "float"], + ["normalSeamWeight", 24, "float"], + ["textureSeamWeight", 28, "float"], + ["maxCost", 32, "float"], + ["maxIterations", 36, "i32"] + ]; + for (const [name, offset, type] of fields) { + if (opts[name] !== void 0) m.setValue(ptr + offset, opts[name], type); + } + if (opts.useInputMeshUvs !== void 0) m.setValue(ptr + 40, opts.useInputMeshUvs ? 1 : 0, "i8"); + if (opts.fixWinding !== void 0) m.setValue(ptr + 41, opts.fixWinding ? 1 : 0, "i8"); + } + _writePackOptions(ptr, opts) { + const m = this._m; + const intFields = [ + ["maxChartSize", 0, "i32"], + ["padding", 4, "i32"], + ["texelsPerUnit", 8, "float"], + ["resolution", 12, "i32"] + ]; + for (const [name, offset, type] of intFields) { + if (opts[name] !== void 0) m.setValue(ptr + offset, opts[name], type); + } + const boolFields = [ + ["bilinear", 16], + ["blockAlign", 17], + ["bruteForce", 18], + ["createImage", 19], + ["rotateChartsToAxis", 20], + ["rotateCharts", 21] + ]; + for (const [name, offset] of boolFields) { + if (opts[name] !== void 0) m.setValue(ptr + offset, opts[name] ? 1 : 0, "i8"); + } + } +}; +function addMeshErrorString(module, error) { + const ptr = module._xatlasAddMeshErrorString(error); + return module.UTF8ToString(ptr); +} +function progressCategoryString(module, category) { + const ptr = module._xatlasProgressCategoryString(category); + return module.UTF8ToString(ptr); +} +async function createXAtlas() { + const module = await xatlas_default(); + return { + createAtlas: () => new XAtlas(module), + addMeshErrorString: (error) => addMeshErrorString(module, error), + progressCategoryString: (category) => progressCategoryString(module, category), + ChartType, + IndexFormat, + AddMeshError, + ProgressCategory + }; +} +// ---- END VENDORED xatlas-wasm glue ---- + +const WASM_PATH = 'assets/xatlas.wasm'; +/** xatlas's brute-force packer gives the tightest atlas but its cost grows with chart + * count; above this many triangles the (still non-overlapping) random placement is used */ +const BRUTE_FORCE_MAX = 4000; + +/** + * Instantiate the xatlas runtime from the packaged wasm. + * @param {(path: string) => string|null} assetUrl + * @returns {Promise} the emscripten Module + */ +async function instantiateXatlas(assetUrl) { + const url = assetUrl(WASM_PATH); + if (!url) throw new Error(WASM_PATH + ' is missing from the module package'); + const response = await fetch(url); + if (!response.ok) throw new Error('could not read ' + WASM_PATH + ' (' + response.status + ')'); + // arrayBuffer + instantiate rather than instantiateStreaming: a module asset is a blob + // with no MIME type, which streaming compilation rejects (the uv-unwrap-module lesson) + const bytes = await response.arrayBuffer(); + /** @type {(error: any) => void} */ + let fail = () => {}; + const failed = new Promise((_, reject) => (fail = reject)); + const ready = createXAtlasModule({ + /** @param {any} imports @param {(instance: any, module: any) => void} receive */ + instantiateWasm(imports, receive) { + WebAssembly.instantiate(bytes, imports).then( + (result) => receive(result.instance, result.module), + fail + ); + return {}; // emscripten: an empty object means "instantiating asynchronously" + }, + print: () => {}, + /** @param {string} text */ + printErr: (text) => console.warn('smart-unwrap (xatlas): ' + text) + }); + // the glue's instantiateWasm path has no reject of its own: race our failure against it + return Promise.race([ready, failed]); +} + +/** @param {number} value @param {number} min @param {number} max */ +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} + +/** twice the signed area of a uv triangle: > 0 = counter-clockwise @param {number[][]} t */ +function signedArea2(t) { + return (t[1][0] - t[0][0]) * (t[2][1] - t[0][1]) - (t[2][0] - t[0][0]) * (t[1][1] - t[0][1]); +} + +/** + * Weld by EXACT position, so xatlas sees the connectivity. The editor hands over unindexed + * triangles (every corner its own vertex); without a weld every triangle would be its own + * chart. Exact equality is the right test: a mesh's shared corners are bit-identical + * copies of one vertex. + * @param {any[]} faces + */ +function weldFaces(faces) { + /** @type {Map} */ + const seen = new Map(); + /** @type {number[]} */ + const positions = []; + const indices = new Uint32Array(faces.length * 3); + for (let k = 0; k < faces.length; k++) + for (let c = 0; c < 3; c++) { + const p = faces[k].corners[c]; + const x = p.x ?? p[0]; + const y = p.y ?? p[1]; + const z = p.z ?? p[2]; + const key = x + ',' + y + ',' + z; + let index = seen.get(key); + if (index === undefined) { + index = positions.length / 3; + seen.set(key, index); + positions.push(x, y, z); + } + indices[k * 3 + c] = index; + } + return { positions: new Float32Array(positions), indices }; +} + +/** + * Per input face, the three output vertex indices of `mesh`, matched by xref (the input + * vertex each output vertex came from) rather than by slot — robust if xatlas ever rotates + * a face's corners. + * @param {any} mesh @param {Uint32Array} inputIndices @param {number} k + */ +function outputCorners(mesh, inputIndices, k) { + const out = [mesh.indices[k * 3], mesh.indices[k * 3 + 1], mesh.indices[k * 3 + 2]]; + return out.map((vi, c) => { + const want = inputIndices[k * 3 + c]; + if (mesh.vertices[vi].xref === want) return vi; + return out.find((o) => mesh.vertices[o].xref === want) ?? vi; + }); +} + +/** + * The unwrap itself — `faces` is core's UnwrapFace list ({corners: [Vector3 x3], tri}), + * the result is core's UnwrapResult: `uvs[k]` = the three [u, v] corners of faces[k], in + * 0..1, and `islands` = groups of indices into `faces` (one per xatlas chart). + * + * WINDING: every chart must come out unmirrored, or painted text and decals land on the + * model backwards. xatlas's `fixWinding` flattens every chart counter-clockwise, but its + * packer's `rotateCharts` turns a chart by TRANSPOSING it (swapping u and v), which is a + * mirror: a 216-triangle sphere came back with 2 of 6 charts clockwise. So rotateCharts is + * off by default (rotateChartsToAxis, a true rotation, stays on) — a slightly looser + * pack for charts that all read the right way round. The unit test holds every triangle + * to counter-clockwise. + * + * Options (all optional; the UV editor passes `{margin: 0.02}`): + * margin gap between charts as a fraction of the texture (default 0.01, as the + * built-in packer) — converted to xatlas texel padding at `resolution` + * resolution the texture size the padding is computed for (default 512) + * padding xatlas padding in texels, overriding `margin` + * chartOptions / packOptions raw xatlas ChartOptions / PackOptions, merged last + * (e.g. `packOptions: {rotateCharts: true}` for the tighter, mirroring pack) + * + * @param {any} xa the instantiated emscripten Module + * @param {any[]} faces @param {any} [options] + */ +function unwrapFaces(xa, faces, options = {}) { + const faceCount = faces.length; + const welded = weldFaces(faces); + + const resolution = Math.round(clamp(Number(options.resolution) || 512, 64, 8192)); + const margin = clamp(Number(options.margin ?? 0.01) || 0, 0, 0.25); + // xatlas pads EACH chart, so the gap between two neighbours is about twice this + const padding = + options.padding != null + ? Math.round(clamp(Number(options.padding) || 0, 0, 64)) + : Math.max(1, Math.round((margin * resolution) / 2)); + + const atlas = new XAtlas(xa); + try { + const error = atlas.addMesh(welded); + if (error !== AddMeshError.Success) throw new Error(addMeshErrorString(xa, error)); + atlas.computeCharts({ fixWinding: true, ...(options.chartOptions ?? {}) }); + const packOptions = { + resolution, + padding, + bilinear: true, + bruteForce: faceCount <= BRUTE_FORCE_MAX, + rotateCharts: false, + ...(options.packOptions ?? {}) + }; + atlas.packCharts(packOptions); + // a fixed resolution can spill into several atlases, which would OVERLAP once + // normalised: re-pack into one, letting xatlas pick the scale + if (atlas.atlasCount > 1) atlas.packCharts({ ...packOptions, resolution: 0, texelsPerUnit: 0 }); + const mesh = atlas.getMesh(0); + if (mesh.indexCount !== faceCount * 3) + throw new Error('xatlas returned ' + mesh.indexCount / 3 + ' faces for ' + faceCount); + // one scale for both axes: a non-square atlas must not stretch the charts + const scale = Math.max(atlas.width, atlas.height) || 1; + + /** @type {number[][][]} */ + const uvs = new Array(faceCount); + let signed = 0; + for (let k = 0; k < faceCount; k++) { + uvs[k] = outputCorners(mesh, welded.indices, k).map((vi) => { + const vertex = mesh.vertices[vi]; + // a face xatlas ignored (zero area, NaN) is in no chart: park it at the origin + return vertex.chartIndex < 0 ? [0, 0] : [vertex.uv[0] / scale, vertex.uv[1] / scale]; + }); + signed += signedArea2(uvs[k]); + } + // guard: should the charts ever all come out clockwise, ONE flip of v for the whole + // atlas makes them counter-clockwise (a mirror of everything stays overlap-free) + if (signed < 0) for (const corners of uvs) for (const uv of corners) uv[1] = 1 - uv[1]; + for (const corners of uvs) + for (const uv of corners) { + uv[0] = clamp(uv[0], 0, 1); + uv[1] = clamp(uv[1], 0, 1); + } + + /** @type {number[][]} */ + const islands = mesh.charts.map((/** @type {any} */ chart) => Array.from(chart.faces)); + // the ignored faces ride one extra island, so every face is accounted for (as the + // built-ins do) + const charted = new Uint8Array(faceCount); + for (const island of islands) for (const fi of island) charted[fi] = 1; + const loose = []; + for (let k = 0; k < faceCount; k++) if (!charted[k]) loose.push(k); + if (loose.length) islands.push(loose); + return { uvs, islands }; + } finally { + atlas.destroy(); + } +} + +export default { + id: 'smart-unwrap', + name: 'Smart unwrap (xatlas)', + version: '1.0.0', + description: + 'Adds "Smart (xatlas)" to the UV editor\'s Unwrap menu: automatic seams, flattened charts and an overlap-free packed atlas, from xatlas compiled to WebAssembly.', + + /** @param {any} api */ + register(api) { + /** @type {Promise|null} the runtime, instantiated on the first unwrap */ + let runtime = null; + + const ensureRuntime = () => { + if (!runtime) + runtime = instantiateXatlas((path) => api.assetUrl(path)).catch((error) => { + runtime = null; // let the next unwrap try again + throw error; + }); + return runtime; + }; + + // the SDK journals this registration and removes it from the UV editor's menu when + // the module unloads (sdk/backends.js onDispose) — nothing to undo here + const registered = api.registerUnwrapBackend( + 'xatlas', + 'Smart (xatlas)', + /** @param {any[]} faces @param {any} [options] */ + async (faces, options = {}) => { + try { + const xa = await ensureRuntime(); + return unwrapFaces(xa, faces, options); + } catch (error) { + // a wasm abort leaves that runtime unusable: start fresh next time + runtime = null; + const message = error instanceof Error ? error.message : String(error); + console.warn('smart-unwrap failed', error); + api.toast?.('Smart unwrap failed: ' + message); + return null; // core reports "That unwrap produced nothing" and commits nothing + } + } + ); + + // drop the cached runtime (and with it the wasm memory) on unload / live update + api.onUnload?.(() => { + runtime = null; + }); + + return registered; + } +}; diff --git a/modules/smart-unwrap/test/run.mjs b/modules/smart-unwrap/test/run.mjs new file mode 100644 index 0000000..92a81f6 --- /dev/null +++ b/modules/smart-unwrap/test/run.mjs @@ -0,0 +1,284 @@ +// Pure-node tests for smart-unwrap — run WITHOUT the app: +// node modules/smart-unwrap/test/run.mjs (npm run test:smart-unwrap) +// +// Loads the REAL module.js and the REAL packaged assets/xatlas.wasm through a fake api +// whose assetUrl hands out blob URLs — exactly what the app's userModules does with a +// zip's files — then drives the registered backend the way uvEditor.unwrapObject does: +// unindexed triangles in object space, `{corners: [Vector3 x3], tri}` per face. +// +// It asserts PROPERTIES, not floats (xatlas's packer is allowed to change its mind): +// every uv in 0..1, three corners per face, no degenerate uv triangle, islands partition +// the faces, and no two islands overlap in uv space (rasterised on a grid). The overlap, +// area and range checks are each shown to FAIL on a deliberately broken result, so a +// green run means they can see what they claim to. + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import * as THREE from 'three'; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const MODULE_DIR = path.join(HERE, '..'); + +let failures = 0; +let passes = 0; +/** @param {boolean} ok @param {string} label */ +function check(ok, label) { + console.log((ok ? 'PASS ' : 'FAIL ') + label); + if (ok) passes++; + else failures++; +} + +// ---- the fake api ---------------------------------------------------------- +const manifest = JSON.parse(fs.readFileSync(path.join(MODULE_DIR, 'manifest.json'), 'utf8')); +/** @type {Record} */ +const assets = {}; +for (const file of manifest.files) { + if (file === manifest.entry) continue; + assets[file] = URL.createObjectURL(new Blob([fs.readFileSync(path.join(MODULE_DIR, file))])); +} +let assetReads = 0; +/** @type {{key: string, label: string, run: Function}[]} */ +const backends = []; +/** @type {Function[]} */ +const unloads = []; +/** @type {string[]} */ +const toasts = []; +const api = { + THREE, + /** @param {string} p */ + assetUrl: (p) => { + if (p.endsWith('.wasm')) assetReads++; + return assets[p] ?? null; + }, + /** @param {string} key @param {string} label @param {Function} run */ + registerUnwrapBackend: (key, label, run) => { + backends.push({ key, label, run }); + return Promise.resolve(); + }, + /** @param {Function} fn */ + onUnload: (fn) => unloads.push(fn), + /** @param {string} text */ + toast: (text) => toasts.push(text) +}; + +const mod = (await import(pathToFileURL(path.join(MODULE_DIR, 'module.js')).href)).default; +check(mod.id === manifest.id && mod.version === manifest.version, `module.js id/version match the manifest (${mod.id} ${mod.version})`); +await mod.register(api); +check(backends.length === 1 && backends[0].key === 'xatlas', 'register() adds ONE unwrap backend under key "xatlas"'); +check(backends[0].label === 'Smart (xatlas)', 'labelled "Smart (xatlas)" for the Unwrap menu'); +check(unloads.length === 1, 'it registers an onUnload hook (drops the cached runtime)'); +const run = backends[0].run; + +// ---- meshes -> the faces uvEditor.unwrapObject passes ------------------------ +/** @param {any} geometry */ +function facesOf(geometry) { + const g = geometry.index ? geometry.toNonIndexed() : geometry; + const pos = g.attributes.position; + const faces = []; + for (let i = 0; i < pos.count; i += 3) + faces.push({ + corners: [0, 1, 2].map((c) => new THREE.Vector3().fromBufferAttribute(pos, i + c)), + tri: i / 3 + }); + return faces; +} + +// ---- the property checks (each returns {ok, detail}) ------------------------ +/** @param {number[][]} t */ +const areaOf = (t) => ((t[1][0] - t[0][0]) * (t[2][1] - t[0][1]) - (t[2][0] - t[0][0]) * (t[1][1] - t[0][1])) / 2; + +/** @param {any} result @param {number} n */ +function checkShape(result, n) { + const corners = result?.uvs?.reduce((sum, /** @type {any} */ f) => sum + f.length, 0) ?? 0; + return { ok: result?.uvs?.length === n && corners === n * 3, detail: `${corners} corners for ${n} tris` }; +} +/** @param {any} result */ +function checkRange(result) { + let bad = 0; + for (const face of result.uvs) + for (const [u, v] of face) if (!(u >= 0 && u <= 1 && v >= 0 && v <= 1)) bad++; + return { ok: bad === 0, detail: `${bad} uvs outside 0..1` }; +} +/** @param {any} result */ +function checkArea(result) { + let degenerate = 0; + let total = 0; + for (const face of result.uvs) { + const a = Math.abs(areaOf(face)); + total += a; + if (!(a > 1e-12)) degenerate++; + } + return { ok: degenerate === 0 && total <= 1 + 1e-9, detail: `${degenerate} zero-area uv tris, total uv area ${total.toFixed(3)}` }; +} +/** every face in exactly one island @param {any} result @param {number} n */ +function checkPartition(result, n) { + const seen = new Uint8Array(n); + let dup = 0; + let outOfRange = 0; + for (const island of result.islands) + for (const fi of island) { + if (!(fi >= 0 && fi < n)) outOfRange++; + else if (seen[fi]++) dup++; + } + const missing = seen.reduce((m, s) => m + (s ? 0 : 1), 0); + return { ok: !dup && !outOfRange && !missing, detail: `${result.islands.length} islands, ${missing} faces missing, ${dup} doubled` }; +} +/** + * Rasterise every uv triangle onto a GRID x GRID raster (sample = cell centre, strictly + * inside the triangle) and record which island owns each cell. A cell claimed by two + * different islands is an overlap — texture painted there would land on both. + * @param {any} result @param {number} [grid] + */ +function checkNoOverlap(result, grid = 512) { + const owner = new Int32Array(grid * grid).fill(-1); + let overlaps = 0; + const islandOf = new Int32Array(result.uvs.length).fill(-1); + result.islands.forEach((/** @type {number[]} */ island, /** @type {number} */ i) => { + for (const fi of island) islandOf[fi] = i; + }); + result.uvs.forEach((/** @type {number[][]} */ t, /** @type {number} */ fi) => { + const area2 = areaOf(t) * 2; + if (Math.abs(area2) < 1e-15) return; + const uMin = Math.max(0, Math.floor(Math.min(t[0][0], t[1][0], t[2][0]) * grid)); + const uMax = Math.min(grid - 1, Math.ceil(Math.max(t[0][0], t[1][0], t[2][0]) * grid)); + const vMin = Math.max(0, Math.floor(Math.min(t[0][1], t[1][1], t[2][1]) * grid)); + const vMax = Math.min(grid - 1, Math.ceil(Math.max(t[0][1], t[1][1], t[2][1]) * grid)); + for (let y = vMin; y <= vMax; y++) + for (let x = uMin; x <= uMax; x++) { + const pu = (x + 0.5) / grid; + const pv = (y + 0.5) / grid; + // barycentric, same sign as the triangle's winding = strictly inside + const w0 = ((t[1][0] - pu) * (t[2][1] - pv) - (t[2][0] - pu) * (t[1][1] - pv)) / area2; + const w1 = ((t[2][0] - pu) * (t[0][1] - pv) - (t[0][0] - pu) * (t[2][1] - pv)) / area2; + const w2 = 1 - w0 - w1; + if (w0 <= 1e-9 || w1 <= 1e-9 || w2 <= 1e-9) continue; + const cell = y * grid + x; + const island = islandOf[fi]; + if (owner[cell] === -1) owner[cell] = island; + else if (owner[cell] !== island) overlaps++; + } + }); + return { ok: overlaps === 0, detail: `${overlaps} cells claimed by two islands (${grid}x${grid})` }; +} +/** positive uv winding = charts read unmirrored with three's v-up uvs @param {any} result */ +function windingCounts(result) { + let ccw = 0; + let cw = 0; + for (const face of result.uvs) (areaOf(face) > 0 ? ccw++ : cw++); + return { ccw, cw }; +} + +/** @param {string} name @param {any} geometry @param {any} [options] */ +async function suite(name, geometry, options = { margin: 0.02 }) { + console.log(`\n=== ${name} ===`); + const faces = facesOf(geometry); + const started = performance.now(); + const result = await run(faces, options); + const ms = performance.now() - started; + check(!!result && Array.isArray(result.uvs) && Array.isArray(result.islands), `${name}: returns {uvs, islands} (${faces.length} tris, ${ms.toFixed(0)} ms)`); + if (!result) return null; + for (const [label, outcome] of [ + ['corner count = 3 x tris', checkShape(result, faces.length)], + ['every uv in [0,1]', checkRange(result)], + ['every uv triangle has area > 0, total <= 1', checkArea(result)], + ['islands partition the faces', checkPartition(result, faces.length)], + ['no two islands overlap in uv space', checkNoOverlap(result)] + ]) + check(/** @type {any} */ (outcome).ok, `${name}: ${label} — ${/** @type {any} */ (outcome).detail}`); + const w = windingCounts(result); + check(w.cw === 0, `${name}: every uv triangle winds counter-clockwise (unmirrored) — ${w.ccw} ccw / ${w.cw} cw`); + return { faces, result }; +} + +// ---- (a) unit cube, (b) uv sphere, (c) cylinder ------------------------------ +const cube = await suite('unit cube', new THREE.BoxGeometry(1, 1, 1)); +if (cube) { + check(cube.faces.length === 12, 'the cube is 12 triangles (premise)'); + check(cube.result.islands.length <= 6, `cube: at most 6 islands (${cube.result.islands.length})`); + check(assetReads === 1, `the wasm was read once, on the first unwrap (${assetReads})`); +} +const sphere = await suite('uv sphere', new THREE.SphereGeometry(1, 12, 10)); +if (sphere) check(sphere.faces.length >= 180 && sphere.faces.length <= 240, `the sphere is ~200 triangles (${sphere.faces.length})`); +const cylinder = await suite('cylinder', new THREE.CylinderGeometry(0.5, 0.5, 2, 24, 4)); +check(assetReads === 1, `...and cached: three meshes, still one wasm read (${assetReads})`); + +// a SCOPED unwrap (Edit Mesh face pick) hands over a subset — the top half of the sphere +await suite('sphere, upper half only (scoped unwrap)', (() => { + const g = new THREE.SphereGeometry(1, 12, 10).toNonIndexed(); + const pos = g.attributes.position; + const keep = []; + for (let i = 0; i < pos.count; i += 3) if (pos.getY(i) + pos.getY(i + 1) + pos.getY(i + 2) > 0) for (let c = 0; c < 3; c++) keep.push(pos.getX(i + c), pos.getY(i + c), pos.getZ(i + c)); + const half = new THREE.BufferGeometry(); + half.setAttribute('position', new THREE.Float32BufferAttribute(keep, 3)); + return half; +})()); + +// a heavier mesh: the main-thread cost the README quotes +await suite('dense sphere', new THREE.SphereGeometry(1, 96, 64), {}); + +// ---- the weld matters: without it every triangle is its own chart ------------- +if (cube) { + const loose = cube.faces.map((/** @type {any} */ f) => ({ ...f, corners: f.corners.map((/** @type {any} */ p) => p.clone().addScalar((f.tri + 1) * 1e-3)) })); + const r = await run(loose, { margin: 0.02 }); + check(!!r && r.islands.length > 6, `counterfactual: unwelded triangles fall apart into ${r?.islands.length} islands, so the weld is what gives the cube <= 6`); +} + +// ---- COUNTERFACTUALS: the checks catch broken results -------------------------- +console.log('\n=== counterfactuals (each check must FAIL on a broken result) ==='); +if (sphere) { + const clone = (/** @type {any} */ r) => ({ uvs: r.uvs.map((/** @type {number[][]} */ f) => f.map((c) => [...c])), islands: r.islands.map((/** @type {number[]} */ i) => [...i]) }); + // stacked: every island moved to the same corner — the classic "unwrap" that overlaps + const stacked = clone(sphere.result); + for (const island of stacked.islands) { + let uMin = Infinity; + let vMin = Infinity; + for (const fi of island) for (const [u, v] of stacked.uvs[fi]) { uMin = Math.min(uMin, u); vMin = Math.min(vMin, v); } + for (const fi of island) stacked.uvs[fi] = stacked.uvs[fi].map(([u, v]) => [u - uMin, v - vMin]); + } + const overlapStacked = checkNoOverlap(stacked); + check(!overlapStacked.ok, `overlap check FAILS on islands stacked at the origin — ${overlapStacked.detail}`); + // duplicated island: island 1 is a copy of island 0's placement + const dup = clone(sphere.result); + const [first, second] = [dup.islands[0], dup.islands[1]]; + for (let k = 0; k < second.length; k++) dup.uvs[second[k]] = dup.uvs[first[k % first.length]].map((c) => [...c]); + const overlapDup = checkNoOverlap(dup); + check(!overlapDup.ok, `overlap check FAILS when one island duplicates another's uvs — ${overlapDup.detail}`); + // collapsed: every corner at one point + const collapsed = { uvs: sphere.result.uvs.map(() => [[0.5, 0.5], [0.5, 0.5], [0.5, 0.5]]), islands: sphere.result.islands }; + const areaCollapsed = checkArea(collapsed); + check(!areaCollapsed.ok, `area check FAILS on all uvs collapsed to one point — ${areaCollapsed.detail}`); + // out of range: shifted past the edge + const shifted = clone(sphere.result); + for (const f of shifted.uvs) for (const c of f) c[0] += 0.5; + const rangeShifted = checkRange(shifted); + check(!rangeShifted.ok, `range check FAILS on uvs shifted by +0.5 — ${rangeShifted.detail}`); + // mirrored charts: xatlas's own rotateCharts transposes charts — the reason it is off + const mirrored = await run(sphere.faces, { margin: 0.02, packOptions: { rotateCharts: true } }); + const mw = windingCounts(mirrored); + check(mw.cw > 0 && checkNoOverlap(mirrored).ok, `winding check FAILS with xatlas's rotateCharts on (${mw.cw} clockwise = mirrored triangles, though still overlap-free)`); + // and the island partition notices a lost face + const lost = clone(sphere.result); + lost.islands[0] = lost.islands[0].slice(1); + check(!checkPartition(lost, sphere.faces.length).ok, 'partition check FAILS when an island drops a face'); +} + +// ---- lifecycle: unload drops the runtime, the next run re-instantiates ---------- +unloads.forEach((fn) => fn()); +const again = await run(facesOf(new THREE.BoxGeometry(2, 1, 0.5)), { margin: 0.02 }); +check(!!again && checkNoOverlap(again).ok, 'after onUnload the backend still runs (fresh runtime)'); +check(assetReads === 2, `...and it re-read the wasm instead of keeping the old instance (${assetReads})`); + +// ---- failure path: a missing wasm toasts and returns null (core commits nothing) ----- +const realUrl = assets['assets/xatlas.wasm']; +unloads.forEach((fn) => fn()); +delete assets['assets/xatlas.wasm']; +const failed = await run(facesOf(new THREE.BoxGeometry()), {}); +check(failed === null && toasts.some((t) => t.includes('Smart unwrap failed')), `a missing wasm returns null and toasts (${toasts.at(-1)})`); +assets['assets/xatlas.wasm'] = realUrl; +const recovered = await run(facesOf(new THREE.BoxGeometry()), {}); +check(!!recovered && checkPartition(recovered, 12).ok, '...and the next run recovers once the asset is back'); + +console.log(`\n${passes} passed, ${failures} failed`); +console.log(failures === 0 ? 'ALL PASS' : failures + ' FAILURES'); +process.exit(failures === 0 ? 0 : 1); diff --git a/modules/smart-unwrap/vendor.mjs b/modules/smart-unwrap/vendor.mjs new file mode 100644 index 0000000..98ce33b --- /dev/null +++ b/modules/smart-unwrap/vendor.mjs @@ -0,0 +1,97 @@ +// vendor.mjs — refresh the vendored xatlas-wasm build inside this module. +// +// mkdir /tmp/xw && cd /tmp/xw && npm pack xatlas-wasm@0.1.3 --ignore-scripts && tar xzf *.tgz +// node modules/smart-unwrap/vendor.mjs /tmp/xw/package/dist/index.mjs +// +// xatlas-wasm ships ONE file: an esbuild'd ES module whose emscripten glue carries the +// .wasm inline as a binary string (SINGLE_FILE). A module entry must be self-contained +// and small enough to read, so this script splits that file in two WITHOUT running it: +// +// assets/xatlas.wasm the binary, decoded from the string literal by hand +// (a JS string-literal parser + emscripten's binaryDecode maths) +// module.js the glue spliced between the VENDORED markers, with exactly two +// edits: the inline binary is dropped (findWasmBinary returns +// nothing — the wasm arrives through Module.instantiateWasm from +// api.assetUrl) and the trailing `export {...}` is removed (the +// module's own `export default` is the only export). +// +// Nothing from the package is executed here: it is read as text. + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const source = process.argv[2]; +if (!source) { + console.error('usage: node vendor.mjs /dist/index.mjs'); + process.exit(1); +} +const text = fs.readFileSync(source, 'utf8'); +const lines = text.split('\n'); + +// ---- 1. the binary -------------------------------------------------------- +const OPEN = "return binaryDecode('"; +const blobLine = lines.findIndex((line) => line.trimStart().startsWith(OPEN)); +if (blobLine < 0) throw new Error('no inline wasm (binaryDecode literal) found'); +const literal = lines[blobLine]; +let i = literal.indexOf(OPEN) + OPEN.length; +/** @type {number[]} */ +const codes = []; +const SIMPLE = { n: 10, r: 13, t: 9, b: 8, f: 12, v: 11, '\\': 92, "'": 39, '"': 34 }; +for (;;) { + const ch = literal[i]; + if (ch === undefined) throw new Error('unterminated wasm literal'); + if (ch === "'") break; + if (ch !== '\\') { + const cp = /** @type {number} */ (literal.codePointAt(i)); + codes.push(cp); + i += cp > 0xffff ? 2 : 1; + continue; + } + const e = literal[i + 1]; + i += 2; + if (e in SIMPLE) codes.push(SIMPLE[/** @type {keyof typeof SIMPLE} */ (e)]); + else if (e === '0' && !/[0-9]/.test(literal[i])) codes.push(0); + else if (e === 'x') { + codes.push(parseInt(literal.slice(i, i + 2), 16)); + i += 2; + } else if (e === 'u' && literal[i] === '{') { + const end = literal.indexOf('}', i); + codes.push(parseInt(literal.slice(i + 1, end), 16)); + i = end + 1; + } else if (e === 'u') { + codes.push(parseInt(literal.slice(i, i + 4), 16)); + i += 4; + } else throw new Error('unhandled escape \\' + e); +} +if (literal.slice(i) !== "');") throw new Error('unexpected text after the wasm literal'); +const wasm = new Uint8Array(codes.length); +// emscripten's binaryDecode: o[i] = ~c >> 8 & c +for (let k = 0; k < codes.length; k++) wasm[k] = (~codes[k] >> 8) & codes[k]; +if (!WebAssembly.validate(wasm)) throw new Error('decoded bytes are not a valid wasm module'); +fs.writeFileSync(path.join(HERE, 'assets', 'xatlas.wasm'), wasm); + +// ---- 2. the glue ---------------------------------------------------------- +const exportLine = lines.findIndex((line) => line.startsWith('export {')); +if (exportLine < 0) throw new Error('no trailing export block found'); +const glue = lines + .slice(0, exportLine) + .map((line, n) => + n === blobLine ? ' return void 0; // smart-unwrap: wasm supplied via Module.instantiateWasm' : line + ); +if (glue.some((line) => /^\s*(import|export)[\s{*'"]/.test(line))) + throw new Error('the glue has a static import/export the module loader cannot take'); + +const BEGIN = '// ---- BEGIN VENDORED xatlas-wasm glue'; +const END = '// ---- END VENDORED xatlas-wasm glue'; +const modulePath = path.join(HERE, 'module.js'); +const current = fs.readFileSync(modulePath, 'utf8'); +const a = current.indexOf(BEGIN); +const b = current.indexOf(END); +if (a < 0 || b < a) throw new Error('module.js has no VENDORED markers'); +const beginLineEnd = current.indexOf('\n', a) + 1; +fs.writeFileSync(modulePath, current.slice(0, beginLineEnd) + glue.join('\n') + '\n' + current.slice(b)); +console.log( + 'vendored: assets/xatlas.wasm ' + wasm.length + ' bytes, glue ' + glue.length + ' lines into module.js' +); diff --git a/package.json b/package.json index 6392f44..83c372e 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "build:health": "esbuild modules/health/src/index.js --bundle --format=esm --outfile=modules/health/module.js", "test:health": "node modules/health/test/run.mjs", "build:waves": "node modules/waves/build-gltf.mjs && esbuild modules/waves/src/index.js --bundle --format=esm --outfile=modules/waves/module.js && node modules/waves/emit-def.mjs", - "test:waves": "node modules/waves/test/run.mjs" + "test:waves": "node modules/waves/test/run.mjs", + "test:smart-unwrap": "node modules/smart-unwrap/test/run.mjs" }, "devDependencies": { "esbuild": "0.28.2",