├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ ├── pr.yml │ └── stale.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── example.ts ├── index.html ├── tsconfig.json └── webpack.config.js ├── jestconfig.json ├── package-lock.json ├── package.json ├── src ├── CSG.ts ├── NBuf.ts ├── Node.ts ├── Plane.ts ├── Polygon.ts ├── Vector.ts ├── Vertex.ts ├── __tests__ │ └── csg.test.ts └── index.ts ├── tsconfig-cjs.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | lib 5 | # don't lint nyc coverage output 6 | coverage 7 | 8 | example/index.html 9 | example/webpack.config.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "jest"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:jest/recommended", 10 | "prettier" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/no-explicit-any": "off", 14 | "@typescript-eslint/explicit-function-return-type": "off", 15 | "@typescript-eslint/explicit-module-boundary-types": "off" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [samalexander,manthrax] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **Minimal reproduction of issue** 13 | Provide a [Stackblitz](https://stackblitz.com/) (or similar) that demonstrates the issue. 14 | 15 | **Expected behavior** 16 | A clear and concise description of what you expected to happen. 17 | 18 | **Screenshots** 19 | If applicable, add screenshots to help explain your problem. 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR test 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Cache node modules 13 | id: cache-node 14 | uses: actions/cache@v2 15 | env: 16 | cache-name: cache-node-modules 17 | with: 18 | path: node_modules 19 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 20 | restore-keys: | 21 | ${{ runner.os }}-node- 22 | 23 | - name: Use Node.js 14.x 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: 14.x 27 | 28 | - name: Install Dependencies 29 | if: steps.cache-node.outputs.cache-hit != 'true' 30 | run: npm install 31 | 32 | - name: Lint & format 33 | run: | 34 | npm run lint 35 | npx prettier --check . 36 | 37 | - name: Test 38 | run: npm run test 39 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v3 11 | with: 12 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' 13 | stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.' 14 | close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' 15 | close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.' 16 | days-before-issue-stale: 30 17 | days-before-pr-stale: 45 18 | days-before-issue-close: 5 19 | days-before-pr-close: 10 20 | exempt-pr-labels: 'dependencies' 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ 89 | 90 | # Compiler output 91 | /lib 92 | 93 | # IDE - VSCode 94 | .vscode/* 95 | !.vscode/settings.json 96 | !.vscode/tasks.json 97 | !.vscode/launch.json 98 | !.vscode/extensions.json 99 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | 5 | # Compiler output 6 | /lib 7 | 8 | # changelog is generated automatically 9 | CHANGELOG.md 10 | 11 | .husky/ 12 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "bracketSpacing": true 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [3.2.0](https://github.com/samalexander/three-csg-ts/compare/v3.1.14...v3.2.0) (2024-05-28) 6 | 7 | 8 | ### Features 9 | 10 | * fix for TypedArray added in r154 ([07fc9de](https://github.com/samalexander/three-csg-ts/commit/07fc9de2cfe256cb0b12282b662ecd5129878311)) 11 | 12 | ### [3.1.14](https://github.com/samalexander/three-csg-ts/compare/v3.1.13...v3.1.14) (2024-02-29) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * upgrade dependencies ([00d6e77](https://github.com/samalexander/three-csg-ts/commit/00d6e77cfafb265a6de1980a1cf2f4e91d02e839)) 18 | 19 | ### [3.1.13](https://github.com/samalexander/three-csg-ts/compare/v3.1.12...v3.1.13) (2023-03-16) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * Fix indexing error at CSG obtainment from Geometry ([#58](https://github.com/samalexander/three-csg-ts/issues/58)) ([fd17d3d](https://github.com/samalexander/three-csg-ts/commit/fd17d3df2b60bea9e8fbb1da62ae159c782b5632)) 25 | 26 | ### [3.1.12](https://github.com/samalexander/three-csg-ts/compare/v3.1.11...v3.1.12) (2023-03-02) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * improve types ([22459f2](https://github.com/samalexander/three-csg-ts/commit/22459f285a9896caa13095f34b103e4398297eb1)) 32 | * upgrade dependencies ([9ad1400](https://github.com/samalexander/three-csg-ts/commit/9ad140066865f2bcd30d4b5d9d60861282ec63b6)) 33 | 34 | ### [3.1.11](https://github.com/samalexander/three-csg-ts/compare/v3.1.10...v3.1.11) (2022-08-22) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * Fix finding group by index ([#53](https://github.com/samalexander/three-csg-ts/issues/53)) ([c0abddd](https://github.com/samalexander/three-csg-ts/commit/c0abddd202653e1e787352b9f34bd062c07ae4a8)) 40 | * upgrade dependencies ([c916120](https://github.com/samalexander/three-csg-ts/commit/c916120705955fc4598678f09fb87a8734165734)) 41 | 42 | ### [3.1.10](https://github.com/Jiro-Digital/three-csg-ts/compare/v3.1.9...v3.1.10) (2022-02-23) 43 | 44 | 45 | ### Bug Fixes 46 | 47 | * upgrade dependencies ([942634c](https://github.com/Jiro-Digital/three-csg-ts/commit/942634c981995fa78863972c79f732d9426773a5)) 48 | * upgrade dependencies ([06ebed6](https://github.com/Jiro-Digital/three-csg-ts/commit/06ebed65a639da9aafd1be9739c938981420333c)) 49 | * upgrade dependencies ([df16ef9](https://github.com/Jiro-Digital/three-csg-ts/commit/df16ef9f4512ce85c2a6931121d5ea148d33f807)) 50 | 51 | ### [3.1.9](https://github.com/Jiro-Digital/three-csg-ts/compare/v3.1.8...v3.1.9) (2021-09-06) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * filter out polygons with NaN normals after cloning CSG ([#41](https://github.com/Jiro-Digital/three-csg-ts/issues/41)) ([d4db844](https://github.com/Jiro-Digital/three-csg-ts/commit/d4db8444baed6a232f628dacf2cada605e0b92fc)) 57 | 58 | ### [3.1.8](https://github.com/Jiro-Digital/three-csg-ts/compare/v3.1.7...v3.1.8) (2021-08-31) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * ops on extruded geometry not working ([#39](https://github.com/Jiro-Digital/three-csg-ts/issues/39)) ([e14048f](https://github.com/Jiro-Digital/three-csg-ts/commit/e14048f413525e8d6dbc795f3aac64d0ab6503c4)) 64 | 65 | ### [3.1.7](https://github.com/Jiro-Digital/three-csg-ts/compare/v3.1.6...v3.1.7) (2021-08-17) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * example tests all 3 operators ([0d041dd](https://github.com/Jiro-Digital/three-csg-ts/commit/0d041ddf42f94a761bae6be7dd3945b0ba2b7411)) 71 | * upgrade dependencies ([46f6358](https://github.com/Jiro-Digital/three-csg-ts/commit/46f6358fd4cc354c3180b75a3329210cb4d1fad0)) 72 | 73 | ### [3.1.6](https://github.com/Jiro-Digital/three-csg-ts/compare/v3.1.5...v3.1.6) (2021-07-16) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * fallback to polygon with no objectIndex ([#35](https://github.com/Jiro-Digital/three-csg-ts/issues/35)) ([6baf82f](https://github.com/Jiro-Digital/three-csg-ts/commit/6baf82ff3dab74f8a9b4fd29b3fe5aa30bd45773)) 79 | * guard against uvattr being undefined ([#36](https://github.com/Jiro-Digital/three-csg-ts/issues/36)) ([7224bdb](https://github.com/Jiro-Digital/three-csg-ts/commit/7224bdbcd44dedc41b87f9946063af6d8c9cf75d)) 80 | * upgrade dependencies ([8bdba75](https://github.com/Jiro-Digital/three-csg-ts/commit/8bdba75057a441409c30331f05d3605393c3872c)) 81 | 82 | ### [3.1.5](https://github.com/Jiro-Digital/three-csg-ts/compare/v3.1.4...v3.1.5) (2021-07-12) 83 | 84 | 85 | ### Bug Fixes 86 | 87 | * Add checks for missing UVs. ([#34](https://github.com/Jiro-Digital/three-csg-ts/issues/34)) ([026d5b6](https://github.com/Jiro-Digital/three-csg-ts/commit/026d5b6878f067bd7e30dcca2dc6e1c78056e1b5)) 88 | * upgrade dependencies ([7d05397](https://github.com/Jiro-Digital/three-csg-ts/commit/7d05397bf244fed56c05b2831c1f8eb7d81427bf)) 89 | 90 | ### [3.1.4](https://github.com/Jiro-Digital/three-csg-ts/compare/v3.1.3...v3.1.4) (2021-06-22) 91 | 92 | 93 | ### Bug Fixes 94 | 95 | * Filter out polys with NaN normals indicating degenerate polygons ([#30](https://github.com/Jiro-Digital/three-csg-ts/issues/30)) ([b835954](https://github.com/Jiro-Digital/three-csg-ts/commit/b835954964e6154b4ddb748b159b7116db0159fc)) 96 | * upgrade dependencies ([dfe9746](https://github.com/Jiro-Digital/three-csg-ts/commit/dfe9746917c6d79472e4188b712eedefa438b4b9)) 97 | * upgrade dependencies ([91f6da7](https://github.com/Jiro-Digital/three-csg-ts/commit/91f6da70c3bcddde44db4e10cebc763a7446d194)) 98 | 99 | ### [3.1.3](https://github.com/Jiro-Digital/three-csg-ts/compare/v3.1.2...v3.1.3) (2021-06-07) 100 | 101 | 102 | ### Bug Fixes 103 | 104 | * Fix polygons creation for geometries without groups ([#29](https://github.com/Jiro-Digital/three-csg-ts/issues/29)) ([bc792e3](https://github.com/Jiro-Digital/three-csg-ts/commit/bc792e3711c32d2b24dda34874469c3d5e1d9ebf)) 105 | 106 | ### [3.1.2](https://github.com/Jiro-Digital/three-csg-ts/compare/v3.1.1...v3.1.2) (2021-06-06) 107 | 108 | 109 | ### Bug Fixes 110 | 111 | * upgrade dependencies ([b505442](https://github.com/Jiro-Digital/three-csg-ts/commit/b505442648ba4db83f7a217a131cf0b83d8c4730)) 112 | 113 | ### [3.1.1](https://github.com/Jiro-Digital/three-csg-ts/compare/v3.1.0...v3.1.1) (2021-05-14) 114 | 115 | 116 | ### Bug Fixes 117 | 118 | * re-add types field in package.json ([93cb572](https://github.com/Jiro-Digital/three-csg-ts/commit/93cb5725bfb98cf4b451bf8cd0a7c8a8279d475f)) 119 | 120 | ## [3.1.0](https://github.com/Jiro-Digital/three-csg-ts/compare/v3.0.1...v3.1.0) (2021-05-14) 121 | 122 | 123 | ### Features 124 | 125 | * add static CSG wrapper methods ([860143c](https://github.com/Jiro-Digital/three-csg-ts/commit/860143c43c1215421ca1a69b932f96a2fa95371e)) 126 | 127 | ### [3.0.1](https://github.com/Jiro-Digital/three-csg-ts/compare/v3.0.0...v3.0.1) (2021-05-14) 128 | 129 | 130 | ### Bug Fixes 131 | 132 | * re-add CSG toGeometry method ([0d82369](https://github.com/Jiro-Digital/three-csg-ts/commit/0d82369d2b3b8d18b9a987ee8b2a06a11f678c61)) 133 | 134 | ## [3.0.0](https://github.com/Jiro-Digital/three-csg-ts/compare/v2.3.0...v3.0.0) (2021-05-14) 135 | 136 | 137 | ### ⚠ BREAKING CHANGES 138 | 139 | * now supports three v0.125.0 and above 140 | 141 | ### Features 142 | 143 | * add support for three >= 0.125.0 ([b8f514f](https://github.com/Jiro-Digital/three-csg-ts/commit/b8f514f4e345cadbd5895bd6fa995a8777882bee)) 144 | 145 | 146 | ### Bug Fixes 147 | 148 | * add dev test setup ([f1ad8b5](https://github.com/Jiro-Digital/three-csg-ts/commit/f1ad8b5274066d2b6b8480f866ee3f17ba3f64f6)) 149 | 150 | ## [2.3.0](https://github.com/Jiro-Digital/three-csg-ts/compare/v2.2.2...v2.3.0) (2021-05-14) 151 | 152 | 153 | ### Features 154 | 155 | * add esm build ([7f797a3](https://github.com/Jiro-Digital/three-csg-ts/commit/7f797a30adf2a20c27b6997ada558e19bc5752ce)) 156 | 157 | 158 | ### Bug Fixes 159 | 160 | * upgrade dependencies ([6d1bf6e](https://github.com/Jiro-Digital/three-csg-ts/commit/6d1bf6efba9537de4ec33b02117981d3c4d70672)) 161 | 162 | ### [2.2.2](https://github.com/Jiro-Digital/three-csg-ts/compare/v2.2.1...v2.2.2) (2021-02-04) 163 | 164 | 165 | ### Bug Fixes 166 | 167 | * pin three peer dependency ([a4dad9a](https://github.com/Jiro-Digital/three-csg-ts/commit/a4dad9a0c2dbde721f58355aa2424bcf53f1d261)) 168 | 169 | ### [2.2.1](https://github.com/Jiro-Digital/three-csg-ts/compare/v2.2.0...v2.2.1) (2021-02-04) 170 | 171 | 172 | ### Bug Fixes 173 | 174 | * pin three peer dependency below 0.125.0 ([49a7b4f](https://github.com/Jiro-Digital/three-csg-ts/commit/49a7b4f3c3c55506fb607f44b885e857ce616154)) 175 | 176 | ## [2.2.0](https://github.com/Jiro-Digital/three-csg-ts/compare/v2.1.0...v2.2.0) (2021-01-26) 177 | 178 | 179 | ### Features 180 | 181 | * support conversion to geometry ([9fb107d](https://github.com/Jiro-Digital/three-csg-ts/commit/9fb107dc73188e07e1086bd4872572731651b112)) 182 | 183 | ## [2.1.0](https://github.com/Jiro-Digital/three-csg-ts/compare/v2.0.1...v2.1.0) (2021-01-26) 184 | 185 | 186 | ### Features 187 | 188 | * Add support for multiple materials ([94cf1ba](https://github.com/Jiro-Digital/three-csg-ts/commit/94cf1bac10aa69e256fbdd9405d682433c8eacde)) 189 | 190 | 191 | ### Bug Fixes 192 | 193 | * add missing return types ([f9672a3](https://github.com/Jiro-Digital/three-csg-ts/commit/f9672a397644cdb9f355a6d2cb3410031bd0e0f2)) 194 | * update dependencies ([d6a02b5](https://github.com/Jiro-Digital/three-csg-ts/commit/d6a02b53e96404a5e52e50b7fa39cb4e956d9e3f)) 195 | 196 | ### [2.0.1](https://github.com/Jiro-Digital/three-csg-ts/compare/v2.0.0...v2.0.1) (2021-01-13) 197 | 198 | 199 | ### Bug Fixes 200 | 201 | * add types for fromGeometry method ([b216185](https://github.com/Jiro-Digital/three-csg-ts/commit/b216185fe9dfb98a096b8245d780621a1b5f896e)) 202 | * clone matrix before invert ([#14](https://github.com/Jiro-Digital/three-csg-ts/issues/14)) ([6907456](https://github.com/Jiro-Digital/three-csg-ts/commit/690745649a7e637da49035db0dd7e21c1eaea8cb)) 203 | * upgrade dependencies ([5223398](https://github.com/Jiro-Digital/three-csg-ts/commit/52233988a46cbee38c89d027b45bd2c5f55c19df)) 204 | * upgrade dependencies ([a4a7a2c](https://github.com/Jiro-Digital/three-csg-ts/commit/a4a7a2c1a2f998c864f6a2f35a562d4ebf280caa)) 205 | * upgrade dependencies ([a45a222](https://github.com/Jiro-Digital/three-csg-ts/commit/a45a22252172794b052e3b79135d0496385df27b)) 206 | * upgrade dependencies ([a821f57](https://github.com/Jiro-Digital/three-csg-ts/commit/a821f578f8ca6cf689809d404ab148e34988c944)) 207 | 208 | ## [2.0.0](https://github.com/Jiro-Digital/three-csg-ts/compare/v1.0.6...v2.0.0) (2020-12-13) 209 | 210 | 211 | ### ⚠ BREAKING CHANGES 212 | 213 | * target es6 214 | 215 | ### Features 216 | 217 | * target es6 ([6cea95e](https://github.com/Jiro-Digital/three-csg-ts/commit/6cea95e49dfe04c6984dcdb9ce075c84830e6453)) 218 | 219 | 220 | ### Bug Fixes 221 | 222 | * handle Matrix4 getInverse deprecation in three r123 ([d04eb4b](https://github.com/Jiro-Digital/three-csg-ts/commit/d04eb4bb2efa01a26353d50ea50a678c4f2f2feb)) 223 | * upgrade dependencies ([b40b1a0](https://github.com/Jiro-Digital/three-csg-ts/commit/b40b1a0741bd147cf13fdb6ee1856862648d2c5c)) 224 | 225 | ### [1.0.6](https://github.com/Jiro-Digital/three-csg-ts/compare/v1.0.5...v1.0.6) (2020-10-27) 226 | 227 | 228 | ### Bug Fixes 229 | 230 | * add missing super call ([d4d3f65](https://github.com/Jiro-Digital/three-csg-ts/commit/d4d3f65ce66ac26dc8f13c8722119153d16fe58e)) 231 | * upgrade dependencies ([fce017d](https://github.com/Jiro-Digital/three-csg-ts/commit/fce017d64a671cb713eabc06ddee471aedf6d286)) 232 | 233 | ### [1.0.5](https://github.com/Jiro-Digital/three-csg-ts/compare/v1.0.4...v1.0.5) (2020-10-06) 234 | 235 | 236 | ### Bug Fixes 237 | 238 | * add organize imports plugin ([859aaa9](https://github.com/Jiro-Digital/three-csg-ts/commit/859aaa937c0dc288ce6f940cc7dad59d84ea391e)) 239 | * upgrade dependencies ([5e14d46](https://github.com/Jiro-Digital/three-csg-ts/commit/5e14d4627649c7ee835aee3fd767aaa828e920db)) 240 | * Upgrade dependencies ([03d07a6](https://github.com/Jiro-Digital/three-csg-ts/commit/03d07a647cb69de53c67022f13ec50442acaf211)) 241 | 242 | ### [1.0.4](https://github.com/JiroUK/three-csg-ts/compare/v1.0.3...v1.0.4) (2020-04-14) 243 | 244 | 245 | ### Bug Fixes 246 | 247 | * Update README ([351f54b](https://github.com/JiroUK/three-csg-ts/commit/351f54b3a389d1584ddb4a59b2afddf715d68793)) 248 | 249 | ### [1.0.3](https://github.com/JiroUK/three-csg-ts/compare/v1.0.2...v1.0.3) (2020-04-07) 250 | 251 | 252 | ### Bug Fixes 253 | 254 | * Upgrade dependencies ([de79f76](https://github.com/JiroUK/three-csg-ts/commit/de79f765daaf57caba523b0f282b6fcc280bf64b)) 255 | 256 | ### 1.0.2 (2020-03-31) 257 | 258 | 259 | ### Bug Fixes 260 | 261 | * Fix deprecation introduced by threejs r113 ([#6](https://github.com/JiroUK/three-csg-ts/issues/6)) ([c77bbdf](https://github.com/JiroUK/three-csg-ts/commit/c77bbdf9930de29c4f46c7142823e1bfb43609f9)) 262 | * Fix readme ([102aad4](https://github.com/JiroUK/three-csg-ts/commit/102aad41bfc16606b1c137d02e124565f403e429)) 263 | * Upgrade dependencies ([c5ef698](https://github.com/JiroUK/three-csg-ts/commit/c5ef69818c8dc56798f682899a3645eac09da524)) 264 | * **csg:** Add check for uv property in toMesh ([27d9575](https://github.com/JiroUK/three-csg-ts/commit/27d95757bd5d5e3492de83f1074ceb1d57a133e7)) 265 | * **csg:** Check faceVertexUvs exists ([#3](https://github.com/JiroUK/three-csg-ts/issues/3)) ([057bd3f](https://github.com/JiroUK/three-csg-ts/commit/057bd3f0b7d90669afa61e07075dad3cb4093eb3)) 266 | * **deps:** Upgrade dependencies ([473e6f4](https://github.com/JiroUK/three-csg-ts/commit/473e6f4ce06e4fd70698536551c9913ad1bdc894)) 267 | * **deps:** Upgrade dependencies ([580b3d8](https://github.com/JiroUK/three-csg-ts/commit/580b3d839afb5a025d0b6ccf87ccff6a2c763c9b)) 268 | * **deps:** Upgrade dependencies ([abf55fd](https://github.com/JiroUK/three-csg-ts/commit/abf55fd25513d5e6c49b90c6171e7a894b384da4)) 269 | * **deps:** Upgrade dependencies ([baca9f7](https://github.com/JiroUK/three-csg-ts/commit/baca9f74ddcf631226f9a31f949ed6444e3934f6)) 270 | * **deps:** Upgrade dependencies ([a6ed69e](https://github.com/JiroUK/three-csg-ts/commit/a6ed69eb43a66d3b4dee5b8c8ad18606d0313ab9)) 271 | * **deps:** Upgrade dependencies ([5cfc090](https://github.com/JiroUK/three-csg-ts/commit/5cfc0904a2dd30310d412819e9d92f33b1d6af10)) 272 | * **readme:** Update README ([c6329c9](https://github.com/JiroUK/three-csg-ts/commit/c6329c95998e5322b9d41fe10fe01f4bd0d88fce)) 273 | 274 | ### [1.0.1](https://github.com/JiroUK/three-csg-ts/compare/v1.0.0...v1.0.1) (2020-02-25) 275 | 276 | 277 | ### Bug Fixes 278 | 279 | * Fix readme ([66fc21b](https://github.com/JiroUK/three-csg-ts/commit/66fc21b)) 280 | 281 | ## 1.0.0 (2020-02-25) 282 | 283 | 284 | ### Bug Fixes 285 | 286 | * Fix deprecation introduced by threejs r113 ([#6](https://github.com/JiroUK/three-csg-ts/issues/6)) ([55b5fdf](https://github.com/JiroUK/three-csg-ts/commit/55b5fdf)) 287 | * **csg:** Add check for uv property in toMesh ([d491502](https://github.com/JiroUK/three-csg-ts/commit/d491502)) 288 | * **csg:** Check faceVertexUvs exists ([#3](https://github.com/JiroUK/three-csg-ts/issues/3)) ([59d2dca](https://github.com/JiroUK/three-csg-ts/commit/59d2dca)) 289 | * **deps:** Upgrade dependencies ([7b744e2](https://github.com/JiroUK/three-csg-ts/commit/7b744e2)) 290 | * **deps:** Upgrade dependencies ([338d745](https://github.com/JiroUK/three-csg-ts/commit/338d745)) 291 | * **deps:** Upgrade dependencies ([c4ea9c1](https://github.com/JiroUK/three-csg-ts/commit/c4ea9c1)) 292 | * **deps:** Upgrade dependencies ([8a833c2](https://github.com/JiroUK/three-csg-ts/commit/8a833c2)) 293 | * **deps:** Upgrade dependencies ([ce936de](https://github.com/JiroUK/three-csg-ts/commit/ce936de)) 294 | * **deps:** Upgrade dependencies ([43705b9](https://github.com/JiroUK/three-csg-ts/commit/43705b9)) 295 | * **readme:** Update README ([cfb8c08](https://github.com/JiroUK/three-csg-ts/commit/cfb8c08)) 296 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jiro Digital Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # three-csg-ts 2 | 3 | CSG (Constructive Solid Geometry) library for [three.js](https://github.com/mrdoob/three.js/) with Typescript support. 4 | 5 | _This is a typescript rewrite of [THREE-CSGMesh](https://github.com/manthrax/THREE-CSGMesh)._ 6 | 7 | [Example](https://stackblitz.com/edit/three-csg-ts?file=index.ts) 8 | 9 | ![Screenshot 2021-07-19 at 17 32 20](https://user-images.githubusercontent.com/935782/126194933-45ac18d0-2459-4213-97d2-d46ffb67483c.png) 10 | 11 | ## Concept 12 | 13 | CSG is the name of a technique for generating a new geometry as a function of two input geometries. 14 | 15 | CSG is sometimes referred to as "Boolean" operators in 3d modelling packages. 16 | 17 | Internally it uses a structure called a BSP (binary space partitioning) tree to carry out these operations. 18 | 19 | The supported operations are .subtract, .union, and .intersect. 20 | 21 | By using different combinations of these 3 operations, and changing the order of the input models, you can construct any combination of the input models. 22 | 23 | ## Installation 24 | 25 | - Install with npm `npm i -save three-csg-ts` 26 | - Install with yarn `yarn add three-csg-ts` 27 | 28 | ## Example usage 29 | 30 | ```ts 31 | import * as THREE from 'three'; 32 | import { CSG } from 'three-csg-ts'; 33 | 34 | // Make 2 meshes.. 35 | const box = new THREE.Mesh( 36 | new THREE.BoxGeometry(2, 2, 2), 37 | new THREE.MeshNormalMaterial() 38 | ); 39 | const sphere = new THREE.Mesh(new THREE.SphereGeometry(1.2, 8, 8)); 40 | 41 | // Make sure the .matrix of each mesh is current 42 | box.updateMatrix(); 43 | sphere.updateMatrix(); 44 | 45 | // Perform CSG operations 46 | // The result is a THREE.Mesh that you can add to your scene... 47 | const subRes = CSG.subtract(box, sphere); 48 | const uniRes = CSG.union(box, sphere); 49 | const intRes = CSG.intersect(box, sphere); 50 | ``` 51 | -------------------------------------------------------------------------------- /example/example.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 3 | import { CSG } from '../src'; 4 | 5 | let camera: THREE.PerspectiveCamera, 6 | scene: THREE.Scene, 7 | renderer: THREE.WebGLRenderer, 8 | results = new Array(); 9 | const box = new THREE.Mesh( 10 | new THREE.BoxGeometry(2, 2, 2), 11 | new THREE.MeshNormalMaterial() 12 | ), 13 | sphere = new THREE.Mesh( 14 | new THREE.SphereGeometry(1.2, 8, 8), 15 | new THREE.MeshNormalMaterial() 16 | ); 17 | 18 | init(); 19 | animate(); 20 | 21 | function init() { 22 | renderer = new THREE.WebGLRenderer({ antialias: true }); 23 | renderer.setSize(window.innerWidth, window.innerHeight); 24 | document.body.appendChild(renderer.domElement); 25 | 26 | scene = new THREE.Scene(); 27 | 28 | camera = new THREE.PerspectiveCamera( 29 | 45, 30 | window.innerWidth / window.innerHeight, 31 | 1, 32 | 10000 33 | ); 34 | const controls = new OrbitControls(camera, renderer.domElement); 35 | camera.position.set(0, 20, 10); 36 | controls.update(); 37 | } 38 | 39 | function recompute() { 40 | for (const result of results) { 41 | result.parent.remove(result); 42 | result.geometry.dispose(); 43 | } 44 | results = []; 45 | 46 | box.updateMatrix(); 47 | sphere.updateMatrix(); 48 | 49 | // ops with box as base mesh 50 | results.push(CSG.subtract(box, sphere)); 51 | results.push(CSG.union(box, sphere)); 52 | results.push(CSG.intersect(box, sphere)); 53 | // ops with sphere as base mesh 54 | results.push(CSG.subtract(sphere, box)); 55 | results.push(CSG.union(sphere, box)); 56 | results.push(CSG.intersect(sphere, box)); 57 | 58 | for (let i = 0; i < results.length; i++) { 59 | const result = results[i]; 60 | scene.add(result); 61 | 62 | result.position.z += -5 + (i % 3) * 5; 63 | result.position.x += -5 + ((i / 3) | 0) * 10; 64 | } 65 | } 66 | 67 | function animate() { 68 | requestAnimationFrame(animate); 69 | 70 | const time = performance.now(); 71 | sphere.position.x = Math.sin(time * 0.001) * 2; 72 | sphere.position.z = Math.cos(time * 0.0011) * 0.5; 73 | recompute(); 74 | 75 | renderer.render(scene, camera); 76 | } 77 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | three-csg-ts example 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "ES2020", 5 | "moduleResolution": "node", 6 | "resolveJsonModule": true, 7 | "esModuleInterop": true, 8 | "sourceMap": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: path.resolve(__dirname, 'example.ts'), 6 | devtool: 'inline-source-map', 7 | devServer: { 8 | static: path.resolve(__dirname), 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.ts$/, 14 | use: 'ts-loader', 15 | exclude: /node_modules/, 16 | }, 17 | ], 18 | }, 19 | resolve: { 20 | extensions: ['.ts', '.js'], 21 | }, 22 | output: { 23 | filename: 'bundle.js', 24 | path: path.resolve(__dirname), 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /jestconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "transform": { 3 | "^.+\\.(t|j)sx?$": "ts-jest" 4 | }, 5 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 6 | "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"] 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three-csg-ts", 3 | "version": "3.2.0", 4 | "description": "CSG library for use with THREE.js", 5 | "main": "lib/cjs/index.js", 6 | "module": "lib/esm/index.js", 7 | "types": "lib/esm/index.d.ts", 8 | "scripts": { 9 | "start": "webpack serve --open --config example/webpack.config.js", 10 | "test": "jest --config jestconfig.json", 11 | "clean": "rimraf lib", 12 | "build": "npm run clean && mkdir lib && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json", 13 | "lint": "eslint .", 14 | "prepare": "npm run build && husky install", 15 | "prepublishOnly": "npm test && npm run lint", 16 | "release": "standard-version", 17 | "prerelease": "npm run lint" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/samalexander/three-csg-ts.git" 22 | }, 23 | "keywords": [ 24 | "three", 25 | "threejs", 26 | "three-js", 27 | "csg", 28 | "computational-solid-geometry" 29 | ], 30 | "author": "Sam Alexander", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/samalexander/three-csg-ts/issues" 34 | }, 35 | "homepage": "https://github.com/samalexander/three-csg-ts#readme", 36 | "devDependencies": { 37 | "@types/jest": "^27.0.1", 38 | "@types/node": "^14.14.45", 39 | "@types/three": "^0.164.1", 40 | "@typescript-eslint/eslint-plugin": "^4.23.0", 41 | "@typescript-eslint/parser": "^4.23.0", 42 | "eslint": "^7.26.0", 43 | "eslint-config-prettier": "^8.3.0", 44 | "eslint-plugin-jest": "^24.3.6", 45 | "husky": "^6.0.0", 46 | "jest": "^27.0.4", 47 | "lint-staged": "^11.0.0", 48 | "prettier": "^2.3.0", 49 | "prettier-plugin-organize-imports": "^2.0.0", 50 | "rimraf": "^3.0.2", 51 | "standard-version": "^9.3.0", 52 | "three": "^0.164.1", 53 | "ts-jest": "^27.0.5", 54 | "ts-loader": "^9.2.3", 55 | "typescript": "^4.2.4", 56 | "webpack": "^5.38.1", 57 | "webpack-cli": "^4.7.2", 58 | "webpack-dev-server": "^4.2.1" 59 | }, 60 | "peerDependencies": { 61 | "@types/three": ">= 0.154.0", 62 | "three": ">= 0.154.0" 63 | }, 64 | "files": [ 65 | "lib/**/*" 66 | ], 67 | "lint-staged": { 68 | "*/**": [ 69 | "eslint --fix", 70 | "prettier --write" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/CSG.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Material, 5 | Matrix3, 6 | Matrix4, 7 | Mesh, 8 | TypedArray, 9 | Vector3, 10 | } from 'three'; 11 | import { NBuf2, NBuf3 } from './NBuf'; 12 | import { Node } from './Node'; 13 | import { Polygon } from './Polygon'; 14 | import { Vector } from './Vector'; 15 | import { Vertex } from './Vertex'; 16 | 17 | /** 18 | * Holds a binary space partition tree representing a 3D solid. Two solids can 19 | * be combined using the `union()`, `subtract()`, and `intersect()` methods. 20 | */ 21 | export class CSG { 22 | static fromPolygons(polygons: Polygon[]): CSG { 23 | const csg = new CSG(); 24 | csg.polygons = polygons; 25 | return csg; 26 | } 27 | 28 | static fromGeometry(geom: BufferGeometry, objectIndex?: any): CSG { 29 | let polys = []; 30 | const posattr = geom.attributes.position; 31 | const normalattr = geom.attributes.normal; 32 | const uvattr = geom.attributes.uv; 33 | const colorattr = geom.attributes.color; 34 | const grps = geom.groups; 35 | let index: TypedArray; 36 | 37 | if (geom.index) { 38 | index = geom.index.array; 39 | } else { 40 | index = new Uint16Array((posattr.array.length / posattr.itemSize) | 0); 41 | for (let i = 0; i < index.length; i++) index[i] = i; 42 | } 43 | 44 | const triCount = (index.length / 3) | 0; 45 | polys = new Array(triCount); 46 | 47 | for (let i = 0, pli = 0, l = index.length; i < l; i += 3, pli++) { 48 | const vertices = new Array(3); 49 | for (let j = 0; j < 3; j++) { 50 | const vi = index[i + j]; 51 | const vp = vi * 3; 52 | const vt = vi * 2; 53 | const x = posattr.array[vp]; 54 | const y = posattr.array[vp + 1]; 55 | const z = posattr.array[vp + 2]; 56 | const nx = normalattr.array[vp]; 57 | const ny = normalattr.array[vp + 1]; 58 | const nz = normalattr.array[vp + 2]; 59 | const u = uvattr?.array[vt]; 60 | const v = uvattr?.array[vt + 1]; 61 | 62 | vertices[j] = new Vertex( 63 | new Vector(x, y, z), 64 | new Vector(nx, ny, nz), 65 | new Vector(u, v, 0), 66 | colorattr && 67 | new Vector( 68 | colorattr.array[vp], 69 | colorattr.array[vp + 1], 70 | colorattr.array[vp + 2] 71 | ) 72 | ); 73 | } 74 | 75 | if (objectIndex === undefined && grps && grps.length > 0) { 76 | for (const grp of grps) { 77 | if (i >= grp.start && i < grp.start + grp.count) { 78 | polys[pli] = new Polygon(vertices, grp.materialIndex); 79 | } 80 | } 81 | } else { 82 | polys[pli] = new Polygon(vertices, objectIndex); 83 | } 84 | } 85 | return CSG.fromPolygons( 86 | polys.filter((p) => !Number.isNaN(p.plane.normal.x)) 87 | ); 88 | } 89 | 90 | static toGeometry(csg: CSG, toMatrix: Matrix4): BufferGeometry { 91 | let triCount = 0; 92 | const ps = csg.polygons; 93 | for (const p of ps) { 94 | triCount += p.vertices.length - 2; 95 | } 96 | const geom = new BufferGeometry(); 97 | 98 | const vertices = new NBuf3(triCount * 3 * 3); 99 | const normals = new NBuf3(triCount * 3 * 3); 100 | const uvs = new NBuf2(triCount * 2 * 3); 101 | let colors: NBuf3; 102 | const grps = []; 103 | const dgrp = []; 104 | for (const p of ps) { 105 | const pvs = p.vertices; 106 | const pvlen = pvs.length; 107 | if (p.shared !== undefined) { 108 | if (!grps[p.shared]) grps[p.shared] = []; 109 | } 110 | if (pvlen && pvs[0].color !== undefined) { 111 | if (!colors) colors = new NBuf3(triCount * 3 * 3); 112 | } 113 | for (let j = 3; j <= pvlen; j++) { 114 | const grp = p.shared === undefined ? dgrp : grps[p.shared]; 115 | grp.push(vertices.top / 3, vertices.top / 3 + 1, vertices.top / 3 + 2); 116 | vertices.write(pvs[0].pos); 117 | vertices.write(pvs[j - 2].pos); 118 | vertices.write(pvs[j - 1].pos); 119 | normals.write(pvs[0].normal); 120 | normals.write(pvs[j - 2].normal); 121 | normals.write(pvs[j - 1].normal); 122 | if (uvs) { 123 | uvs.write(pvs[0].uv); 124 | uvs.write(pvs[j - 2].uv); 125 | uvs.write(pvs[j - 1].uv); 126 | } 127 | 128 | if (colors) { 129 | colors.write(pvs[0].color); 130 | colors.write(pvs[j - 2].color); 131 | colors.write(pvs[j - 1].color); 132 | } 133 | } 134 | } 135 | geom.setAttribute('position', new BufferAttribute(vertices.array, 3)); 136 | geom.setAttribute('normal', new BufferAttribute(normals.array, 3)); 137 | uvs && geom.setAttribute('uv', new BufferAttribute(uvs.array, 2)); 138 | colors && geom.setAttribute('color', new BufferAttribute(colors.array, 3)); 139 | for (let gi = 0; gi < grps.length; gi++) { 140 | if (grps[gi] === undefined) { 141 | grps[gi] = []; 142 | } 143 | } 144 | if (grps.length) { 145 | let index = []; 146 | let gbase = 0; 147 | for (let gi = 0; gi < grps.length; gi++) { 148 | geom.addGroup(gbase, grps[gi].length, gi); 149 | gbase += grps[gi].length; 150 | index = index.concat(grps[gi]); 151 | } 152 | geom.addGroup(gbase, dgrp.length, grps.length); 153 | index = index.concat(dgrp); 154 | geom.setIndex(index); 155 | } 156 | const inv = new Matrix4().copy(toMatrix).invert(); 157 | geom.applyMatrix4(inv); 158 | geom.computeBoundingSphere(); 159 | geom.computeBoundingBox(); 160 | 161 | return geom; 162 | } 163 | 164 | static fromMesh(mesh: Mesh, objectIndex?: any): CSG { 165 | const csg = CSG.fromGeometry(mesh.geometry, objectIndex); 166 | const ttvv0 = new Vector3(); 167 | const tmpm3 = new Matrix3(); 168 | tmpm3.getNormalMatrix(mesh.matrix); 169 | for (let i = 0; i < csg.polygons.length; i++) { 170 | const p = csg.polygons[i]; 171 | for (let j = 0; j < p.vertices.length; j++) { 172 | const v = p.vertices[j]; 173 | v.pos.copy(ttvv0.copy(v.pos.toVector3()).applyMatrix4(mesh.matrix)); 174 | v.normal.copy(ttvv0.copy(v.normal.toVector3()).applyMatrix3(tmpm3)); 175 | } 176 | } 177 | return csg; 178 | } 179 | 180 | static toMesh( 181 | csg: CSG, 182 | toMatrix: Matrix4, 183 | toMaterial?: Material | Material[] 184 | ): Mesh { 185 | const geom = CSG.toGeometry(csg, toMatrix); 186 | const m = new Mesh(geom, toMaterial); 187 | m.matrix.copy(toMatrix); 188 | m.matrix.decompose(m.position, m.quaternion, m.scale); 189 | m.rotation.setFromQuaternion(m.quaternion); 190 | m.updateMatrixWorld(); 191 | m.castShadow = m.receiveShadow = true; 192 | return m; 193 | } 194 | 195 | static union(meshA: Mesh, meshB: Mesh): Mesh { 196 | const csgA = CSG.fromMesh(meshA); 197 | const csgB = CSG.fromMesh(meshB); 198 | return CSG.toMesh(csgA.union(csgB), meshA.matrix, meshA.material); 199 | } 200 | 201 | static subtract(meshA: Mesh, meshB: Mesh): Mesh { 202 | const csgA = CSG.fromMesh(meshA); 203 | const csgB = CSG.fromMesh(meshB); 204 | return CSG.toMesh(csgA.subtract(csgB), meshA.matrix, meshA.material); 205 | } 206 | 207 | static intersect(meshA: Mesh, meshB: Mesh): Mesh { 208 | const csgA = CSG.fromMesh(meshA); 209 | const csgB = CSG.fromMesh(meshB); 210 | return CSG.toMesh(csgA.intersect(csgB), meshA.matrix, meshA.material); 211 | } 212 | 213 | private polygons: Polygon[] = []; 214 | 215 | clone(): CSG { 216 | const csg = new CSG(); 217 | csg.polygons = this.polygons 218 | .map((p) => p.clone()) 219 | .filter((p) => Number.isFinite(p.plane.w)); 220 | return csg; 221 | } 222 | 223 | toPolygons(): Polygon[] { 224 | return this.polygons; 225 | } 226 | 227 | union(csg: CSG): CSG { 228 | const a = new Node(this.clone().polygons); 229 | const b = new Node(csg.clone().polygons); 230 | a.clipTo(b); 231 | b.clipTo(a); 232 | b.invert(); 233 | b.clipTo(a); 234 | b.invert(); 235 | a.build(b.allPolygons()); 236 | return CSG.fromPolygons(a.allPolygons()); 237 | } 238 | 239 | subtract(csg: CSG): CSG { 240 | const a = new Node(this.clone().polygons); 241 | const b = new Node(csg.clone().polygons); 242 | a.invert(); 243 | a.clipTo(b); 244 | b.clipTo(a); 245 | b.invert(); 246 | b.clipTo(a); 247 | b.invert(); 248 | a.build(b.allPolygons()); 249 | a.invert(); 250 | return CSG.fromPolygons(a.allPolygons()); 251 | } 252 | 253 | intersect(csg: CSG): CSG { 254 | const a = new Node(this.clone().polygons); 255 | const b = new Node(csg.clone().polygons); 256 | a.invert(); 257 | b.clipTo(a); 258 | b.invert(); 259 | a.clipTo(b); 260 | b.clipTo(a); 261 | a.build(b.allPolygons()); 262 | a.invert(); 263 | return CSG.fromPolygons(a.allPolygons()); 264 | } 265 | 266 | // Return a new CSG solid with solid and empty space switched. This solid is 267 | // not modified. 268 | inverse(): CSG { 269 | const csg = this.clone(); 270 | for (const p of csg.polygons) { 271 | p.flip(); 272 | } 273 | return csg; 274 | } 275 | 276 | toMesh(toMatrix: Matrix4, toMaterial?: Material | Material[]): Mesh { 277 | return CSG.toMesh(this, toMatrix, toMaterial); 278 | } 279 | 280 | toGeometry(toMatrix: Matrix4): BufferGeometry { 281 | return CSG.toGeometry(this, toMatrix); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/NBuf.ts: -------------------------------------------------------------------------------- 1 | import { Vector } from './Vector'; 2 | 3 | export class NBuf3 { 4 | top = 0; 5 | array: Float32Array; 6 | 7 | constructor(ct: number) { 8 | this.array = new Float32Array(ct); 9 | } 10 | 11 | write(v: Vector): void { 12 | this.array[this.top++] = v.x; 13 | this.array[this.top++] = v.y; 14 | this.array[this.top++] = v.z; 15 | } 16 | } 17 | 18 | export class NBuf2 { 19 | top = 0; 20 | array: Float32Array; 21 | 22 | constructor(ct: number) { 23 | this.array = new Float32Array(ct); 24 | } 25 | 26 | write(v: Vector): void { 27 | this.array[this.top++] = v.x; 28 | this.array[this.top++] = v.y; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Node.ts: -------------------------------------------------------------------------------- 1 | import { Plane } from './Plane'; 2 | import { Polygon } from './Polygon'; 3 | 4 | /** 5 | * Holds a node in a BSP tree. A BSP tree is built from a collection of polygons 6 | * by picking a polygon to split along. That polygon (and all other coplanar 7 | * polygons) are added directly to that node and the other polygons are added to 8 | * the front and/or back subtrees. This is not a leafy BSP tree since there is 9 | * no distinction between internal and leaf nodes. 10 | */ 11 | export class Node { 12 | polygons: Polygon[]; 13 | plane: Plane; 14 | front: Node; 15 | back: Node; 16 | 17 | constructor(polygons?: Polygon[]) { 18 | this.plane = null; 19 | this.front = null; 20 | this.back = null; 21 | this.polygons = []; 22 | if (polygons) this.build(polygons); 23 | } 24 | 25 | clone(): Node { 26 | const node = new Node(); 27 | node.plane = this.plane && this.plane.clone(); 28 | node.front = this.front && this.front.clone(); 29 | node.back = this.back && this.back.clone(); 30 | node.polygons = this.polygons.map((p) => p.clone()); 31 | return node; 32 | } 33 | 34 | // Convert solid space to empty space and empty space to solid space. 35 | invert(): void { 36 | for (let i = 0; i < this.polygons.length; i++) this.polygons[i].flip(); 37 | 38 | this.plane && this.plane.flip(); 39 | this.front && this.front.invert(); 40 | this.back && this.back.invert(); 41 | const temp = this.front; 42 | this.front = this.back; 43 | this.back = temp; 44 | } 45 | 46 | // Recursively remove all polygons in `polygons` that are inside this BSP 47 | // tree. 48 | clipPolygons(polygons: Polygon[]): Polygon[] { 49 | if (!this.plane) return polygons.slice(); 50 | 51 | let front = new Array(), 52 | back = new Array(); 53 | 54 | for (let i = 0; i < polygons.length; i++) { 55 | this.plane.splitPolygon(polygons[i], front, back, front, back); 56 | } 57 | 58 | if (this.front) front = this.front.clipPolygons(front); 59 | this.back ? (back = this.back.clipPolygons(back)) : (back = []); 60 | 61 | return front.concat(back); 62 | } 63 | 64 | // Remove all polygons in this BSP tree that are inside the other BSP tree 65 | // `bsp`. 66 | clipTo(bsp: Node): void { 67 | this.polygons = bsp.clipPolygons(this.polygons); 68 | if (this.front) this.front.clipTo(bsp); 69 | if (this.back) this.back.clipTo(bsp); 70 | } 71 | 72 | // Return a list of all polygons in this BSP tree. 73 | allPolygons(): Polygon[] { 74 | let polygons = this.polygons.slice(); 75 | if (this.front) polygons = polygons.concat(this.front.allPolygons()); 76 | if (this.back) polygons = polygons.concat(this.back.allPolygons()); 77 | return polygons; 78 | } 79 | 80 | // Build a BSP tree out of `polygons`. When called on an existing tree, the 81 | // new polygons are filtered down to the bottom of the tree and become new 82 | // nodes there. Each set of polygons is partitioned using the first polygon 83 | // (no heuristic is used to pick a good split). 84 | build(polygons: Polygon[]): void { 85 | if (!polygons.length) return; 86 | if (!this.plane) this.plane = polygons[0].plane.clone(); 87 | const front = [], 88 | back = []; 89 | for (let i = 0; i < polygons.length; i++) { 90 | this.plane.splitPolygon( 91 | polygons[i], 92 | this.polygons, 93 | this.polygons, 94 | front, 95 | back 96 | ); 97 | } 98 | if (front.length) { 99 | if (!this.front) this.front = new Node(); 100 | this.front.build(front); 101 | } 102 | if (back.length) { 103 | if (!this.back) this.back = new Node(); 104 | this.back.build(back); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Plane.ts: -------------------------------------------------------------------------------- 1 | import { Polygon } from './Polygon'; 2 | import { Vector } from './Vector'; 3 | 4 | /** 5 | * Represents a plane in 3D space. 6 | */ 7 | export class Plane { 8 | static EPSILON = 1e-5; 9 | 10 | constructor(public normal: Vector, public w: number) { 11 | this.normal = normal; 12 | this.w = w; 13 | } 14 | 15 | clone(): Plane { 16 | return new Plane(this.normal.clone(), this.w); 17 | } 18 | 19 | flip(): void { 20 | this.normal.negate(); 21 | this.w = -this.w; 22 | } 23 | 24 | // Split `polygon` by this plane if needed, then put the polygon or polygon 25 | // fragments in the appropriate lists. Coplanar polygons go into either 26 | // `coplanarFront` or `coplanarBack` depending on their orientation with 27 | // respect to this plane. Polygons in front or in back of this plane go into 28 | // either `front` or `back`. 29 | splitPolygon( 30 | polygon: Polygon, 31 | coplanarFront: Polygon[], 32 | coplanarBack: Polygon[], 33 | front: Polygon[], 34 | back: Polygon[] 35 | ): void { 36 | const COPLANAR = 0; 37 | const FRONT = 1; 38 | const BACK = 2; 39 | const SPANNING = 3; 40 | 41 | // Classify each point as well as the entire polygon into one of the above 42 | // four classes. 43 | let polygonType = 0; 44 | const types = []; 45 | for (let i = 0; i < polygon.vertices.length; i++) { 46 | const t = this.normal.dot(polygon.vertices[i].pos) - this.w; 47 | const type = 48 | t < -Plane.EPSILON ? BACK : t > Plane.EPSILON ? FRONT : COPLANAR; 49 | polygonType |= type; 50 | types.push(type); 51 | } 52 | 53 | // Put the polygon in the correct list, splitting it when necessary. 54 | switch (polygonType) { 55 | case COPLANAR: 56 | (this.normal.dot(polygon.plane.normal) > 0 57 | ? coplanarFront 58 | : coplanarBack 59 | ).push(polygon); 60 | break; 61 | case FRONT: 62 | front.push(polygon); 63 | break; 64 | case BACK: 65 | back.push(polygon); 66 | break; 67 | case SPANNING: { 68 | const f = [], 69 | b = []; 70 | for (let i = 0; i < polygon.vertices.length; i++) { 71 | const j = (i + 1) % polygon.vertices.length; 72 | const ti = types[i], 73 | tj = types[j]; 74 | const vi = polygon.vertices[i], 75 | vj = polygon.vertices[j]; 76 | if (ti != BACK) f.push(vi); 77 | if (ti != FRONT) b.push(ti != BACK ? vi.clone() : vi); 78 | if ((ti | tj) == SPANNING) { 79 | const t = 80 | (this.w - this.normal.dot(vi.pos)) / 81 | this.normal.dot(new Vector().copy(vj.pos).sub(vi.pos)); 82 | const v = vi.interpolate(vj, t); 83 | f.push(v); 84 | b.push(v.clone()); 85 | } 86 | } 87 | if (f.length >= 3) front.push(new Polygon(f, polygon.shared)); 88 | if (b.length >= 3) back.push(new Polygon(b, polygon.shared)); 89 | break; 90 | } 91 | } 92 | } 93 | 94 | static fromPoints(a: Vector, b: Vector, c: Vector): Plane { 95 | const n = new Vector() 96 | .copy(b) 97 | .sub(a) 98 | .cross(new Vector().copy(c).sub(a)) 99 | .normalize(); 100 | return new Plane(n.clone(), n.dot(a)); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Polygon.ts: -------------------------------------------------------------------------------- 1 | import { Plane } from './Plane'; 2 | import { Vertex } from './Vertex'; 3 | 4 | /** 5 | * Represents a convex polygon. The vertices used to initialize a polygon must 6 | * be coplanar and form a convex loop. They do not have to be `Vertex` 7 | * instances but they must behave similarly (duck typing can be used for 8 | * customization). 9 | * 10 | * Each convex polygon has a `shared` property, which is shared between all 11 | * polygons that are clones of each other or were split from the same polygon. 12 | * This can be used to define per-polygon properties (such as surface color). 13 | */ 14 | export class Polygon { 15 | plane: Plane; 16 | 17 | constructor(public vertices: Vertex[], public shared: any) { 18 | this.plane = Plane.fromPoints( 19 | vertices[0].pos, 20 | vertices[1].pos, 21 | vertices[2].pos 22 | ); 23 | } 24 | 25 | clone(): Polygon { 26 | return new Polygon( 27 | this.vertices.map((v) => v.clone()), 28 | this.shared 29 | ); 30 | } 31 | 32 | flip(): void { 33 | this.vertices.reverse().map((v) => v.flip()); 34 | this.plane.flip(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Vector.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | 3 | /** 4 | * Represents a 3D vector. 5 | */ 6 | export class Vector { 7 | constructor(public x = 0, public y = 0, public z = 0) {} 8 | 9 | copy(v: Vector | Vector3): Vector { 10 | this.x = v.x; 11 | this.y = v.y; 12 | this.z = v.z; 13 | return this; 14 | } 15 | 16 | clone(): Vector { 17 | return new Vector(this.x, this.y, this.z); 18 | } 19 | 20 | negate(): Vector { 21 | this.x *= -1; 22 | this.y *= -1; 23 | this.z *= -1; 24 | return this; 25 | } 26 | 27 | add(a: Vector): Vector { 28 | this.x += a.x; 29 | this.y += a.y; 30 | this.z += a.z; 31 | return this; 32 | } 33 | 34 | sub(a: Vector): Vector { 35 | this.x -= a.x; 36 | this.y -= a.y; 37 | this.z -= a.z; 38 | return this; 39 | } 40 | 41 | times(a: number): Vector { 42 | this.x *= a; 43 | this.y *= a; 44 | this.z *= a; 45 | return this; 46 | } 47 | 48 | dividedBy(a: number): Vector { 49 | this.x /= a; 50 | this.y /= a; 51 | this.z /= a; 52 | return this; 53 | } 54 | 55 | lerp(a: Vector, t: number): Vector { 56 | return this.add(new Vector().copy(a).sub(this).times(t)); 57 | } 58 | 59 | unit(): Vector { 60 | return this.dividedBy(this.length()); 61 | } 62 | 63 | length(): number { 64 | return Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2); 65 | } 66 | 67 | normalize(): Vector { 68 | return this.unit(); 69 | } 70 | 71 | cross(b: Vector): Vector { 72 | const a = this.clone(); 73 | const ax = a.x, 74 | ay = a.y, 75 | az = a.z; 76 | const bx = b.x, 77 | by = b.y, 78 | bz = b.z; 79 | 80 | this.x = ay * bz - az * by; 81 | this.y = az * bx - ax * bz; 82 | this.z = ax * by - ay * bx; 83 | 84 | return this; 85 | } 86 | 87 | dot(b: Vector): number { 88 | return this.x * b.x + this.y * b.y + this.z * b.z; 89 | } 90 | 91 | toVector3(): Vector3 { 92 | return new Vector3(this.x, this.y, this.z); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Vertex.ts: -------------------------------------------------------------------------------- 1 | import { Vector } from './Vector'; 2 | 3 | /** 4 | * Represents a vertex of a polygon. Use your own vertex class instead of this 5 | * one to provide additional features like texture coordinates and vertex 6 | * colors. Custom vertex classes need to provide a `pos` property and `clone()`, 7 | * `flip()`, and `interpolate()` methods that behave analogous to the ones 8 | * defined by `CSG.Vertex`. This class provides `normal` so convenience 9 | * functions like `CSG.sphere()` can return a smooth vertex normal, but `normal` 10 | * is not used anywhere else. 11 | */ 12 | export class Vertex { 13 | pos: Vector; 14 | normal: Vector; 15 | uv: Vector; 16 | color: Vector; 17 | 18 | constructor(pos: Vector, normal: Vector, uv: Vector, color: Vector) { 19 | this.pos = new Vector().copy(pos); 20 | this.normal = new Vector().copy(normal); 21 | this.uv = new Vector().copy(uv); 22 | this.uv.z = 0; 23 | color && (this.color = new Vector().copy(color)); 24 | } 25 | 26 | clone(): Vertex { 27 | return new Vertex(this.pos, this.normal, this.uv, this.color); 28 | } 29 | 30 | // Invert all orientation-specific data (e.g. vertex normal). Called when the 31 | // orientation of a polygon is flipped. 32 | flip(): void { 33 | this.normal.negate(); 34 | } 35 | 36 | // Create a new vertex between this vertex and `other` by linearly 37 | // interpolating all properties using a parameter of `t`. Subclasses should 38 | // override this to interpolate additional properties. 39 | interpolate(other: Vertex, t: number): Vertex { 40 | return new Vertex( 41 | this.pos.clone().lerp(other.pos, t), 42 | this.normal.clone().lerp(other.normal, t), 43 | this.uv.clone().lerp(other.uv, t), 44 | this.color && other.color && this.color.clone().lerp(other.color, t) 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/__tests__/csg.test.ts: -------------------------------------------------------------------------------- 1 | import { BoxGeometry, BufferGeometry, Matrix4, Mesh, Vector3 } from 'three'; 2 | import { CSG } from '../index'; 3 | 4 | describe('CSG instance methods', () => { 5 | test('clone', () => { 6 | const csg = new CSG(); 7 | expect(csg.clone()).toEqual(csg); 8 | }); 9 | 10 | // Make 2 box meshes 11 | const meshA = new Mesh(new BoxGeometry(1, 1, 1)); 12 | const meshB = new Mesh(new BoxGeometry(1, 1, 1)); 13 | // offset one of the boxes by half its width 14 | meshB.position.add(new Vector3(0.5, 0.5, 0.5)); 15 | // Make sure the .matrix of each mesh is current 16 | meshA.updateMatrix(); 17 | meshB.updateMatrix(); 18 | // Create a bsp tree from each of the meshes 19 | const bspA = CSG.fromMesh(meshA); 20 | const bspB = CSG.fromMesh(meshB); 21 | 22 | test('inverse', () => { 23 | expect(bspA.inverse()).toBeTruthy(); 24 | }); 25 | 26 | test('toPolygons', () => { 27 | expect(bspA.toPolygons()).toBeTruthy(); 28 | }); 29 | 30 | test('subtract', () => { 31 | // Subtract one bsp from the other 32 | const bspC = bspA.subtract(bspB); 33 | // Get the resulting mesh from the result bsp 34 | const meshC = CSG.toMesh(bspC, meshA.matrix); 35 | 36 | expect(meshC).toBeTruthy(); 37 | }); 38 | 39 | test('union', () => { 40 | // Union one bsp with the other 41 | const bspC = bspA.union(bspB); 42 | // Get the resulting mesh from the result bsp 43 | const meshC = CSG.toMesh(bspC, meshA.matrix); 44 | 45 | expect(meshC).toBeTruthy(); 46 | }); 47 | 48 | test('intersect', () => { 49 | // Intersect one bsp with the other 50 | const bspC = bspA.intersect(bspB); 51 | // Get the resulting mesh from the result bsp 52 | const meshC = CSG.toMesh(bspC, meshA.matrix); 53 | 54 | expect(meshC).toBeTruthy(); 55 | }); 56 | 57 | test('custom geometry', () => { 58 | const box = new BoxGeometry(1, 1, 1); 59 | const g = new BufferGeometry().copy(box); 60 | const csg = CSG.fromGeometry(g); 61 | 62 | const result = CSG.toMesh(csg, new Matrix4()); 63 | expect(result).toBeTruthy(); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CSG'; 2 | -------------------------------------------------------------------------------- /tsconfig-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "./lib/cjs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "ES2020", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "outDir": "./lib/esm", 8 | "typeRoots": ["node_modules/@types"], 9 | "esModuleInterop": true 10 | }, 11 | "include": ["src"], 12 | "exclude": ["node_modules", "**/__tests__/*"] 13 | } 14 | --------------------------------------------------------------------------------