├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ ├── build.js.yml │ └── tests.js.yml ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── firebase.json ├── jestconfig.json ├── loc ├── base.ts ├── en_GB.ts ├── en_US.ts ├── es_ES.ts ├── fr_FR.ts ├── ja_JP.ts ├── ru_RU.ts ├── zh_CN.ts └── zh_TW.ts ├── package-lock.json ├── package.json ├── res ├── atlases │ ├── vanilla.atlas │ └── vanilla.png ├── block_ids.ts ├── palettes │ ├── all.ts │ ├── colourful.ts │ ├── greyscale.ts │ └── schematic-friendly.ts ├── samples │ ├── editor.png │ ├── noodles.png │ ├── skull.jpg │ ├── skull.mtl │ ├── skull.obj │ └── skull.txt ├── shaders │ ├── block_fragment.fs │ ├── block_vertex.vs │ ├── debug_fragment.fs │ ├── debug_vertex.vs │ ├── solid_tri_fragment.fs │ ├── solid_tri_vertex.vs │ ├── texture_tri_fragment.fs │ ├── texture_tri_vertex.vs │ ├── voxel_fragment.fs │ └── voxel_vertex.vs └── static │ ├── debug.png │ ├── debug_alpha.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src ├── analytics.ts ├── app_context.ts ├── atlas.ts ├── block_assigner.ts ├── block_atlas.ts ├── block_mesh.ts ├── bounds.ts ├── buffer.ts ├── camera.ts ├── colour.ts ├── config.ts ├── constants.ts ├── dither.ts ├── event.ts ├── exporters │ ├── base_exporter.ts │ ├── exporters.ts │ ├── indexed_json_exporter .ts │ ├── litematic_exporter.ts │ ├── nbt_exporter.ts │ ├── schem_exporter.ts │ ├── schematic_exporter.ts │ └── uncompressed_json_exporter.ts ├── geometry.ts ├── global.d.ts ├── hash_map.ts ├── importers │ ├── base_importer.ts │ ├── gltf_loader.ts │ ├── importers.ts │ └── obj_importer.ts ├── lighting.ts ├── linear_allocator.ts ├── localiser.ts ├── main.ts ├── material-map.ts ├── math.ts ├── mesh.ts ├── mouse.ts ├── occlusion.ts ├── palette.ts ├── progress.ts ├── ray.ts ├── render_buffer.ts ├── renderer.ts ├── shaders.ts ├── status.ts ├── texture.ts ├── triangle.ts ├── ui │ ├── components │ │ ├── base.ts │ │ ├── button.ts │ │ ├── checkbox.ts │ │ ├── colour.ts │ │ ├── combobox.ts │ │ ├── config.ts │ │ ├── file_input.ts │ │ ├── full_config.ts │ │ ├── header.ts │ │ ├── image.ts │ │ ├── material_type.ts │ │ ├── number_input.ts │ │ ├── palette.ts │ │ ├── placeholder.ts │ │ ├── slider.ts │ │ ├── solid_material.ts │ │ ├── textured_material.ts │ │ ├── toolbar_item.ts │ │ └── vector.ts │ ├── console.ts │ ├── icons.ts │ ├── layout.ts │ └── misc.ts ├── util.ts ├── util │ ├── error_util.ts │ ├── file_util.ts │ ├── log_util.ts │ ├── math_util.ts │ ├── nbt_util.ts │ ├── path_util.ts │ ├── regex_util.ts │ ├── type_util.ts │ └── ui_util.ts ├── vector.ts ├── voxel_mesh.ts ├── voxelisers │ ├── base-voxeliser.ts │ ├── bvh-ray-voxeliser-plus-thickness.ts │ ├── bvh-ray-voxeliser.ts │ ├── normal-corrected-ray-voxeliser.ts │ ├── ray-voxeliser.ts │ └── voxelisers.ts ├── worker.ts ├── worker_client.ts ├── worker_controller.ts ├── worker_interface.worker.ts └── worker_types.ts ├── styles.css ├── template.html ├── tests ├── __mocks__ │ └── fileMock.js ├── buffer.test.ts ├── data │ ├── cube.mtl │ └── cube.obj ├── linear_allocator.test.ts ├── palette.test.ts ├── preamble.ts ├── ray.test.ts ├── util.test.ts └── voxel_mesh.test.ts ├── tools ├── build-atlas.ts ├── headless-config.ts ├── headless.ts ├── misc.ts ├── models-ignore-list.txt ├── new-palette-blocks.txt └── run-headless.ts ├── tsconfig.json ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true 6 | }, 7 | "extends": [ 8 | "google" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest" 13 | }, 14 | "plugins": [ 15 | "@typescript-eslint", 16 | "simple-import-sort" 17 | ], 18 | "rules": { 19 | "camelcase": "off", 20 | "linebreak-style": "off", 21 | "object-curly-spacing": "off", 22 | "max-len": "off", 23 | "require-jsdoc": "off", 24 | "valid-jsdoc": "off", 25 | "indent": ["error", 4, { "SwitchCase": 1 }], 26 | "no-multi-spaces": "off", 27 | "no-array-constructor": "off", 28 | "guard-for-in": "off", 29 | "func-call-spacing": "off", 30 | "no-trailing-spaces": "off", 31 | "new-cap": "off", 32 | "no-console": "warn", 33 | "no-unused-vars": "warn", 34 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 35 | "block-spacing": [2, "always"], 36 | "semi": "error", 37 | "spaced-comment": "off", 38 | "keyword-spacing": "off", 39 | "space-before-function-paren": "off", 40 | "simple-import-sort/imports": "error", 41 | "simple-import-sort/exports": "error" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: lucasdower 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/build.js.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Install modules 9 | run: npm ci 10 | - name: Run build 11 | run: npm run build -------------------------------------------------------------------------------- /.github/workflows/tests.js.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Install modules 9 | run: npm ci 10 | - name: Run tests 11 | run: npm test 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | ObjToSchematic-win32-x64 3 | ObjToSchematic-linux-x64 4 | ObjToSchematic-darwin-x64 5 | /res/atlases/*.atlas 6 | /res/atlases/*.png 7 | !/res/atlases/vanilla.atlas 8 | !/res/atlases/vanilla.png 9 | /res/palettes/empty.palette 10 | /dist 11 | /dev 12 | /gen 13 | /tools/blocks 14 | /tools/models 15 | /tests/out 16 | /logs/ 17 | /release/ 18 | notes.txt 19 | *.DS_Store 20 | .dependency-cruiser.js 21 | dependencygraph.svg 22 | .firebase 23 | /webpack/ 24 | .firebaserc -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug Main Process", 11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 12 | "program": "${workspaceRoot}/index.js", 13 | "runtimeArgs": [ 14 | ".", 15 | // this args for attaching render process 16 | "--remote-debugging-port=9222" 17 | ], 18 | "windows": { 19 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 20 | }, 21 | "protocol": "legacy" 22 | }, 23 | { 24 | "type": "chrome", 25 | "request": "attach", 26 | "name": "Attach to Render Process", 27 | "port": 9222, 28 | "webRoot": "${workspaceRoot}/html" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Lucas Dower 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "webpack", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /jestconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "transform": { 3 | "^.+\\.(t|j)sx?$": "ts-jest" 4 | }, 5 | "testRegex": "(/test/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 6 | "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], 7 | "modulePathIgnorePatterns": ["/tests/full/"], 8 | "moduleNameMapper": { 9 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|vs|fs)$": "/tests/__mocks__/fileMock.js", 10 | "\\.(css|less)$": "/tests/__mocks__/styleMock.js" 11 | } 12 | } -------------------------------------------------------------------------------- /loc/base.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial } from '../src/util/type_util'; 2 | import { en_GB } from './en_GB'; 3 | import { en_US } from './en_US'; 4 | import { es_ES } from './es_ES'; 5 | import { fr_FR } from './fr_FR'; 6 | import { ru_RU } from './ru_RU'; 7 | import { zh_CN } from './zh_CN'; 8 | import { zh_TW } from './zh_TW'; 9 | import { ja_JP } from './ja_JP'; 10 | 11 | export type TTranslationMap = typeof en_GB.translations; 12 | 13 | export type TLocaleDefinition = { 14 | display_name: string, 15 | language_code: string, 16 | translations: DeepPartial, 17 | }; 18 | 19 | export const locales = [ 20 | en_GB, 21 | en_US, 22 | es_ES, 23 | ru_RU, 24 | zh_CN, 25 | zh_TW, 26 | fr_FR, 27 | ja_JP, 28 | ]; 29 | -------------------------------------------------------------------------------- /loc/en_US.ts: -------------------------------------------------------------------------------- 1 | // Credits: 2 | // LucasDower 3 | 4 | import { TLocaleDefinition } from './base'; 5 | 6 | export const en_US: TLocaleDefinition = { 7 | display_name: 'American English', 8 | language_code: 'en-US', 9 | translations: { 10 | init: { 11 | initialising: 'Initializing...', 12 | }, 13 | import: { 14 | invalid_encoding: 'Unrecognized character found, please encode using UTF-8', 15 | }, 16 | voxelise: { 17 | heading: '3. VOXELIZE', 18 | button: 'Voxelize mesh', 19 | components: { 20 | colour: 'Color', 21 | }, 22 | }, 23 | assign: { 24 | components: { 25 | colour_accuracy: 'Color accuracy', 26 | }, 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /loc/ru_RU.ts: -------------------------------------------------------------------------------- 1 | // Credits: 2 | // bookshelfich 3 | 4 | import { TLocaleDefinition } from './base'; 5 | 6 | export const ru_RU: TLocaleDefinition = { 7 | display_name: 'Ру́сский', 8 | language_code: 'ru-RU', 9 | translations: { 10 | import: { 11 | button: 'Загрузить mesh', 12 | missing_normals: 'Некоторые вертикали не умеют данных нормалей, это может привести к некорректно ассигнованию вокселей.', 13 | }, 14 | materials: { 15 | components: { 16 | 'texture_filtering': 'Текстурный фильтр', 17 | 'linear': 'Линейный', 18 | 'nearest': 'Ближайший', 19 | }, 20 | }, 21 | voxelise: { 22 | button: 'Вокселизованный mesh', 23 | components: { 24 | algorithm: 'Алгоритм', 25 | ambient_occlusion: 'Параметр непроходимости окружающей среды', 26 | multisampling: 'Мультисэмплинг', 27 | voxel_overlap: 'Нахлёст вокселей', 28 | ray_based: 'Основано на лучах', 29 | bvh_ray: 'Основываться на лучах BVH', 30 | average_recommended: 'Среднее (рекомендуется)', 31 | first: 'Первый', 32 | }, 33 | }, 34 | assign: { 35 | button: 'Назначить блок', 36 | blocks_missing_textures: '{{count, number}} блоки палитры имеют пропущенные атласы текстур и не будет использоваться.', 37 | falling_blocks: '{{count, number}} блоки упадёт под действием гравитации при установке.', 38 | components: { 39 | texture_atlas: 'Текстурный атлас', 40 | block_palette: 'Палитра блоков', 41 | dithering: 'Сглаживание', 42 | fallable_blocks: 'Падающие блоки', 43 | colour_accuracy: 'Точность цвета', 44 | replace_falling: 'Заменять падающие блоки на не падающие', 45 | replace_fallable: 'Заменять блоки, подверженные падению, на непадающие', 46 | do_nothing: 'Ничего не делать', 47 | }, 48 | }, 49 | export: { 50 | button: 'Экспортировать структуры', 51 | schematic_unsupported_blocks: '{{count, number}} блоки не поддерживаются форматом .schematic. Блоки камня будет использоваться в этих местах. Попробуйте использовать schematic-friendly палитру, или используете другой экспортёр', 52 | nbt_exporter_too_big: 'Структурные блоки поддерживают только размер 48x48x48 блоков. Блоки за пределами будут убраны.', 53 | components: { 54 | exporter: 'Формат экспорта', 55 | litematic: 'Файл Litematic (.litematic)', 56 | schematic: 'Файл схематики (.schematic)', 57 | sponge_schematic: 'Файл схематики Sponge (.schem)', 58 | structure_blocks: 'Файл структурного блока (.nbt)', 59 | }, 60 | }, 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "objtoschematic", 3 | "private": true, 4 | "version": "0.0.0", 5 | "description": "A tool to convert 3D models into Minecraft formats such as .schematic, .litematic, .schem and .nbt", 6 | "main": "./dist/src/main.js", 7 | "engines": { 8 | "node": ">=16.8.0" 9 | }, 10 | "scripts": { 11 | "lint": "eslint --fix src tools tests --ext .ts", 12 | "lint-src": "eslint --fix src --ext .ts", 13 | "test": "jest --config jestconfig.json", 14 | "start": "webpack serve --config ./webpack.dev.js", 15 | "dist": "webpack --config ./webpack.prod.js", 16 | "atlas": "ts-node ./tools/build-atlas.ts", 17 | "headless": "ts-node ./tools/run-headless.ts", 18 | "build": "tsc" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/LucasDower/ObjToSchematic.git" 23 | }, 24 | "author": "Lucas Dower", 25 | "license": "BSD-3-Clause", 26 | "bugs": { 27 | "url": "https://github.com/LucasDower/ObjToSchematic/issues" 28 | }, 29 | "homepage": "https://github.com/LucasDower/ObjToSchematic#readme", 30 | "devDependencies": { 31 | "@types/adm-zip": "^0.5.0", 32 | "@types/jest": "^27.4.1", 33 | "@types/jquery": "^3.5.6", 34 | "@types/merge-images": "^1.2.1", 35 | "@types/pngjs": "^6.0.1", 36 | "@types/prompt": "^1.1.2", 37 | "@types/sharp": "^0.31.0", 38 | "@types/varint": "^6.0.0", 39 | "@typescript-eslint/eslint-plugin": "^5.9.1", 40 | "@typescript-eslint/parser": "^5.9.1", 41 | "browserify-zlib": "^0.2.0", 42 | "bvh-tree": "^1.0.1", 43 | "chalk": "^4.1.2", 44 | "commander": "^11.0.0", 45 | "css-loader": "^6.7.3", 46 | "eslint": "^8.7.0", 47 | "eslint-config-google": "^0.14.0", 48 | "eslint-plugin-simple-import-sort": "^8.0.0", 49 | "file-loader": "^6.2.0", 50 | "ga-gtag": "^1.1.7", 51 | "hsv-rgb": "^1.0.0", 52 | "html-webpack-plugin": "^5.5.0", 53 | "i18next": "^22.4.14", 54 | "jest": "^27.5.1", 55 | "jpeg-js": "^0.4.4", 56 | "json-loader": "^0.5.7", 57 | "jszip": "^3.10.1", 58 | "node-polyfill-webpack-plugin": "^2.0.1", 59 | "pngjs": "^7.0.0", 60 | "prismarine-nbt": "^2.2.1", 61 | "prompt": "^1.2.1", 62 | "raw-loader": "^4.0.2", 63 | "sharp": "^0.31.3", 64 | "style-loader": "^3.3.1", 65 | "ts-jest": "^27.1.3", 66 | "ts-loader": "^9.4.2", 67 | "ts-node": "^10.1.0", 68 | "twgl.js": "^5.3.0", 69 | "typescript": "^4.3.5", 70 | "varint-array": "^0.0.0", 71 | "webpack": "^5.75.0", 72 | "webpack-cli": "^5.0.1", 73 | "webpack-dev-server": "^4.11.1", 74 | "webpack-merge": "^5.8.0", 75 | "webpack-strip-block": "^0.3.0", 76 | "worker-loader": "^3.0.8" 77 | }, 78 | "dependencies": { 79 | "@loaders.gl/core": "^3.3.1", 80 | "@loaders.gl/gltf": "^3.3.1", 81 | "split.js": "^1.6.5" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /res/atlases/vanilla.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasDower/ObjToSchematic/b611b52c7a87cad99127908598bab019fe9e7c3f/res/atlases/vanilla.png -------------------------------------------------------------------------------- /res/palettes/colourful.ts: -------------------------------------------------------------------------------- 1 | export const PALETTE_COLOURFUL = [ 2 | 'minecraft:black_concrete', 3 | 'minecraft:black_concrete_powder', 4 | 'minecraft:black_glazed_terracotta', 5 | 'minecraft:black_terracotta', 6 | 'minecraft:black_wool', 7 | 'minecraft:blue_concrete', 8 | 'minecraft:blue_concrete_powder', 9 | 'minecraft:blue_glazed_terracotta', 10 | 'minecraft:blue_terracotta', 11 | 'minecraft:blue_wool', 12 | 'minecraft:brown_concrete', 13 | 'minecraft:brown_concrete_powder', 14 | 'minecraft:brown_glazed_terracotta', 15 | 'minecraft:brown_terracotta', 16 | 'minecraft:brown_wool', 17 | 'minecraft:cyan_concrete', 18 | 'minecraft:cyan_concrete_powder', 19 | 'minecraft:cyan_glazed_terracotta', 20 | 'minecraft:cyan_terracotta', 21 | 'minecraft:cyan_wool', 22 | 'minecraft:gray_concrete', 23 | 'minecraft:gray_concrete_powder', 24 | 'minecraft:gray_glazed_terracotta', 25 | 'minecraft:gray_terracotta', 26 | 'minecraft:gray_wool', 27 | 'minecraft:green_concrete', 28 | 'minecraft:green_concrete_powder', 29 | 'minecraft:green_glazed_terracotta', 30 | 'minecraft:green_terracotta', 31 | 'minecraft:green_wool', 32 | 'minecraft:light_blue_concrete', 33 | 'minecraft:light_blue_concrete_powder', 34 | 'minecraft:light_blue_glazed_terracotta', 35 | 'minecraft:light_blue_terracotta', 36 | 'minecraft:light_blue_wool', 37 | 'minecraft:light_gray_concrete', 38 | 'minecraft:light_gray_concrete_powder', 39 | 'minecraft:light_gray_glazed_terracotta', 40 | 'minecraft:light_gray_terracotta', 41 | 'minecraft:light_gray_wool', 42 | 'minecraft:lime_concrete', 43 | 'minecraft:lime_concrete_powder', 44 | 'minecraft:lime_glazed_terracotta', 45 | 'minecraft:lime_terracotta', 46 | 'minecraft:lime_wool', 47 | 'minecraft:magenta_concrete', 48 | 'minecraft:magenta_concrete_powder', 49 | 'minecraft:magenta_glazed_terracotta', 50 | 'minecraft:magenta_terracotta', 51 | 'minecraft:magenta_wool', 52 | 'minecraft:orange_concrete', 53 | 'minecraft:orange_concrete_powder', 54 | 'minecraft:orange_glazed_terracotta', 55 | 'minecraft:orange_terracotta', 56 | 'minecraft:orange_wool', 57 | 'minecraft:pink_concrete', 58 | 'minecraft:pink_concrete_powder', 59 | 'minecraft:pink_glazed_terracotta', 60 | 'minecraft:pink_terracotta', 61 | 'minecraft:pink_wool', 62 | 'minecraft:purple_concrete', 63 | 'minecraft:purple_concrete_powder', 64 | 'minecraft:purple_glazed_terracotta', 65 | 'minecraft:purple_terracotta', 66 | 'minecraft:purple_wool', 67 | 'minecraft:red_concrete', 68 | 'minecraft:red_concrete_powder', 69 | 'minecraft:red_glazed_terracotta', 70 | 'minecraft:red_terracotta', 71 | 'minecraft:red_wool', 72 | 'minecraft:white_concrete', 73 | 'minecraft:white_concrete_powder', 74 | 'minecraft:white_glazed_terracotta', 75 | 'minecraft:white_terracotta', 76 | 'minecraft:white_wool', 77 | 'minecraft:yellow_concrete', 78 | 'minecraft:yellow_concrete_powder', 79 | 'minecraft:yellow_glazed_terracotta', 80 | 'minecraft:yellow_terracotta', 81 | 'minecraft:yellow_wool', 82 | ]; 83 | -------------------------------------------------------------------------------- /res/palettes/greyscale.ts: -------------------------------------------------------------------------------- 1 | export const PALETTE_GREYSCALE = [ 2 | 'minecraft:andesite', 3 | 'minecraft:basalt', 4 | 'minecraft:bedrock', 5 | 'minecraft:blackstone', 6 | 'minecraft:black_concrete', 7 | 'minecraft:black_concrete_powder', 8 | 'minecraft:black_terracotta', 9 | 'minecraft:black_wool', 10 | 'minecraft:bone_block', 11 | 'minecraft:chiseled_polished_blackstone', 12 | 'minecraft:chiseled_quartz_block', 13 | 'minecraft:chiseled_stone_bricks', 14 | 'minecraft:coal_block', 15 | 'minecraft:coal_ore', 16 | 'minecraft:cobblestone', 17 | 'minecraft:cracked_polished_blackstone_bricks', 18 | 'minecraft:cracked_stone_bricks', 19 | 'minecraft:cyan_terracotta', 20 | 'minecraft:dead_brain_coral_block', 21 | 'minecraft:dead_bubble_coral_block', 22 | 'minecraft:dead_fire_coral_block', 23 | 'minecraft:dead_horn_coral_block', 24 | 'minecraft:dead_tube_coral_block', 25 | 'minecraft:diorite', 26 | 'minecraft:gravel', 27 | 'minecraft:gray_concrete', 28 | 'minecraft:gray_concrete_powder', 29 | 'minecraft:gray_wool', 30 | 'minecraft:iron_block', 31 | 'minecraft:light_gray_concrete', 32 | 'minecraft:light_gray_concrete_powder', 33 | 'minecraft:light_gray_wool', 34 | 'minecraft:netherite_block', 35 | 'minecraft:polished_andesite', 36 | 'minecraft:polished_basalt', 37 | 'minecraft:polished_blackstone', 38 | 'minecraft:polished_blackstone_bricks', 39 | 'minecraft:polished_diorite', 40 | 'minecraft:quartz_block', 41 | 'minecraft:quartz_bricks', 42 | 'minecraft:quartz_pillar', 43 | 'minecraft:smooth_quartz', 44 | 'minecraft:smooth_stone', 45 | 'minecraft:snow_block', 46 | 'minecraft:stone', 47 | 'minecraft:stone_bricks', 48 | 'minecraft:white_concrete', 49 | 'minecraft:white_concrete_powder', 50 | 'minecraft:white_wool', 51 | ]; 52 | -------------------------------------------------------------------------------- /res/samples/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasDower/ObjToSchematic/b611b52c7a87cad99127908598bab019fe9e7c3f/res/samples/editor.png -------------------------------------------------------------------------------- /res/samples/noodles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasDower/ObjToSchematic/b611b52c7a87cad99127908598bab019fe9e7c3f/res/samples/noodles.png -------------------------------------------------------------------------------- /res/samples/skull.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasDower/ObjToSchematic/b611b52c7a87cad99127908598bab019fe9e7c3f/res/samples/skull.jpg -------------------------------------------------------------------------------- /res/samples/skull.mtl: -------------------------------------------------------------------------------- 1 | newmtl skull 2 | Kd 1.000000 1.000000 1.000000 3 | map_Kd skull.jpg 4 | -------------------------------------------------------------------------------- /res/samples/skull.txt: -------------------------------------------------------------------------------- 1 | "Homo erectus georgicus" (https://skfb.ly/6ADT8) by Geoffrey Marchal is licensed under Creative Commons Attribution-NonCommercial (http://creativecommons.org/licenses/by-nc/4.0/). 2 | 3 | Model simplified in Blender -------------------------------------------------------------------------------- /res/shaders/block_fragment.fs: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform sampler2D u_texture; 4 | uniform float u_atlasSize; 5 | uniform bool u_nightVision; 6 | 7 | varying float v_lighting; 8 | varying vec4 v_occlusion; 9 | varying vec2 v_texcoord; 10 | varying vec2 v_blockTexcoord; 11 | varying float v_blockLighting; 12 | varying float v_sliced; 13 | 14 | float dither8x8(vec2 position, float alpha) { 15 | int x = int(mod(position.x, 8.0)); 16 | int y = int(mod(position.y, 8.0)); 17 | int index = x + y * 8; 18 | float limit = 0.0; 19 | 20 | if (x < 8) { 21 | if (index == 0) limit = 0.015625; 22 | if (index == 1) limit = 0.515625; 23 | if (index == 2) limit = 0.140625; 24 | if (index == 3) limit = 0.640625; 25 | if (index == 4) limit = 0.046875; 26 | if (index == 5) limit = 0.546875; 27 | if (index == 6) limit = 0.171875; 28 | if (index == 7) limit = 0.671875; 29 | if (index == 8) limit = 0.765625; 30 | if (index == 9) limit = 0.265625; 31 | if (index == 10) limit = 0.890625; 32 | if (index == 11) limit = 0.390625; 33 | if (index == 12) limit = 0.796875; 34 | if (index == 13) limit = 0.296875; 35 | if (index == 14) limit = 0.921875; 36 | if (index == 15) limit = 0.421875; 37 | if (index == 16) limit = 0.203125; 38 | if (index == 17) limit = 0.703125; 39 | if (index == 18) limit = 0.078125; 40 | if (index == 19) limit = 0.578125; 41 | if (index == 20) limit = 0.234375; 42 | if (index == 21) limit = 0.734375; 43 | if (index == 22) limit = 0.109375; 44 | if (index == 23) limit = 0.609375; 45 | if (index == 24) limit = 0.953125; 46 | if (index == 25) limit = 0.453125; 47 | if (index == 26) limit = 0.828125; 48 | if (index == 27) limit = 0.328125; 49 | if (index == 28) limit = 0.984375; 50 | if (index == 29) limit = 0.484375; 51 | if (index == 30) limit = 0.859375; 52 | if (index == 31) limit = 0.359375; 53 | if (index == 32) limit = 0.0625; 54 | if (index == 33) limit = 0.5625; 55 | if (index == 34) limit = 0.1875; 56 | if (index == 35) limit = 0.6875; 57 | if (index == 36) limit = 0.03125; 58 | if (index == 37) limit = 0.53125; 59 | if (index == 38) limit = 0.15625; 60 | if (index == 39) limit = 0.65625; 61 | if (index == 40) limit = 0.8125; 62 | if (index == 41) limit = 0.3125; 63 | if (index == 42) limit = 0.9375; 64 | if (index == 43) limit = 0.4375; 65 | if (index == 44) limit = 0.78125; 66 | if (index == 45) limit = 0.28125; 67 | if (index == 46) limit = 0.90625; 68 | if (index == 47) limit = 0.40625; 69 | if (index == 48) limit = 0.25; 70 | if (index == 49) limit = 0.75; 71 | if (index == 50) limit = 0.125; 72 | if (index == 51) limit = 0.625; 73 | if (index == 52) limit = 0.21875; 74 | if (index == 53) limit = 0.71875; 75 | if (index == 54) limit = 0.09375; 76 | if (index == 55) limit = 0.59375; 77 | if (index == 56) limit = 0.9999; 78 | if (index == 57) limit = 0.5; 79 | if (index == 58) limit = 0.875; 80 | if (index == 59) limit = 0.375; 81 | if (index == 60) limit = 0.96875; 82 | if (index == 61) limit = 0.46875; 83 | if (index == 62) limit = 0.84375; 84 | if (index == 63) limit = 0.34375; 85 | } 86 | 87 | return alpha < limit ? 0.0 : 1.0; 88 | } 89 | 90 | void main() { 91 | float u = v_texcoord.x; 92 | float v = v_texcoord.y; 93 | 94 | float a = v_occlusion.x; 95 | float b = v_occlusion.y; 96 | float c = v_occlusion.z; 97 | float d = v_occlusion.w; 98 | float g = v*(u*b + (1.0-u)*d) + (1.0-v)*(u*a + (1.0-u)*c); 99 | 100 | vec2 tex = v_blockTexcoord + (v_texcoord / (u_atlasSize * 3.0)); 101 | vec4 diffuse = texture2D(u_texture, tex).rgba; 102 | 103 | float alpha = dither8x8(gl_FragCoord.xy, diffuse.a); 104 | if (alpha < 0.5) 105 | { 106 | discard; 107 | } 108 | 109 | if(v_sliced > 0.5) { 110 | discard; 111 | } 112 | 113 | gl_FragColor = vec4(diffuse.rgb * v_lighting * g * (u_nightVision ? 1.0 : v_blockLighting), 1.0); 114 | } 115 | -------------------------------------------------------------------------------- /res/shaders/block_vertex.vs: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform mat4 u_worldViewProjection; 4 | uniform sampler2D u_texture; 5 | uniform float u_voxelSize; 6 | uniform vec3 u_gridOffset; 7 | uniform bool u_nightVision; 8 | uniform float u_sliceHeight; 9 | 10 | attribute vec3 position; 11 | attribute vec3 normal; 12 | attribute vec4 occlusion; 13 | attribute vec2 texcoord; 14 | attribute vec2 blockTexcoord; 15 | attribute vec3 blockPosition; 16 | attribute float lighting; 17 | 18 | varying float v_lighting; 19 | varying vec4 v_occlusion; 20 | varying vec2 v_texcoord; 21 | varying vec2 v_blockTexcoord; 22 | varying float v_blockLighting; 23 | varying float v_sliced; 24 | 25 | vec3 light = vec3(0.78, 0.98, 0.59); 26 | 27 | void main() { 28 | v_texcoord = texcoord; 29 | v_occlusion = occlusion; 30 | v_blockTexcoord = blockTexcoord; 31 | v_lighting = dot(light, abs(normal)); 32 | v_blockLighting = lighting; 33 | 34 | v_sliced = blockPosition.y > u_sliceHeight ? 1.0 : 0.0; 35 | 36 | // Disable ambient occlusion on the top layer of the slice view 37 | bool isBlockOnTopLayer = (v_sliced < 0.5 && abs(blockPosition.y - u_sliceHeight) < 0.5); 38 | if (isBlockOnTopLayer) 39 | { 40 | 41 | if (normal.y > 0.5) 42 | { 43 | v_occlusion = vec4(1.0, 1.0, 1.0, 1.0); 44 | } 45 | else if (normal.x > 0.5 || normal.z > 0.5 || normal.x < -0.5 || normal.z < -0.5) 46 | { 47 | v_occlusion = vec4(1.0, v_occlusion.y, 1.0, v_occlusion.w); 48 | } 49 | } 50 | 51 | gl_Position = u_worldViewProjection * vec4((position + u_gridOffset) * u_voxelSize, 1.0); 52 | } 53 | -------------------------------------------------------------------------------- /res/shaders/debug_fragment.fs: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | varying vec4 v_colour; 4 | 5 | void main() { 6 | gl_FragColor = v_colour; 7 | } 8 | -------------------------------------------------------------------------------- /res/shaders/debug_vertex.vs: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform mat4 u_worldViewProjection; 4 | uniform vec3 u_worldOffset; 5 | 6 | attribute vec3 position; 7 | attribute vec4 colour; 8 | 9 | varying vec4 v_colour; 10 | 11 | void main() { 12 | v_colour = colour; 13 | gl_Position = u_worldViewProjection * vec4(position + u_worldOffset, 1.0); 14 | } 15 | -------------------------------------------------------------------------------- /res/shaders/solid_tri_fragment.fs: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform vec3 u_lightWorldPos; 4 | uniform vec4 u_fillColour; 5 | uniform vec3 u_cameraDir; 6 | uniform float u_fresnelExponent; 7 | uniform float u_fresnelMix; 8 | 9 | varying vec3 v_lighting; 10 | varying vec3 v_normal; 11 | 12 | float dither8x8(vec2 position, float alpha) { 13 | int x = int(mod(position.x, 8.0)); 14 | int y = int(mod(position.y, 8.0)); 15 | int index = x + y * 8; 16 | float limit = 0.0; 17 | 18 | if (x < 8) { 19 | if (index == 0) limit = 0.015625; 20 | if (index == 1) limit = 0.515625; 21 | if (index == 2) limit = 0.140625; 22 | if (index == 3) limit = 0.640625; 23 | if (index == 4) limit = 0.046875; 24 | if (index == 5) limit = 0.546875; 25 | if (index == 6) limit = 0.171875; 26 | if (index == 7) limit = 0.671875; 27 | if (index == 8) limit = 0.765625; 28 | if (index == 9) limit = 0.265625; 29 | if (index == 10) limit = 0.890625; 30 | if (index == 11) limit = 0.390625; 31 | if (index == 12) limit = 0.796875; 32 | if (index == 13) limit = 0.296875; 33 | if (index == 14) limit = 0.921875; 34 | if (index == 15) limit = 0.421875; 35 | if (index == 16) limit = 0.203125; 36 | if (index == 17) limit = 0.703125; 37 | if (index == 18) limit = 0.078125; 38 | if (index == 19) limit = 0.578125; 39 | if (index == 20) limit = 0.234375; 40 | if (index == 21) limit = 0.734375; 41 | if (index == 22) limit = 0.109375; 42 | if (index == 23) limit = 0.609375; 43 | if (index == 24) limit = 0.953125; 44 | if (index == 25) limit = 0.453125; 45 | if (index == 26) limit = 0.828125; 46 | if (index == 27) limit = 0.328125; 47 | if (index == 28) limit = 0.984375; 48 | if (index == 29) limit = 0.484375; 49 | if (index == 30) limit = 0.859375; 50 | if (index == 31) limit = 0.359375; 51 | if (index == 32) limit = 0.0625; 52 | if (index == 33) limit = 0.5625; 53 | if (index == 34) limit = 0.1875; 54 | if (index == 35) limit = 0.6875; 55 | if (index == 36) limit = 0.03125; 56 | if (index == 37) limit = 0.53125; 57 | if (index == 38) limit = 0.15625; 58 | if (index == 39) limit = 0.65625; 59 | if (index == 40) limit = 0.8125; 60 | if (index == 41) limit = 0.3125; 61 | if (index == 42) limit = 0.9375; 62 | if (index == 43) limit = 0.4375; 63 | if (index == 44) limit = 0.78125; 64 | if (index == 45) limit = 0.28125; 65 | if (index == 46) limit = 0.90625; 66 | if (index == 47) limit = 0.40625; 67 | if (index == 48) limit = 0.25; 68 | if (index == 49) limit = 0.75; 69 | if (index == 50) limit = 0.125; 70 | if (index == 51) limit = 0.625; 71 | if (index == 52) limit = 0.21875; 72 | if (index == 53) limit = 0.71875; 73 | if (index == 54) limit = 0.09375; 74 | if (index == 55) limit = 0.59375; 75 | if (index == 56) limit = 1.0; 76 | if (index == 57) limit = 0.5; 77 | if (index == 58) limit = 0.875; 78 | if (index == 59) limit = 0.375; 79 | if (index == 60) limit = 0.96875; 80 | if (index == 61) limit = 0.46875; 81 | if (index == 62) limit = 0.84375; 82 | if (index == 63) limit = 0.34375; 83 | } 84 | 85 | return alpha < limit ? 0.0 : 1.0; 86 | } 87 | 88 | void main() { 89 | float alpha = dither8x8(gl_FragCoord.xy, u_fillColour.a); 90 | if (alpha < 0.5) 91 | { 92 | discard; 93 | } 94 | 95 | float lighting = abs(dot(v_normal, normalize(u_lightWorldPos))); 96 | lighting = (clamp(lighting, 0.0, 1.0) * 0.66) + 0.33; 97 | 98 | vec3 preFresnelColour = u_fillColour.rgb * lighting; 99 | float fresnel = 1.0 - abs(dot(u_cameraDir, v_normal)); 100 | float fresnelFactor = pow(fresnel, u_fresnelExponent) * u_fresnelMix; 101 | 102 | vec3 postFresnelColour = mix(preFresnelColour, vec3(1.0, 1.0, 1.0), fresnelFactor); 103 | 104 | gl_FragColor = vec4(postFresnelColour, 1.0); 105 | } 106 | -------------------------------------------------------------------------------- /res/shaders/solid_tri_vertex.vs: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform vec3 u_lightWorldPos; 4 | uniform mat4 u_worldViewProjection; 5 | uniform mat4 u_worldInverseTranspose; 6 | uniform vec4 u_fillColour; 7 | uniform vec3 u_cameraDir; 8 | 9 | attribute vec3 position; 10 | attribute vec2 texcoord; 11 | attribute vec3 normal; 12 | 13 | varying vec3 v_normal; 14 | 15 | void main() { 16 | v_normal = normal; 17 | 18 | gl_Position = u_worldViewProjection * vec4(position, 1.0); 19 | } 20 | -------------------------------------------------------------------------------- /res/shaders/texture_tri_fragment.fs: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform vec3 u_lightWorldPos; 4 | uniform vec3 u_cameraDir; 5 | 6 | uniform sampler2D u_texture; 7 | uniform sampler2D u_alpha; 8 | uniform int u_alphaChannel; 9 | uniform float u_alphaFactor; 10 | uniform float u_fresnelExponent; 11 | uniform float u_fresnelMix; 12 | 13 | varying vec2 v_texcoord; 14 | varying vec3 v_normal; 15 | 16 | float dither8x8(vec2 position, float alpha) { 17 | int x = int(mod(position.x, 8.0)); 18 | int y = int(mod(position.y, 8.0)); 19 | int index = x + y * 8; 20 | float limit = 0.0; 21 | 22 | if (x < 8) { 23 | if (index == 0) limit = 0.015625; 24 | if (index == 1) limit = 0.515625; 25 | if (index == 2) limit = 0.140625; 26 | if (index == 3) limit = 0.640625; 27 | if (index == 4) limit = 0.046875; 28 | if (index == 5) limit = 0.546875; 29 | if (index == 6) limit = 0.171875; 30 | if (index == 7) limit = 0.671875; 31 | if (index == 8) limit = 0.765625; 32 | if (index == 9) limit = 0.265625; 33 | if (index == 10) limit = 0.890625; 34 | if (index == 11) limit = 0.390625; 35 | if (index == 12) limit = 0.796875; 36 | if (index == 13) limit = 0.296875; 37 | if (index == 14) limit = 0.921875; 38 | if (index == 15) limit = 0.421875; 39 | if (index == 16) limit = 0.203125; 40 | if (index == 17) limit = 0.703125; 41 | if (index == 18) limit = 0.078125; 42 | if (index == 19) limit = 0.578125; 43 | if (index == 20) limit = 0.234375; 44 | if (index == 21) limit = 0.734375; 45 | if (index == 22) limit = 0.109375; 46 | if (index == 23) limit = 0.609375; 47 | if (index == 24) limit = 0.953125; 48 | if (index == 25) limit = 0.453125; 49 | if (index == 26) limit = 0.828125; 50 | if (index == 27) limit = 0.328125; 51 | if (index == 28) limit = 0.984375; 52 | if (index == 29) limit = 0.484375; 53 | if (index == 30) limit = 0.859375; 54 | if (index == 31) limit = 0.359375; 55 | if (index == 32) limit = 0.0625; 56 | if (index == 33) limit = 0.5625; 57 | if (index == 34) limit = 0.1875; 58 | if (index == 35) limit = 0.6875; 59 | if (index == 36) limit = 0.03125; 60 | if (index == 37) limit = 0.53125; 61 | if (index == 38) limit = 0.15625; 62 | if (index == 39) limit = 0.65625; 63 | if (index == 40) limit = 0.8125; 64 | if (index == 41) limit = 0.3125; 65 | if (index == 42) limit = 0.9375; 66 | if (index == 43) limit = 0.4375; 67 | if (index == 44) limit = 0.78125; 68 | if (index == 45) limit = 0.28125; 69 | if (index == 46) limit = 0.90625; 70 | if (index == 47) limit = 0.40625; 71 | if (index == 48) limit = 0.25; 72 | if (index == 49) limit = 0.75; 73 | if (index == 50) limit = 0.125; 74 | if (index == 51) limit = 0.625; 75 | if (index == 52) limit = 0.21875; 76 | if (index == 53) limit = 0.71875; 77 | if (index == 54) limit = 0.09375; 78 | if (index == 55) limit = 0.59375; 79 | if (index == 56) limit = 1.0; 80 | if (index == 57) limit = 0.5; 81 | if (index == 58) limit = 0.875; 82 | if (index == 59) limit = 0.375; 83 | if (index == 60) limit = 0.96875; 84 | if (index == 61) limit = 0.46875; 85 | if (index == 62) limit = 0.84375; 86 | if (index == 63) limit = 0.34375; 87 | } 88 | 89 | return alpha < limit ? 0.0 : 1.0; 90 | } 91 | 92 | /* 93 | const float ditherThreshold[64] = float[64]( 94 | 0.015625, 0.51562, 0.14062, 0.64062, 0.04687, 0.54687, 0.17187, 0.67187, 95 | 0.76562, 0.26562, 0.89062, 0.39062, 0.79687, 0.29687, 0.92187, 0.42187, 96 | 0.20312, 0.70312, 0.07812, 0.57812, 0.23437, 0.73437, 0.10937, 0.60937, 97 | 0.95312, 0.45312, 0.82812, 0.32812, 0.98437, 0.48437, 0.85937, 0.35937, 98 | 0.0625, 0.5625, 0.1875, 0.6875, 0.03125, 0.53125, 0.15625, 0.65625, 99 | 0.8125, 0.3125, 0.9375, 0.4375, 0.78125, 0.28125, 0.90625, 0.40625, 100 | 0.25, 0.75, 0.125, 0.625, 0.21875, 0.71875, 0.09375, 0.59375, 101 | 1.0, 0.5, 0.875, 0.375, 0.96875, 0.46875, 0.84375, 0.34375 102 | ); 103 | */ 104 | 105 | void main() { 106 | vec2 tex = vec2(v_texcoord.x, 1.0 - v_texcoord.y); 107 | vec4 diffuse = texture2D(u_texture, tex).rgba; 108 | vec4 alphaSample = texture2D(u_alpha, tex); 109 | 110 | float alpha = 1.0; 111 | if (u_alphaChannel == 0) { 112 | alpha = alphaSample.r; 113 | } else if (u_alphaChannel == 1) { 114 | alpha = alphaSample.g; 115 | } else if (u_alphaChannel == 2) { 116 | alpha = alphaSample.b; 117 | } else if (u_alphaChannel == 3) { 118 | alpha = alphaSample.a; 119 | } 120 | 121 | alpha *= u_alphaFactor; 122 | 123 | alpha = dither8x8(gl_FragCoord.xy, alpha); 124 | if (alpha < 0.5) 125 | { 126 | discard; 127 | } 128 | 129 | float lighting = abs(dot(v_normal, normalize(u_lightWorldPos))); 130 | lighting = (clamp(lighting, 0.0, 1.0) * 0.66) + 0.33; 131 | 132 | vec3 preFresnelColour = diffuse.rgb * lighting; 133 | float fresnel = 1.0 - abs(dot(u_cameraDir, v_normal)); 134 | float fresnelFactor = pow(fresnel, u_fresnelExponent) * u_fresnelMix; 135 | 136 | vec3 postFresnelColour = mix(preFresnelColour, vec3(1.0, 1.0, 1.0), fresnelFactor); 137 | 138 | gl_FragColor = vec4(postFresnelColour, 1.0); 139 | } 140 | -------------------------------------------------------------------------------- /res/shaders/texture_tri_vertex.vs: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform vec3 u_lightWorldPos; 4 | uniform mat4 u_worldViewProjection; 5 | uniform mat4 u_worldInverseTranspose; 6 | uniform vec3 u_cameraDir; 7 | 8 | attribute vec3 position; 9 | attribute vec2 texcoord; 10 | attribute vec3 normal; 11 | 12 | varying vec2 v_texcoord; 13 | varying vec3 v_normal; 14 | 15 | void main() { 16 | v_texcoord = texcoord; 17 | v_normal = normal; 18 | 19 | gl_Position = u_worldViewProjection * vec4(position, 1.0); 20 | } 21 | -------------------------------------------------------------------------------- /res/shaders/voxel_fragment.fs: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform bool u_ambientOcclusion; 4 | uniform float u_globalAlpha; 5 | 6 | varying float v_lighting; 7 | varying vec4 v_occlusion; 8 | varying vec2 v_texcoord; 9 | varying vec4 v_colour; 10 | 11 | float dither8x8(vec2 position, float alpha) { 12 | int x = int(mod(position.x, 8.0)); 13 | int y = int(mod(position.y, 8.0)); 14 | int index = x + y * 8; 15 | float limit = 0.0; 16 | 17 | if (x < 8) { 18 | if (index == 0) limit = 0.015625; 19 | if (index == 1) limit = 0.515625; 20 | if (index == 2) limit = 0.140625; 21 | if (index == 3) limit = 0.640625; 22 | if (index == 4) limit = 0.046875; 23 | if (index == 5) limit = 0.546875; 24 | if (index == 6) limit = 0.171875; 25 | if (index == 7) limit = 0.671875; 26 | if (index == 8) limit = 0.765625; 27 | if (index == 9) limit = 0.265625; 28 | if (index == 10) limit = 0.890625; 29 | if (index == 11) limit = 0.390625; 30 | if (index == 12) limit = 0.796875; 31 | if (index == 13) limit = 0.296875; 32 | if (index == 14) limit = 0.921875; 33 | if (index == 15) limit = 0.421875; 34 | if (index == 16) limit = 0.203125; 35 | if (index == 17) limit = 0.703125; 36 | if (index == 18) limit = 0.078125; 37 | if (index == 19) limit = 0.578125; 38 | if (index == 20) limit = 0.234375; 39 | if (index == 21) limit = 0.734375; 40 | if (index == 22) limit = 0.109375; 41 | if (index == 23) limit = 0.609375; 42 | if (index == 24) limit = 0.953125; 43 | if (index == 25) limit = 0.453125; 44 | if (index == 26) limit = 0.828125; 45 | if (index == 27) limit = 0.328125; 46 | if (index == 28) limit = 0.984375; 47 | if (index == 29) limit = 0.484375; 48 | if (index == 30) limit = 0.859375; 49 | if (index == 31) limit = 0.359375; 50 | if (index == 32) limit = 0.0625; 51 | if (index == 33) limit = 0.5625; 52 | if (index == 34) limit = 0.1875; 53 | if (index == 35) limit = 0.6875; 54 | if (index == 36) limit = 0.03125; 55 | if (index == 37) limit = 0.53125; 56 | if (index == 38) limit = 0.15625; 57 | if (index == 39) limit = 0.65625; 58 | if (index == 40) limit = 0.8125; 59 | if (index == 41) limit = 0.3125; 60 | if (index == 42) limit = 0.9375; 61 | if (index == 43) limit = 0.4375; 62 | if (index == 44) limit = 0.78125; 63 | if (index == 45) limit = 0.28125; 64 | if (index == 46) limit = 0.90625; 65 | if (index == 47) limit = 0.40625; 66 | if (index == 48) limit = 0.25; 67 | if (index == 49) limit = 0.75; 68 | if (index == 50) limit = 0.125; 69 | if (index == 51) limit = 0.625; 70 | if (index == 52) limit = 0.21875; 71 | if (index == 53) limit = 0.71875; 72 | if (index == 54) limit = 0.09375; 73 | if (index == 55) limit = 0.59375; 74 | if (index == 56) limit = 0.9999; 75 | if (index == 57) limit = 0.5; 76 | if (index == 58) limit = 0.875; 77 | if (index == 59) limit = 0.375; 78 | if (index == 60) limit = 0.96875; 79 | if (index == 61) limit = 0.46875; 80 | if (index == 62) limit = 0.84375; 81 | if (index == 63) limit = 0.34375; 82 | } 83 | 84 | return alpha < limit ? 0.0 : 1.0; 85 | } 86 | 87 | void main() { 88 | float u = v_texcoord.x; 89 | float v = v_texcoord.y; 90 | 91 | float g = 1.0; 92 | if (u_ambientOcclusion) 93 | { 94 | float a = v_occlusion.x; 95 | float b = v_occlusion.y; 96 | float c = v_occlusion.z; 97 | float d = v_occlusion.w; 98 | g = v*(u*b + (1.0-u)*d) + (1.0-v)*(u*a + (1.0-u)*c); 99 | } 100 | 101 | float alpha = dither8x8(gl_FragCoord.xy, v_colour.a); 102 | if (alpha < 0.5) 103 | { 104 | discard; 105 | } 106 | 107 | gl_FragColor = vec4(v_colour.rgb * (v_lighting * g), 1.0); 108 | } 109 | -------------------------------------------------------------------------------- /res/shaders/voxel_vertex.vs: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform mat4 u_worldViewProjection; 4 | uniform float u_voxelSize; 5 | uniform vec3 u_gridOffset; 6 | uniform bool u_ambientOcclusion; 7 | 8 | attribute vec3 position; 9 | attribute vec3 normal; 10 | attribute vec4 colour; 11 | attribute vec4 occlusion; 12 | attribute vec2 texcoord; 13 | 14 | varying float v_lighting; 15 | varying vec4 v_occlusion; 16 | varying vec2 v_texcoord; 17 | varying vec4 v_colour; 18 | 19 | vec3 light = vec3(0.78, 0.98, 0.59); 20 | 21 | void main() { 22 | v_lighting = dot(light, abs(normal)); 23 | v_occlusion = occlusion; 24 | v_texcoord = texcoord; 25 | v_colour = colour; 26 | 27 | gl_Position = u_worldViewProjection * vec4((position.xyz + u_gridOffset) * u_voxelSize, 1.0); 28 | } 29 | -------------------------------------------------------------------------------- /res/static/debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasDower/ObjToSchematic/b611b52c7a87cad99127908598bab019fe9e7c3f/res/static/debug.png -------------------------------------------------------------------------------- /res/static/debug_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasDower/ObjToSchematic/b611b52c7a87cad99127908598bab019fe9e7c3f/res/static/debug_alpha.png -------------------------------------------------------------------------------- /res/static/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasDower/ObjToSchematic/b611b52c7a87cad99127908598bab019fe9e7c3f/res/static/icon.icns -------------------------------------------------------------------------------- /res/static/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasDower/ObjToSchematic/b611b52c7a87cad99127908598bab019fe9e7c3f/res/static/icon.ico -------------------------------------------------------------------------------- /res/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasDower/ObjToSchematic/b611b52c7a87cad99127908598bab019fe9e7c3f/res/static/icon.png -------------------------------------------------------------------------------- /src/analytics.ts: -------------------------------------------------------------------------------- 1 | import { AppConfig } from './config'; 2 | import { AppConsole } from './ui/console'; 3 | const gtag = require('ga-gtag'); 4 | 5 | export class AppAnalytics { 6 | private _ready: boolean; 7 | 8 | private static _instance: AppAnalytics; 9 | public static get Get() { 10 | return this._instance || (this._instance = new this()); 11 | } 12 | 13 | private constructor() { 14 | this._ready = false; 15 | } 16 | 17 | public static Init() { 18 | gtag.install('G-W0SCWQ7HGJ', { 'send_page_view': true }); 19 | gtag.gtag('js', new Date()); 20 | gtag.gtag('config', 'G-W0SCWQ7HGJ', AppConfig.Get.VERSION_TYPE === 'd' ? { 'debug_mode': true } : {}); 21 | this.Get._ready = true; 22 | 23 | this.Event('init', { 24 | version: AppConfig.Get.getVersionString(), 25 | }) 26 | } 27 | 28 | public static Event(id: string, attributes?: any) { 29 | if (this.Get._ready) { 30 | console.log('[Analytics]: Tracked event', id, attributes); 31 | gtag.gtag('event', id, Object.assign(attributes ?? {}, AppConfig.Get.VERSION_TYPE === 'd' ? { 'debug_mode': true } : {})); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/atlas.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import ATLAS_VANILLA from '../res/atlases/vanilla.atlas'; 4 | import { RGBA } from './colour'; 5 | import { AppTypes, AppUtil, TOptional, UV } from './util'; 6 | import { ASSERT } from './util/error_util'; 7 | import { AppPaths } from './util/path_util'; 8 | 9 | export type TAtlasBlockFace = { 10 | name: string, 11 | texcoord: UV, 12 | colour: RGBA, 13 | std: number, 14 | } 15 | 16 | export type TAtlasBlock = { 17 | name: string; 18 | colour: RGBA; 19 | faces: { 20 | up: TAtlasBlockFace, 21 | down: TAtlasBlockFace, 22 | north: TAtlasBlockFace, 23 | east: TAtlasBlockFace, 24 | south: TAtlasBlockFace, 25 | west: TAtlasBlockFace, 26 | }; 27 | } 28 | 29 | /** 30 | * Atlases, unlike palettes, are not currently designed to be user-facing or 31 | * programmatically created. This class simply facilitates loading .atlas 32 | * files. 33 | */ 34 | export class Atlas { 35 | public static ATLAS_NAME_REGEX: RegExp = /^[a-zA-Z\-]+$/; 36 | 37 | private _blocks: Map; 38 | private _atlasSize: number; 39 | private _atlasName: string; 40 | 41 | private constructor(atlasName: string) { 42 | this._blocks = new Map(); 43 | this._atlasSize = 0; 44 | this._atlasName = atlasName; 45 | } 46 | 47 | public getBlocks() { 48 | return this._blocks; 49 | } 50 | 51 | public static load(atlasName: string): TOptional { 52 | ASSERT(atlasName === 'vanilla'); 53 | 54 | const atlas = new Atlas(atlasName); 55 | 56 | const atlasJSON = JSON.parse(ATLAS_VANILLA); 57 | 58 | ASSERT(atlasJSON.formatVersion === 3, `The '${atlasName}' texture atlas uses an outdated format and needs to be recreated`); 59 | 60 | const atlasData = atlasJSON; 61 | atlas._atlasSize = atlasData.atlasSize; 62 | 63 | const getTextureUV = (name: string) => { 64 | const tex = atlasData.textures[name]; 65 | return new UV( 66 | (3 * tex.atlasColumn + 1) / (atlas._atlasSize * 3), 67 | (3 * tex.atlasRow + 1) / (atlas._atlasSize * 3), 68 | ); 69 | }; 70 | 71 | for (const block of atlasData.blocks) { 72 | ASSERT(AppUtil.Text.isNamespacedBlock(block.name), 'Atlas block not namespaced'); 73 | 74 | const atlasBlock: TAtlasBlock = { 75 | name: block.name, 76 | colour: block.colour, 77 | faces: { 78 | up: { 79 | name: block.faces.up, 80 | texcoord: getTextureUV(block.faces.up), 81 | std: atlasData.textures[block.faces.up].std, 82 | colour: atlasData.textures[block.faces.up].colour, 83 | }, 84 | down: { 85 | name: block.faces.down, 86 | texcoord: getTextureUV(block.faces.down), 87 | std: atlasData.textures[block.faces.down].std, 88 | colour: atlasData.textures[block.faces.down].colour, 89 | }, 90 | north: { 91 | name: block.faces.north, 92 | texcoord: getTextureUV(block.faces.north), 93 | std: atlasData.textures[block.faces.north].std, 94 | colour: atlasData.textures[block.faces.north].colour, 95 | }, 96 | east: { 97 | name: block.faces.east, 98 | texcoord: getTextureUV(block.faces.east), 99 | std: atlasData.textures[block.faces.east].std, 100 | colour: atlasData.textures[block.faces.east].colour, 101 | }, 102 | south: { 103 | name: block.faces.south, 104 | texcoord: getTextureUV(block.faces.south), 105 | std: atlasData.textures[block.faces.south].std, 106 | colour: atlasData.textures[block.faces.south].colour, 107 | }, 108 | west: { 109 | name: block.faces.west, 110 | texcoord: getTextureUV(block.faces.west), 111 | std: atlasData.textures[block.faces.west].std, 112 | colour: atlasData.textures[block.faces.west].colour, 113 | }, 114 | }, 115 | }; 116 | 117 | atlas._blocks.set(block.name, atlasBlock); 118 | } 119 | 120 | return atlas; 121 | } 122 | 123 | public getAtlasSize(): number { 124 | return this._atlasSize; 125 | } 126 | 127 | public getAtlasTexturePath() { 128 | return path.join(AppPaths.Get.atlases, `./${this._atlasName}.png`); 129 | } 130 | 131 | /* 132 | public getBlocks(): TAtlasBlock[] { 133 | return Array.from(this._blocks.values()); 134 | } 135 | */ 136 | 137 | public hasBlock(blockName: AppTypes.TNamespacedBlockName): boolean { 138 | return this._blocks.has(blockName); 139 | } 140 | 141 | public static getVanillaAtlas(): TOptional { 142 | return Atlas.load('vanilla'); 143 | } 144 | 145 | private static _isValidAtlasName(atlasName: string): boolean { 146 | return atlasName.length > 0 && Atlas.ATLAS_NAME_REGEX.test(atlasName); 147 | } 148 | 149 | private static _getAtlasPath(atlasName: string): string { 150 | return path.join(AppPaths.Get.atlases, `./${atlasName}.atlas`); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/block_atlas.ts: -------------------------------------------------------------------------------- 1 | import { RGBA } from './colour'; 2 | import { UV } from './util'; 3 | 4 | export interface TextureInfo { 5 | name: string 6 | texcoord: UV 7 | } 8 | 9 | export interface FaceInfo { 10 | [face: string]: TextureInfo, 11 | up: TextureInfo, 12 | down: TextureInfo, 13 | north: TextureInfo, 14 | south: TextureInfo, 15 | east: TextureInfo, 16 | west: TextureInfo 17 | } 18 | 19 | export interface BlockInfo { 20 | name: string; 21 | colour: RGBA; 22 | faces: FaceInfo 23 | } 24 | -------------------------------------------------------------------------------- /src/bounds.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from './vector'; 2 | 3 | /** 4 | * A 3D cuboid volume defined by two opposing corners 5 | */ 6 | export class Bounds { 7 | private _min: Vector3; 8 | private _max: Vector3; 9 | 10 | constructor(min: Vector3, max: Vector3) { 11 | this._min = min; 12 | this._max = max; 13 | } 14 | 15 | public extendByPoint(point: Vector3) { 16 | this._min = Vector3.min(this._min, point); 17 | this._max = Vector3.max(this._max, point); 18 | } 19 | 20 | public extendByVolume(volume: Bounds) { 21 | this._min = Vector3.min(this._min, volume._min); 22 | this._max = Vector3.max(this._max, volume._max); 23 | } 24 | 25 | // TODO: rename to `createInfinitesimalBounds` 26 | public static getInfiniteBounds() { 27 | return new Bounds( 28 | new Vector3(Infinity, Infinity, Infinity), 29 | new Vector3(-Infinity, -Infinity, -Infinity), 30 | ); 31 | } 32 | 33 | public get min() { 34 | return this._min; 35 | } 36 | 37 | public get max() { 38 | return this._max; 39 | } 40 | 41 | // TODO: Rename to `calcCentre` 42 | public getCentre() { 43 | const extents = Vector3.sub(this._max, this._min).divScalar(2); 44 | return Vector3.add(this.min, extents); 45 | } 46 | 47 | // TODO: Rename to `calcDimensions` 48 | public getDimensions() { 49 | return Vector3.sub(this._max, this._min); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { RGBA } from './colour'; 2 | import { LOG } from './util/log_util'; 3 | 4 | export class AppConfig { 5 | /* Singleton */ 6 | private static _instance: AppConfig; 7 | public static get Get() { 8 | return this._instance || (this._instance = new this()); 9 | } 10 | 11 | public readonly RELEASE_MODE; 12 | public readonly MAJOR_VERSION = 0; 13 | public readonly MINOR_VERSION = 9; 14 | public readonly HOTFIX_VERSION = 0; 15 | public readonly VERSION_TYPE: 'd' | 'a' | 'r' = 'r'; // dev, alpha, or release build 16 | public readonly MINECRAFT_VERSION = '1.20.1'; 17 | 18 | public readonly LOCALE = 'en-GB'; 19 | public readonly VOXEL_BUFFER_CHUNK_SIZE = 50_000; 20 | public readonly AMBIENT_OCCLUSION_OVERRIDE_CORNER = true; 21 | public readonly USE_WORKER_THREAD = true; 22 | public readonly MULTISAMPLE_COUNT = 16; 23 | public readonly ALPHA_BIAS = 1.0; 24 | public readonly ANGLE_SNAP_RADIUS_DEGREES = 10.0; 25 | public readonly RENDER_TRIANGLE_THRESHOLD = 1_000_000; 26 | public readonly MAXIMUM_IMAGE_MEM_ALLOC = 2048; 27 | public readonly CAMERA_FOV_DEGREES = 30.0; 28 | public readonly CAMERA_MINIMUM_DISTANCE = 0.125; 29 | public readonly CAMERA_DEFAULT_DISTANCE_UNITS = 4.0; 30 | public readonly CAMERA_DEFAULT_AZIMUTH_RADIANS = -1.0; 31 | public readonly CAMERA_DEFAULT_ELEVATION_RADIANS = 1.3; 32 | public readonly CAMERA_SENSITIVITY_ROTATION = 0.005; 33 | public readonly CAMERA_SENSITIVITY_ZOOM = 0.0025; 34 | public readonly CONSTRAINT_MINIMUM_HEIGHT = 3; 35 | public CONSTRAINT_MAXIMUM_HEIGHT = 380; 36 | public readonly SMOOTHNESS_MAX = 3.0; 37 | public readonly CAMERA_SMOOTHING = 1.0; 38 | public readonly VIEWPORT_BACKGROUND_COLOUR: RGBA = { 39 | r: 0.125, 40 | g: 0.125, 41 | b: 0.125, 42 | a: 1.0, 43 | }; 44 | public readonly FRESNEL_EXPONENT = 3.0; 45 | public readonly FRESNEL_MIX = 0.3; 46 | 47 | private constructor() { 48 | this.RELEASE_MODE = this.VERSION_TYPE === 'r'; 49 | } 50 | 51 | public dumpConfig() { 52 | LOG(this); 53 | } 54 | 55 | public getVersionString() { 56 | return `v${this.MAJOR_VERSION}.${this.MINOR_VERSION}.${this.HOTFIX_VERSION}${this.VERSION_TYPE}`; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | 2 | import { AppTypes } from './util'; 3 | import { AppPaths, PathUtil } from './util/path_util'; 4 | 5 | export namespace AppConstants { 6 | export const FACES_PER_VOXEL = 6; 7 | export const VERTICES_PER_FACE = 4; 8 | export const INDICES_PER_VOXEL = 24; 9 | export const COMPONENT_PER_SIZE_OFFSET = FACES_PER_VOXEL * VERTICES_PER_FACE; 10 | 11 | export namespace ComponentSize { 12 | export const LIGHTING = 1; 13 | export const TEXCOORD = 2; 14 | export const POSITION = 3; 15 | export const COLOUR = 4; 16 | export const NORMAL = 3; 17 | export const INDICES = 3; 18 | export const OCCLUSION = 4; 19 | } 20 | 21 | export namespace VoxelMeshBufferComponentOffsets { 22 | export const LIGHTING = ComponentSize.LIGHTING * COMPONENT_PER_SIZE_OFFSET; 23 | export const TEXCOORD = ComponentSize.TEXCOORD * COMPONENT_PER_SIZE_OFFSET; 24 | export const POSITION = ComponentSize.POSITION * COMPONENT_PER_SIZE_OFFSET; 25 | export const COLOUR = ComponentSize.COLOUR * COMPONENT_PER_SIZE_OFFSET; 26 | export const NORMAL = ComponentSize.NORMAL * COMPONENT_PER_SIZE_OFFSET; 27 | export const INDICES = 36; 28 | export const OCCLUSION = ComponentSize.OCCLUSION * COMPONENT_PER_SIZE_OFFSET; 29 | } 30 | 31 | export const DATA_VERSION = 3105; // 1.19 32 | } 33 | 34 | export class AppRuntimeConstants { 35 | /* Singleton */ 36 | private static _instance: AppRuntimeConstants; 37 | public static get Get() { 38 | return this._instance || (this._instance = new this()); 39 | } 40 | 41 | public readonly FALLABLE_BLOCKS = new Set([ 42 | 'minecraft:anvil', 43 | 'minecraft:lime_concrete_powder', 44 | 'minecraft:orange_concrete_powder', 45 | 'minecraft:black_concrete_powder', 46 | 'minecraft:brown_concrete_powder', 47 | 'minecraft:cyan_concrete_powder', 48 | 'minecraft:light_gray_concrete_powder', 49 | 'minecraft:purple_concrete_powder', 50 | 'minecraft:magenta_concrete_powder', 51 | 'minecraft:light_blue_concrete_powder', 52 | 'minecraft:yellow_concrete_powder', 53 | 'minecraft:white_concrete_powder', 54 | 'minecraft:blue_concrete_powder', 55 | 'minecraft:red_concrete_powder', 56 | 'minecraft:gray_concrete_powder', 57 | 'minecraft:pink_concrete_powder', 58 | 'minecraft:green_concrete_powder', 59 | 'minecraft:dragon_egg', 60 | 'minecraft:gravel', 61 | 'minecraft:pointed_dripstone', 62 | 'minecraft:red_sand', 63 | 'minecraft:sand', 64 | 'minecraft:scaffolding', 65 | ]); 66 | 67 | public readonly TRANSPARENT_BLOCKS = new Set([ 68 | 'minecraft:frosted_ice', 69 | 'minecraft:glass', 70 | 'minecraft:white_stained_glass', 71 | 'minecraft:orange_stained_glass', 72 | 'minecraft:magenta_stained_glass', 73 | 'minecraft:light_blue_stained_glass', 74 | 'minecraft:yellow_stained_glass', 75 | 'minecraft:lime_stained_glass', 76 | 'minecraft:pink_stained_glass', 77 | 'minecraft:gray_stained_glass', 78 | 'minecraft:light_gray_stained_glass', 79 | 'minecraft:cyan_stained_glass', 80 | 'minecraft:purple_stained_glass', 81 | 'minecraft:blue_stained_glass', 82 | 'minecraft:brown_stained_glass', 83 | 'minecraft:green_stained_glass', 84 | 'minecraft:red_stained_glass', 85 | 'minecraft:black_stained_glass', 86 | 'minecraft:ice', 87 | 'minecraft:oak_leaves', 88 | 'minecraft:spruce_leaves', 89 | 'minecraft:birch_leaves', 90 | 'minecraft:jungle_leaves', 91 | 'minecraft:acacia_leaves', 92 | 'minecraft:dark_oak_leaves', 93 | 'minecraft:mangrove_leaves', 94 | 'minecraft:azalea_leaves', 95 | 'minecraft:flowering_azalea_leaves', 96 | 'minecraft:slime_block', 97 | 'minecraft:honey_block', 98 | ]); 99 | 100 | public readonly GRASS_LIKE_BLOCKS = new Set([ 101 | 'minecraft:grass_block', 102 | 'minecraft:grass_path', 103 | 'minecraft:podzol', 104 | 'minecraft:crimson_nylium', 105 | 'minecraft:warped_nylium', 106 | 'minecraft:mycelium', 107 | 'minecraft:farmland', 108 | ]); 109 | 110 | public readonly EMISSIVE_BLOCKS = new Set([ 111 | 'minecraft:respawn_anchor', 112 | 'minecraft:magma_block', 113 | 'minecraft:sculk_catalyst', 114 | 'minecraft:crying_obsidian', 115 | 'minecraft:shroomlight', 116 | 'minecraft:sea_lantern', 117 | 'minecraft:jack_o_lantern', 118 | 'minecraft:glowstone', 119 | 'minecraft:pearlescent_froglight', 120 | 'minecraft:verdant_froglight', 121 | 'minecraft:ochre_froglight', 122 | ]); 123 | 124 | private constructor() { 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/dither.ts: -------------------------------------------------------------------------------- 1 | import { RGBA_255 } from './colour'; 2 | import { AppConfig } from './config'; 3 | import { ASSERT } from './util/error_util'; 4 | import { Vector3 } from './vector'; 5 | 6 | export class Ditherer { 7 | public static ditherRandom(colour: RGBA_255, magnitude: number) { 8 | const offset = (Math.random() - 0.5) * magnitude; 9 | 10 | colour.r += offset; 11 | colour.g += offset; 12 | colour.b += offset; 13 | } 14 | 15 | public static ditherOrdered(colour: RGBA_255, position: Vector3, magnitude: number) { 16 | const map = this._getThresholdValue( 17 | Math.abs(position.x % 4), 18 | Math.abs(position.y % 4), 19 | Math.abs(position.z % 4), 20 | ); 21 | 22 | const offset = map * magnitude; 23 | 24 | colour.r += offset; 25 | colour.g += offset; 26 | colour.b += offset; 27 | } 28 | 29 | private static _mapMatrix = [ 30 | 0, 16, 2, 18, 48, 32, 50, 34, 31 | 6, 22, 4, 20, 54, 38, 52, 36, 32 | 24, 40, 26, 42, 8, 56, 10, 58, 33 | 30, 46, 28, 44, 14, 62, 12, 60, 34 | 3, 19, 5, 21, 51, 35, 53, 37, 35 | 1, 17, 7, 23, 49, 33, 55, 39, 36 | 27, 43, 29, 45, 11, 59, 13, 61, 37 | 25, 41, 31, 47, 9, 57, 15, 63, 38 | ]; 39 | 40 | private static _getThresholdValue(x: number, y: number, z: number) { 41 | const size = 4; 42 | ASSERT(0 <= x && x < size && 0 <= y && y < size && 0 <= z && z < size); 43 | const index = (x + (size * y) + (size * size * z)); 44 | ASSERT(0 <= index && index < size * size * size); 45 | return (Ditherer._mapMatrix[index] / (size * size * size)) - 0.5; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/event.ts: -------------------------------------------------------------------------------- 1 | import { AppContext } from './app_context'; 2 | import { UI } from './ui/layout'; 3 | import { ASSERT } from './util/error_util'; 4 | import { LOG } from './util/log_util'; 5 | 6 | /* eslint-disable */ 7 | export enum EAppEvent { 8 | onTaskStart, 9 | onTaskProgress, 10 | onTaskEnd, 11 | onComboBoxChanged, 12 | onLanguageChanged, 13 | } 14 | /* eslint-enable */ 15 | 16 | export class EventManager { 17 | private _eventsToListeners: Map void)[]>; 18 | private _appContext?: AppContext; 19 | 20 | private static _instance: EventManager; 21 | public static get Get() { 22 | return this._instance || (this._instance = new this()); 23 | } 24 | 25 | private constructor() { 26 | this._eventsToListeners = new Map(); 27 | } 28 | 29 | public bindToContext(context: AppContext) { 30 | this._appContext = context; 31 | } 32 | 33 | public init() { 34 | EventManager.Get.add(EAppEvent.onTaskStart, (...data) => { 35 | const lastAction = this._appContext?.getLastAction(); 36 | if (lastAction !== undefined) { 37 | UI.Get.getActionButton(lastAction) 38 | ?.startLoading() 39 | .setProgress(0.0); 40 | } 41 | }); 42 | 43 | EventManager.Get.add(EAppEvent.onTaskProgress, (...data) => { 44 | ASSERT(this._appContext !== undefined, 'Not bound to context'); 45 | const lastAction = this._appContext?.getLastAction(); 46 | if (lastAction !== undefined) { 47 | UI.Get.getActionButton(lastAction) 48 | ?.setProgress(data[0][1]); 49 | } 50 | }); 51 | 52 | EventManager.Get.add(EAppEvent.onTaskEnd, (...data) => { 53 | const lastAction = this._appContext?.getLastAction(); 54 | if (lastAction !== undefined) { 55 | UI.Get.getActionButton(lastAction) 56 | ?.resetLoading(); 57 | } 58 | }); 59 | } 60 | 61 | public add(event: EAppEvent, delegate: (...args: any[]) => void) { 62 | if (!this._eventsToListeners.has(event)) { 63 | this._eventsToListeners.set(event, []); 64 | } 65 | ASSERT(this._eventsToListeners.get(event) !== undefined, 'No event listener list'); 66 | this._eventsToListeners.get(event)!.push(delegate); 67 | } 68 | 69 | public broadcast(event: EAppEvent, ...payload: any) { 70 | if (event !== EAppEvent.onTaskProgress) { 71 | LOG('[BROADCAST]', EAppEvent[event], payload); 72 | } 73 | 74 | const listeners = this._eventsToListeners.get(event); 75 | if (listeners) { 76 | for (const listener of listeners) { 77 | listener(payload); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/exporters/base_exporter.ts: -------------------------------------------------------------------------------- 1 | import { BlockMesh } from '../block_mesh'; 2 | 3 | export type TStructureRegion = { name: string, content: Buffer }; 4 | 5 | export type TStructureExport = 6 | | { type: 'single', extension: string, content: Buffer } 7 | | { type: 'multiple', extension: string, regions: TStructureRegion[] } 8 | 9 | export abstract class IExporter { 10 | /** The file type extension of this exporter. 11 | * @note Do not include the dot prefix, e.g. 'obj' not '.obj'. 12 | */ 13 | public abstract getFormatFilter(): { 14 | name: string, 15 | extension: string, 16 | } 17 | 18 | /** 19 | * Export a block mesh to a file. 20 | * @param blockMesh The block mesh to export. 21 | * @param filePath The location to save the file to. 22 | */ 23 | public abstract export(blockMesh: BlockMesh): TStructureExport; 24 | } 25 | -------------------------------------------------------------------------------- /src/exporters/exporters.ts: -------------------------------------------------------------------------------- 1 | import { IExporter } from './base_exporter'; 2 | import { IndexedJSONExporter } from './indexed_json_exporter '; 3 | import { Litematic } from './litematic_exporter'; 4 | import { NBTExporter } from './nbt_exporter'; 5 | import { SchemExporter } from './schem_exporter'; 6 | import { Schematic } from './schematic_exporter'; 7 | import { UncompressedJSONExporter } from './uncompressed_json_exporter'; 8 | 9 | export type TExporters = 10 | 'schematic' | 11 | 'litematic' | 12 | 'schem' | 13 | 'nbt' | 14 | 'uncompressed_json' | 15 | 'indexed_json'; 16 | 17 | export class ExporterFactory { 18 | public static GetExporter(voxeliser: TExporters): IExporter { 19 | switch (voxeliser) { 20 | case 'schematic': 21 | return new Schematic(); 22 | case 'litematic': 23 | return new Litematic(); 24 | case 'schem': 25 | return new SchemExporter(); 26 | case 'nbt': 27 | return new NBTExporter(); 28 | case 'uncompressed_json': 29 | return new UncompressedJSONExporter(); 30 | case 'indexed_json': 31 | return new IndexedJSONExporter(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/exporters/indexed_json_exporter .ts: -------------------------------------------------------------------------------- 1 | import { BlockMesh } from '../block_mesh'; 2 | import { IExporter, TStructureExport } from './base_exporter'; 3 | 4 | export class IndexedJSONExporter extends IExporter { 5 | public override getFormatFilter() { 6 | return { 7 | name: 'Indexed JSON', 8 | extension: 'json', 9 | }; 10 | } 11 | 12 | public override export(blockMesh: BlockMesh): TStructureExport { 13 | const blocks = blockMesh.getBlocks(); 14 | 15 | const blocksUsed = blockMesh.getBlockPalette(); 16 | const blockToIndex = new Map(); 17 | const indexToBlock = new Map(); 18 | for (let i = 0; i < blocksUsed.length; ++i) { 19 | blockToIndex.set(blocksUsed[i], i); 20 | indexToBlock.set(i, blocksUsed[i]); 21 | } 22 | 23 | const blockArray = new Array>(); 24 | 25 | // Serialise all block except for the last one. 26 | for (let i = 0; i < blocks.length; ++i) { 27 | const block = blocks[i]; 28 | const pos = block.voxel.position; 29 | blockArray.push([pos.x, pos.y, pos.z, blockToIndex.get(block.blockInfo.name)!]); 30 | } 31 | 32 | const json = JSON.stringify({ 33 | blocks: Object.fromEntries(indexToBlock), 34 | xyzi: blockArray, 35 | }); 36 | 37 | return { type: 'single', extension: '.json', content: Buffer.from(json) }; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/exporters/schem_exporter.ts: -------------------------------------------------------------------------------- 1 | import { NBT, TagType } from 'prismarine-nbt'; 2 | 3 | import { BlockMesh } from '../block_mesh'; 4 | import { AppConstants } from '../constants'; 5 | import { AppUtil } from '../util'; 6 | import { LOG } from '../util/log_util'; 7 | import { MathUtil } from '../util/math_util'; 8 | import { saveNBT } from '../util/nbt_util'; 9 | import { Vector3 } from '../vector'; 10 | import { IExporter, TStructureExport } from './base_exporter'; 11 | 12 | export class SchemExporter extends IExporter { 13 | private static SCHEMA_VERSION = 2; 14 | 15 | public override getFormatFilter() { 16 | return { 17 | name: 'Sponge Schematic', 18 | extension: 'schem', 19 | }; 20 | } 21 | 22 | public override export(blockMesh: BlockMesh): TStructureExport { 23 | const bounds = blockMesh.getVoxelMesh().getBounds(); 24 | const sizeVector = bounds.getDimensions().add(1); 25 | 26 | // https://github.com/SpongePowered/Schematic-Specification/blob/master/versions/schematic-3.md#paletteObject 27 | // const blockMapping: BlockMapping = {}; 28 | const blockMapping: {[name: string]: { type: TagType, value: any }} = { 29 | 'minecraft:air': { type: TagType.Int, value: 0 }, 30 | }; 31 | 32 | let blockIndex = 1; 33 | for (const blockName of blockMesh.getBlockPalette()) { 34 | const namespacedBlockName = AppUtil.Text.namespaceBlock(blockName); 35 | 36 | blockMapping[namespacedBlockName] = { type: TagType.Int, value: blockIndex }; 37 | ++blockIndex; 38 | } 39 | LOG(blockMapping); 40 | 41 | // const paletteObject = SchemExporter._createBlockStatePalette(blockMapping); 42 | const blockData = new Array(sizeVector.x * sizeVector.y * sizeVector.z).fill(0); 43 | for (const block of blockMesh.getBlocks()) { 44 | const indexVector = Vector3.sub(block.voxel.position, bounds.min); 45 | const bufferIndex = SchemExporter._getBufferIndex(sizeVector, indexVector); 46 | const namespacedBlockName = AppUtil.Text.namespaceBlock(block.blockInfo.name); 47 | blockData[bufferIndex] = blockMapping[namespacedBlockName].value; 48 | } 49 | 50 | const blockEncoding: number[] = []; 51 | for (let i = 0; i < blockData.length; ++i) { 52 | let id = blockData[i]; 53 | 54 | while ((id & -128) != 0) { 55 | blockEncoding.push(id & 127 | 128); 56 | id >>>= 7; 57 | } 58 | blockEncoding.push(id); 59 | } 60 | 61 | for (let i = 0; i < blockEncoding.length; ++i) { 62 | blockEncoding[i] = MathUtil.int8(blockEncoding[i]); 63 | } 64 | 65 | const nbt: NBT = { 66 | type: TagType.Compound, 67 | name: 'Schematic', 68 | value: { 69 | Version: { type: TagType.Int, value: SchemExporter.SCHEMA_VERSION }, 70 | DataVersion: { type: TagType.Int, value: AppConstants.DATA_VERSION }, 71 | Width: { type: TagType.Short, value: sizeVector.x }, 72 | Height: { type: TagType.Short, value: sizeVector.y }, 73 | Length: { type: TagType.Short, value: sizeVector.z }, 74 | PaletteMax: { type: TagType.Int, value: blockIndex }, 75 | Palette: { type: TagType.Compound, value: blockMapping }, 76 | BlockData: { type: TagType.ByteArray, value: blockEncoding }, 77 | }, 78 | }; 79 | 80 | return { type: 'single', extension: '.schem', content: saveNBT(nbt) }; 81 | } 82 | 83 | private static _getBufferIndex(dimensions: Vector3, vec: Vector3) { 84 | return vec.x + (vec.z * dimensions.x) + (vec.y * dimensions.x * dimensions.z); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/exporters/schematic_exporter.ts: -------------------------------------------------------------------------------- 1 | //import { NBT, TagType } from 'prismarine-nbt'; 2 | 3 | import { NBT, TagType } from 'prismarine-nbt'; 4 | 5 | import { BLOCK_IDS } from '../../res/block_ids'; 6 | import { BlockMesh } from '../block_mesh'; 7 | import { LOC } from '../localiser'; 8 | import { StatusHandler } from '../status'; 9 | import { LOG_WARN } from '../util/log_util'; 10 | import { saveNBT } from '../util/nbt_util'; 11 | import { Vector3 } from '../vector'; 12 | import { IExporter, TStructureExport } from './base_exporter'; 13 | 14 | export class Schematic extends IExporter { 15 | public override getFormatFilter() { 16 | return { 17 | name: 'Schematic', 18 | extension: 'schematic', 19 | }; 20 | } 21 | 22 | public override export(blockMesh: BlockMesh): TStructureExport { 23 | const nbt = this._convertToNBT(blockMesh); 24 | return { type: 'single', extension: '.schematic', content: saveNBT(nbt) }; 25 | } 26 | 27 | private _convertToNBT(blockMesh: BlockMesh): NBT { 28 | const bounds = blockMesh.getVoxelMesh().getBounds(); 29 | const sizeVector = Vector3.sub(bounds.max, bounds.min).add(1); 30 | 31 | const bufferSize = sizeVector.x * sizeVector.y * sizeVector.z; 32 | const blocksData = Array(bufferSize); 33 | const metaData = Array(bufferSize); 34 | 35 | // TODO Unimplemented 36 | const schematicBlocks: { [blockName: string]: { id: number, meta: number, name: string } } = BLOCK_IDS; 37 | 38 | const blocks = blockMesh.getBlocks(); 39 | const unsupportedBlocks = new Set(); 40 | let numBlocksUnsupported = 0; 41 | for (const block of blocks) { 42 | const indexVector = Vector3.sub(block.voxel.position, bounds.min); 43 | const index = this._getBufferIndex(indexVector, sizeVector); 44 | if (block.blockInfo.name in schematicBlocks) { 45 | const schematicBlock = schematicBlocks[block.blockInfo.name]; 46 | blocksData[index] = new Int8Array([schematicBlock.id])[0]; 47 | metaData[index] = new Int8Array([schematicBlock.meta])[0]; 48 | } else { 49 | blocksData[index] = 1; // Default to a Stone block 50 | metaData[index] = 0; 51 | unsupportedBlocks.add(block.blockInfo.name); 52 | ++numBlocksUnsupported; 53 | } 54 | } 55 | 56 | if (unsupportedBlocks.size > 0) { 57 | StatusHandler.warning(LOC('export.schematic_unsupported_blocks', { count: numBlocksUnsupported, unique: unsupportedBlocks.size })); 58 | LOG_WARN(unsupportedBlocks); 59 | } 60 | 61 | // TODO Unimplemented 62 | const nbt: NBT = { 63 | type: TagType.Compound, 64 | name: 'Schematic', 65 | value: { 66 | Width: { type: TagType.Short, value: sizeVector.x }, 67 | Height: { type: TagType.Short, value: sizeVector.y }, 68 | Length: { type: TagType.Short, value: sizeVector.z }, 69 | Materials: { type: TagType.String, value: 'Alpha' }, 70 | Blocks: { type: TagType.ByteArray, value: blocksData }, 71 | Data: { type: TagType.ByteArray, value: metaData }, 72 | Entities: { type: TagType.List, value: { type: TagType.Int, value: Array(0) } }, 73 | TileEntities: { type: TagType.List, value: { type: TagType.Int, value: Array(0) } }, 74 | }, 75 | }; 76 | 77 | return nbt; 78 | } 79 | 80 | private _getBufferIndex(vec: Vector3, sizeVector: Vector3) { 81 | return (sizeVector.z * sizeVector.x * vec.y) + (sizeVector.x * vec.z) + vec.x; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/exporters/uncompressed_json_exporter.ts: -------------------------------------------------------------------------------- 1 | import { BlockMesh } from '../block_mesh'; 2 | import { IExporter, TStructureExport } from './base_exporter'; 3 | 4 | export class UncompressedJSONExporter extends IExporter { 5 | public override getFormatFilter() { 6 | return { 7 | name: 'Uncompressed JSON', 8 | extension: 'json', 9 | }; 10 | } 11 | 12 | public override export(blockMesh: BlockMesh): TStructureExport { 13 | const blocks = blockMesh.getBlocks(); 14 | 15 | const lines = new Array(); 16 | lines.push('['); 17 | 18 | // Serialise all block except for the last one. 19 | for (let i = 0; i < blocks.length - 1; ++i) { 20 | const block = blocks[i]; 21 | const pos = block.voxel.position; 22 | lines.push(`{ "x": ${pos.x}, "y": ${pos.y}, "z": ${pos.z}, "block_name": "${block.blockInfo.name}" },`); 23 | } 24 | 25 | // Serialise the last block but don't include the comma at the end. 26 | { 27 | const block = blocks[blocks.length - 1]; 28 | const pos = block.voxel.position; 29 | lines.push(`{ "x": ${pos.x}, "y": ${pos.y}, "z": ${pos.z}, "block_name": "${block.blockInfo.name}" }`); 30 | } 31 | 32 | lines.push(']'); 33 | 34 | const json = lines.join(''); 35 | 36 | return { type: 'single', extension: '.json', content: Buffer.from(json) }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vs'; 2 | 3 | declare module '*.fs'; 4 | 5 | declare module '*.png'; 6 | 7 | declare module '*.atlas'; 8 | 9 | declare module '*.worker.ts' { 10 | export default {} as typeof Worker & (new () => Worker); 11 | } 12 | -------------------------------------------------------------------------------- /src/hash_map.ts: -------------------------------------------------------------------------------- 1 | export interface IHashable { 2 | hash(): number; 3 | equals(other: IHashable): boolean; 4 | } 5 | -------------------------------------------------------------------------------- /src/importers/base_importer.ts: -------------------------------------------------------------------------------- 1 | import { Mesh } from '../mesh'; 2 | 3 | export abstract class IImporter { 4 | public abstract import(file: File): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/importers/importers.ts: -------------------------------------------------------------------------------- 1 | import { ASSERT } from '../util/error_util'; 2 | import { IImporter } from './base_importer'; 3 | import { GltfLoader } from './gltf_loader'; 4 | import { ObjImporter } from './obj_importer'; 5 | 6 | export type TImporters = 'obj' | 'gltf'; 7 | 8 | export class ImporterFactor { 9 | public static GetImporter(importer: TImporters): IImporter { 10 | switch (importer) { 11 | case 'obj': 12 | return new ObjImporter(); 13 | case 'gltf': 14 | return new GltfLoader(); 15 | default: 16 | ASSERT(false); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/linear_allocator.ts: -------------------------------------------------------------------------------- 1 | export class LinearAllocator { 2 | private _items: Array; 3 | private _nextIndex: number; 4 | private _max: number; 5 | private _itemConstructor: () => T; 6 | 7 | public constructor(getNewItem: () => T) { 8 | this._items = new Array(); 9 | this._nextIndex = 0; 10 | this._max = 0; 11 | this._itemConstructor = getNewItem; 12 | } 13 | 14 | private _add(item: T) { 15 | this._items[this._nextIndex] = item; 16 | ++this._nextIndex; 17 | this._max = Math.max(this._max, this._nextIndex); 18 | } 19 | 20 | public reset() { 21 | this._nextIndex = 0; 22 | } 23 | 24 | public get(index: number): T | undefined { 25 | return this._items[index]; 26 | } 27 | 28 | public size() { 29 | return this._nextIndex; 30 | } 31 | 32 | public place(): T { 33 | if (this._nextIndex >= this._max) { 34 | //console.log('Adding new item at index', this._nextIndex); 35 | const newItem = this._itemConstructor(); 36 | this._add(newItem); 37 | return newItem; 38 | } else { 39 | ++this._nextIndex; 40 | //console.log('Returning item at index', this._nextIndex - 1); 41 | return this._items[this._nextIndex - 1]; 42 | } 43 | } 44 | 45 | public max() { 46 | return this._max; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/localiser.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | 3 | import { locales, TTranslationMap } from '../loc/base'; 4 | import { AppConfig } from './config'; 5 | import { EAppEvent, EventManager } from './event'; 6 | import { ASSERT } from './util/error_util'; 7 | import { DeepPartial, TBrand } from './util/type_util'; 8 | 9 | 10 | // https://stackoverflow.com/questions/58277973/how-to-type-check-i18n-dictionaries-with-typescript 11 | // get all possible key paths 12 | export type DeepKeys = T extends object ? { 13 | [K in keyof T]-?: `${K & string}` | Concat> 14 | }[keyof T] : ''; 15 | 16 | // or: only get leaf and no intermediate key path 17 | export type DeepLeafKeys = T extends object ? 18 | { [K in keyof T]-?: Concat> }[keyof T] : ''; 19 | 20 | // https://stackoverflow.com/questions/58277973/how-to-type-check-i18n-dictionaries-with-typescript 21 | export type Concat = 22 | `${K}${'' extends P ? '' : '.'}${P}` 23 | 24 | export type TLocalisedString = TBrand; 25 | 26 | export type TLocalisedKey = DeepLeafKeys; 27 | 28 | export class Localiser { 29 | /* Singleton */ 30 | private static _instance: Localiser; 31 | public static get Get() { 32 | return this._instance || (this._instance = new this()); 33 | } 34 | 35 | public async init() { 36 | const localResources: { [code: string]: { translation: DeepPartial } } = {}; 37 | locales.forEach((locale) => { 38 | localResources[locale.language_code] = { translation: locale.translations }; 39 | }); 40 | 41 | await i18next.init({ 42 | lng: AppConfig.Get.LOCALE, 43 | fallbackLng: 'en-GB', 44 | debug: true, 45 | resources: localResources, 46 | }); 47 | 48 | ASSERT(i18next.isInitialized, 'i18next not initialised'); 49 | } 50 | 51 | public async changeLanguage(languageKey: string) { 52 | await i18next.changeLanguage(languageKey); 53 | EventManager.Get.broadcast(EAppEvent.onLanguageChanged); 54 | } 55 | 56 | public translate(p: DeepLeafKeys, options?: any): TLocalisedString { 57 | return (i18next.t(p, options) as unknown) as TLocalisedString; 58 | } 59 | 60 | public getCurrentLanguage() { 61 | return i18next.language; 62 | } 63 | } 64 | 65 | export const LOC = Localiser.Get.translate; 66 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { AppContext } from './app_context'; 2 | 3 | AppContext.init(); 4 | 5 | // Begin draw loop 6 | function render() { 7 | AppContext.draw(); 8 | requestAnimationFrame(render); 9 | } 10 | requestAnimationFrame(render); 11 | -------------------------------------------------------------------------------- /src/material-map.ts: -------------------------------------------------------------------------------- 1 | import { RGBAColours, RGBAUtil } from './colour'; 2 | import { MaterialMap, MaterialType } from './mesh'; 3 | import { EImageChannel, TTransparencyTypes } from './texture'; 4 | import { ASSERT } from './util/error_util'; 5 | 6 | export class MaterialMapManager { 7 | public materials: MaterialMap; 8 | 9 | public constructor(materials: MaterialMap) { 10 | this.materials = materials; 11 | } 12 | 13 | public changeTransparencyType(materialName: string, newTransparencyType: TTransparencyTypes) { 14 | const currentMaterial = this.materials.get(materialName); 15 | ASSERT(currentMaterial !== undefined, 'Cannot change transparency type of non-existent material'); 16 | ASSERT(currentMaterial.type === MaterialType.textured); 17 | 18 | switch (newTransparencyType) { 19 | case 'None': 20 | currentMaterial.transparency = { type: 'None' }; 21 | break; 22 | case 'UseAlphaMap': 23 | currentMaterial.transparency = { 24 | type: 'UseAlphaMap', 25 | alpha: undefined, 26 | channel: EImageChannel.R, 27 | }; 28 | break; 29 | case 'UseAlphaValue': 30 | currentMaterial.transparency = { 31 | type: 'UseAlphaValue', 32 | alpha: 1.0, 33 | }; 34 | break; 35 | case 'UseDiffuseMapAlphaChannel': 36 | currentMaterial.transparency = { 37 | type: 'UseDiffuseMapAlphaChannel', 38 | }; 39 | break; 40 | } 41 | 42 | this.materials.set(materialName, currentMaterial); 43 | } 44 | 45 | /** 46 | * Convert a material to a new type, i.e. textured to solid. 47 | * Will return if the material is already the given type. 48 | */ 49 | public changeMaterialType(materialName: string, newMaterialType: MaterialType) { 50 | const currentMaterial = this.materials.get(materialName); 51 | ASSERT(currentMaterial !== undefined, 'Cannot change material type of non-existent material'); 52 | 53 | if (currentMaterial.type === newMaterialType) { 54 | return; 55 | } 56 | 57 | switch (newMaterialType) { 58 | case MaterialType.solid: 59 | ASSERT(currentMaterial.type === MaterialType.textured, 'Old material expect to be texture'); 60 | this.materials.set(materialName, { 61 | type: MaterialType.solid, 62 | colour: RGBAUtil.randomPretty(), 63 | canBeTextured: true, 64 | needsAttention: true, 65 | }); 66 | break; 67 | case MaterialType.textured: 68 | ASSERT(currentMaterial.type === MaterialType.solid, 'Old material expect to be solid'); 69 | this.materials.set(materialName, { 70 | type: MaterialType.textured, 71 | transparency: { 72 | type: 'None', 73 | }, 74 | extension: 'repeat', 75 | interpolation: 'linear', 76 | needsAttention: true, 77 | diffuse: undefined, 78 | }); 79 | break; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/math.ts: -------------------------------------------------------------------------------- 1 | import { ASSERT } from './util/error_util'; 2 | import { Vector3 } from './vector'; 3 | 4 | export namespace AppMath { 5 | export const RADIANS_0 = degreesToRadians(0.0); 6 | export const RADIANS_90 = degreesToRadians(90.0); 7 | export const RADIANS_180 = degreesToRadians(180.0); 8 | export const RADIANS_270 = degreesToRadians(270.0); 9 | 10 | export function lerp(value: number, start: number, end: number) { 11 | return (1 - value) * start + value * end; 12 | } 13 | 14 | export function nearlyEqual(a: number, b: number, tolerance: number = 0.0001) { 15 | return Math.abs(a - b) < tolerance; 16 | } 17 | 18 | export function degreesToRadians(degrees: number) { 19 | return degrees * (Math.PI / 180.0); 20 | } 21 | 22 | /** 23 | * Converts a float in [0, 1] to an int in [0, 255] 24 | * @param decimal A number in [0, 1] 25 | */ 26 | export function uint8(decimal: number) { 27 | return Math.floor(decimal * 255); 28 | } 29 | 30 | export function largestPowerOfTwoLessThanN(n: number) { 31 | return Math.floor(Math.log2(n)); 32 | } 33 | } 34 | 35 | export const argMax = (array: [number]) => { 36 | return array.map((x, i) => [x, i]).reduce((r, a) => (a[0] > r[0] ? a : r))[1]; 37 | }; 38 | 39 | export const clamp = (value: number, min: number, max: number) => { 40 | return Math.max(Math.min(max, value), min); 41 | }; 42 | 43 | export const floorToNearest = (value: number, base: number) => { 44 | return Math.floor(value / base) * base; 45 | }; 46 | 47 | export const ceilToNearest = (value: number, base: number) => { 48 | return Math.ceil(value / base) * base; 49 | }; 50 | 51 | export const roundToNearest = (value: number, base: number) => { 52 | return Math.round(value / base) * base; 53 | }; 54 | 55 | export const between = (value: number, min: number, max: number) => { 56 | return min <= value && value <= max; 57 | }; 58 | 59 | export const mapRange = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => { 60 | return (value - fromMin) / (fromMax - fromMin) * (toMax - toMin) + toMin; 61 | }; 62 | 63 | export const wayThrough = (value: number, min: number, max: number) => { 64 | // ASSERT(value >= min && value <= max); 65 | return (value - min) / (max - min); 66 | }; 67 | 68 | /** 69 | * Throws is any number in args is NaN 70 | */ 71 | export const checkNaN = (...args: number[]) => { 72 | const existsNaN = args.some((arg) => { 73 | return isNaN(arg); 74 | }); 75 | ASSERT(!existsNaN, 'Found NaN'); 76 | }; 77 | 78 | export const degreesToRadians = Math.PI / 180; 79 | 80 | export class SmoothVariable { 81 | private _actual: number; 82 | private _target: number; 83 | private _smoothing: number; 84 | private _min: number; 85 | private _max: number; 86 | 87 | public constructor(value: number, smoothing: number) { 88 | this._actual = value; 89 | this._target = value; 90 | this._smoothing = smoothing; 91 | this._min = -Infinity; 92 | this._max = Infinity; 93 | } 94 | 95 | public setClamp(min: number, max: number) { 96 | this._min = min; 97 | this._max = max; 98 | } 99 | 100 | public addToTarget(delta: number) { 101 | this._target = clamp(this._target + delta, this._min, this._max); 102 | } 103 | 104 | public setTarget(target: number) { 105 | this._target = clamp(target, this._min, this._max); 106 | } 107 | 108 | public setActual(actual: number) { 109 | this._actual = actual; 110 | } 111 | 112 | public tick() { 113 | this._actual += (this._target - this._actual) * this._smoothing; 114 | } 115 | 116 | public getActual() { 117 | return this._actual; 118 | } 119 | 120 | public getTarget() { 121 | return this._target; 122 | } 123 | } 124 | 125 | export class SmoothVectorVariable { 126 | private _actual: Vector3; 127 | private _target: Vector3; 128 | private _smoothing: number; 129 | 130 | public constructor(value: Vector3, smoothing: number) { 131 | this._actual = value; 132 | this._target = value; 133 | this._smoothing = smoothing; 134 | } 135 | 136 | public addToTarget(delta: Vector3) { 137 | this._target = Vector3.add(this._target, delta); 138 | } 139 | 140 | public setTarget(target: Vector3) { 141 | this._target = target; 142 | } 143 | 144 | public tick() { 145 | this._actual.add(Vector3.sub(this._target, this._actual).mulScalar(this._smoothing)); 146 | } 147 | 148 | public getActual() { 149 | return this._actual; 150 | } 151 | 152 | public getTarget() { 153 | return this._target; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/mouse.ts: -------------------------------------------------------------------------------- 1 | import { Renderer } from './renderer'; 2 | 3 | interface MouseState { 4 | x: number, 5 | y: number, 6 | buttons: number 7 | } 8 | 9 | export class MouseManager { 10 | private _gl: WebGLRenderingContext; 11 | 12 | private static readonly MOUSE_LEFT = 1; 13 | private static readonly MOUSE_RIGHT = 2; 14 | 15 | private prevMouse: MouseState; 16 | private currMouse: MouseState; 17 | 18 | private static _instance: MouseManager; 19 | 20 | public static get Get() { 21 | return this._instance || (this._instance = new this(Renderer.Get._gl)); 22 | } 23 | 24 | private constructor(gl: WebGLRenderingContext) { 25 | this._gl = gl; 26 | 27 | this.currMouse = { x: -1, y: -1, buttons: 0 }; 28 | this.prevMouse = { x: -1, y: -1, buttons: 0 }; 29 | } 30 | 31 | public init() { 32 | document.addEventListener('mousemove', (e) => { 33 | this.onMouseMove(e); 34 | }); 35 | } 36 | 37 | public onMouseMove(e: MouseEvent) { 38 | this.currMouse = { x: e.clientX, y: e.clientY, buttons: e.buttons }; 39 | } 40 | 41 | public isMouseLeftDown() { 42 | this.currMouse.buttons & MouseManager.MOUSE_LEFT; 43 | } 44 | 45 | public isMouseRightDown() { 46 | this.currMouse.buttons & MouseManager.MOUSE_RIGHT; 47 | } 48 | 49 | public getMouseDelta() { 50 | const delta = { 51 | dx: this.currMouse.x - this.prevMouse.x, 52 | dy: -(this.currMouse.y - this.prevMouse.y), 53 | }; 54 | this.prevMouse = this.currMouse; 55 | return delta; 56 | }; 57 | 58 | public getMousePosNorm() { 59 | const normX = 2 * (this.currMouse.x / this._gl.canvas.width ) - 1; 60 | const normY = -(2 * (this.currMouse.y / this._gl.canvas.height) - 1); 61 | return { x: normX, y: normY }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/progress.ts: -------------------------------------------------------------------------------- 1 | import { EAppEvent, EventManager } from './event'; 2 | import { ASSERT } from './util/error_util'; 3 | import { LOGF } from './util/log_util'; 4 | 5 | export type TTaskID = 6 | | 'Importing' 7 | | 'MeshBuffer' 8 | | 'Voxelising' 9 | | 'VoxelMeshBuffer' 10 | | 'Assigning' 11 | | 'BlockMeshBuffer' 12 | | 'Exporting'; 13 | 14 | export type TTaskHandle = { 15 | nextPercentage: number, 16 | id: TTaskID, 17 | } 18 | 19 | export class ProgressManager { 20 | /* Singleton */ 21 | private static _instance: ProgressManager; 22 | public static get Get() { 23 | return this._instance || (this._instance = new this()); 24 | } 25 | 26 | private _tasks: TTaskID[]; 27 | 28 | private constructor() { 29 | this._tasks = []; 30 | } 31 | 32 | /** 33 | * Start tracking the progress of a task. 34 | * @param taskId The id of the task (created here). 35 | */ 36 | public start(taskId: TTaskID): TTaskHandle { 37 | ASSERT(!this._tasks.includes(taskId), `Task with id '${taskId}' already being tracked`); 38 | this._tasks.push(taskId); 39 | EventManager.Get.broadcast(EAppEvent.onTaskStart, taskId); 40 | 41 | LOGF(`[PROGRESS]: Start '${taskId} (${this._tasks.length} task(s))'`); 42 | 43 | return { 44 | nextPercentage: 0.0, 45 | id: taskId, 46 | }; 47 | } 48 | 49 | /** 50 | * Announce progress has been made on a task. 51 | * @param taskId The id of the task (created in `start`). 52 | * @param percentage A number between 0.0 and 1.0, inclusive. 53 | */ 54 | public progress(tracker: TTaskHandle, percentage: number) { 55 | if (percentage > tracker.nextPercentage) { 56 | //LOGF(`[PROGRESS]: Progress '${tracker.id}' (${this._tasks.length} task(s))'`); 57 | EventManager.Get.broadcast(EAppEvent.onTaskProgress, tracker.id, percentage); 58 | tracker.nextPercentage += 0.05; 59 | } 60 | } 61 | 62 | /** 63 | * Announce a task has completed. 64 | * @param taskId The id of the task (created in `start`). 65 | */ 66 | public end(tracker: TTaskHandle) { 67 | LOGF(`[PROGRESS]: End '${tracker.id}' (${this._tasks.length} task(s))'`); 68 | 69 | const taskIndex = this._tasks.findIndex((task) => { return task === tracker.id; }); 70 | ASSERT(taskIndex !== -1, `Task with that id '${tracker.id}' is not being tracked, ${this._tasks}`); 71 | this._tasks.splice(taskIndex, 1); 72 | EventManager.Get.broadcast(EAppEvent.onTaskEnd, tracker.id); 73 | } 74 | 75 | public clear() { 76 | this._tasks = []; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ray.ts: -------------------------------------------------------------------------------- 1 | import { ASSERT } from './util/error_util'; 2 | import { Vector3 } from './vector'; 3 | 4 | const EPSILON = 0.0000001; 5 | 6 | /* eslint-disable */ 7 | export enum Axes { 8 | x, y, z, 9 | } 10 | /* eslint-enable */ 11 | 12 | export function axesToDirection(axis: Axes) { 13 | if (axis === Axes.x) { 14 | return new Vector3(1, 0, 0); 15 | } 16 | if (axis === Axes.y) { 17 | return new Vector3(0, 1, 0); 18 | } 19 | if (axis === Axes.z) { 20 | return new Vector3(0, 0, 1); 21 | } 22 | ASSERT(false); 23 | } 24 | 25 | export interface Ray { 26 | origin: Vector3, 27 | axis: Axes 28 | } 29 | 30 | export function rayIntersectTriangle(ray: Ray, v0: Vector3, v1: Vector3, v2: Vector3): (Vector3 | undefined) { 31 | const edge1 = Vector3.sub(v1, v0); 32 | const edge2 = Vector3.sub(v2, v0); 33 | 34 | const rayDirection = axesToDirection(ray.axis); 35 | const h = Vector3.cross(rayDirection, edge2); 36 | const a = Vector3.dot(edge1, h); 37 | 38 | if (a > -EPSILON && a < EPSILON) { 39 | return; // Ray is parallel to triangle 40 | } 41 | 42 | const f = 1.0 / a; 43 | const s = Vector3.sub(ray.origin, v0); 44 | const u = f * Vector3.dot(s, h); 45 | 46 | if (u < 0.0 || u > 1.0) { 47 | return; 48 | } 49 | 50 | const q = Vector3.cross(s, edge1); 51 | const v = f * Vector3.dot(rayDirection, q); 52 | 53 | if (v < 0.0 || u + v > 1.0) { 54 | return; 55 | } 56 | 57 | const t = f * Vector3.dot(edge2, q); 58 | 59 | if (t > EPSILON) { 60 | return Vector3.add(ray.origin, Vector3.mulScalar(rayDirection, t)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/shaders.ts: -------------------------------------------------------------------------------- 1 | import * as twgl from 'twgl.js'; 2 | 3 | import FRAG_BLOCK from '../res/shaders/block_fragment.fs'; 4 | import VERT_BLOCK from '../res/shaders/block_vertex.vs'; 5 | import FRAG_DEBUG from '../res/shaders/debug_fragment.fs'; 6 | import VERT_DEBUG from '../res/shaders/debug_vertex.vs'; 7 | import FRAG_TRI_SOLID from '../res/shaders/solid_tri_fragment.fs'; 8 | import VERT_TRI_SOLID from '../res/shaders/solid_tri_vertex.vs'; 9 | import FRAG_TRI_TEXTURE from '../res/shaders/texture_tri_fragment.fs'; 10 | import VERT_TRI_TEXTURE from '../res/shaders/texture_tri_vertex.vs'; 11 | import FRAG_VOXEL from '../res/shaders/voxel_fragment.fs'; 12 | import VERT_VOXEL from '../res/shaders/voxel_vertex.vs'; 13 | import { Renderer } from './renderer'; 14 | 15 | export class ShaderManager { 16 | public readonly textureTriProgram: twgl.ProgramInfo; 17 | public readonly solidTriProgram: twgl.ProgramInfo; 18 | public readonly voxelProgram: twgl.ProgramInfo; 19 | public readonly blockProgram: twgl.ProgramInfo; 20 | public readonly debugProgram: twgl.ProgramInfo; 21 | 22 | private static _instance: ShaderManager; 23 | public static get Get() { 24 | return this._instance || (this._instance = new this()); 25 | } 26 | 27 | private constructor() { 28 | const gl = Renderer.Get._gl; 29 | 30 | this.textureTriProgram = twgl.createProgramInfo(gl, [VERT_TRI_TEXTURE, FRAG_TRI_TEXTURE]); 31 | 32 | this.solidTriProgram = twgl.createProgramInfo(gl, [VERT_TRI_SOLID, FRAG_TRI_SOLID]); 33 | 34 | this.voxelProgram = twgl.createProgramInfo(gl, [VERT_VOXEL, FRAG_VOXEL]); 35 | 36 | this.blockProgram = twgl.createProgramInfo(gl, [VERT_BLOCK, FRAG_BLOCK]); 37 | 38 | this.debugProgram = twgl.createProgramInfo(gl, [VERT_DEBUG, FRAG_DEBUG]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/status.ts: -------------------------------------------------------------------------------- 1 | import { TLocalisedString } from './localiser'; 2 | import { TMessage } from './ui/console'; 3 | import { LOG, LOG_ERROR, LOG_WARN } from './util/log_util'; 4 | 5 | /** 6 | * `StatusHandler` is used to track success, info, warning, and error messages. 7 | * There are separate singletons for the Client and Worker so when the Worker 8 | * has completed a Job it needs to send its status messages to the Client 9 | * along with its payload so that the messages can be displayed in the console. 10 | */ 11 | export class StatusHandler { 12 | /** Singleton accessor */ 13 | private static _instance: StatusHandler; 14 | public static get Get() { 15 | return this._instance || (this._instance = new this()); 16 | } 17 | 18 | private _messages: TMessage[]; 19 | 20 | private constructor() { 21 | this._messages = []; 22 | } 23 | 24 | public clear() { 25 | this._messages = []; 26 | } 27 | 28 | public static success(message: TLocalisedString) { 29 | this.Get._messages.push({ text: message, type: 'success' }); 30 | } 31 | 32 | public static info(message: TLocalisedString) { 33 | this.Get._messages.push({ text: message, type: 'info' }); 34 | } 35 | 36 | public static warning(message: TLocalisedString) { 37 | this.Get._messages.push({ text: message, type: 'warning' }); 38 | } 39 | 40 | public static error(message: TLocalisedString) { 41 | this.Get._messages.push({ text: message, type: 'error' }); 42 | } 43 | 44 | public static getAll() { 45 | return this.Get._messages; 46 | } 47 | 48 | public dump() { 49 | this._messages.forEach((message) => { 50 | switch (message.type) { 51 | case 'info': 52 | case 'success': 53 | LOG(message.text); 54 | break; 55 | case 'warning': 56 | LOG_WARN(message.text); 57 | break; 58 | case 'error': 59 | LOG_ERROR(message.text); 60 | break; 61 | } 62 | }); 63 | 64 | return this; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/triangle.ts: -------------------------------------------------------------------------------- 1 | import { Bounds } from './bounds'; 2 | import { UV } from './util'; 3 | import { Vector3 } from './vector'; 4 | export class Triangle { 5 | public v0: Vector3; 6 | public v1: Vector3; 7 | public v2: Vector3; 8 | 9 | constructor(v0: Vector3, v1: Vector3, v2: Vector3) { 10 | this.v0 = v0; 11 | this.v1 = v1; 12 | this.v2 = v2; 13 | } 14 | 15 | public getCentre(): Vector3 { 16 | return Vector3.divScalar(Vector3.add(Vector3.add(this.v0, this.v1), this.v2), 3.0); 17 | } 18 | 19 | public getArea(): number { 20 | const a = Vector3.sub(this.v0, this.v1).magnitude(); 21 | const b = Vector3.sub(this.v1, this.v2).magnitude(); 22 | const c = Vector3.sub(this.v2, this.v0).magnitude(); 23 | const p = (a + b + c) / 2; 24 | return Math.sqrt(p * (p - a) * (p - b) * (p - c)); 25 | } 26 | 27 | public getNormal(): Vector3 { 28 | const u = Vector3.sub(this.v0, this.v1); 29 | const v = Vector3.sub(this.v0, this.v2); 30 | return Vector3.cross(u, v).normalise(); 31 | } 32 | 33 | public getBounds(): Bounds { 34 | return new Bounds( 35 | new Vector3( 36 | Math.min(this.v0.x, this.v1.x, this.v2.x), 37 | Math.min(this.v0.y, this.v1.y, this.v2.y), 38 | Math.min(this.v0.z, this.v1.z, this.v2.z), 39 | ), 40 | new Vector3( 41 | Math.max(this.v0.x, this.v1.x, this.v2.x), 42 | Math.max(this.v0.y, this.v1.y, this.v2.y), 43 | Math.max(this.v0.z, this.v1.z, this.v2.z), 44 | ), 45 | ); 46 | } 47 | } 48 | 49 | export class UVTriangle extends Triangle { 50 | public uv0: UV; 51 | public uv1: UV; 52 | public uv2: UV; 53 | 54 | public n0: Vector3; 55 | public n1: Vector3; 56 | public n2: Vector3; 57 | 58 | constructor(v0: Vector3, v1: Vector3, v2: Vector3, n0: Vector3, n1: Vector3, n2: Vector3, uv0: UV, uv1: UV, uv2: UV) { 59 | super(v0, v1, v2); 60 | 61 | this.n0 = n0; 62 | this.n1 = n1; 63 | this.n2 = n2; 64 | 65 | this.uv0 = uv0; 66 | this.uv1 = uv1; 67 | this.uv2 = uv2; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/ui/components/base.ts: -------------------------------------------------------------------------------- 1 | import { getRandomID } from '../../util'; 2 | import { UIUtil } from '../../util/ui_util'; 3 | 4 | export interface IInterfaceItem { 5 | generateHTML: () => string; 6 | registerEvents: () => void; 7 | finalise: () => void; 8 | } 9 | 10 | /** 11 | * The base UI class from which user interactable DOM elements are built from. 12 | * Each `BaseComponent` can be enabled/disabled. 13 | */ 14 | export abstract class BaseComponent implements IInterfaceItem { 15 | private _id: string; 16 | private _isEnabled: boolean; 17 | private _isHovered: boolean; 18 | private _obeyGroupEnables: boolean; 19 | 20 | public constructor() { 21 | this._id = getRandomID(); 22 | this._isEnabled = true; 23 | this._isHovered = false; 24 | this._obeyGroupEnables = true; 25 | } 26 | 27 | /** 28 | * Get whether or not this UI element is interactable. 29 | * @deprecated Use the enabled() getter. 30 | */ 31 | public getEnabled() { 32 | return this._isEnabled; 33 | } 34 | 35 | /** 36 | * Alias of `getEnabled` 37 | */ 38 | public get enabled() { 39 | return this._isEnabled; 40 | } 41 | 42 | public get disabled() { 43 | return !this._isEnabled; 44 | } 45 | 46 | /** 47 | * Set whether or not this UI element is interactable. 48 | */ 49 | public setEnabled(isEnabled: boolean, isGroupEnable: boolean = true) { 50 | if (isEnabled && isGroupEnable && !this._obeyGroupEnables) { 51 | return; 52 | } 53 | this._isEnabled = isEnabled; 54 | this._onEnabledChanged(); 55 | } 56 | 57 | protected _setHovered(isHovered: boolean) { 58 | this._isHovered = isHovered; 59 | } 60 | 61 | public get hovered() { 62 | return this._isHovered; 63 | } 64 | 65 | /** 66 | * Sets whether or not this element should be enabled when the group 67 | * is it apart of becomes enabled. This is useful if an element should 68 | * only be enabled if another element has a particular value. If this is 69 | * false then there needs to be a some event added to manually enable this 70 | * element. 71 | */ 72 | public setShouldObeyGroupEnables(obey: boolean) { 73 | this._obeyGroupEnables = obey; 74 | return this; 75 | } 76 | 77 | /** 78 | * The actual HTML that represents this UI element. It is recommended to 79 | * give the outermost element that ID generated for this BaseComponent so 80 | * that `getElement()` returns all elements created here. 81 | */ 82 | public abstract generateHTML(): string; 83 | 84 | /** 85 | * A delegate that is called after the UI element has been added to the DOM. 86 | * Calls to `addEventListener` should be placed here. 87 | */ 88 | public abstract registerEvents(): void; 89 | 90 | public finalise(): void { 91 | this._onEnabledChanged(); 92 | this._updateStyles(); 93 | } 94 | 95 | /** 96 | * Returns the actual DOM element that this BaseComponent refers to. 97 | * Calling this before the element is created (i.e. before `generateHTML`) 98 | * is called will throw an error. 99 | */ 100 | protected _getElement() { 101 | return UIUtil.getElementById(this._id) as T; 102 | } 103 | 104 | /** 105 | * Each BaseComponent is assignd an ID that can be used a DOM element with. 106 | */ 107 | protected _getId() { 108 | return this._id; 109 | } 110 | 111 | /** 112 | * A delegate that is called when the enabled status is changed. 113 | */ 114 | protected abstract _onEnabledChanged(): void; 115 | 116 | /** 117 | * Called after _onEnabledChanged() and _onValueChanged() 118 | */ 119 | protected _updateStyles(): void { 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/ui/components/button.ts: -------------------------------------------------------------------------------- 1 | import { TLocalisedString } from '../../localiser'; 2 | import { UIUtil } from '../../util/ui_util'; 3 | import { BaseComponent } from './base'; 4 | 5 | export class ButtonComponent extends BaseComponent { 6 | private _label: TLocalisedString; 7 | private _defaultLabel: TLocalisedString; 8 | private _onClick: () => void; 9 | 10 | public constructor() { 11 | super(); 12 | this._label = 'Unknown' as TLocalisedString; 13 | this._defaultLabel = 'Unknown' as TLocalisedString; 14 | this._onClick = () => { }; 15 | } 16 | 17 | /** 18 | * Sets the delegate that is called when this button is clicked. 19 | */ 20 | public setOnClick(delegate: () => void) { 21 | this._onClick = delegate; 22 | return this; 23 | } 24 | 25 | /** 26 | * Sets the label of this button. 27 | */ 28 | public setLabel(label: TLocalisedString) { 29 | this._label = label; 30 | this._defaultLabel = label; 31 | return this; 32 | } 33 | 34 | public updateLabel() { 35 | const labelElement = UIUtil.getElementById(`${this._getId()}-button-label`); 36 | labelElement.innerHTML = this._label; 37 | return this; 38 | } 39 | 40 | /** 41 | * Override the current label with a new value. 42 | */ 43 | public setLabelOverride(label: TLocalisedString) { 44 | this._label = label; 45 | this.updateLabel(); 46 | return this; 47 | } 48 | 49 | /** 50 | * Remove the label override and set the label back to its default 51 | */ 52 | public removeLabelOverride() { 53 | this._label = this._defaultLabel; 54 | this.updateLabel(); 55 | return this; 56 | } 57 | 58 | /** 59 | * Start the loading animation 60 | */ 61 | public startLoading() { 62 | this._getElement().classList.add('button-loading'); 63 | return this; 64 | } 65 | 66 | /** 67 | * Set the progress bar progress. 68 | * @param progress A number between 0.0 and 1.0 inclusive. 69 | */ 70 | public setProgress(progress: number) { 71 | const progressBarElement = UIUtil.getElementById(this._getProgressBarId()); 72 | progressBarElement.style.width = `${progress * 100}%`; 73 | return this; 74 | } 75 | 76 | /** 77 | * Stop the loading animation 78 | */ 79 | public stopLoading() { 80 | this._getElement().classList.remove('button-loading'); 81 | return this; 82 | } 83 | 84 | public resetLoading() { 85 | this.stopLoading(); 86 | this.setProgress(0.0); 87 | } 88 | 89 | public override generateHTML() { 90 | return ` 91 |
92 |
93 |
${this._label}
94 |
95 |
96 |
97 | `; 98 | } 99 | 100 | public override registerEvents(): void { 101 | this._getElement().addEventListener('click', () => { 102 | if (this.enabled) { 103 | this._onClick?.(); 104 | } 105 | }); 106 | 107 | this._getElement().addEventListener('mouseenter', () => { 108 | this._setHovered(true); 109 | this._updateStyles(); 110 | }); 111 | 112 | this._getElement().addEventListener('mouseleave', () => { 113 | this._setHovered(false); 114 | this._updateStyles(); 115 | }); 116 | } 117 | 118 | protected override _onEnabledChanged() { 119 | this._updateStyles(); 120 | } 121 | 122 | public override finalise(): void { 123 | this._updateStyles(); 124 | } 125 | 126 | /** 127 | * Gets the ID of the DOM element for the button's progress bar. 128 | */ 129 | private _getProgressBarId() { 130 | return this._getId() + '-progress'; 131 | } 132 | 133 | protected _updateStyles(): void { 134 | UIUtil.updateStyles(this._getElement(), { 135 | isActive: true, 136 | isEnabled: this.enabled, 137 | isHovered: this.hovered, 138 | }); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/ui/components/colour.ts: -------------------------------------------------------------------------------- 1 | import { RGBA, RGBAUtil } from '../../colour'; 2 | import { ConfigComponent } from './config'; 3 | 4 | export class ColourComponent extends ConfigComponent { 5 | public constructor(colour: RGBA) { 6 | super(colour); 7 | } 8 | 9 | protected override _generateInnerHTML(): string { 10 | return ``; 11 | } 12 | 13 | public override registerEvents(): void { 14 | this._getElement().addEventListener('change', () => { 15 | const newColour = RGBAUtil.fromHexString(this._getElement().value); 16 | this._setValue(newColour); 17 | }); 18 | } 19 | 20 | protected _onEnabledChanged(): void { 21 | super._onEnabledChanged(); 22 | 23 | if (this.enabled) { 24 | this._getElement().disabled = false; 25 | } else { 26 | this._getElement().disabled = true; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ui/components/combobox.ts: -------------------------------------------------------------------------------- 1 | import { TTranslationMap } from '../../../loc/base'; 2 | import { DeepLeafKeys, LOC } from '../../localiser'; 3 | import { UIUtil } from '../../util/ui_util'; 4 | import { AppIcons } from '../icons'; 5 | import { HTMLBuilder } from '../misc'; 6 | import { ConfigComponent } from './config'; 7 | 8 | export type ComboBoxItem = { 9 | payload: T, 10 | displayLocKey: DeepLeafKeys, 11 | } | { 12 | payload: T, 13 | displayText: string, 14 | }; 15 | 16 | export class ComboboxComponent extends ConfigComponent { 17 | private _items: ComboBoxItem[]; 18 | 19 | public constructor() { 20 | super(); 21 | this._items = []; 22 | } 23 | 24 | public addItems(items: ComboBoxItem[]) { 25 | items.forEach((item) => { 26 | this.addItem(item); 27 | }); 28 | return this; 29 | } 30 | 31 | public addItem(item: ComboBoxItem) { 32 | if (this._items.length === 0) { 33 | this.setDefaultValue(item.payload); 34 | } 35 | 36 | this._items.push(item); 37 | //this._setValue(this._items[0].payload); 38 | return this; 39 | } 40 | 41 | public override registerEvents(): void { 42 | this._getElement().addEventListener('mouseenter', () => { 43 | this._setHovered(true); 44 | this._updateStyles(); 45 | }); 46 | 47 | this._getElement().addEventListener('mouseleave', () => { 48 | this._setHovered(false); 49 | this._updateStyles(); 50 | }); 51 | 52 | this._getElement().addEventListener('change', (e: Event) => { 53 | const selectedValue = this._items[this._getElement().selectedIndex].payload; 54 | this._setValue(selectedValue); 55 | }); 56 | } 57 | 58 | public setOptionEnabled(index: number, enabled: boolean) { 59 | const option = UIUtil.getElementById(this._getId() + '-' + index) as HTMLOptionElement; 60 | option.disabled = !enabled; 61 | } 62 | 63 | public override _generateInnerHTML() { 64 | const builder = new HTMLBuilder(); 65 | 66 | builder.add('
'); 67 | builder.add(`'); 76 | 77 | builder.add(`
`); 78 | builder.add(AppIcons.ARROW_DOWN); 79 | builder.add(`
`); 80 | builder.add('
'); 81 | 82 | return builder.toString(); 83 | } 84 | 85 | protected _onValueChanged(): void { 86 | super._onValueChanged(); 87 | } 88 | 89 | protected _onEnabledChanged(): void { 90 | super._onEnabledChanged(); 91 | this._getElement().disabled = this.disabled; 92 | this._updateStyles(); 93 | } 94 | 95 | protected override _updateStyles(): void { 96 | UIUtil.updateStyles(this._getElement(), { 97 | isHovered: this.hovered, 98 | isEnabled: this.enabled, 99 | isActive: false, 100 | }); 101 | 102 | const arrowElement = UIUtil.getElementById(this._getId() + '-arrow'); 103 | arrowElement.classList.remove('text-dark'); 104 | arrowElement.classList.remove('text-standard'); 105 | arrowElement.classList.remove('text-light'); 106 | if (this.enabled) { 107 | if (this.hovered) { 108 | arrowElement.classList.add('text-light'); 109 | } else { 110 | arrowElement.classList.add('text-standard'); 111 | } 112 | } else { 113 | arrowElement.classList.add('text-dark'); 114 | } 115 | } 116 | 117 | public override finalise(): void { 118 | super.finalise(); 119 | 120 | const selectedIndex = this._items.findIndex((item) => item.payload === this.getValue()); 121 | const element = this._getElement(); 122 | 123 | element.selectedIndex = selectedIndex; 124 | 125 | this._updateStyles(); 126 | } 127 | 128 | public override refresh(): void { 129 | super.refresh(); 130 | 131 | this._items.forEach((item, index) => { 132 | if ('displayLocKey' in item) { 133 | const element = UIUtil.getElementById(this._getId() + '-' + index) as HTMLOptionElement; 134 | element.text = LOC(item.displayLocKey); 135 | } 136 | }); 137 | } 138 | 139 | public setValue(value: T) { 140 | this._setValue(value); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/ui/components/file_input.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { ASSERT } from '../../util/error_util'; 4 | import { UIUtil } from '../../util/ui_util'; 5 | import { ConfigComponent } from './config'; 6 | import { AppIcons } from '../icons'; 7 | import { LOC } from '../../localiser'; 8 | 9 | export class FileComponent extends ConfigComponent { 10 | private _loadedFilePath: string | null; 11 | 12 | public constructor() { 13 | super(); 14 | this._loadedFilePath = null; 15 | } 16 | 17 | protected override _generateInnerHTML() { 18 | return ` 19 |
20 | 21 | ${this._loadedFilePath ?? LOC('import.components.no_file_chosen')} 22 |
23 | `; 24 | } 25 | 26 | public override registerEvents(): void { 27 | this._getElement().addEventListener('mouseenter', () => { 28 | this._setHovered(true); 29 | this._updateStyles(); 30 | }); 31 | 32 | this._getElement().addEventListener('mouseleave', () => { 33 | this._setHovered(false); 34 | this._updateStyles(); 35 | }); 36 | 37 | const inputElement = UIUtil.getElementById(this._getId() + '-input') as HTMLInputElement; 38 | 39 | inputElement.addEventListener('change', () => { 40 | const files = inputElement.files; 41 | if (files?.length === 1) { 42 | const file = files.item(0); 43 | ASSERT(file !== null); 44 | this._loadedFilePath = file.name; 45 | this._setValue(file); 46 | } 47 | }); 48 | 49 | this._getElement().addEventListener('click', () => { 50 | if (this.enabled) { 51 | inputElement.click(); 52 | } 53 | }); 54 | } 55 | 56 | protected _onValueChanged(): void { 57 | this._updateStyles(); 58 | } 59 | 60 | protected _onEnabledChanged(): void { 61 | super._onEnabledChanged(); 62 | 63 | this._updateStyles(); 64 | } 65 | 66 | protected override _updateStyles() { 67 | if (this._loadedFilePath) { 68 | const parsedPath = path.parse(this._loadedFilePath); 69 | this._getElement().innerHTML = parsedPath.name + parsedPath.ext; 70 | } else { 71 | this._getElement().innerHTML = `${LOC('import.components.no_file_chosen')}`; 72 | } 73 | 74 | UIUtil.updateStyles(this._getElement(), { 75 | isHovered: this.hovered, 76 | isEnabled: this.enabled, 77 | isActive: false, 78 | }); 79 | } 80 | 81 | public override refresh(): void { 82 | this._getElement().innerHTML = `${LOC('import.components.no_file_chosen')}`; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/ui/components/full_config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigComponent } from './config'; 2 | 3 | /** 4 | * A `FullConfigComponent` is a UI element that has a value the user can change. 5 | * For example, sliders, comboboxes and checkboxes are `ConfigComponent`. 6 | */ 7 | export abstract class FullConfigComponent extends ConfigComponent { 8 | public override generateHTML() { 9 | return ` 10 |
11 |
12 | ${this._label} 13 |
14 | ${this._generateInnerHTML()} 15 |
16 | `; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/components/image.ts: -------------------------------------------------------------------------------- 1 | import { LOC } from '../../localiser'; 2 | import { TImageRawWrap } from '../../texture'; 3 | import { getRandomID } from '../../util'; 4 | import { ASSERT } from '../../util/error_util'; 5 | import { UIUtil } from '../../util/ui_util'; 6 | import { AppIcons } from '../icons'; 7 | import { ConfigComponent } from './config'; 8 | import { ToolbarItemComponent } from './toolbar_item'; 9 | 10 | export class ImageComponent extends ConfigComponent, HTMLImageElement> { 11 | private _switchElement: ToolbarItemComponent; 12 | 13 | private _imageId: string; 14 | 15 | public constructor(param?: TImageRawWrap) { 16 | super(Promise.resolve(param ?? { raw: '', filetype: 'png' })); 17 | 18 | this._switchElement = new ToolbarItemComponent({ id: 'sw', iconSVG: AppIcons.UPLOAD }) 19 | .setLabel(LOC('materials.components.choose')) 20 | .onClick(() => { 21 | const inputElement = UIUtil.getElementById(this._getId() + '-input') as HTMLInputElement; 22 | inputElement.click(); 23 | }); 24 | 25 | this._imageId = getRandomID(); 26 | } 27 | 28 | public override _generateInnerHTML() { 29 | return ` 30 |
31 |
32 | Texture Preview 33 |
34 |
35 |
${AppIcons.IMAGE_MISSING}
36 |
${LOC('materials.components.no_image_loaded')}
37 |
38 |
39 |
40 |
41 | 42 | ${this._switchElement.generateHTML()} 43 |
44 |
45 | `; 46 | } 47 | 48 | public override registerEvents(): void { 49 | this._switchElement.registerEvents(); 50 | 51 | const inputElement = UIUtil.getElementById(this._getId() + '-input') as HTMLInputElement; 52 | inputElement.addEventListener('change', () => { 53 | const files = inputElement.files; 54 | if (files?.length === 1) { 55 | const file = files.item(0); 56 | ASSERT(file !== null); 57 | ASSERT(file.type === 'image/jpeg' || file.type === 'image/png', 'Unexpected image type'); 58 | 59 | this._setValue(new Promise((res, rej) => { 60 | const fileReader = new FileReader(); 61 | fileReader.onload = function () { 62 | if (typeof fileReader.result === 'string') { 63 | // convert image file to base64 string 64 | res({ filetype: file.type === 'image/jpeg' ? 'jpg' : 'png', raw: fileReader.result }); 65 | } else { 66 | rej(Error()); 67 | } 68 | }; 69 | fileReader.readAsDataURL(file); 70 | })); 71 | } 72 | }); 73 | } 74 | 75 | protected override _onEnabledChanged(): void { 76 | super._onEnabledChanged(); 77 | 78 | const imageElement = UIUtil.getElementById(this._imageId) as HTMLImageElement; 79 | const placeholderComponent = UIUtil.getElementById(this._imageId + '-placeholder'); 80 | if (!this.enabled) { 81 | imageElement.classList.add('disabled'); 82 | placeholderComponent.classList.add('disabled'); 83 | } else { 84 | imageElement.classList.remove('disabled'); 85 | placeholderComponent.classList.remove('disabled'); 86 | } 87 | 88 | this._switchElement.setEnabled(this.enabled); 89 | } 90 | 91 | protected override _onValueChanged(): void { 92 | const inputElement = UIUtil.getElementById(this._imageId) as HTMLImageElement; 93 | const placeholderComponent = UIUtil.getElementById(this._imageId + '-placeholder'); 94 | 95 | this.getValue() 96 | .then((res) => { 97 | if (res.raw === '') { 98 | throw Error(); 99 | } 100 | this._switchElement.setActive(false); 101 | inputElement.src = res.raw; 102 | inputElement.style.display = 'unset'; 103 | placeholderComponent.style.display = 'none'; 104 | }) 105 | .catch((err) => { 106 | this._switchElement.setActive(true); 107 | inputElement.src = ''; 108 | inputElement.style.display = 'none'; 109 | placeholderComponent.style.display = 'flex'; 110 | }); 111 | } 112 | 113 | public override finalise(): void { 114 | super.finalise(); 115 | 116 | this._onValueChanged(); 117 | this._onEnabledChanged(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/ui/components/material_type.ts: -------------------------------------------------------------------------------- 1 | import { LOC } from '../../localiser'; 2 | import { MaterialType, SolidMaterial, TexturedMaterial } from '../../mesh'; 3 | import { AppIcons } from '../icons'; 4 | import { ConfigComponent } from './config'; 5 | import { ToolbarItemComponent } from './toolbar_item'; 6 | 7 | export class MaterialTypeComponent extends ConfigComponent { 8 | private _solidButton: ToolbarItemComponent; 9 | private _texturedButton: ToolbarItemComponent; 10 | private _material: SolidMaterial | TexturedMaterial; 11 | 12 | public constructor(material: SolidMaterial | TexturedMaterial) { 13 | super(material.type); 14 | this._material = material; 15 | 16 | this._solidButton = new ToolbarItemComponent({ id: 'sw1', iconSVG: AppIcons.COLOUR_SWATCH }) 17 | .setLabel(LOC('materials.components.solid')) 18 | .setGrow() 19 | .onClick(() => { 20 | if (this._material.type === MaterialType.textured) { 21 | this._onClickChangeTypeDelegate?.(); 22 | } 23 | }); 24 | 25 | this._texturedButton = new ToolbarItemComponent({ id: 'sw2', iconSVG: AppIcons.IMAGE }) 26 | .setLabel(LOC('materials.components.textured')) 27 | .setGrow() 28 | .onClick(() => { 29 | if (this._material.type === MaterialType.solid) { 30 | this._onClickChangeTypeDelegate?.(); 31 | } 32 | }); 33 | } 34 | 35 | public override _generateInnerHTML() { 36 | return ` 37 |
38 | ${this._solidButton.generateHTML()} 39 | ${this._texturedButton.generateHTML()} 40 |
41 | `; 42 | } 43 | 44 | public override finalise(): void { 45 | this._solidButton.finalise(); 46 | this._texturedButton.finalise(); 47 | 48 | this._solidButton.setActive(this._material.type === MaterialType.solid); 49 | this._texturedButton.setActive(this._material.type === MaterialType.textured); 50 | } 51 | 52 | public override registerEvents(): void { 53 | this._solidButton.registerEvents(); 54 | this._texturedButton.registerEvents(); 55 | } 56 | 57 | protected override _onEnabledChanged(): void { 58 | super._onEnabledChanged(); 59 | 60 | this._solidButton.setEnabled(this.enabled); 61 | this._texturedButton.setEnabled(this.enabled && (this._material.type === MaterialType.textured || this._material.canBeTextured)); 62 | } 63 | 64 | protected override _onValueChanged(): void { 65 | } 66 | 67 | private _onClickChangeTypeDelegate?: () => void; 68 | public onClickChangeTypeDelegate(delegate: () => void) { 69 | this._onClickChangeTypeDelegate = delegate; 70 | return this; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/ui/components/number_input.ts: -------------------------------------------------------------------------------- 1 | import { UIUtil } from '../../util/ui_util'; 2 | import { ConfigComponent } from './config'; 3 | 4 | export class NumberComponent extends ConfigComponent { 5 | private _min: number; 6 | private _max: number; 7 | private _step: number; 8 | private _hovering: boolean; 9 | 10 | public constructor() { 11 | super(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)); 12 | this._min = 0; 13 | this._max = 1; 14 | this._step = 0.1; 15 | this._hovering = false; 16 | } 17 | 18 | /** 19 | * Set the minimum value the input can be set to. 20 | */ 21 | public setMin(min: number) { 22 | this._min = min; 23 | return this; 24 | } 25 | 26 | /** 27 | * Set the maximum value the input can be set to. 28 | */ 29 | public setMax(max: number) { 30 | this._max = max; 31 | return this; 32 | } 33 | 34 | /** 35 | * Set the number of steps to display the value to. 36 | */ 37 | public setStep(step: number) { 38 | this._step = step; 39 | return this; 40 | } 41 | 42 | public override registerEvents() { 43 | this._getElement().addEventListener('change', () => { 44 | this._setValue(parseInt(this._getElement().value)); 45 | }); 46 | 47 | this._getElement().addEventListener('mouseenter', () => { 48 | this._setHovered(true); 49 | this._updateStyles(); 50 | }); 51 | 52 | this._getElement().addEventListener('mouseleave', () => { 53 | this._setHovered(false); 54 | this._updateStyles(); 55 | }); 56 | } 57 | 58 | public override _generateInnerHTML() { 59 | return ` 60 | 61 | `; 62 | } 63 | 64 | protected override _onEnabledChanged() { 65 | super._onEnabledChanged(); 66 | 67 | const element = this._getElement(); 68 | element.disabled = !this.enabled; 69 | 70 | this._updateStyles(); 71 | } 72 | 73 | private _onTypedValue() { 74 | } 75 | 76 | protected _onValueChanged(): void { 77 | } 78 | 79 | protected override _updateStyles(): void { 80 | UIUtil.updateStyles(UIUtil.getElementById(this._getId()), { 81 | isActive: false, 82 | isEnabled: this.enabled, 83 | isHovered: this.hovered, 84 | }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/ui/components/placeholder.ts: -------------------------------------------------------------------------------- 1 | import { TTranslationMap } from '../../../loc/base'; 2 | import { DeepLeafKeys, LOC, TLocalisedString } from '../../localiser'; 3 | import { UIUtil } from '../../util/ui_util'; 4 | import { ConfigComponent } from './config'; 5 | 6 | export class PlaceholderComponent extends ConfigComponent { 7 | private placeholderLocKey?: string; 8 | private _placeholderlabel?: TLocalisedString; 9 | 10 | public constructor() { 11 | super(undefined); 12 | } 13 | 14 | public setPlaceholderText

>(p: P) { 15 | this.placeholderLocKey = p; 16 | this._placeholderlabel = LOC(p); 17 | return this; 18 | } 19 | 20 | public override refresh(): void { 21 | super.refresh(); 22 | 23 | this._placeholderlabel = LOC(this.placeholderLocKey as any); 24 | const placeholderElement = UIUtil.getElementById(`${this._getId()}-placeholder-text`); 25 | placeholderElement.innerHTML = this._placeholderlabel; 26 | } 27 | 28 | public override generateHTML(): string { 29 | return ` 30 |

31 |
32 |
33 | ${this._generateInnerHTML()} 34 |
35 | `; 36 | } 37 | 38 | protected override _generateInnerHTML(): string { 39 | return ` 40 |
41 | ${this._placeholderlabel} 42 |
43 | `; 44 | } 45 | 46 | public override registerEvents(): void { 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ui/components/solid_material.ts: -------------------------------------------------------------------------------- 1 | import { SolidMaterial } from '../../mesh'; 2 | import { ColourComponent } from './colour'; 3 | import { ConfigComponent } from './config'; 4 | import { MaterialTypeComponent } from './material_type'; 5 | import { SliderComponent } from './slider'; 6 | 7 | export class SolidMaterialComponent extends ConfigComponent { 8 | private _typeElement: MaterialTypeComponent; 9 | private _ColourComponent: ColourComponent; 10 | private _alphaElement: SliderComponent; 11 | 12 | public constructor(materialName: string, material: SolidMaterial) { 13 | super(material); 14 | 15 | this._typeElement = new MaterialTypeComponent(material) 16 | .setLabel('materials.components.material_type'); 17 | 18 | this._ColourComponent = new ColourComponent(material.colour) 19 | .setLabel('voxelise.components.colour'); 20 | 21 | this._alphaElement = new SliderComponent() 22 | .setLabel('materials.components.alpha') 23 | .setMin(0.0) 24 | .setMax(1.0) 25 | .setDefaultValue(material.colour.a) 26 | .setDecimals(2) 27 | .setStep(0.01); 28 | 29 | this.setCanMinimise(); 30 | } 31 | 32 | public override refresh() { 33 | super.refresh(); 34 | 35 | this._typeElement.refresh(); 36 | this._ColourComponent.refresh(); 37 | this._alphaElement.refresh(); 38 | } 39 | 40 | public override registerEvents(): void { 41 | this._typeElement.registerEvents(); 42 | this._ColourComponent.registerEvents(); 43 | this._alphaElement.registerEvents(); 44 | 45 | this._typeElement.onClickChangeTypeDelegate(() => { 46 | this._onChangeTypeDelegate?.(); 47 | }); 48 | 49 | this._ColourComponent.addValueChangedListener((newColour) => { 50 | this.getValue().colour.r = newColour.r; 51 | this.getValue().colour.g = newColour.g; 52 | this.getValue().colour.b = newColour.b; 53 | }); 54 | 55 | this._alphaElement.addValueChangedListener((newAlpha) => { 56 | this.getValue().colour.a = newAlpha; 57 | }); 58 | } 59 | 60 | public override finalise(): void { 61 | super.finalise(); 62 | 63 | this._typeElement.finalise(); 64 | this._ColourComponent.finalise(); 65 | this._alphaElement.finalise(); 66 | } 67 | 68 | protected override _generateInnerHTML(): string { 69 | return ` 70 |
71 | ${this._typeElement.generateHTML()} 72 | ${this._ColourComponent.generateHTML()} 73 | ${this._alphaElement.generateHTML()} 74 |
75 | `; 76 | } 77 | 78 | protected override _onValueChanged(): void { 79 | } 80 | 81 | protected override _onEnabledChanged(): void { 82 | super._onEnabledChanged(); 83 | 84 | this._typeElement.setEnabled(this.enabled); 85 | this._ColourComponent.setEnabled(this.enabled); 86 | this._alphaElement.setEnabled(this.enabled); 87 | } 88 | 89 | private _onChangeTypeDelegate?: () => void; 90 | public onChangeTypeDelegate(delegate: () => void) { 91 | this._onChangeTypeDelegate = delegate; 92 | return this; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/ui/components/toolbar_item.ts: -------------------------------------------------------------------------------- 1 | import { ASSERT } from '../../util/error_util'; 2 | import { UIUtil } from '../../util/ui_util'; 3 | import { BaseComponent } from './base'; 4 | import { LOC, TLocalisedKey } from '../../localiser'; 5 | 6 | export type TToolbarBooleanProperty = 'enabled' | 'active'; 7 | 8 | export type TToolbarItemParams = { 9 | id: string, 10 | iconSVG: string; 11 | } 12 | 13 | export class ToolbarItemComponent extends BaseComponent { 14 | private _iconSVG: SVGSVGElement; 15 | private _label: string; 16 | private _onClick?: () => void; 17 | private _isActive: boolean; 18 | private _grow: boolean; 19 | private _tooltipLocKey: TLocalisedKey | null; 20 | 21 | public constructor(params: TToolbarItemParams) { 22 | super(); 23 | 24 | this._isActive = false; 25 | this._grow = false; 26 | 27 | { 28 | const parser = new DOMParser(); 29 | const svgParse = parser.parseFromString(params.iconSVG, 'text/html'); 30 | const svgs = svgParse.getElementsByTagName('svg'); 31 | ASSERT(svgs.length === 1, 'Missing SVG'); 32 | 33 | this._iconSVG = svgs[0]; 34 | this._iconSVG.id = this._getId() + '-svg'; 35 | } 36 | 37 | this._label = ''; 38 | this._tooltipLocKey = null; 39 | } 40 | 41 | public setGrow() { 42 | this._grow = true; 43 | return this; 44 | } 45 | 46 | public updateTranslation() { 47 | if (this._tooltipLocKey) { 48 | UIUtil.getElementById(this._getId() + '-tooltip').innerHTML = LOC(this._tooltipLocKey); 49 | } 50 | } 51 | 52 | public setActive(isActive: boolean) { 53 | this._isActive = isActive; 54 | this._updateStyles(); 55 | } 56 | 57 | public setLabel(label: string) { 58 | this._label = label; 59 | return this; 60 | } 61 | 62 | public tick() { 63 | if (this._isEnabledDelegate !== undefined) { 64 | const newIsEnabled = this._isEnabledDelegate(); 65 | if (newIsEnabled != this.enabled) { 66 | this.setEnabled(newIsEnabled); 67 | this._updateStyles(); 68 | } 69 | } 70 | 71 | if (this._isActiveDelegate !== undefined) { 72 | const newIsActive = this._isActiveDelegate(); 73 | if (newIsActive !== this._isActive) { 74 | this._isActive = newIsActive; 75 | this._updateStyles(); 76 | } 77 | } 78 | } 79 | 80 | protected _onEnabledChanged(): void { 81 | this._updateStyles(); 82 | } 83 | 84 | private _isActiveDelegate?: () => boolean; 85 | public isActive(delegate: () => boolean) { 86 | this._isActiveDelegate = delegate; 87 | return this; 88 | } 89 | 90 | private _isEnabledDelegate?: () => boolean; 91 | public isEnabled(delegate: () => boolean) { 92 | this._isEnabledDelegate = delegate; 93 | return this; 94 | } 95 | 96 | public onClick(delegate: () => void) { 97 | this._onClick = delegate; 98 | 99 | return this; 100 | } 101 | 102 | public setTooltip(text: TLocalisedKey) { 103 | this._tooltipLocKey = text; 104 | return this; 105 | } 106 | 107 | public generateHTML() { 108 | if (this._grow) { 109 | return ` 110 |
111 | ${this._iconSVG.outerHTML} ${this._label} 112 |
113 | `; 114 | } else { 115 | if (this._tooltipLocKey === null) { 116 | return ` 117 |
118 | ${this._iconSVG.outerHTML} ${this._label} 119 |
120 | `; 121 | } else { 122 | return ` 123 |
124 | ${this._iconSVG.outerHTML} ${this._label} 125 | ${LOC(this._tooltipLocKey)} 126 |
127 | `; 128 | } 129 | } 130 | } 131 | 132 | public registerEvents(): void { 133 | const element = document.getElementById(this._getId()) as HTMLDivElement; 134 | ASSERT(element !== null); 135 | 136 | element.addEventListener('click', () => { 137 | if (this.enabled && this._onClick) { 138 | this._onClick(); 139 | } 140 | }); 141 | 142 | element.addEventListener('mouseenter', () => { 143 | this._setHovered(true); 144 | this._updateStyles(); 145 | }); 146 | 147 | element.addEventListener('mouseleave', () => { 148 | this._setHovered(false); 149 | this._updateStyles(); 150 | }); 151 | } 152 | 153 | public override finalise(): void { 154 | this._updateStyles(); 155 | } 156 | 157 | private _getSVGElement() { 158 | const svgId = this._getId() + '-svg'; 159 | return UIUtil.getElementById(svgId); 160 | } 161 | 162 | protected override _updateStyles() { 163 | UIUtil.updateStyles(this._getElement(), { 164 | isActive: this._isActive, 165 | isEnabled: this.enabled, 166 | isHovered: this.hovered, 167 | }); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/ui/console.ts: -------------------------------------------------------------------------------- 1 | import { TLocalisedString } from '../localiser'; 2 | import { LOG, LOG_ERROR, LOG_WARN } from '../util/log_util'; 3 | import { UIUtil } from '../util/ui_util'; 4 | import { HTMLBuilder } from './misc'; 5 | 6 | export type TMessage = { text: TLocalisedString, type: 'success' | 'info' | 'warning' | 'error' }; 7 | 8 | export class AppConsole { 9 | private static _instance: AppConsole; 10 | public static get Get() { 11 | return this._instance || (this._instance = new this()); 12 | } 13 | 14 | private _built: boolean; 15 | private _messages: TMessage[]; 16 | 17 | private constructor() { 18 | this._built = false; 19 | this._messages = []; 20 | } 21 | 22 | public build() { 23 | const messagesHTML = new HTMLBuilder(); 24 | 25 | messagesHTML.add('
'); 26 | { 27 | this._messages.forEach((message) => { 28 | messagesHTML.add(this._getMessageHTML(message)); 29 | }); 30 | } 31 | messagesHTML.add('
'); 32 | 33 | messagesHTML.placeInto('console'); 34 | 35 | this._built = true; 36 | 37 | this._scrollToBottom(); 38 | } 39 | 40 | public addLast() { 41 | if (!this._built) { 42 | return; 43 | } 44 | 45 | const consoleElement = UIUtil.getElementById('inner-console') as HTMLDivElement; 46 | consoleElement.innerHTML += this._getMessageHTML(this._messages[this._messages.length - 1]); 47 | 48 | this._scrollToBottom(); 49 | } 50 | 51 | private _getMessageHTML(message: TMessage) { 52 | switch (message.type) { 53 | case 'success': 54 | return `
[OKAY]: ${message.text}
`; 55 | case 'info': 56 | return `
[INFO]: ${message.text}
`; 57 | case 'warning': 58 | return `
[WARN]: ${message.text}
`; 59 | case 'error': 60 | return `
[UHOH]: ${message.text}
`; 61 | } 62 | } 63 | 64 | public static add(message: TMessage) { 65 | switch (message.type) { 66 | case 'error': 67 | this.error(message.text); 68 | break; 69 | case 'warning': 70 | this.warning(message.text); 71 | break; 72 | case 'info': 73 | this.info(message.text); 74 | break; 75 | case 'success': 76 | this.success(message.text); 77 | break; 78 | } 79 | } 80 | 81 | public static success(message: TLocalisedString) { 82 | LOG(message); 83 | this.Get._messages.push({ text: message, type: 'success' }); 84 | this.Get.addLast(); 85 | } 86 | 87 | public static info(message: TLocalisedString) { 88 | LOG(message); 89 | this.Get._messages.push({ text: message, type: 'info' }); 90 | this.Get.addLast(); 91 | } 92 | 93 | public static warning(message: TLocalisedString) { 94 | LOG_WARN(message); 95 | this.Get._messages.push({ text: message, type: 'warning' }); 96 | this.Get.addLast(); 97 | } 98 | 99 | public static error(message: TLocalisedString) { 100 | LOG_ERROR(message); 101 | this.Get._messages.push({ text: message, type: 'error' }); 102 | this.Get.addLast(); 103 | } 104 | 105 | private _scrollToBottom() { 106 | const consoleElement = UIUtil.getElementById('inner-console'); 107 | consoleElement.scrollTop = consoleElement.scrollHeight; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/ui/misc.ts: -------------------------------------------------------------------------------- 1 | import { ASSERT } from '../util/error_util'; 2 | 3 | export class HTMLBuilder { 4 | private _html: string; 5 | 6 | public constructor() { 7 | this._html = ''; 8 | } 9 | 10 | public add(html: string) { 11 | this._html += html; 12 | return this; 13 | } 14 | 15 | public toString() { 16 | return this._html; 17 | } 18 | 19 | public placeInto(elementId: string) { 20 | const element = document.getElementById(elementId); 21 | ASSERT(element !== null, `Could not place HTML into element with id '${elementId}'`); 22 | element.innerHTML = this._html; 23 | } 24 | } 25 | 26 | export namespace MiscComponents { 27 | export function createGroupHeader(label: string) { 28 | return ` 29 |
30 |
31 | ${label} 32 |
33 |
34 | `; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { AppMath } from "./math"; 2 | 3 | export namespace AppUtil { 4 | export namespace Text { 5 | export function capitaliseFirstLetter(text: string) { 6 | return text.charAt(0).toUpperCase() + text.slice(1); 7 | } 8 | 9 | /** 10 | * Namespaces a block name if it is not already namespaced 11 | * For example `namespaceBlock('stone')` returns `'minecraft:stone'` 12 | */ 13 | export function namespaceBlock(blockName: string): AppTypes.TNamespacedBlockName { 14 | // https://minecraft.wiki/w/Resource_location#Namespaces 15 | return isNamespacedBlock(blockName) ? blockName : ('minecraft:' + blockName); 16 | } 17 | 18 | export function isNamespacedBlock(blockName: string): boolean { 19 | return blockName.includes(':'); 20 | } 21 | } 22 | 23 | export namespace Array { 24 | 25 | /** 26 | * An optimised function for repeating a subarray contained within a buffer multiple times by 27 | * repeatedly doubling the subarray's length. 28 | */ 29 | export function repeatedFill(buffer: Float32Array, start: number, startLength: number, desiredCount: number) { 30 | const pow = AppMath.largestPowerOfTwoLessThanN(desiredCount); 31 | 32 | let len = startLength; 33 | for (let i = 0; i < pow; ++i) { 34 | buffer.copyWithin(start + len, start, start + len); 35 | len *= 2; 36 | } 37 | 38 | const finalLength = desiredCount * startLength; 39 | buffer.copyWithin(start + len, start, start + finalLength - len); 40 | } 41 | } 42 | } 43 | 44 | /* eslint-disable */ 45 | export enum EAction { 46 | Settings = 0, 47 | Import = 1, 48 | Materials = 2, 49 | Voxelise = 3, 50 | Assign = 4, 51 | Export = 5, 52 | MAX = 6, 53 | } 54 | /* eslint-enable */ 55 | 56 | export namespace AppTypes { 57 | export type TNamespacedBlockName = string; 58 | } 59 | 60 | export class UV { 61 | public u: number; 62 | public v: number; 63 | 64 | constructor(u: number, v: number) { 65 | this.u = u; 66 | this.v = v; 67 | } 68 | 69 | public copy() { 70 | return new UV(this.u, this.v); 71 | } 72 | } 73 | 74 | /* eslint-disable */ 75 | export enum ColourSpace { 76 | RGB, 77 | LAB 78 | } 79 | /* eslint-enable */ 80 | 81 | export type TOptional = T | undefined; 82 | 83 | export function getRandomID(): string { 84 | return (Math.random() + 1).toString(36).substring(7); 85 | } 86 | -------------------------------------------------------------------------------- /src/util/error_util.ts: -------------------------------------------------------------------------------- 1 | import { TLocalisedString } from '../localiser'; 2 | 3 | export class AppError extends Error { 4 | constructor(msg: TLocalisedString) { 5 | super(msg); 6 | Object.setPrototypeOf(this, AppError.prototype); 7 | } 8 | } 9 | 10 | export function ASSERT(condition: any, errorMessage: string = 'Assertion Failed'): asserts condition { 11 | if (!condition) { 12 | Error(errorMessage); 13 | throw Error(errorMessage); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/util/file_util.ts: -------------------------------------------------------------------------------- 1 | import { TStructureExport } from "../exporters/base_exporter"; 2 | import JSZip, { file } from 'jszip'; 3 | 4 | export function download(content: any, filename: string) { 5 | const a = document.createElement('a'); // Create "a" element 6 | const blob = new Blob([content]); // Create a blob (file-like object) 7 | const url = URL.createObjectURL(blob); // Create an object URL from blob 8 | 9 | a.setAttribute('href', url); // Set "a" element link 10 | a.setAttribute('download', filename); // Set download filename 11 | a.click(); 12 | } 13 | 14 | export function downloadAsZip(zipFilename: string, files: { content: any, filename: string }[]) { 15 | const zip = new JSZip(); 16 | 17 | files.forEach((file) => { 18 | zip.file(file.filename, file.content); 19 | }); 20 | 21 | zip.generateAsync({type:"blob"}).then(function(content) { 22 | download(content, zipFilename); 23 | }); 24 | } -------------------------------------------------------------------------------- /src/util/log_util.ts: -------------------------------------------------------------------------------- 1 | import util from 'util'; 2 | 3 | import { AppConfig } from '../config'; 4 | import { AppPaths, PathUtil } from './path_util'; 5 | 6 | /** 7 | * Logs to console and file if logging `LOG` is enabled. 8 | * This should be used for verbose logs. 9 | * @see LOG_MAJOR 10 | */ 11 | export const LOG = (...data: any[]) => { 12 | if (Logger.Get.isLOGEnabled()) { 13 | // eslint-disable-next-line no-console 14 | console.log(...data); 15 | } 16 | }; 17 | 18 | export const LOGF = LOG; 19 | 20 | /** 21 | * Logs to console and file if logging `LOG_MAJOR` is enabled. 22 | * This is identical to `LOG` but can be enabled/disabled separately. 23 | * This should be used for important logs. 24 | * @see LOG 25 | */ 26 | export const LOG_MAJOR = (...data: any[]) => { 27 | if (Logger.Get.isLOGMAJOREnabled()) { 28 | // eslint-disable-next-line no-console 29 | console.log(...data); 30 | } 31 | }; 32 | 33 | /** 34 | * Logs a warning to the console and file if logging `LOG_WARN` is enabled. 35 | */ 36 | export const LOG_WARN = (...data: any[]) => { 37 | if (Logger.Get.isLOGWARNEnabled()) { 38 | // eslint-disable-next-line no-console 39 | console.warn(...data); 40 | } 41 | }; 42 | 43 | /** 44 | * Starts a timer. 45 | * @see `TIME_END` To stop the timer. 46 | * @param label The ID of this timer. 47 | */ 48 | export const TIME_START = (label: string) => { 49 | if (Logger.Get.isLOGTIMEEnabled()) { 50 | // eslint-disable-next-line no-console 51 | console.time(label); 52 | } 53 | }; 54 | 55 | /** 56 | * Stops a timer and prints the time elapsed. Not logged to file. 57 | * @see `TIME_START` To start the timer. 58 | * @param label The ID of this timer. 59 | */ 60 | export const TIME_END = (label: string) => { 61 | if (Logger.Get.isLOGTIMEEnabled()) { 62 | // eslint-disable-next-line no-console 63 | console.timeEnd(label); 64 | } 65 | }; 66 | 67 | /** 68 | * Logs an error to the console and file, always. 69 | */ 70 | export const LOG_ERROR = (...data: any[]) => { 71 | // eslint-disable-next-line no-console 72 | console.error(...data); 73 | }; 74 | 75 | /** 76 | * Logger controls enable/disabling the logging functions above. 77 | */ 78 | export class Logger { 79 | /* Singleton */ 80 | private static _instance: Logger; 81 | public static get Get() { 82 | return this._instance || (this._instance = new this()); 83 | } 84 | 85 | private _enabledLOG: boolean; 86 | private _enabledLOGMAJOR: boolean; 87 | private _enabledLOGWARN: boolean; 88 | private _enabledLOGTIME: boolean; 89 | 90 | private _enabledLogToFile?: boolean; 91 | 92 | private constructor() { 93 | this._enabledLOG = false; 94 | this._enabledLOGMAJOR = false; 95 | this._enabledLOGWARN = false; 96 | this._enabledLOGTIME = false; 97 | } 98 | 99 | /** 100 | * Allow `LOG` calls to be printed to the console and to the log file if setup. 101 | */ 102 | public enableLOG() { 103 | this._enabledLOG = true; 104 | } 105 | 106 | /** 107 | * Prevent `LOG` calls to be printed to the console and to the log file if setup. 108 | */ 109 | public disableLOG() { 110 | this._enabledLOG = false; 111 | } 112 | 113 | /** 114 | * Allow `LOG_MAJOR` calls to be printed to the console and to the log file if setup. 115 | */ 116 | public enableLOGMAJOR() { 117 | this._enabledLOGMAJOR = true; 118 | } 119 | 120 | /** 121 | * Prevent `LOG_MAJOR` calls to be printed to the console and to the log file if setup. 122 | */ 123 | public disableLOGMAJOR() { 124 | this._enabledLOGMAJOR = false; 125 | } 126 | 127 | /** 128 | * Allow `LOG_WARN` calls to be printed to the console and to the log file if setup. 129 | */ 130 | public enableLOGWARN() { 131 | this._enabledLOGWARN = true; 132 | } 133 | 134 | /** 135 | * Prevent `LOG_WARN` calls to be printed to the console and to the log file if setup. 136 | */ 137 | public disableLOGWARN() { 138 | this._enabledLOGWARN = false; 139 | } 140 | 141 | /** 142 | * Allow `TIME_START`/`TIME_END` calls to be printed to the console and to the log file if setup. 143 | */ 144 | public enableLOGTIME() { 145 | this._enabledLOGTIME = true; 146 | } 147 | 148 | /** 149 | * Prevent `TIME_START`/`TIME_END` calls to be printed to the console and to the log file if setup. 150 | */ 151 | public disableLOGTIME() { 152 | this._enabledLOGTIME = false; 153 | } 154 | 155 | /** 156 | * Prevent console log calls to logged to the log file if setup. 157 | */ 158 | public disableLogToFile() { 159 | this._enabledLogToFile = false; 160 | } 161 | 162 | /** 163 | * Whether or not `LOG` calls should be printed to the console and log file. 164 | */ 165 | public isLOGEnabled() { 166 | return this._enabledLOG; 167 | } 168 | 169 | /** 170 | * Whether or not `LOG_MAJOR` calls should be printed to the console and log file. 171 | */ 172 | public isLOGMAJOREnabled() { 173 | return this._enabledLOGMAJOR; 174 | } 175 | 176 | /** 177 | * Whether or not `LOG_WARN` calls should be printed to the console and log file. 178 | */ 179 | public isLOGWARNEnabled() { 180 | return this._enabledLOGWARN; 181 | } 182 | 183 | /** 184 | * Whether or not `TIME_START`/`TIME_END` calls should be printed to the console and log file. 185 | */ 186 | public isLOGTIMEEnabled() { 187 | return this._enabledLOGTIME; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/util/math_util.ts: -------------------------------------------------------------------------------- 1 | export namespace MathUtil { 2 | 3 | export function uint8(x: number) { 4 | return x & 0xFF; 5 | } 6 | 7 | export function int8(x: number) { 8 | return uint8(x + 0x80) - 0x80; 9 | } 10 | 11 | export function uint32(x: number) { 12 | return x >>> 0; 13 | } 14 | 15 | export function int32(x: number) { 16 | return uint32(x + 0x80000000) - 0x80000000; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/util/nbt_util.ts: -------------------------------------------------------------------------------- 1 | import { NBT, writeUncompressed } from 'prismarine-nbt'; 2 | import zlib from 'zlib'; 3 | 4 | export function saveNBT(nbt: NBT) { 5 | const uncompressedBuffer = writeUncompressed(nbt, 'big'); 6 | return zlib.gzipSync(uncompressedBuffer); 7 | } 8 | -------------------------------------------------------------------------------- /src/util/path_util.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export namespace PathUtil { 4 | export function join(...paths: string[]) { 5 | return path.join(...paths); 6 | } 7 | } 8 | 9 | export class AppPaths { 10 | /* Singleton */ 11 | private static _instance: AppPaths; 12 | public static get Get() { 13 | return this._instance || (this._instance = new this()); 14 | } 15 | 16 | private _base: string; 17 | 18 | private constructor() { 19 | this._base = PathUtil.join(__dirname, '../../..'); 20 | } 21 | 22 | public setBaseDir(dir: string) { 23 | this._base = dir; 24 | //const parsed = path.parse(dir); 25 | //ASSERT(parsed.base === 'ObjToSchematic', `AppPaths: Not correct base ${dir}`); 26 | } 27 | 28 | public get base() { 29 | return this._base; 30 | } 31 | 32 | public get resources() { 33 | return PathUtil.join(this._base, './res/'); 34 | } 35 | 36 | public get tools() { 37 | return PathUtil.join(this._base, './tools/'); 38 | } 39 | 40 | public get tests() { 41 | return PathUtil.join(this._base, './tests/'); 42 | } 43 | 44 | public get testData() { 45 | return PathUtil.join(this._base, './tests/data/'); 46 | } 47 | 48 | public get atlases() { 49 | return PathUtil.join(this.resources, './atlases/'); 50 | } 51 | 52 | public get palettes() { 53 | return PathUtil.join(this.resources, './palettes/'); 54 | } 55 | 56 | public get static() { 57 | return PathUtil.join(this.resources, './static/'); 58 | } 59 | 60 | public get shaders() { 61 | return PathUtil.join(this.resources, './shaders/'); 62 | } 63 | 64 | public get logs() { 65 | return PathUtil.join(this._base, './logs/'); 66 | } 67 | 68 | /** 69 | * The `gen` directory stores any data generated at runtime. 70 | * This can safely be deleted when the program is not running and will 71 | * be empted upon each startup. 72 | */ 73 | public get gen() { 74 | return PathUtil.join(this._base, './gen/'); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/util/regex_util.ts: -------------------------------------------------------------------------------- 1 | /** Regex for non-zero whitespace */ 2 | export const REGEX_NZ_WS = /[ \t]+/; 3 | 4 | /** Regex for number */ 5 | export const REGEX_NUMBER = /[0-9eE+\.\-]+/; 6 | 7 | export const REGEX_NZ_ANY = /.+/; 8 | 9 | export function regexCapture(identifier: string, regex: RegExp) { 10 | return new RegExp(`(?<${identifier}>${regex.source}`); 11 | } 12 | 13 | export function regexOptional(regex: RegExp) { 14 | return new RegExp(`(${regex})?`); 15 | } 16 | 17 | export function buildRegex(...args: (string | RegExp)[]) { 18 | return new RegExp(args.map((r) => { 19 | if (r instanceof RegExp) { 20 | return r.source; 21 | } 22 | return r; 23 | }).join('')); 24 | } 25 | 26 | export class RegExpBuilder { 27 | private _components: string[]; 28 | 29 | public constructor() { 30 | this._components = []; 31 | } 32 | 33 | public add(item: string | RegExp, capture?: string, optional: boolean = false): RegExpBuilder { 34 | let regex: string; 35 | if (item instanceof RegExp) { 36 | regex = item.source; 37 | } else { 38 | regex = item; 39 | } 40 | if (capture) { 41 | regex = `(?<${capture}>${regex})`; 42 | } 43 | if (optional) { 44 | regex = `(${regex})?`; 45 | } 46 | this._components.push(regex); 47 | return this; 48 | } 49 | 50 | public addMany(items: (string | RegExp)[], optional: boolean = false): RegExpBuilder { 51 | let toAdd: string = ''; 52 | for (const item of items) { 53 | if (item instanceof RegExp) { 54 | toAdd += item.source; 55 | } else { 56 | toAdd += item; 57 | } 58 | } 59 | this._components.push(optional ? `(${toAdd})?` : toAdd); 60 | return this; 61 | } 62 | 63 | public addNonzeroWhitespace(): RegExpBuilder { 64 | this.add(REGEX_NZ_WS); 65 | return this; 66 | } 67 | 68 | public toRegExp(): RegExp { 69 | return new RegExp(this._components.join('')); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/util/type_util.ts: -------------------------------------------------------------------------------- 1 | export type TBrand = K & { __brand: T }; 2 | 3 | export type Vector3Hash = TBrand; 4 | 5 | export type TDithering = 'off' | 'random' | 'ordered'; 6 | 7 | export type TAxis = 'x' | 'y' | 'z'; 8 | 9 | export type TTexelExtension = 'repeat' | 'clamp'; 10 | 11 | export type TTexelInterpolation = 'nearest' | 'linear'; 12 | 13 | export type DeepPartial = T extends object ? { 14 | [P in keyof T]?: DeepPartial; 15 | } : T; 16 | -------------------------------------------------------------------------------- /src/util/ui_util.ts: -------------------------------------------------------------------------------- 1 | import { ASSERT } from './error_util'; 2 | 3 | export type TStyleParams = { 4 | isHovered: boolean, 5 | isEnabled: boolean, 6 | isActive: boolean, 7 | } 8 | 9 | export namespace UIUtil { 10 | export function getElementById(id: string) { 11 | const element = document.getElementById(id); 12 | ASSERT(element !== null, `Attempting to getElement of nonexistent element: ${id}`); 13 | return element as HTMLElement; 14 | } 15 | 16 | export function clearStyles(element: HTMLElement) { 17 | element.classList.remove('disabled'); 18 | element.classList.remove('hover'); 19 | element.classList.remove('active'); 20 | } 21 | 22 | export function updateStyles(element: HTMLElement, style: TStyleParams) { 23 | clearStyles(element); 24 | 25 | if (style.isActive) { 26 | element.classList.add('active'); 27 | } 28 | 29 | if (!style.isEnabled) { 30 | element.classList.add('disabled'); 31 | } 32 | 33 | if (style.isHovered && style.isEnabled) { 34 | element.classList.add('hover'); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/voxelisers/base-voxeliser.ts: -------------------------------------------------------------------------------- 1 | import { RGBA, RGBAColours, RGBAUtil } from '../colour'; 2 | import { AppConfig } from '../config'; 3 | import { LOC } from '../localiser'; 4 | import { MaterialType, Mesh } from '../mesh'; 5 | import { StatusHandler } from '../status'; 6 | import { Triangle, UVTriangle } from '../triangle'; 7 | import { UV } from '../util'; 8 | import { ASSERT } from '../util/error_util'; 9 | import { Vector3 } from '../vector'; 10 | import { VoxelMesh } from '../voxel_mesh'; 11 | import { VoxeliseParams } from '../worker_types'; 12 | 13 | export abstract class IVoxeliser { 14 | public voxelise(mesh: Mesh, voxeliseParams: VoxeliseParams.Input): VoxelMesh { 15 | const voxelMesh = this._voxelise(mesh, voxeliseParams); 16 | 17 | StatusHandler.info(LOC('voxelise.voxel_count', { count: voxelMesh.getVoxelCount() })); 18 | 19 | const dim = voxelMesh.getBounds().getDimensions().add(1); 20 | StatusHandler.info(LOC('voxelise.voxel_mesh_dimensions', { x: dim.x, y: dim.y, z: dim.z })); 21 | 22 | return voxelMesh; 23 | } 24 | 25 | protected abstract _voxelise(mesh: Mesh, voxeliseParams: VoxeliseParams.Input): VoxelMesh; 26 | 27 | /** 28 | * `Location` should be in block-space. 29 | */ 30 | protected _getVoxelColour(mesh: Mesh, triangle: UVTriangle, materialName: string, location: Vector3, multisample: boolean): RGBA { 31 | const material = mesh.getMaterialByName(materialName); 32 | ASSERT(material !== undefined); 33 | 34 | if (material.type === MaterialType.solid) { 35 | return RGBAUtil.copy(material.colour); 36 | } 37 | 38 | const samples: RGBA[] = []; 39 | for (let i = 0; i < (multisample ? AppConfig.Get.MULTISAMPLE_COUNT : 1); ++i) { 40 | const offset = Vector3.random().sub(0.5); 41 | samples.push(this._internalGetVoxelColour( 42 | mesh, 43 | triangle, 44 | materialName, 45 | offset.add(location), 46 | )); 47 | } 48 | 49 | return RGBAUtil.average(...samples); 50 | } 51 | 52 | private _internalGetVoxelColour(mesh: Mesh, triangle: UVTriangle, materialName: string, location: Vector3) { 53 | const material = mesh.getMaterialByName(materialName); 54 | ASSERT(material !== undefined && material.type === MaterialType.textured); 55 | 56 | const area01 = new Triangle(triangle.v0, triangle.v1, location).getArea(); 57 | const area12 = new Triangle(triangle.v1, triangle.v2, location).getArea(); 58 | const area20 = new Triangle(triangle.v2, triangle.v0, location).getArea(); 59 | const total = area01 + area12 + area20; 60 | 61 | const w0 = area12 / total; 62 | const w1 = area20 / total; 63 | const w2 = area01 / total; 64 | 65 | const uv = new UV( 66 | triangle.uv0.u * w0 + triangle.uv1.u * w1 + triangle.uv2.u * w2, 67 | triangle.uv0.v * w0 + triangle.uv1.v * w1 + triangle.uv2.v * w2, 68 | ); 69 | 70 | if (isNaN(uv.u) || isNaN(uv.v)) { 71 | RGBAUtil.copy(RGBAColours.MAGENTA); 72 | } 73 | 74 | return mesh.sampleTextureMaterial(materialName, uv); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/voxelisers/bvh-ray-voxeliser.ts: -------------------------------------------------------------------------------- 1 | import { Mesh } from '../mesh'; 2 | import { ProgressManager } from '../progress'; 3 | import { Axes, axesToDirection, Ray } from '../ray'; 4 | import { ASSERT } from '../util/error_util'; 5 | import { LOG } from '../util/log_util'; 6 | import { Vector3 } from '../vector'; 7 | import { VoxelMesh } from '../voxel_mesh'; 8 | import { VoxeliseParams } from '../worker_types'; 9 | import { IVoxeliser } from './base-voxeliser'; 10 | 11 | const bvhtree = require('bvh-tree'); 12 | 13 | /** 14 | * This voxeliser works by projecting rays onto each triangle 15 | * on each of the principle angles and testing for intersections 16 | */ 17 | export class BVHRayVoxeliser extends IVoxeliser { 18 | protected override _voxelise(mesh: Mesh, voxeliseParams: VoxeliseParams.Input): VoxelMesh { 19 | const voxelMesh = new VoxelMesh(voxeliseParams); 20 | 21 | const meshDimensions = mesh.getBounds().getDimensions(); 22 | let scale: number; 23 | let offset = new Vector3(0.0, 0.0, 0.0); 24 | switch (voxeliseParams.constraintAxis) { 25 | case 'x': 26 | scale = (voxeliseParams.size - 1) / meshDimensions.x; 27 | offset = (voxeliseParams.size % 2 === 0) ? new Vector3(0.5, 0.0, 0.0) : new Vector3(0.0, 0.0, 0.0); 28 | break; 29 | case 'y': 30 | scale = (voxeliseParams.size - 1) / meshDimensions.y; 31 | offset = (voxeliseParams.size % 2 === 0) ? new Vector3(0.0, 0.5, 0.0) : new Vector3(0.0, 0.0, 0.0); 32 | break; 33 | case 'z': 34 | scale = (voxeliseParams.size - 1) / meshDimensions.z; 35 | offset = (voxeliseParams.size % 2 === 0) ? new Vector3(0.0, 0.0, 0.5) : new Vector3(0.0, 0.0, 0.0); 36 | break; 37 | } 38 | 39 | mesh.setTransform((vertex: Vector3) => { 40 | return vertex.copy().mulScalar(scale).add(offset); 41 | }); 42 | 43 | // Build BVH 44 | const triangles = Array<{ x: Number, y: Number, z: Number }[]>(mesh._tris.length); 45 | for (let triIndex = 0; triIndex < mesh.getTriangleCount(); ++triIndex) { 46 | const positionData = mesh.getVertices(triIndex); 47 | triangles[triIndex] = [positionData.v0, positionData.v1, positionData.v2]; 48 | } 49 | 50 | const MAX_TRIANGLES_PER_NODE = 8; 51 | LOG('Creating BVH...'); 52 | const bvh = new bvhtree.BVH(triangles, MAX_TRIANGLES_PER_NODE); 53 | LOG('BVH created...'); 54 | 55 | // Generate rays 56 | const bounds = mesh.getBounds(); 57 | bounds.min.floor(); 58 | bounds.max.ceil(); 59 | 60 | const planeDims = Vector3.sub(bounds.max, bounds.min).add(1); 61 | const numRays = (planeDims.x * planeDims.y) + (planeDims.x * planeDims.z) + (planeDims.y * planeDims.z); 62 | const rays = new Array(numRays); 63 | let rayIndex = 0; 64 | { 65 | // Generate x-plane rays 66 | for (let y = bounds.min.y; y <= bounds.max.y; ++y) { 67 | for (let z = bounds.min.z; z <= bounds.max.z; ++z) { 68 | rays[rayIndex++] = { 69 | origin: new Vector3(bounds.min.x - 1, y, z), 70 | axis: Axes.x, 71 | }; 72 | } 73 | } 74 | // Generate y-plane rays 75 | for (let x = bounds.min.x; x <= bounds.max.x; ++x) { 76 | for (let z = bounds.min.z; z <= bounds.max.z; ++z) { 77 | rays[rayIndex++] = { 78 | origin: new Vector3(x, bounds.min.y - 1, z), 79 | axis: Axes.y, 80 | }; 81 | } 82 | } 83 | // Generate z-plane rays 84 | for (let x = bounds.min.x; x <= bounds.max.x; ++x) { 85 | for (let y = bounds.min.y; y <= bounds.max.y; ++y) { 86 | rays[rayIndex++] = { 87 | origin: new Vector3(x, y, bounds.min.z - 1), 88 | axis: Axes.z, 89 | }; 90 | } 91 | } 92 | } 93 | ASSERT(rays.length === rayIndex); 94 | LOG('Rays created...'); 95 | 96 | // Ray test BVH 97 | const taskHandle = ProgressManager.Get.start('Voxelising'); 98 | for (rayIndex = 0; rayIndex < rays.length; ++rayIndex) { 99 | ProgressManager.Get.progress(taskHandle, rayIndex / rays.length); 100 | 101 | const ray = rays[rayIndex]; 102 | const intersections = bvh.intersectRay(ray.origin, axesToDirection(ray.axis), false); 103 | for (const intersection of intersections) { 104 | const point = intersection.intersectionPoint; 105 | const position = new Vector3(point.x, point.y, point.z); 106 | 107 | const voxelColour = this._getVoxelColour( 108 | mesh, 109 | mesh.getUVTriangle(intersection.triangleIndex), 110 | mesh.getMaterialByTriangle(intersection.triangleIndex), 111 | position, 112 | voxeliseParams.useMultisampleColouring, 113 | ); 114 | 115 | voxelMesh.addVoxel(position, voxelColour); 116 | } 117 | } 118 | ProgressManager.Get.end(taskHandle); 119 | 120 | mesh.clearTransform(); 121 | 122 | return voxelMesh; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/voxelisers/voxelisers.ts: -------------------------------------------------------------------------------- 1 | import { ASSERT } from '../util/error_util'; 2 | import { IVoxeliser } from './base-voxeliser'; 3 | import { BVHRayVoxeliser } from './bvh-ray-voxeliser'; 4 | import { BVHRayVoxeliserPlusThickness } from './bvh-ray-voxeliser-plus-thickness'; 5 | import { NormalCorrectedRayVoxeliser } from './normal-corrected-ray-voxeliser'; 6 | import { RayVoxeliser } from './ray-voxeliser'; 7 | 8 | export type TVoxelisers = 'bvh-ray' | 'ncrb' | 'ray-based' | 'bvh-ray-plus-thickness'; 9 | 10 | export class VoxeliserFactory { 11 | public static GetVoxeliser(voxeliser: TVoxelisers): IVoxeliser { 12 | switch (voxeliser) { 13 | case 'bvh-ray': 14 | return new BVHRayVoxeliser(); 15 | case 'ncrb': 16 | return new NormalCorrectedRayVoxeliser(); 17 | case 'ray-based': 18 | return new RayVoxeliser(); 19 | case 'bvh-ray-plus-thickness': 20 | return new BVHRayVoxeliserPlusThickness(); 21 | default: 22 | ASSERT(false, 'Unreachable'); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/worker.ts: -------------------------------------------------------------------------------- 1 | import { ProgressManager } from './progress'; 2 | import { StatusHandler } from './status'; 3 | import { AppError } from './util/error_util'; 4 | import { LOG_ERROR } from './util/log_util'; 5 | import { WorkerClient } from './worker_client'; 6 | import { TFromWorkerMessage, TToWorkerMessage } from './worker_types'; 7 | 8 | export async function doWork(message: TToWorkerMessage): Promise { 9 | StatusHandler.Get.clear(); 10 | 11 | if (message.action !== 'RenderNextVoxelMeshChunk' && message.action !== 'RenderNextBlockMeshChunk') { 12 | ProgressManager.Get.clear(); 13 | } 14 | 15 | try { 16 | switch (message.action) { 17 | case 'Init': 18 | return Promise.resolve({ 19 | action: 'Init', 20 | result: WorkerClient.Get.init(message.params), 21 | messages: StatusHandler.getAll(), 22 | }); 23 | case 'Settings': { 24 | const result = await WorkerClient.Get.settings(message.params); 25 | 26 | return Promise.resolve({ 27 | action: 'Settings', 28 | result: result, 29 | messages: StatusHandler.getAll(), 30 | }); 31 | } 32 | case 'Import': 33 | const result = await WorkerClient.Get.import(message.params); 34 | 35 | return Promise.resolve({ 36 | action: 'Import', 37 | result: result, 38 | messages: StatusHandler.getAll(), 39 | }); 40 | case 'SetMaterials': 41 | return Promise.resolve({ 42 | action: 'SetMaterials', 43 | result: WorkerClient.Get.setMaterials(message.params), 44 | messages: StatusHandler.getAll(), 45 | }); 46 | case 'RenderMesh': 47 | return Promise.resolve({ 48 | action: 'RenderMesh', 49 | result: WorkerClient.Get.renderMesh(message.params), 50 | messages: StatusHandler.getAll(), 51 | }); 52 | case 'Voxelise': 53 | return Promise.resolve({ 54 | action: 'Voxelise', 55 | result: WorkerClient.Get.voxelise(message.params), 56 | messages: StatusHandler.getAll(), 57 | }); 58 | /* 59 | case 'RenderVoxelMesh': 60 | return { 61 | action: 'RenderVoxelMesh', 62 | result: WorkerClient.Get.renderVoxelMesh(message.params), 63 | messages: StatusHandler.getAll(), 64 | }; 65 | */ 66 | case 'RenderNextVoxelMeshChunk': 67 | return Promise.resolve({ 68 | action: 'RenderNextVoxelMeshChunk', 69 | result: WorkerClient.Get.renderChunkedVoxelMesh(message.params), 70 | messages: StatusHandler.getAll(), 71 | }); 72 | case 'Assign': 73 | return Promise.resolve({ 74 | action: 'Assign', 75 | result: WorkerClient.Get.assign(message.params), 76 | messages: StatusHandler.getAll(), 77 | }); 78 | /* 79 | case 'RenderBlockMesh': 80 | return { 81 | action: 'RenderBlockMesh', 82 | result: WorkerClient.Get.renderBlockMesh(message.params), 83 | messages: StatusHandler.getAll(), 84 | }; 85 | */ 86 | case 'RenderNextBlockMeshChunk': 87 | return Promise.resolve({ 88 | action: 'RenderNextBlockMeshChunk', 89 | result: WorkerClient.Get.renderChunkedBlockMesh(message.params), 90 | messages: StatusHandler.getAll(), 91 | }); 92 | case 'Export': 93 | return Promise.resolve({ 94 | action: 'Export', 95 | result: WorkerClient.Get.export(message.params), 96 | messages: StatusHandler.getAll(), 97 | }); 98 | } 99 | } catch (e: any) { 100 | LOG_ERROR(e); 101 | return { action: e instanceof AppError ? 'KnownError' : 'UnknownError', error: e as Error }; 102 | } 103 | } 104 | 105 | -------------------------------------------------------------------------------- /src/worker_controller.ts: -------------------------------------------------------------------------------- 1 | import { AppConfig } from './config'; 2 | import { EAppEvent, EventManager } from './event'; 3 | import { AppError, ASSERT } from './util/error_util'; 4 | import { LOG } from './util/log_util'; 5 | import { doWork } from './worker'; 6 | // @ts-ignore 7 | import AppWorker from './worker_interface.worker.ts'; 8 | import { TFromWorkerMessage, TToWorkerMessage } from './worker_types'; 9 | 10 | export type TWorkerJob = { 11 | id: string, 12 | payload: TToWorkerMessage, 13 | callback?: (payload: TFromWorkerMessage) => void, // Called with the payload of the next message received by the worker 14 | } 15 | 16 | export class WorkerController { 17 | private _worker?: Worker; 18 | private _jobQueue: TWorkerJob[]; 19 | private _jobPending: TWorkerJob | undefined; 20 | private _jobStartTime: number; 21 | private _timerOn: boolean; 22 | 23 | public constructor() { 24 | if (AppConfig.Get.USE_WORKER_THREAD) { 25 | this._worker = new AppWorker(); 26 | if (this._worker) { 27 | this._worker.onmessage = this._onWorkerMessage.bind(this); 28 | } 29 | } 30 | 31 | this._jobQueue = []; 32 | this._jobStartTime = 0; 33 | this._timerOn = false; 34 | } 35 | 36 | public async execute(payload: TToWorkerMessage): Promise { 37 | return new Promise((res, rej) => { 38 | const success = this.addJob({ 39 | id: 'ExecuteJob', 40 | payload: payload, 41 | callback: res, 42 | }); 43 | ASSERT(success, 'Already performing a job'); 44 | }); 45 | } 46 | 47 | public addJob(newJob: TWorkerJob): boolean { 48 | const isJobAlreadyQueued = this._jobQueue.some((queuedJob) => { return queuedJob.id === newJob.id; }); 49 | if (isJobAlreadyQueued) { 50 | LOG('[WorkerController]: Job already queued with ID', newJob.id); 51 | return false; 52 | } 53 | 54 | this._jobQueue.push(newJob); 55 | this._tryStartNextJob(); 56 | 57 | return true; 58 | } 59 | 60 | public isBusy() { 61 | return this._jobPending !== undefined && AppConfig.Get.USE_WORKER_THREAD; 62 | } 63 | 64 | private _onWorkerMessage(payload: MessageEvent) { 65 | ASSERT(this._jobPending !== undefined, `Received worker message when no job is pending`); 66 | 67 | if (payload.data.action === 'Progress') { 68 | switch (payload.data.payload.type) { 69 | case 'Started': 70 | EventManager.Get.broadcast(EAppEvent.onTaskStart, payload.data.payload.taskId); 71 | break; 72 | case 'Progress': 73 | EventManager.Get.broadcast(EAppEvent.onTaskProgress, payload.data.payload.taskId, payload.data.payload.percentage); 74 | break; 75 | case 'Finished': 76 | EventManager.Get.broadcast(EAppEvent.onTaskEnd, payload.data.payload.taskId); 77 | break; 78 | } 79 | return; 80 | } 81 | 82 | let endTimer = true; 83 | if (payload.data.action === 'RenderNextVoxelMeshChunk') { 84 | if (payload.data.result.moreVoxelsToBuffer) { 85 | endTimer = false; 86 | } 87 | } else if (payload.data.action === 'RenderNextBlockMeshChunk') { 88 | if (payload.data.result.moreBlocksToBuffer) { 89 | endTimer = false; 90 | } 91 | } 92 | 93 | if (endTimer) { 94 | const deltaTime = Date.now() - this._jobStartTime; 95 | LOG(`[WorkerController]: '${this._jobPending.id}' completed in ${deltaTime}ms`); 96 | this._timerOn = false; 97 | } 98 | 99 | if (this._jobPending.callback) { 100 | this._jobPending.callback(payload.data); 101 | } 102 | this._jobPending = undefined; 103 | 104 | this._tryStartNextJob(); 105 | } 106 | 107 | private _tryStartNextJob() { 108 | if (this.isBusy()) { 109 | return; 110 | } 111 | 112 | this._jobPending = this._jobQueue.shift(); 113 | if (this._jobPending === undefined) { 114 | return; 115 | } 116 | 117 | if (!this._timerOn) { 118 | LOG(`[WorkerController]: Starting Job '${this._jobPending.id}' (${this._jobQueue.length} remaining)`); 119 | LOG(`[WorkerController]: ${JSON.stringify(this._jobPending.payload, null, 4)}`); 120 | this._jobStartTime = Date.now(); 121 | this._timerOn = true; 122 | } 123 | 124 | if (AppConfig.Get.USE_WORKER_THREAD) { 125 | ASSERT(this._worker !== undefined, 'No worker instance'); 126 | this._worker.postMessage(this._jobPending.payload); 127 | } else { 128 | const pendingJob = this._jobPending; 129 | 130 | doWork(this._jobPending.payload).then((result) => { 131 | if (pendingJob.callback) { 132 | pendingJob.callback(result); 133 | } 134 | }); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/worker_interface.worker.ts: -------------------------------------------------------------------------------- 1 | const workerInstance = require('./worker'); 2 | 3 | addEventListener('message', async (e) => { 4 | const result = await workerInstance.doWork(e.data); 5 | postMessage(result); 6 | }); 7 | -------------------------------------------------------------------------------- /template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ObjToSchematic Web 6 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /tests/buffer.test.ts: -------------------------------------------------------------------------------- 1 | import { AttributeData, MergeAttributeData } from '../src/render_buffer'; 2 | import { TEST_PREAMBLE } from './preamble'; 3 | 4 | test('MergeAttributeData #1', () => { 5 | TEST_PREAMBLE(); 6 | 7 | const a: AttributeData = { 8 | indices: new Uint32Array([0, 1, 2]), 9 | custom: { 10 | position: [1, 2, 3, 4, 5, 6, 7, 8, 9], 11 | colour: [1, 0, 0, 0, 1, 0, 0, 0, 1], 12 | }, 13 | }; 14 | const b = MergeAttributeData(a); 15 | expect(JSON.stringify(a)).toEqual(JSON.stringify(b)); 16 | }); 17 | 18 | test('MergeAttributeData #2', () => { 19 | TEST_PREAMBLE(); 20 | 21 | const a: AttributeData = { 22 | indices: new Uint32Array([0, 1, 2]), 23 | custom: { 24 | position: [1, 2, 3, 4, 5, 6, 7, 8, 9], 25 | colour: [1, 0, 0, 0, 1, 0, 0, 0, 1], 26 | }, 27 | }; 28 | const b: AttributeData = { 29 | indices: new Uint32Array([0, 1, 2]), 30 | custom: { 31 | position: [10, 11, 12, 13, 14, 15, 16, 17, 18], 32 | colour: [0, 1, 1, 1, 0, 1, 1, 1, 0], 33 | }, 34 | }; 35 | const cActual = MergeAttributeData(a, b); 36 | const cExpect: AttributeData = { 37 | indices: new Uint32Array([0, 1, 2, 3, 4, 5]), 38 | custom: { 39 | position: [ 40 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 41 | 10, 11, 12, 13, 14, 15, 16, 17, 18, 42 | ], 43 | colour: [ 44 | 1, 0, 0, 0, 1, 0, 0, 0, 1, 45 | 0, 1, 1, 1, 0, 1, 1, 1, 0, 46 | ], 47 | }, 48 | }; 49 | expect(JSON.stringify(cActual)).toEqual(JSON.stringify(cExpect)); 50 | }); 51 | 52 | test('MergeAttributeData #3', () => { 53 | TEST_PREAMBLE(); 54 | 55 | const a: AttributeData = { 56 | indices: new Uint32Array([0, 1]), 57 | custom: { 58 | data: [1, 2], 59 | }, 60 | }; 61 | const b: AttributeData = { 62 | indices: new Uint32Array([0, 1]), 63 | custom: { 64 | data: [3, 4], 65 | }, 66 | }; 67 | const c: AttributeData = { 68 | indices: new Uint32Array([0, 1]), 69 | custom: { 70 | data: [5, 6], 71 | }, 72 | }; 73 | const dActual = MergeAttributeData(a, b, c); 74 | const dExpect: AttributeData = { 75 | indices: new Uint32Array([0, 1, 2, 3, 4, 5]), 76 | custom: { 77 | data: [1, 2, 3, 4, 5, 6], 78 | }, 79 | }; 80 | expect(JSON.stringify(dActual)).toEqual(JSON.stringify(dExpect)); 81 | }); 82 | -------------------------------------------------------------------------------- /tests/data/cube.mtl: -------------------------------------------------------------------------------- 1 | # Blender 3.1.0 MTL File: 'None' 2 | # www.blender.org 3 | 4 | newmtl black 5 | Ns 250.000000 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 0.000000 0.000000 0.000000 8 | Ks 0.500000 0.500000 0.500000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.450000 11 | d 1.000000 12 | illum 2 13 | 14 | newmtl blue 15 | Ns 250.000000 16 | Ka 1.000000 1.000000 1.000000 17 | Kd 0.000000 0.000000 1.000000 18 | Ks 0.500000 0.500000 0.500000 19 | Ke 0.000000 0.000000 0.000000 20 | Ni 1.450000 21 | d 1.000000 22 | illum 2 23 | 24 | newmtl cyan 25 | Ns 250.000000 26 | Ka 1.000000 1.000000 1.000000 27 | Kd 0.000000 1.000000 1.000000 28 | Ks 0.500000 0.500000 0.500000 29 | Ke 0.000000 0.000000 0.000000 30 | Ni 1.450000 31 | d 1.000000 32 | illum 2 33 | 34 | newmtl green 35 | Ns 250.000000 36 | Ka 1.000000 1.000000 1.000000 37 | Kd 0.000000 1.000000 0.000000 38 | Ks 0.500000 0.500000 0.500000 39 | Ke 0.000000 0.000000 0.000000 40 | Ni 1.450000 41 | d 1.000000 42 | illum 2 43 | 44 | newmtl magenta 45 | Ns 250.000000 46 | Ka 1.000000 1.000000 1.000000 47 | Kd 1.000000 0.000000 1.000000 48 | Ks 0.500000 0.500000 0.500000 49 | Ke 0.000000 0.000000 0.000000 50 | Ni 1.450000 51 | d 1.000000 52 | illum 2 53 | 54 | newmtl red 55 | Ns 250.000000 56 | Ka 1.000000 1.000000 1.000000 57 | Kd 1.000000 0.000000 0.000000 58 | Ks 0.500000 0.500000 0.500000 59 | Ke 0.000000 0.000000 0.000000 60 | Ni 1.450000 61 | d 1.000000 62 | illum 2 63 | 64 | newmtl white 65 | Ns 250.000000 66 | Ka 1.000000 1.000000 1.000000 67 | Kd 1.000000 1.000000 1.000000 68 | Ks 0.500000 0.500000 0.500000 69 | Ke 0.000000 0.000000 0.000000 70 | Ni 1.450000 71 | d 1.000000 72 | illum 2 73 | 74 | newmtl yellow 75 | Ns 250.000000 76 | Ka 1.000000 1.000000 1.000000 77 | Kd 1.000000 1.000000 0.000000 78 | Ks 0.500000 0.500000 0.500000 79 | Ke 0.000000 0.000000 0.000000 80 | Ni 1.450000 81 | d 1.000000 82 | illum 2 83 | -------------------------------------------------------------------------------- /tests/data/cube.obj: -------------------------------------------------------------------------------- 1 | # Blender 3.1.0 2 | # www.blender.org 3 | mtllib cube.mtl 4 | o Cube 5 | v -1.000000 -1.000000 1.000000 6 | v -1.000000 1.000000 1.000000 7 | v -1.000000 -1.000000 -1.000000 8 | v -1.000000 1.000000 -1.000000 9 | v 1.000000 -1.000000 1.000000 10 | v 1.000000 1.000000 1.000000 11 | v 1.000000 -1.000000 -1.000000 12 | v 1.000000 1.000000 -1.000000 13 | v -1.000000 -1.000000 0.000000 14 | v -1.000000 1.000000 0.000000 15 | v 1.000000 -1.000000 0.000000 16 | v 1.000000 1.000000 0.000000 17 | v -1.000000 0.000000 1.000000 18 | v -1.000000 0.000000 -1.000000 19 | v 1.000000 0.000000 -1.000000 20 | v 1.000000 0.000000 1.000000 21 | v 1.000000 0.000000 0.000000 22 | v -1.000000 0.000000 0.000000 23 | v 0.000000 -1.000000 -1.000000 24 | v 0.000000 1.000000 -1.000000 25 | v 0.000000 -1.000000 1.000000 26 | v 0.000000 1.000000 1.000000 27 | v 0.000000 1.000000 0.000000 28 | v 0.000000 -1.000000 0.000000 29 | v 0.000000 0.000000 1.000000 30 | v 0.000000 0.000000 -1.000000 31 | vn -1.0000 -0.0000 -0.0000 32 | vn -0.0000 -0.0000 -1.0000 33 | vn 1.0000 -0.0000 -0.0000 34 | vn -0.0000 -0.0000 1.0000 35 | vn -0.0000 -1.0000 -0.0000 36 | vn -0.0000 1.0000 -0.0000 37 | vt 0.375000 0.000000 38 | vt 0.375000 1.000000 39 | vt 0.125000 0.750000 40 | vt 0.625000 1.000000 41 | vt 0.875000 0.750000 42 | vt 0.625000 0.000000 43 | vt 0.375000 0.250000 44 | vt 0.125000 0.500000 45 | vt 0.875000 0.500000 46 | vt 0.625000 0.250000 47 | vt 0.375000 0.750000 48 | vt 0.625000 0.750000 49 | vt 0.375000 0.500000 50 | vt 0.625000 0.500000 51 | vt 0.375000 0.125000 52 | vt 0.125000 0.625000 53 | vt 0.875000 0.625000 54 | vt 0.625000 0.125000 55 | vt 0.375000 0.625000 56 | vt 0.625000 0.625000 57 | vt 0.500000 0.000000 58 | vt 0.500000 1.000000 59 | vt 0.500000 0.250000 60 | vt 0.500000 0.500000 61 | vt 0.500000 0.750000 62 | vt 0.500000 0.625000 63 | vt 0.500000 0.125000 64 | vt 0.375000 0.375000 65 | vt 0.250000 0.500000 66 | vt 0.750000 0.500000 67 | vt 0.625000 0.375000 68 | vt 0.375000 0.875000 69 | vt 0.250000 0.750000 70 | vt 0.750000 0.750000 71 | vt 0.625000 0.875000 72 | vt 0.750000 0.625000 73 | vt 0.250000 0.625000 74 | vt 0.500000 0.875000 75 | vt 0.500000 0.375000 76 | s 0 77 | usemtl blue 78 | f 18/27/1 10/18/1 4/10/1 14/23/1 79 | usemtl green 80 | f 26/39/2 20/31/2 8/14/2 15/24/2 81 | usemtl red 82 | f 17/26/3 12/20/3 6/12/3 16/25/3 83 | usemtl white 84 | f 25/38/4 22/35/4 2/4/4 13/22/4 85 | usemtl yellow 86 | f 24/37/5 11/19/5 5/11/5 21/33/5 87 | usemtl white 88 | f 23/36/6 10/17/6 2/5/6 22/34/6 89 | usemtl blue 90 | f 20/30/6 4/9/6 10/17/6 23/36/6 91 | usemtl cyan 92 | f 19/29/5 7/13/5 11/19/5 24/37/5 93 | usemtl green 94 | f 15/24/3 8/14/3 12/20/3 17/26/3 95 | usemtl white 96 | f 13/21/1 2/6/1 10/18/1 18/27/1 97 | usemtl black 98 | f 1/1/1 13/21/1 18/27/1 9/15/1 99 | usemtl cyan 100 | f 7/13/3 15/24/3 17/26/3 11/19/3 101 | usemtl black 102 | f 21/32/4 25/38/4 13/22/4 1/2/4 103 | usemtl yellow 104 | f 11/19/3 17/26/3 16/25/3 5/11/3 105 | usemtl cyan 106 | f 19/28/2 26/39/2 15/24/2 7/13/2 107 | usemtl magenta 108 | f 9/15/1 18/27/1 14/23/1 3/7/1 109 | f 3/7/2 14/23/2 26/39/2 19/28/2 110 | usemtl yellow 111 | f 5/11/4 16/25/4 25/38/4 21/32/4 112 | usemtl magenta 113 | f 3/8/5 19/29/5 24/37/5 9/16/5 114 | usemtl green 115 | f 8/14/6 20/30/6 23/36/6 12/20/6 116 | usemtl red 117 | f 12/20/6 23/36/6 22/34/6 6/12/6 118 | usemtl black 119 | f 9/16/5 24/37/5 21/33/5 1/3/5 120 | usemtl red 121 | f 16/25/4 6/12/4 22/35/4 25/38/4 122 | usemtl blue 123 | f 14/23/2 4/10/2 20/31/2 26/39/2 124 | -------------------------------------------------------------------------------- /tests/linear_allocator.test.ts: -------------------------------------------------------------------------------- 1 | import { LinearAllocator } from '../src/linear_allocator'; 2 | import { Vector3 } from '../src/vector'; 3 | import { TEST_PREAMBLE } from './preamble'; 4 | 5 | test('RegExpBuilder', () => { 6 | TEST_PREAMBLE(); 7 | 8 | const vec = new LinearAllocator(() => { 9 | return new Vector3(0, 0, 0); 10 | }); 11 | 12 | expect(vec.size()).toBe(0); 13 | expect(vec.max()).toBe(0); 14 | const first = vec.place(); 15 | first.x = 1; 16 | expect(vec.size()).toBe(1); 17 | expect(vec.max()).toBe(1); 18 | const second = vec.place(); 19 | second.x = 2; 20 | expect(vec.size()).toBe(2); 21 | expect(vec.max()).toBe(2); 22 | vec.reset(); 23 | expect(vec.size()).toBe(0); 24 | expect(vec.max()).toBe(2); 25 | const newFirst = vec.place(); 26 | expect(newFirst.x).toBe(1); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/palette.test.ts: -------------------------------------------------------------------------------- 1 | import { Palette } from '../src/palette'; 2 | 3 | test('Palette', () => { 4 | const myPalette = Palette.create(); 5 | myPalette.add(['minecraft:stone']); 6 | expect(myPalette.count()).toBe(1); 7 | myPalette.remove('minecraft:stone'); 8 | expect(myPalette.count()).toBe(0); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/preamble.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import { Logger } from '../src/util/log_util'; 4 | import { AppPaths, PathUtil } from '../src/util/path_util'; 5 | 6 | export const TEST_PREAMBLE = () => { 7 | Logger.Get.disableLogToFile(); 8 | AppPaths.Get.setBaseDir(PathUtil.join(__dirname, '..')); 9 | 10 | const outPath = PathUtil.join(AppPaths.Get.tests, './out/'); 11 | if (!fs.existsSync(outPath)) { 12 | fs.mkdirSync(outPath); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /tests/ray.test.ts: -------------------------------------------------------------------------------- 1 | import { Axes, Ray, rayIntersectTriangle } from '../src/ray'; 2 | import { Triangle } from '../src/triangle'; 3 | import { ASSERT } from '../src/util/error_util'; 4 | import { Vector3 } from '../src/vector'; 5 | import { TEST_PREAMBLE } from './preamble'; 6 | 7 | test('rayIntersectTriangle x-axis #1', () => { 8 | TEST_PREAMBLE(); 9 | 10 | const ray: Ray = { 11 | origin: new Vector3(-1, 0, 0), 12 | axis: Axes.x, 13 | }; 14 | const tri = new Triangle( 15 | new Vector3(5, -1, -1), 16 | new Vector3(5, 0, 1), 17 | new Vector3(5, 1, -1), 18 | ); 19 | const intersects = rayIntersectTriangle(ray, tri.v0, tri.v1, tri.v2); 20 | expect(intersects).toBeDefined(); 21 | ASSERT(intersects); 22 | expect(intersects.equals(new Vector3(5, 0, 0))).toEqual(true); 23 | }); 24 | 25 | test('rayIntersectTriangle x-axis #2', () => { 26 | TEST_PREAMBLE(); 27 | 28 | const ray: Ray = { 29 | origin: new Vector3(1, 0, 0), 30 | axis: Axes.x, 31 | }; 32 | const tri = new Triangle( 33 | new Vector3(0, -1, -1), 34 | new Vector3(0, 0, 1), 35 | new Vector3(0, 1, -1), 36 | ); 37 | const intersects = rayIntersectTriangle(ray, tri.v0, tri.v1, tri.v2); 38 | expect(intersects).toBeUndefined(); 39 | }); 40 | 41 | test('rayIntersectTriangle y-axis #1', () => { 42 | TEST_PREAMBLE(); 43 | 44 | const ray: Ray = { 45 | origin: new Vector3(0, -1, 0), 46 | axis: Axes.y, 47 | }; 48 | const tri = new Triangle( 49 | new Vector3(-1, 6, -1), 50 | new Vector3(0, 6, 1), 51 | new Vector3(1, 6, -1), 52 | ); 53 | const intersects = rayIntersectTriangle(ray, tri.v0, tri.v1, tri.v2); 54 | expect(intersects).toBeDefined(); 55 | ASSERT(intersects); 56 | expect(intersects.equals(new Vector3(0, 6, 0))).toEqual(true); 57 | }); 58 | 59 | test('rayIntersectTriangle y-axis #2', () => { 60 | TEST_PREAMBLE(); 61 | 62 | const ray: Ray = { 63 | origin: new Vector3(0, 1, 0), 64 | axis: Axes.y, 65 | }; 66 | const tri = new Triangle( 67 | new Vector3(-1, 0, -1), 68 | new Vector3(0, 0, 1), 69 | new Vector3(1, 0, -1), 70 | ); 71 | const intersects = rayIntersectTriangle(ray, tri.v0, tri.v1, tri.v2); 72 | expect(intersects).toBeUndefined(); 73 | }); 74 | 75 | test('rayIntersectTriangle z-axis #1', () => { 76 | TEST_PREAMBLE(); 77 | 78 | const ray: Ray = { 79 | origin: new Vector3(0, 0, -1), 80 | axis: Axes.z, 81 | }; 82 | const tri = new Triangle( 83 | new Vector3(-1, -1, 7), 84 | new Vector3(0, 1, 7), 85 | new Vector3(1, -1, 7), 86 | ); 87 | const intersects = rayIntersectTriangle(ray, tri.v0, tri.v1, tri.v2); 88 | expect(intersects).toBeDefined(); 89 | ASSERT(intersects); 90 | expect(intersects.equals(new Vector3(0, 0, 7))).toEqual(true); 91 | }); 92 | 93 | test('rayIntersectTriangle z-axis #2', () => { 94 | TEST_PREAMBLE(); 95 | 96 | const ray: Ray = { 97 | origin: new Vector3(0, 0, 1), 98 | axis: Axes.z, 99 | }; 100 | const tri = new Triangle( 101 | new Vector3(-1, -1, 0), 102 | new Vector3(0, 1, 0), 103 | new Vector3(1, -1, 0), 104 | ); 105 | const intersects = rayIntersectTriangle(ray, tri.v0, tri.v1, tri.v2); 106 | expect(intersects).toBeUndefined(); 107 | }); 108 | -------------------------------------------------------------------------------- /tests/util.test.ts: -------------------------------------------------------------------------------- 1 | import { AppUtil } from '../src/util'; 2 | import { ASSERT } from '../src/util/error_util'; 3 | import { REGEX_NUMBER, REGEX_NZ_ANY, RegExpBuilder } from '../src/util/regex_util'; 4 | import { TEST_PREAMBLE } from './preamble'; 5 | 6 | test('RegExpBuilder', () => { 7 | TEST_PREAMBLE(); 8 | 9 | const regex = new RegExpBuilder() 10 | .add(/hello/) 11 | .toRegExp(); 12 | expect(regex.test('hello')).toBe(true); 13 | expect(regex.test('there')).toBe(false); 14 | }); 15 | 16 | test('RegExpBuilder REGEX_NUMBER', () => { 17 | TEST_PREAMBLE(); 18 | 19 | const tests = [ 20 | { f: '0', s: 0 }, 21 | { f: '0.0', s: 0.0 }, 22 | { f: '-0.0', s: -0.0 }, 23 | { f: '1', s: 1 }, 24 | { f: '1.0', s: 1.0 }, 25 | { f: '-1.0', s: -1.0 }, 26 | ]; 27 | for (const t of tests) { 28 | const temp = REGEX_NUMBER.exec(t.f); 29 | ASSERT(temp !== null); 30 | expect(parseFloat(temp[0])).toEqual(t.s); 31 | } 32 | }); 33 | 34 | test('RegExpBuilder Required-whitespace', () => { 35 | TEST_PREAMBLE(); 36 | 37 | const regex = new RegExpBuilder() 38 | .add(/hello/) 39 | .addNonzeroWhitespace() 40 | .add(/there/) 41 | .toRegExp(); 42 | expect(regex.test('hello there')).toBe(true); 43 | expect(regex.test('hello there')).toBe(true); 44 | expect(regex.test('hellothere')).toBe(false); 45 | }); 46 | 47 | test('RegExpBuilder Optional', () => { 48 | TEST_PREAMBLE(); 49 | 50 | const regex = new RegExpBuilder() 51 | .add(/hello/) 52 | .addNonzeroWhitespace() 53 | .addMany([/there/], true) 54 | .toRegExp(); 55 | expect(regex.test('hello there')).toBe(true); 56 | expect(regex.test('hello there')).toBe(true); 57 | expect(regex.test('hello ')).toBe(true); 58 | expect(regex.test('hello')).toBe(false); 59 | }); 60 | 61 | test('RegExpBuilder Capture', () => { 62 | TEST_PREAMBLE(); 63 | 64 | const regex = new RegExpBuilder() 65 | .add(/[0-9]+/, 'myNumber') 66 | .toRegExp(); 67 | const exec = regex.exec('1234'); 68 | expect(exec).toHaveProperty('groups'); 69 | if (exec !== null && exec.groups) { 70 | expect(exec.groups).toHaveProperty('myNumber'); 71 | expect(exec.groups['myNumber']).toBe('1234'); 72 | } 73 | }); 74 | 75 | test('RegExpBuilder Capture-multiple', () => { 76 | TEST_PREAMBLE(); 77 | 78 | const regex = new RegExpBuilder() 79 | .add(/[0-9]+/, 'x') 80 | .addNonzeroWhitespace() 81 | .add(/[0-9]+/, 'y') 82 | .addNonzeroWhitespace() 83 | .add(/[0-9]+/, 'z') 84 | .toRegExp(); 85 | 86 | const exec = regex.exec('123 456 789'); 87 | expect(exec).toHaveProperty('groups'); 88 | if (exec !== null && exec.groups) { 89 | expect(exec.groups).toHaveProperty('x'); 90 | expect(exec.groups).toHaveProperty('y'); 91 | expect(exec.groups).toHaveProperty('z'); 92 | expect(exec.groups['x']).toBe('123'); 93 | expect(exec.groups['y']).toBe('456'); 94 | expect(exec.groups['z']).toBe('789'); 95 | } 96 | }); 97 | 98 | test('RegExpBuilder Capture-multiple', () => { 99 | TEST_PREAMBLE(); 100 | 101 | const regex = new RegExpBuilder() 102 | .add(/f/) 103 | .addNonzeroWhitespace() 104 | .add(REGEX_NUMBER, 'xIndex').addMany(['/'], true).add(REGEX_NUMBER, 'xtIndex', true).addMany(['/', REGEX_NUMBER], true) 105 | .addNonzeroWhitespace() 106 | .add(REGEX_NUMBER, 'yIndex').addMany(['/'], true).add(REGEX_NUMBER, 'ytIndex', true).addMany(['/', REGEX_NUMBER], true) 107 | .addNonzeroWhitespace() 108 | .add(REGEX_NUMBER, 'zIndex').addMany(['/'], true).add(REGEX_NUMBER, 'ztIndex', true).addMany(['/', REGEX_NUMBER], true) 109 | .toRegExp(); 110 | 111 | let exec = regex.exec('f 1/2/3 4/5/6 7/8/9'); 112 | expect(exec).toHaveProperty('groups'); 113 | if (exec !== null && exec.groups) { 114 | expect(exec.groups['xIndex']).toBe('1'); 115 | expect(exec.groups['xtIndex']).toBe('2'); 116 | expect(exec.groups['yIndex']).toBe('4'); 117 | expect(exec.groups['ytIndex']).toBe('5'); 118 | expect(exec.groups['zIndex']).toBe('7'); 119 | expect(exec.groups['ztIndex']).toBe('8'); 120 | } 121 | 122 | exec = regex.exec('f 1//3 4//6 7//9'); 123 | expect(exec).toHaveProperty('groups'); 124 | if (exec !== null && exec.groups) { 125 | expect(exec.groups['xIndex']).toBe('1'); 126 | expect(exec.groups['xtIndex']).toBeUndefined(); 127 | expect(exec.groups['yIndex']).toBe('4'); 128 | expect(exec.groups['ytIndex']).toBeUndefined(); 129 | expect(exec.groups['zIndex']).toBe('7'); 130 | expect(exec.groups['ztIndex']).toBeUndefined(); 131 | } 132 | 133 | exec = regex.exec('f 1 4 7'); 134 | expect(exec).toHaveProperty('groups'); 135 | if (exec !== null && exec.groups) { 136 | expect(exec.groups['xIndex']).toBe('1'); 137 | expect(exec.groups['yIndex']).toBe('4'); 138 | expect(exec.groups['zIndex']).toBe('7'); 139 | } 140 | }); 141 | 142 | test('RegExpBuilder Capture-multiple', () => { 143 | TEST_PREAMBLE(); 144 | 145 | const regex = new RegExpBuilder() 146 | .add(/usemtl/) 147 | .add(/ /) 148 | .add(REGEX_NZ_ANY, 'path') 149 | .toRegExp(); 150 | 151 | const exec = regex.exec('usemtl hellothere.txt'); 152 | expect(exec).toHaveProperty('groups'); 153 | if (exec !== null && exec.groups) { 154 | expect(exec.groups['path']).toBe('hellothere.txt'); 155 | } 156 | }); 157 | 158 | test('Namespace block', () => { 159 | expect(AppUtil.Text.namespaceBlock('stone')).toBe('minecraft:stone'); 160 | }); 161 | 162 | test('Namespace already namespaced block', () => { 163 | expect(AppUtil.Text.namespaceBlock('minecraft:stone')).toBe('minecraft:stone'); 164 | }); 165 | -------------------------------------------------------------------------------- /tests/voxel_mesh.test.ts: -------------------------------------------------------------------------------- 1 | import { RGBAColours } from '../src/colour'; 2 | import { ASSERT } from '../src/util/error_util'; 3 | import { Vector3 } from '../src/vector'; 4 | import { VoxelMesh } from '../src/voxel_mesh'; 5 | import { TEST_PREAMBLE } from './preamble'; 6 | 7 | test('Voxel neighbours', () => { 8 | TEST_PREAMBLE(); 9 | 10 | const voxelMesh = new VoxelMesh({ 11 | voxelOverlapRule: 'first', 12 | enableAmbientOcclusion: true, 13 | 14 | }); 15 | voxelMesh.addVoxel(new Vector3(0, 0, 0), RGBAColours.WHITE); 16 | voxelMesh.addVoxel(new Vector3(1, 1, 0), RGBAColours.WHITE); 17 | voxelMesh.calculateNeighbours(); 18 | 19 | expect(voxelMesh.hasNeighbour(new Vector3(0, 0, 0), new Vector3(1, 1, 0))).toBe(true); 20 | expect(voxelMesh.hasNeighbour(new Vector3(0, 0, 0), new Vector3(-1, -1, 0))).toBe(false); 21 | expect(voxelMesh.hasNeighbour(new Vector3(1, 1, 0), new Vector3(-1, -1, 0))).toBe(true); 22 | expect(voxelMesh.hasNeighbour(new Vector3(1, 1, 0), new Vector3(1, 1, 0))).toBe(false); 23 | }); 24 | 25 | test('Add voxel', () => { 26 | const voxelMesh = new VoxelMesh({ 27 | voxelOverlapRule: 'first', 28 | enableAmbientOcclusion: true, 29 | }); 30 | 31 | voxelMesh.addVoxel(new Vector3(1, 2, 3), RGBAColours.RED); 32 | 33 | expect(voxelMesh.isVoxelAt(new Vector3(1, 2, 3))).toBe(true); 34 | expect(voxelMesh.getVoxelCount()).toBe(1); 35 | const voxel = voxelMesh.getVoxelAt(new Vector3(1, 2, 3)); 36 | expect(voxel).toBeDefined(); ASSERT(voxel); 37 | expect(voxel.position.equals(new Vector3(1, 2, 3))).toBe(true); 38 | expect(voxel.colour).toEqual(RGBAColours.RED); 39 | }); 40 | 41 | test('Voxel overlap first', () => { 42 | const voxelMesh = new VoxelMesh({ 43 | voxelOverlapRule: 'first', 44 | enableAmbientOcclusion: false, 45 | }); 46 | 47 | voxelMesh.addVoxel(new Vector3(1, 2, 3), RGBAColours.RED); 48 | voxelMesh.addVoxel(new Vector3(1, 2, 3), RGBAColours.BLUE); 49 | 50 | expect(voxelMesh.getVoxelAt(new Vector3(1, 2, 3))?.colour).toEqual(RGBAColours.RED); 51 | }); 52 | 53 | test('Voxel overlap average', () => { 54 | const voxelMesh = new VoxelMesh({ 55 | voxelOverlapRule: 'average', 56 | enableAmbientOcclusion: false, 57 | }); 58 | 59 | voxelMesh.addVoxel(new Vector3(1, 2, 3), { r: 1.0, g: 0.5, b: 0.25, a: 1.0 }); 60 | voxelMesh.addVoxel(new Vector3(1, 2, 3), { r: 0.0, g: 0.5, b: 0.75, a: 1.0 }); 61 | 62 | expect(voxelMesh.getVoxelAt(new Vector3(1, 2, 3))?.colour).toEqual({ r: 0.5, g: 0.5, b: 0.5, a: 1.0 }); 63 | }); 64 | -------------------------------------------------------------------------------- /tools/headless-config.ts: -------------------------------------------------------------------------------- 1 | import { PALETTE_ALL_RELEASE } from '../res/palettes/all'; 2 | import { ColourSpace } from '../src/util'; 3 | import { Vector3 } from '../src/vector'; 4 | import { THeadlessConfig } from './headless'; 5 | 6 | export const headlessConfig: THeadlessConfig = { 7 | import: { 8 | file: new File([], '/Users/lucasdower/ObjToSchematic/res/samples/skull.obj'), 9 | rotation: new Vector3(0, 0, 0), 10 | }, 11 | voxelise: { 12 | constraintAxis: 'y', 13 | voxeliser: 'bvh-ray', 14 | size: 80, 15 | useMultisampleColouring: false, 16 | voxelOverlapRule: 'average', 17 | enableAmbientOcclusion: false, // Only want true if exporting to .obj 18 | }, 19 | assign: { 20 | textureAtlas: 'vanilla', // Must be an atlas name that exists in /resources/atlases 21 | blockPalette: PALETTE_ALL_RELEASE, // Must be a palette name that exists in /resources/palettes 22 | dithering: 'ordered', 23 | ditheringMagnitude: 32, 24 | colourSpace: ColourSpace.RGB, 25 | fallable: 'replace-falling', 26 | resolution: 32, 27 | calculateLighting: false, 28 | lightThreshold: 0, 29 | contextualAveraging: true, 30 | errorWeight: 0.0, 31 | }, 32 | export: { 33 | exporter: 'litematic', // 'schematic' / 'litematic', 34 | }, 35 | debug: { 36 | showLogs: true, 37 | showWarnings: true, 38 | showTimings: true, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /tools/headless.ts: -------------------------------------------------------------------------------- 1 | import { StatusHandler } from '../src/status'; 2 | import { LOG_MAJOR, Logger, TIME_END, TIME_START } from '../src/util/log_util'; 3 | import { WorkerClient } from '../src/worker_client'; 4 | import { AssignParams, ExportParams, ImportParams, VoxeliseParams } from '../src/worker_types'; 5 | 6 | export type THeadlessConfig = { 7 | import: ImportParams.Input, 8 | voxelise: VoxeliseParams.Input, 9 | assign: AssignParams.Input, 10 | export: ExportParams.Input, 11 | debug: { 12 | showLogs: boolean, 13 | showWarnings: boolean, 14 | showTimings: boolean, 15 | } 16 | } 17 | 18 | export function runHeadless(headlessConfig: THeadlessConfig) { 19 | if (headlessConfig.debug.showLogs) { 20 | Logger.Get.enableLOGMAJOR(); 21 | } 22 | if (headlessConfig.debug.showWarnings) { 23 | Logger.Get.enableLOGWARN(); 24 | } 25 | if (headlessConfig.debug.showTimings) { 26 | Logger.Get.enableLOGTIME(); 27 | } 28 | 29 | const worker = WorkerClient.Get; 30 | { 31 | TIME_START('[TIMER] Importer'); 32 | LOG_MAJOR('\nImporting...'); 33 | worker.import(headlessConfig.import); 34 | StatusHandler.Get.dump().clear(); 35 | TIME_END('[TIMER] Importer'); 36 | } 37 | { 38 | TIME_START('[TIMER] Voxeliser'); 39 | LOG_MAJOR('\nVoxelising...'); 40 | worker.voxelise(headlessConfig.voxelise); 41 | StatusHandler.Get.dump().clear(); 42 | TIME_END('[TIMER] Voxeliser'); 43 | } 44 | { 45 | TIME_START('[TIMER] Assigner'); 46 | LOG_MAJOR('\nAssigning...'); 47 | worker.assign(headlessConfig.assign); 48 | StatusHandler.Get.dump().clear(); 49 | TIME_END('[TIMER] Assigner'); 50 | } 51 | { 52 | TIME_START('[TIMER] Exporter'); 53 | LOG_MAJOR('\nExporting...'); 54 | 55 | /** 56 | * The OBJExporter is unique in that it uses the actual render buffer used by WebGL 57 | * to create its data, in headless mode this render buffer is not created so we must 58 | * generate it manually 59 | */ 60 | { 61 | let result; 62 | do { 63 | result = worker.renderChunkedVoxelMesh({ 64 | enableAmbientOcclusion: headlessConfig.voxelise.enableAmbientOcclusion, 65 | desiredHeight: headlessConfig.voxelise.size, 66 | }); 67 | } while (result.moreVoxelsToBuffer); 68 | } 69 | 70 | worker.export(headlessConfig.export); 71 | StatusHandler.Get.dump().clear(); 72 | TIME_END('[TIMER] Exporter'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tools/misc.ts: -------------------------------------------------------------------------------- 1 | import { RGBA } from '../src/colour'; 2 | 3 | export function getAverageColour(image: Uint8ClampedArray): RGBA { 4 | let r = 0; 5 | let g = 0; 6 | let b = 0; 7 | let a = 0; 8 | let weight = 0; 9 | for (let x = 0; x < 16; ++x) { 10 | for (let y = 0; y < 16; ++y) { 11 | const index = 4 * (16 * y + x); 12 | const rgba = image.slice(index, index + 4); 13 | const alpha = rgba[3] / 255; 14 | r += (rgba[0] / 255) * alpha; 15 | g += (rgba[1] / 255) * alpha; 16 | b += (rgba[2] / 255) * alpha; 17 | a += alpha; 18 | weight += alpha; 19 | } 20 | } 21 | const numPixels = 16 * 16; 22 | return { 23 | r: r / weight, 24 | g: g / weight, 25 | b: b / weight, 26 | a: a / numPixels, 27 | }; 28 | } 29 | 30 | export function getStandardDeviation(image: Uint8ClampedArray, average: RGBA): number { 31 | let squaredDist = 0.0; 32 | let weight = 0.0; 33 | for (let x = 0; x < 16; ++x) { 34 | for (let y = 0; y < 16; ++y) { 35 | const index = 4 * (16 * y + x); 36 | const rgba = image.slice(index, index + 4); 37 | const alpha = rgba[3] / 255; 38 | weight += alpha; 39 | const r = (rgba[0] / 255) * alpha; 40 | const g = (rgba[1] / 255) * alpha; 41 | const b = (rgba[2] / 255) * alpha; 42 | squaredDist += Math.pow(r - average.r, 2) + Math.pow(g - average.g, 2) + Math.pow(b - average.b, 2); 43 | } 44 | } 45 | return Math.sqrt(squaredDist / weight); 46 | } -------------------------------------------------------------------------------- /tools/models-ignore-list.txt: -------------------------------------------------------------------------------- 1 | structure_block.json 2 | structure_block_data.json 3 | structure_block_save.json 4 | structure_void.json 5 | structure_block_corner.json 6 | structure_block_load.json 7 | spawner.json 8 | brown_mushroom_block_inventory.json 9 | red_mushroom_block_inventory.json 10 | mushroom_stem_inventory.json 11 | redstone_lamp_on.json 12 | piston_inventory.json 13 | sticky_piston_inventory.json 14 | barrel_open.json 15 | respawn_anchor_0.json 16 | respawn_anchor_1.json 17 | respawn_anchor_2.json 18 | respawn_anchor_3.json 19 | respawn_anchor_4.json 20 | sculk_catalyst.json 21 | sculk_catalyst_bloom.json 22 | tnt.json 23 | grass_block_snow.json 24 | acacia_leaves.json 25 | azalea_leaves.json 26 | birch_leaves.json 27 | dark_oak_leaves.json 28 | flowering_azalea_leaves.json 29 | jungle_leaves.json 30 | mangrove_leaves.json 31 | oak_leaves.json 32 | spruce_leaves.json -------------------------------------------------------------------------------- /tools/new-palette-blocks.txt: -------------------------------------------------------------------------------- 1 | black_concrete 2 | black_concrete_powder 3 | black_glazed_terracotta 4 | black_terracotta 5 | black_wool 6 | blue_concrete 7 | blue_concrete_powder 8 | blue_glazed_terracotta 9 | blue_terracotta 10 | blue_wool 11 | brown_concrete 12 | brown_concrete_powder 13 | brown_glazed_terracotta 14 | brown_terracotta 15 | brown_wool 16 | cyan_concrete 17 | cyan_concrete_powder 18 | cyan_glazed_terracotta 19 | cyan_terracotta 20 | cyan_wool 21 | gray_concrete 22 | gray_concrete_powder 23 | gray_glazed_terracotta 24 | gray_terracotta 25 | gray_wool 26 | green_concrete 27 | green_concrete_powder 28 | green_glazed_terracotta 29 | green_terracotta 30 | green_wool 31 | light_blue_concrete 32 | light_blue_concrete_powder 33 | light_blue_glazed_terracotta 34 | light_blue_terracotta 35 | light_blue_wool 36 | light_gray_concrete 37 | light_gray_concrete_powder 38 | light_gray_glazed_terracotta 39 | light_gray_terracotta 40 | light_gray_wool 41 | lime_concrete 42 | lime_concrete_powder 43 | lime_glazed_terracotta 44 | lime_terracotta 45 | lime_wool 46 | magenta_concrete 47 | magenta_concrete_powder 48 | magenta_glazed_terracotta 49 | magenta_terracotta 50 | magenta_wool 51 | orange_concrete 52 | orange_concrete_powder 53 | orange_glazed_terracotta 54 | orange_terracotta 55 | orange_wool 56 | pink_concrete 57 | pink_concrete_powder 58 | pink_glazed_terracotta 59 | pink_terracotta 60 | pink_wool 61 | purple_concrete 62 | purple_concrete_powder 63 | purple_glazed_terracotta 64 | purple_terracotta 65 | purple_wool 66 | red_concrete 67 | red_concrete_powder 68 | red_glazed_terracotta 69 | red_terracotta 70 | red_wool 71 | white_concrete 72 | white_concrete_powder 73 | white_glazed_terracotta 74 | white_terracotta 75 | white_wool 76 | yellow_concrete 77 | yellow_concrete_powder 78 | yellow_glazed_terracotta 79 | yellow_terracotta 80 | yellow_wool -------------------------------------------------------------------------------- /tools/run-headless.ts: -------------------------------------------------------------------------------- 1 | import { LOG_MAJOR } from '../src/util/log_util'; 2 | import { AppPaths, PathUtil } from '../src/util/path_util'; 3 | import { runHeadless } from './headless'; 4 | import { headlessConfig } from './headless-config'; 5 | 6 | void async function main() { 7 | AppPaths.Get.setBaseDir(PathUtil.join(__dirname, '../..')); 8 | 9 | runHeadless(headlessConfig); 10 | 11 | LOG_MAJOR('\nFinished!'); 12 | }(); 13 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: './src/main.ts', 7 | plugins: [ 8 | new NodePolyfillPlugin(), 9 | new HtmlWebpackPlugin({ 10 | template: './template.html', 11 | favicon: './res/static/icon.ico', 12 | }), 13 | ], 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.worker.ts$/, 18 | use: [ 19 | 'worker-loader', 20 | 'ts-loader', 21 | ], 22 | }, 23 | { 24 | test: /\.css$/i, 25 | use: ['style-loader', 'css-loader'], 26 | exclude: /node_modules/, 27 | }, 28 | { 29 | test: /\.vs|fs|atlas$/, 30 | use: 'raw-loader', 31 | exclude: /\.js$/, 32 | exclude: /node_modules/, 33 | }, 34 | { 35 | test: /\.png$/, 36 | use: 'file-loader', 37 | exclude: /node_modules/, 38 | }, 39 | { 40 | test: /\.tsx?$/, 41 | use: 'ts-loader', 42 | exclude: /node_modules/, 43 | }, 44 | ], 45 | }, 46 | resolve: { 47 | extensions: ['.tsx', '.ts', '.js'], 48 | }, 49 | output: { 50 | filename: 'bundle.js', 51 | path: path.resolve(__dirname, './webpack'), 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | const path = require('path'); 4 | 5 | module.exports = merge(common, { 6 | mode: 'development', 7 | devtool: 'eval-source-map', 8 | devServer: { 9 | static: { 10 | directory: path.join(__dirname, './webpack'), 11 | }, 12 | hot: true, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'production', 6 | performance: { 7 | hints: false, 8 | }, 9 | }); 10 | --------------------------------------------------------------------------------