diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..796f8a1 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,17 @@ +ISC License + +Copyright 2024 Evert "Diamond" Prants + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice appear +in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md index b9fcb27..1d65de4 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,21 @@ # Mson -> THREE -WIP do not use - -Parse [Mson](https://github.com/MineLittlePony/Mson). +Convert [Mson](https://github.com/MineLittlePony/Mson) models to THREE.js object JSON or GLTF. ## Setup -1. copy the folder at https://github.com/MineLittlePony/Mson/tree/1.20.2/src/main/resources/assets/mson/models/entity as `inputs/mson` -2. build `npm run build` -3. export `node lib/test-node.js` +1. Clone and `npm install`. Optional dependencies are required when using this on Node.js (see implementation notes below). +2. copy the folder at https://github.com/MineLittlePony/Mson/tree/1.20.2/src/main/resources/assets/mson/models/entity as `inputs/mson` +3. build `npm run build` +4. run `node lib/cli.js export ` -THREE object will be placed in `outputs` +Output will be placed in `outputs`. For more options run `node lib/cli.js export -h`. + +## Implementation notes + +1. Mson models seem to have the X and Y coordinates inverse of what THREE.js uses (and Minecraft for that matter, lol). +2. Many Mson example models do not work. They seem to be all either outdated or just outright broken. Mine Little Pony works fine, though. +3. Most of the code here makes a lot of assumptions. This project is a reverse engineering of the format, not a Java to JavaScript port of Mson's rendering engine. It should not be used in any serious capacity, as it was only made as an experiment. +4. THREE.js GLTF exporter does not support Node.js by default, so some extra junk was included to support it. You can omit pretty much the entire `optionalDependencies` array of items (and `src/cli.ts` + `src/node`) to use this library on the browser. +5. The parser and object generator by themselves have no dependencies except for THREE.js, of course. +6. Output could be further optimized by merging composite objects into a single geometry, but this is not yet done. diff --git a/package-lock.json b/package-lock.json index 5de20df..849b379 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,37 @@ "name": "mson-three", "version": "1.0.0", "license": "ISC", - "dependencies": { - "three": "^0.161.0" - }, "devDependencies": { "@types/node": "^20.11.19", "@types/three": "^0.161.2", "prettier": "^3.2.5", "typescript": "^5.3.3" + }, + "optionalDependencies": { + "buffer": "^6.0.3", + "canvas": "^2.11.2", + "commander": "^12.0.0", + "three": "^0.161.0" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" } }, "node_modules/@types/node": { @@ -51,18 +74,554 @@ "integrity": "sha512-UEMMm/Xn3DtEa+gpzUrOcDj+SJS1tk5YodjwOxcqStNhCfPcwgyC5Srg2ToVKyg2Fhq16Ffpb0UWUQHqoT9AMA==", "dev": true }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/commander": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "optional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, "node_modules/fflate": { "version": "0.6.10", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", "dev": true }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "optional": true + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "optional": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/meshoptimizer": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", "dev": true }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/nan": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prettier": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", @@ -78,10 +637,176 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "optional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/three": { "version": "0.161.0", "resolved": "https://registry.npmjs.org/three/-/three-0.161.0.tgz", - "integrity": "sha512-LC28VFtjbOyEu5b93K0bNRLw1rQlMJ85lilKsYj6dgTu+7i17W+JCCEbvrpmNHF1F3NAUqDSWq50UD7w9H2xQw==" + "integrity": "sha512-LC28VFtjbOyEu5b93K0bNRLw1rQlMJ85lilKsYj6dgTu+7i17W+JCCEbvrpmNHF1F3NAUqDSWq50UD7w9H2xQw==", + "optional": true + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true }, "node_modules/typescript": { "version": "5.3.3", @@ -101,6 +826,49 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "optional": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true } } } diff --git a/package.json b/package.json index 2a6a8af..dae15d4 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,11 @@ "prettier": "^3.2.5", "typescript": "^5.3.3" }, + "optionalDependencies": { + "buffer": "^6.0.3", + "canvas": "^2.11.2", + "commander": "^12.0.0" + }, "dependencies": { "three": "^0.161.0" } diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..0f006f6 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,87 @@ +import { resolve } from 'path'; +import { ModelStore, MsonEvaluate } from './mson'; +import { fillStoreFromFilesystem, saveGeometry, saveModel } from './node'; +import { ThreeBuilder } from './three'; +import { MeshStandardMaterial } from 'three'; +import { Command } from 'commander'; +import { saveGLTF } from './node'; + +interface CliOptions { + format: string; + evaluated?: boolean; + store: string; + target: string; +} + +const store = new ModelStore(); +const evaluate = new MsonEvaluate(store); + +const exportModel = async (model: string, options: CliOptions) => { + console.log(`\nStarting to evaluate model ${model}`); + console.log(` ... Evaluating Mson model`); + const evaluated = evaluate.evaluateModel(model); + + console.log(` ... Building geometry`); + const mat = new MeshStandardMaterial(); + const builder = new ThreeBuilder(mat); + const geometry = builder.buildGeometry(evaluated); + + const outputName = model.replace(/[:\/]/g, '-'); + console.log(`Saving as ${outputName}.${options.format}`); + + const outputs = resolve(process.cwd(), options.target || 'outputs'); + if (options.evaluated) { + await saveModel(outputs, outputName, evaluated); + } + + switch (options.format) { + case 'json': + await saveGeometry(outputs, outputName, geometry); + break; + case 'gltf': + case 'glb': + await saveGLTF(outputs, outputName, geometry, options.format === 'glb'); + break; + default: + throw new Error(`Unexpected format ${options.format}`); + } +}; + +const program = new Command(); +program + .name('mson-three') + .description('Convert Mson models to THREE.js models (and others)') + .version('0.0.1'); + +program + .command('export') + .description('Export a Mson model') + .argument('models...', 'Model(s) to export') + .option( + '-f, --format ', + 'Output format, one of: json, gltf, glb', + 'json', + ) + .option('-e, --evaluated', 'Save the evaluated final Mson json file as well') + .option('-s, --store ', 'Path containing Mson models', 'inputs') + .option('-t, --target ', 'Path to place outputs into', 'outputs') + .action(async (models: string[], options: CliOptions) => { + console.log(` ... Reading Mson resources`); + await fillStoreFromFilesystem( + store, + resolve(process.cwd(), options.store || 'inputs'), + ); + + for (const model of models) { + try { + await exportModel(model, options); + } catch (error: any) { + console.error( + ` !!! Model "${model}" failed: ${error.message}`, + error.stack, + ); + } + } + }); + +program.parse(); diff --git a/src/index.ts b/src/index.ts index cefdf70..8f1f274 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ export * from './mson'; +export * from './three'; diff --git a/src/node/gltf.ts b/src/node/gltf.ts new file mode 100644 index 0000000..4a09aa7 --- /dev/null +++ b/src/node/gltf.ts @@ -0,0 +1,23 @@ +import { Object3D } from 'three'; + +export class GLTF { + static async export( + input: Object3D, + binary: BoolValue, + ): Promise; + static async export( + input: Object3D, + binary: BoolValue, + ): Promise<{ [x: string]: any }>; + static async export( + input: Object3D, + binary?: BoolValue, + ) { + return import('three/examples/jsm/exporters/GLTFExporter.js').then( + ({ GLTFExporter }) => + new GLTFExporter().parseAsync(input, { + binary, + }), + ); + } +} diff --git a/src/util/node.ts b/src/node/index.ts similarity index 83% rename from src/util/node.ts rename to src/node/index.ts index 8279e94..1cff3cd 100644 --- a/src/util/node.ts +++ b/src/node/index.ts @@ -1,3 +1,7 @@ +// Browser API mocks in node.js +import './vendor'; +// ... +import { GLTF } from './gltf'; import { ModelStore, MsonEvaluatedModel } from '../mson'; import { promises as fs } from 'node:fs'; import { join } from 'node:path'; @@ -55,6 +59,19 @@ export const saveGeometry = async ( JSON.stringify(object.toJSON()), ); +export const saveGLTF = async ( + root: string, + name: string, + object: Object3D, + binary = false, +) => { + const model = await GLTF.export(object, binary); + await fs.writeFile( + join(root, `${name}.${binary ? 'glb' : 'gltf'}`), + binary ? Buffer.from(model) : JSON.stringify(model), + ); +}; + export const saveModel = async ( root: string, name: string, diff --git a/src/node/vendor/eventtarget.ts b/src/node/vendor/eventtarget.ts new file mode 100644 index 0000000..fdf1446 --- /dev/null +++ b/src/node/vendor/eventtarget.ts @@ -0,0 +1,87 @@ +// +// None of the following code was written for mson-three explicity. +// attribution is provided where necessary. +// +// all of this junk was copied from abandoned npm modules which mimic browser +// APIs in Node.js in order to make GLTFExporter work. +// +// this is taken from abandoned npm module "eventtarget" by Jesús Leganés Combarro "Piranna" +// +export class EventTarget { + listeners: any = {}; + + addEventListener(type: string | number, listener: any) { + if (!listener) return; + + var listeners_type = this.listeners[type]; + if (listeners_type === undefined) + this.listeners[type] = listeners_type = []; + + for (var i = 0, l; (l = listeners_type[i]); i++) if (l === listener) return; + + listeners_type.push(listener); + } + + dispatchEvent(event: { + type: any; + message?: any; + _dispatched?: any; + cancelable?: any; + defaultPrevented?: any; + isTrusted?: any; + preventDefault?: any; + stopImmediatePropagation?: any; + target?: any; + timeStamp?: any; + }) { + if (event._dispatched) throw 'InvalidStateError'; + event._dispatched = true; + + var type = event.type; + if (type == undefined || type == '') throw 'UNSPECIFIED_EVENT_TYPE_ERR'; + + var listenerArray = this.listeners[type] || []; + + var dummyListener = (this as any)['on' + type]; + if (typeof dummyListener == 'function') + listenerArray = listenerArray.concat(dummyListener); + + var stopImmediatePropagation = false; + + // [ToDo] Use read-only properties instead of attributes when availables + event.cancelable = true; + event.defaultPrevented = false; + event.isTrusted = false; + event.preventDefault = function () { + if (this.cancelable) this.defaultPrevented = true; + }; + event.stopImmediatePropagation = function () { + stopImmediatePropagation = true; + }; + event.target = this; + event.timeStamp = new Date().getTime(); + + for (var i = 0, listener; (listener = listenerArray[i]); i++) { + if (stopImmediatePropagation) break; + + listener.call(this, event); + } + + return !event.defaultPrevented; + } + + removeEventListener(type: string | number, listener: any) { + if (!listener) return; + + var listeners_type = this.listeners[type]; + if (listeners_type === undefined) return; + + for (var i = 0, l; (l = listeners_type[i]); i++) + if (l === listener) { + listeners_type.splice(i, 1); + break; + } + + if (!listeners_type.length) delete this.listeners[type]; + } +} diff --git a/src/node/vendor/index.ts b/src/node/vendor/index.ts new file mode 100644 index 0000000..a5ebd76 --- /dev/null +++ b/src/node/vendor/index.ts @@ -0,0 +1,18 @@ +import { Canvas } from 'canvas'; +import { Blob, FileReader } from './vblob'; + +// Patch global scope to imitate browser environment. +global.Blob = Blob as any; +global.FileReader = FileReader as any; +global.document = { + createElement: (nodeName: string) => { + if (nodeName !== 'canvas') + throw new Error(`Cannot create node ${nodeName}`); + const canvas = new Canvas(256, 256); + // This isn't working — currently need to avoid toBlob(), so export to embedded .gltf not .glb. + // canvas.toBlob = function () { + // return new Blob([this.toBuffer()]); + // }; + return canvas; + }, +} as any; diff --git a/src/node/vendor/vblob.ts b/src/node/vendor/vblob.ts new file mode 100644 index 0000000..9a7c6ec --- /dev/null +++ b/src/node/vendor/vblob.ts @@ -0,0 +1,438 @@ +// +// None of the following code was written for mson-three explicity. +// attribution is provided where necessary. +// +// all of this junk was copied from abandoned npm modules which mimic browser +// APIs in Node.js in order to make GLTFExporter work. +// +// this code is taken from npm package "vblob" by karikera + +import * as fs from 'fs'; +import { randomBytes } from 'crypto'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { Blob as NodeBlob } from 'buffer'; +import { EventTarget } from './eventtarget'; + +interface EventBeforeDispatch { + [key: string]: any; + type: string; +} + +interface Event { + readonly cancelable: boolean; + readonly defaultPrevented: boolean; + readonly isTrusted: boolean; + readonly target: EventTarget | null; + readonly timeStamp: number; + readonly bubbles: boolean; + /** @deprecated */ + cancelBubble: boolean; + readonly composed: boolean; + readonly currentTarget: EventTarget | null; + readonly eventPhase: number; + /** @deprecated */ + returnValue: boolean; + /** @deprecated */ + readonly srcElement: EventTarget | null; + readonly type: string; + + preventDefault(): void; + stopPropagation(): void; + stopImmediatePropagation(): void; + composedPath(): EventTarget[]; + /** @deprecated */ + initEvent(type: string, bubbles: boolean, cancelable: boolean): void; + + readonly NONE: number; + readonly CAPTURING_PHASE: number; + readonly AT_TARGET: number; + readonly BUBBLING_PHASE: number; +} + +interface ProgressEvent extends Event { + readonly lengthComputable: boolean; + readonly loaded: number; + readonly target: T | null; + readonly total: number; +} + +interface EventTargetConstructor { + new (): EventTarget; +} + +interface FileReaderEvent extends Event {} + +function getTempPath(): Promise { + const file = `vblob-${randomBytes(4).readUInt32LE(0)}`; + const path = join(tmpdir(), file); + tempFiles.add(path); + return Promise.resolve(path); +} + +function fdopen(path: string, flags: string): Promise { + return new Promise((resolve, reject) => + fs.open(path, flags, (err, fd) => { + if (err) reject(err); + else resolve(fd); + }), + ); +} + +function fdclose(fd: number): Promise { + return new Promise((resolve, reject) => + fs.close(fd, (err) => { + if (err) reject(err); + else resolve(); + }), + ); +} + +function fdwriteFile(fd: number, path: string): Promise { + return new Promise((resolve, reject) => { + const writer = fs.createWriteStream(null, { fd }); + const reader = fs.createReadStream(path); + reader.on('error', reject); + reader.on('end', resolve); + writer.on('error', reject); + reader.pipe(writer, { end: false }); + }); +} + +function fdwrite(fd: number, str: string | Uint8Array): Promise { + return new Promise((resolve, reject) => + fs.write(fd, str as any, (err) => { + if (err) reject(err); + else resolve(); + }), + ); +} + +function fdread(fd: number, size: number, position: number): Promise { + const buffer = Buffer.alloc(size); + return new Promise((resolve, reject) => + fs.read(fd, buffer, 0, size, position, (err) => { + if (err) reject(err); + else resolve(buffer); + }), + ); +} + +const tempFiles: Set = new Set(); + +const onExit: Array<() => void> = []; + +process.on('exit', (code) => { + for (const cb of onExit) cb(); + process.exit(code); +}); + +onExit.push(() => { + for (const file of tempFiles) { + fs.unlinkSync(file); + } +}); + +interface BlobPropertyBag { + type?: string; + ending?: 'transparent' | 'native'; +} + +export interface Blob { + readonly size: number; + readonly type: string; + slice(start?: number, end?: number, contentType?: string): Blob; +} + +export interface FileReader extends EventTarget { + readonly error: any | null; + readonly readyState: number; + readonly result: any; + readonly EMPTY: number; + readonly LOADING: number; + readonly DONE: number; + + onabort: ((ev: ProgressEvent) => void) | null; + onerror: ((ev: FileReaderEvent) => void) | null; + onload: ((ev: FileReaderEvent) => void) | null; + onloadstart: ((ev: FileReaderEvent) => void) | null; + onloadend: ((ev: FileReaderEvent) => void) | null; + onprogress: ((ev: FileReaderEvent) => void) | null; + + abort(): void; + readAsArrayBuffer(blob: Blob): void; + readAsBinaryString(blob: Blob): void; + readAsDataURL(blob: Blob): void; + readAsText(blob: Blob): void; +} + +export class VBlob implements Blob { + _path: string = ''; + _size: number; + _offset: number = 0; + _type: string; + _writeTask: Promise = Promise.resolve(0); + + private _write(fn: (fd: number) => void | Promise): void { + this._writeTask = this._writeTask.then(async (fd) => { + if (!fd) { + this._path = await getTempPath(); + fd = await fdopen(this._path, 'w+'); + } + await fn(fd); + return fd; + }); + } + + private _writeEnd(): void { + this._writeTask = this._writeTask.then((fd) => fdclose(fd)).then(() => 0); + } + + constructor(array?: any[], options?: BlobPropertyBag) { + this._type = (options && options.type) || ''; + + if (!array) { + this._path = ''; + this._size = 0; + } else { + var size = 0; + for (const value of array) { + if (value instanceof ArrayBuffer) { + if (value.byteLength === 0) continue; + this._write((fd) => fdwrite(fd, new Uint8Array(value))); + size += value.byteLength; + } else if (value instanceof Uint8Array) { + if (value.byteLength === 0) continue; + this._write((fd) => fdwrite(fd, value)); + size += value.byteLength; + } else if ( + value instanceof Int8Array || + value instanceof Uint8ClampedArray || + value instanceof Int16Array || + value instanceof Uint16Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Float32Array || + value instanceof Float64Array || + value instanceof DataView + ) { + if (value.byteLength === 0) continue; + this._write((fd) => + fdwrite( + fd, + new Uint8Array(value.buffer, value.byteOffset, value.byteLength), + ), + ); + size += value.byteLength; + } else if (value instanceof VBlob) { + if (value._size === 0) continue; + this._write((fd) => fdwriteFile(fd, value._path)); + size += value._size; + } else { + const str = value + ''; + if (str.length === 0) continue; + this._write((fd) => fdwrite(fd, str)); + size += str.length; + } + } + this._writeEnd(); + this._size = size; + } + } + + get size(): number { + return this._size; + } + + get type(): string { + return this._type; + } + + slice(start?: number, end?: number, contentType?: string): Blob { + if (!start) start = 0; + else if (start < 0) start = this._size + start; + if (!end) end = this._size; + + if (end < 0) end = this._size - end; + else if (end >= this._size) end = this._size; + if (start >= end) return new VBlob([]); + + const newblob = new VBlob(); + newblob._type = contentType || this._type; + newblob._writeTask = this._writeTask; + newblob._offset = this._offset + start; + newblob._size = end - start; + this._writeTask.then(() => (newblob._path = this._path)); + return newblob; + } + + readBuffer(fd: number): Promise { + return fdread(fd, this._size, this._offset).then((buffer) => buffer.buffer); + } +} + +export var Blob: { new (array?: any[], options?: BlobPropertyBag): Blob } = + global['Blob'] || VBlob; + +interface Aborted { + aborted: boolean; +} +export class VFileReader extends EventTarget implements FileReader { + static readonly EMPTY = 0; + static readonly LOADING = 1; + static readonly DONE = 2; + readonly EMPTY = 0; + readonly LOADING = 1; + readonly DONE = 2; + + onabort: ((ev: ProgressEvent) => void) | null = null; + onerror: ((ev: FileReaderEvent) => void) | null = null; + onload: ((ev: FileReaderEvent) => void) | null = null; + onloadstart: ((ev: FileReaderEvent) => void) | null = null; + onloadend: ((ev: FileReaderEvent) => void) | null = null; + onprogress: ((ev: FileReaderEvent) => void) | null = null; + + private _readyState: 0 | 1 | 2; + private _abort: (() => void) | null = null; + private _abortPromise: Promise | null = null; + + public result: any; + public error: any | null = null; + + constructor() { + super(); + this._readyState = 0; + } + + get readyState(): 0 | 1 | 2 { + return this._readyState; + } + + abort(): void { + if (this._abort !== null) { + this._abort(); + this._abort = null; + this._abortPromise = null; + this.dispatchEvent({ type: 'abort' }); + } + if (this._readyState === 1) { + this._finishWork(); + } + } + + private _startWork(methodName: string): Aborted { + if (this._readyState === 1) { + throw Error( + `Failed to execute '${methodName}' on 'FileReader': The object is already busy reading Blobs.`, + ); + } + this.result = null; + this.error = null; + this._readyState = 1; + const aborted: Aborted = { aborted: false }; + if (this._abortPromise === null) { + this._abortPromise = new Promise((resolve) => { + this._abort = () => { + aborted.aborted = true; + resolve(null); + }; + }); + } + return aborted; + } + + private _finishWork() { + this.dispatchEvent({ type: 'loadend' }); + this._readyState = 2; + } + + private _readBuffer( + methodName: string, + blob: Blob, + cb: (buffer: Buffer) => any, + ): Promise { + const aborted = this._startWork(methodName); + + if (!(blob instanceof VBlob) && !(blob instanceof NodeBlob)) { + throw TypeError( + `vblob cannot handle the ${blob.constructor.name} class.`, + ); + } + + const prom = new Promise((resolve) => process.nextTick(resolve)).then( + () => { + if (aborted.aborted) return null; + this.dispatchEvent({ type: 'loadstart' }); + if (blob instanceof VBlob) { + return this._readVBlob(blob); + } else if (blob instanceof NodeBlob) { + return this._readNodeBlob(blob); + } else { + return null; + } + }, + ); + return Promise.race([this._abortPromise, prom]).then( + (data) => { + if (data === null) return; + if (aborted.aborted) return; + this.result = cb(data); + this.dispatchEvent({ type: 'load' }); + this._finishWork(); + }, + (err) => { + if (aborted.aborted) return; + this.error = err; + this.dispatchEvent({ + type: 'error', + message: err ? err.message : 'Error', + }); + this._finishWork(); + }, + ); + } + + private async _readVBlob(blob: VBlob): Promise { + if (blob._size === 0) { + return Buffer.alloc(0); + } else { + await blob._writeTask; + const fd = await fdopen(blob._path, 'r'); + try { + return await fdread(fd, blob._size, blob._offset); + } finally { + fdclose(fd); + } + } + } + + private async _readNodeBlob(blob: NodeBlob): Promise { + const buf = await blob.arrayBuffer(); + return Buffer.from(buf); + } + + readAsArrayBuffer(blob: Blob): void { + this._readBuffer('readAsArrayBuffer', blob, (data) => data.buffer); + } + readAsBinaryString(blob: Blob): void { + this._readBuffer('readAsBinaryString', blob, (data) => + data.toString('binary'), + ); + } + readAsDataURL(blob: Blob): void { + this._readBuffer( + 'readAsDataURL', + blob, + (data) => + 'data:' + + (blob.type || 'application/octet-stream') + + ';base64,' + + data.toString('base64'), + ); + } + readAsText(blob: Blob): void { + this._readBuffer('readAsText', blob, (data) => data.toString()); + } +} + +export var FileReader: { new (): FileReader } = VFileReader; diff --git a/src/test-node.ts b/src/test-node.ts deleted file mode 100644 index fcadedb..0000000 --- a/src/test-node.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { resolve } from 'path'; -import { ModelStore, MsonEvaluate } from '.'; -import { fillStoreFromFilesystem, saveGeometry, saveModel } from './util/node'; -import { ThreeBuilder } from './three'; -import { DoubleSide, MeshLambertMaterial } from 'three'; - -async function init() { - const store = new ModelStore(); - const evaluate = new MsonEvaluate(store); - const mat = new MeshLambertMaterial(); - // mat.side = DoubleSide; - const builder = new ThreeBuilder(mat); - - await fillStoreFromFilesystem(store, resolve(process.cwd(), 'inputs')); - - // mson:steve - // minelittlepony:steve_pony - const final = evaluate.evaluateModel('minelittlepony:races/steve/alicorn'); - // console.log(final.texture); - const geometry = builder.buildGeometry(final); - const outputName = 'alicorn'; - const outputs = resolve(process.cwd(), 'outputs'); - await saveGeometry(outputs, outputName, geometry); - await saveModel(outputs, outputName, final); -} - -init().catch(console.error); diff --git a/src/three/builder.ts b/src/three/builder.ts index 7c05f5c..6e34970 100644 --- a/src/three/builder.ts +++ b/src/three/builder.ts @@ -1,5 +1,6 @@ import { BoxGeometry, + BufferGeometry, CylinderGeometry, Material, MathUtils, @@ -66,7 +67,10 @@ export class ThreeBuilder { new Vector3(pos.x + size.x / 2, pos.y - size.y / 2, pos.z), }; - constructor(private readonly material: Material) {} + /** + * @param material Material to use for the whole model. + */ + constructor(protected readonly material: Material) {} /** * Create a THREE.js object from MSON evaluated model data. @@ -245,10 +249,7 @@ export class ThreeBuilder { adjustedTranslate.z, ); - // TODO: FIXME: this crap is to hack .toJSON() into including the entire geometry - // instead of just the properties used to initialize it. - (geometry as any).type = 'BufferGeometry'; - delete (geometry as any).parameters; + ThreeBuilder.obfuscateGeometry(geometry); // Map UVs to box UVMapper.mapBoxUVs(size, geometry, texture); @@ -307,10 +308,7 @@ export class ThreeBuilder { geometry = geometry.toNonIndexed() as CylinderGeometry; geometry.computeVertexNormals(); - // TODO: FIXME: this crap is to hack .toJSON() into including the entire geometry - // instead of just the properties used to initialize it. - (geometry as any).type = 'BufferGeometry'; - delete (geometry as any).parameters; + ThreeBuilder.obfuscateGeometry(geometry); const mesh = new Mesh(geometry, this.material); mesh.name = `${name}__cone`; @@ -341,10 +339,7 @@ export class ThreeBuilder { adjustedTranslate.z, ); - // TODO: FIXME: this crap is to hack .toJSON() into including the entire geometry - // instead of just the properties used to initialize it. - (planeGeom as any).type = 'BufferGeometry'; - delete (planeGeom as any).parameters; + ThreeBuilder.obfuscateGeometry(planeGeom); UVMapper.mapPlanarUvs(face, size, planeGeom, texture, mirror); @@ -465,4 +460,14 @@ export class ThreeBuilder { wrapper.updateMatrix(); return wrapper; } + + /** + * This crap is to hack `Object3D.toJSON()` into including the entire geometry + * instead of just the properties used to initialize it. + * @param geometry Geometry + */ + static obfuscateGeometry(geometry: BufferGeometry) { + (geometry as any).type = 'BufferGeometry'; + delete (geometry as any).parameters; + } } diff --git a/src/util/merge-deep.ts b/src/util/merge-deep.ts index 3e100b6..dda36c4 100644 --- a/src/util/merge-deep.ts +++ b/src/util/merge-deep.ts @@ -1,16 +1,16 @@ /** * Simple object check. * @param item - * @returns {boolean} + * @returns {boolean} Is Object, not array, null or undefined. */ -export function isObject(item: any) { +export function isObject(item: any): boolean { return item && typeof item === 'object' && !Array.isArray(item); } /** * Deep merge two objects. - * @param target - * @param ...sources + * @param target Merge `sources` into this object. + * @param ...sources Objects to use in merge. */ export function mergeDeep(target: any, ...sources: any[]) { if (!sources.length) return target; diff --git a/src/util/num-range.ts b/src/util/num-range.ts index 5878d3c..5db0163 100644 --- a/src/util/num-range.ts +++ b/src/util/num-range.ts @@ -1,3 +1,13 @@ +/** + * Convert number from one range to another. + * @example + * // returns 0.5 + * convertRange(0, [-1, 1], [0, 1]); + * @param value Value to convert + * @param r1 Source range + * @param r2 Target range + * @returns Value in new range + */ export function convertRange( value: number, r1: [number, number], diff --git a/tsconfig.json b/tsconfig.json index 7fe3b65..8b79461 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -22,7 +22,7 @@ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ + "module": "Node16" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ @@ -50,7 +50,7 @@ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./lib", /* Specify an output folder for all emitted files. */ + "outDir": "./lib" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ @@ -71,11 +71,11 @@ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ + "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */