├── .editorconfig ├── .gitattributes ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpstan.neon.dist ├── src ├── Axis.php ├── AxisAlignedBB.php ├── Facing.php ├── Math.php ├── Matrix.php ├── RayTraceResult.php ├── Vector2.php ├── Vector3.php ├── VectorMath.php └── VoxelRayTrace.php └── tests └── phpunit ├── FacingTest.php └── Vector3Test.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | root = yes 3 | 4 | [*] 5 | indent_size = 4 6 | indent_style = tab 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | *.php text eol=lf 4 | *.sh text eol=lf 5 | *.txt text eol=lf 6 | *.properties text eol=lf 7 | *.bat text eol=crlf 8 | *.cmd text eol=crlf 9 | *.ps1 text eol=crlf 10 | 11 | # Custom for Visual Studio 12 | *.cs diff=csharp 13 | *.sln merge=union 14 | *.csproj merge=union 15 | *.vbproj merge=union 16 | *.fsproj merge=union 17 | *.dbproj merge=union 18 | 19 | # Standard to msysgit 20 | *.doc diff=astextplain 21 | *.DOC diff=astextplain 22 | *.docx diff=astextplain 23 | *.DOCX diff=astextplain 24 | *.dot diff=astextplain 25 | *.DOT diff=astextplain 26 | *.pdf diff=astextplain 27 | *.PDF diff=astextplain 28 | *.rtf diff=astextplain 29 | *.RTF diff=astextplain 30 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @pmmp/server-developers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: github-actions 10 | directory: "/" 11 | schedule: 12 | interval: monthly 13 | groups: 14 | github-actions: 15 | patterns: ["*"] 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | if: "!contains(github.event.head_commit.message, '[ci skip]')" 9 | strategy: 10 | matrix: 11 | php: ['8.1', '8.2', '8.3', '8.4'] 12 | name: PHP ${{ matrix.php }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@2.31.1 18 | with: 19 | php-version: ${{ matrix.php }} 20 | 21 | - name: Cache Composer packages 22 | uses: actions/cache@v4 23 | with: 24 | path: "~/.cache/composer" 25 | key: "php-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}-v2" 26 | restore-keys: "php-${{ matrix.php }}-composer-" 27 | 28 | - name: Install dependencies 29 | run: composer install --prefer-dist --no-interaction 30 | 31 | - name: Run PHPStan 32 | run: ./vendor/bin/phpstan analyze --no-progress 33 | 34 | - name: Run PHPUnit tests 35 | run: ./vendor/bin/phpunit --bootstrap vendor/autoload.php --fail-on-warning tests/phpunit 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /vendor/ 3 | composer.lock 4 | phpstan.neon 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Math 2 | ![CI](https://github.com/pmmp/Math/workflows/CI/badge.svg) 3 | 4 | PHP library containing math related code used in PocketMine-MP 5 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pocketmine/math", 3 | "description": "PHP library containing math related code used in PocketMine-MP", 4 | "type": "library", 5 | "license": "LGPL-3.0", 6 | "require": { 7 | "php": "^8.1", 8 | "php-64bit": "*" 9 | }, 10 | "require-dev": { 11 | "phpstan/phpstan": "2.1.0", 12 | "phpstan/extension-installer": "^1.0", 13 | "phpstan/phpstan-strict-rules": "^2.0", 14 | "phpunit/phpunit": "^10.0 || ^11.0" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "pocketmine\\math\\": "src/" 19 | } 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "pocketmine\\math\\": "tests/phpunit/" 24 | } 25 | }, 26 | "config": { 27 | "allow-plugins": { 28 | "phpstan/extension-installer": true 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 9 3 | treatPhpDocTypesAsCertain: false 4 | paths: 5 | - src 6 | -------------------------------------------------------------------------------- /src/Axis.php: -------------------------------------------------------------------------------- 1 | "y", 41 | Axis::Z => "z", 42 | Axis::X => "x", 43 | default => throw new \InvalidArgumentException("Invalid axis $axis") 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/AxisAlignedBB.php: -------------------------------------------------------------------------------- 1 | $maxX){ 40 | throw new \InvalidArgumentException("minX $minX is larger than maxX $maxX"); 41 | } 42 | if($minY > $maxY){ 43 | throw new \InvalidArgumentException("minY $minY is larger than maxY $maxY"); 44 | } 45 | if($minZ > $maxZ){ 46 | throw new \InvalidArgumentException("minZ $minZ is larger than maxZ $maxZ"); 47 | } 48 | $this->minX = $minX; 49 | $this->minY = $minY; 50 | $this->minZ = $minZ; 51 | $this->maxX = $maxX; 52 | $this->maxY = $maxY; 53 | $this->maxZ = $maxZ; 54 | } 55 | 56 | /** 57 | * Returns a new AxisAlignedBB extended by the specified X, Y and Z. 58 | * If each of X, Y and Z are positive, the relevant max bound will be increased. If negative, the relevant min 59 | * bound will be decreased. 60 | */ 61 | public function addCoord(float $x, float $y, float $z) : AxisAlignedBB{ 62 | $minX = $this->minX; 63 | $minY = $this->minY; 64 | $minZ = $this->minZ; 65 | $maxX = $this->maxX; 66 | $maxY = $this->maxY; 67 | $maxZ = $this->maxZ; 68 | 69 | if($x < 0){ 70 | $minX += $x; 71 | }elseif($x > 0){ 72 | $maxX += $x; 73 | } 74 | 75 | if($y < 0){ 76 | $minY += $y; 77 | }elseif($y > 0){ 78 | $maxY += $y; 79 | } 80 | 81 | if($z < 0){ 82 | $minZ += $z; 83 | }elseif($z > 0){ 84 | $maxZ += $z; 85 | } 86 | 87 | return new AxisAlignedBB($minX, $minY, $minZ, $maxX, $maxY, $maxZ); 88 | } 89 | 90 | /** 91 | * Outsets the bounds of this AxisAlignedBB by the specified X, Y and Z. 92 | * 93 | * @return $this 94 | */ 95 | public function expand(float $x, float $y, float $z){ 96 | $this->minX -= $x; 97 | $this->minY -= $y; 98 | $this->minZ -= $z; 99 | $this->maxX += $x; 100 | $this->maxY += $y; 101 | $this->maxZ += $z; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Returns an expanded clone of this AxisAlignedBB. 108 | */ 109 | public function expandedCopy(float $x, float $y, float $z) : AxisAlignedBB{ 110 | return (clone $this)->expand($x, $y, $z); 111 | } 112 | 113 | /** 114 | * Shifts this AxisAlignedBB by the given X, Y and Z. 115 | * 116 | * @return $this 117 | */ 118 | public function offset(float $x, float $y, float $z) : AxisAlignedBB{ 119 | $this->minX += $x; 120 | $this->minY += $y; 121 | $this->minZ += $z; 122 | $this->maxX += $x; 123 | $this->maxY += $y; 124 | $this->maxZ += $z; 125 | 126 | return $this; 127 | } 128 | 129 | /** 130 | * Returns an offset clone of this AxisAlignedBB. 131 | */ 132 | public function offsetCopy(float $x, float $y, float $z) : AxisAlignedBB{ 133 | return (clone $this)->offset($x, $y, $z); 134 | } 135 | 136 | /** 137 | * Offsets this AxisAlignedBB in the given direction by the specified distance. 138 | * 139 | * @param int $face one of the Facing::* constants 140 | * 141 | * @return $this 142 | */ 143 | public function offsetTowards(int $face, float $distance) : AxisAlignedBB{ 144 | [$offsetX, $offsetY, $offsetZ] = Facing::OFFSET[$face] ?? throw new \InvalidArgumentException("Invalid Facing $face"); 145 | 146 | return $this->offset($offsetX * $distance, $offsetY * $distance, $offsetZ * $distance); 147 | } 148 | 149 | /** 150 | * Returns an offset clone of this AxisAlignedBB. 151 | */ 152 | public function offsetTowardsCopy(int $face, float $distance) : AxisAlignedBB{ 153 | return (clone $this)->offsetTowards($face, $distance); 154 | } 155 | 156 | /** 157 | * Insets the bounds of this AxisAlignedBB by the specified X, Y and Z. 158 | * 159 | * @return $this 160 | */ 161 | public function contract(float $x, float $y, float $z) : AxisAlignedBB{ 162 | $this->minX += $x; 163 | $this->minY += $y; 164 | $this->minZ += $z; 165 | $this->maxX -= $x; 166 | $this->maxY -= $y; 167 | $this->maxZ -= $z; 168 | 169 | return $this; 170 | } 171 | 172 | /** 173 | * Returns a contracted clone of this AxisAlignedBB. 174 | */ 175 | public function contractedCopy(float $x, float $y, float $z) : AxisAlignedBB{ 176 | return (clone $this)->contract($x, $y, $z); 177 | } 178 | 179 | /** 180 | * Extends the AABB in the given direction. 181 | * 182 | * @param float $distance Negative values pull the face in, positive values push out. 183 | * 184 | * @return $this 185 | * @throws \InvalidArgumentException 186 | */ 187 | public function extend(int $face, float $distance) : AxisAlignedBB{ 188 | match($face){ 189 | Facing::DOWN => $this->minY -= $distance, 190 | Facing::UP => $this->maxY += $distance, 191 | Facing::NORTH => $this->minZ -= $distance, 192 | Facing::SOUTH => $this->maxZ += $distance, 193 | Facing::WEST => $this->minX -= $distance, 194 | Facing::EAST => $this->maxX += $distance, 195 | default => throw new \InvalidArgumentException("Invalid face $face"), 196 | }; 197 | 198 | return $this; 199 | } 200 | 201 | /** 202 | * Returns an extended clone of this bounding box. 203 | * @see AxisAlignedBB::extend() 204 | * 205 | * @throws \InvalidArgumentException 206 | */ 207 | public function extendedCopy(int $face, float $distance) : AxisAlignedBB{ 208 | return (clone $this)->extend($face, $distance); 209 | } 210 | 211 | /** 212 | * Inverse of extend(). 213 | * @see AxisAlignedBB::extend() 214 | * 215 | * @param float $distance Positive values pull the face in, negative values push out. 216 | * 217 | * @return $this 218 | * @throws \InvalidArgumentException 219 | */ 220 | public function trim(int $face, float $distance) : AxisAlignedBB{ 221 | return $this->extend($face, -$distance); 222 | } 223 | 224 | /** 225 | * Returns a trimmed clone of this bounding box. 226 | * @see AxisAlignedBB::trim() 227 | * 228 | * @throws \InvalidArgumentException 229 | */ 230 | public function trimmedCopy(int $face, float $distance) : AxisAlignedBB{ 231 | return $this->extendedCopy($face, -$distance); 232 | } 233 | 234 | /** 235 | * Increases the dimension of the AABB along the given axis. 236 | * 237 | * @param int $axis one of the Axis::* constants 238 | * @param float $distance Negative values reduce width, positive values increase width. 239 | * 240 | * @return $this 241 | * @throws \InvalidArgumentException 242 | */ 243 | public function stretch(int $axis, float $distance) : AxisAlignedBB{ 244 | if($axis === Axis::Y){ 245 | $this->minY -= $distance; 246 | $this->maxY += $distance; 247 | }elseif($axis === Axis::Z){ 248 | $this->minZ -= $distance; 249 | $this->maxZ += $distance; 250 | }elseif($axis === Axis::X){ 251 | $this->minX -= $distance; 252 | $this->maxX += $distance; 253 | }else{ 254 | throw new \InvalidArgumentException("Invalid axis $axis"); 255 | } 256 | return $this; 257 | } 258 | 259 | /** 260 | * Returns a stretched copy of this bounding box. 261 | * @see AxisAlignedBB::stretch() 262 | * 263 | * @throws \InvalidArgumentException 264 | */ 265 | public function stretchedCopy(int $axis, float $distance) : AxisAlignedBB{ 266 | return (clone $this)->stretch($axis, $distance); 267 | } 268 | 269 | /** 270 | * Reduces the dimension of the AABB on the given axis. Inverse of stretch(). 271 | * @see AxisAlignedBB::stretch() 272 | * 273 | * @return $this 274 | * @throws \InvalidArgumentException 275 | */ 276 | public function squash(int $axis, float $distance) : AxisAlignedBB{ 277 | return $this->stretch($axis, -$distance); 278 | } 279 | 280 | /** 281 | * Returns a squashed copy of this bounding box. 282 | * @see AxisAlignedBB::squash() 283 | * 284 | * @throws \InvalidArgumentException 285 | */ 286 | public function squashedCopy(int $axis, float $distance) : AxisAlignedBB{ 287 | return $this->stretchedCopy($axis, -$distance); 288 | } 289 | 290 | public function calculateXOffset(AxisAlignedBB $bb, float $x) : float{ 291 | if($bb->maxY <= $this->minY or $bb->minY >= $this->maxY){ 292 | return $x; 293 | } 294 | if($bb->maxZ <= $this->minZ or $bb->minZ >= $this->maxZ){ 295 | return $x; 296 | } 297 | if($x > 0 and $bb->maxX <= $this->minX){ 298 | $x1 = $this->minX - $bb->maxX; 299 | if($x1 < $x){ 300 | $x = $x1; 301 | } 302 | }elseif($x < 0 and $bb->minX >= $this->maxX){ 303 | $x2 = $this->maxX - $bb->minX; 304 | if($x2 > $x){ 305 | $x = $x2; 306 | } 307 | } 308 | 309 | return $x; 310 | } 311 | 312 | public function calculateYOffset(AxisAlignedBB $bb, float $y) : float{ 313 | if($bb->maxX <= $this->minX or $bb->minX >= $this->maxX){ 314 | return $y; 315 | } 316 | if($bb->maxZ <= $this->minZ or $bb->minZ >= $this->maxZ){ 317 | return $y; 318 | } 319 | if($y > 0 and $bb->maxY <= $this->minY){ 320 | $y1 = $this->minY - $bb->maxY; 321 | if($y1 < $y){ 322 | $y = $y1; 323 | } 324 | }elseif($y < 0 and $bb->minY >= $this->maxY){ 325 | $y2 = $this->maxY - $bb->minY; 326 | if($y2 > $y){ 327 | $y = $y2; 328 | } 329 | } 330 | 331 | return $y; 332 | } 333 | 334 | public function calculateZOffset(AxisAlignedBB $bb, float $z) : float{ 335 | if($bb->maxX <= $this->minX or $bb->minX >= $this->maxX){ 336 | return $z; 337 | } 338 | if($bb->maxY <= $this->minY or $bb->minY >= $this->maxY){ 339 | return $z; 340 | } 341 | if($z > 0 and $bb->maxZ <= $this->minZ){ 342 | $z1 = $this->minZ - $bb->maxZ; 343 | if($z1 < $z){ 344 | $z = $z1; 345 | } 346 | }elseif($z < 0 and $bb->minZ >= $this->maxZ){ 347 | $z2 = $this->maxZ - $bb->minZ; 348 | if($z2 > $z){ 349 | $z = $z2; 350 | } 351 | } 352 | 353 | return $z; 354 | } 355 | 356 | /** 357 | * Returns whether any part of the specified AABB is inside (intersects with) this one. 358 | */ 359 | public function intersectsWith(AxisAlignedBB $bb, float $epsilon = 0.00001) : bool{ 360 | if($bb->maxX - $this->minX > $epsilon and $this->maxX - $bb->minX > $epsilon){ 361 | if($bb->maxY - $this->minY > $epsilon and $this->maxY - $bb->minY > $epsilon){ 362 | return $bb->maxZ - $this->minZ > $epsilon and $this->maxZ - $bb->minZ > $epsilon; 363 | } 364 | } 365 | 366 | return false; 367 | } 368 | 369 | /** 370 | * Returns whether the specified vector is within the bounds of this AABB on all axes. 371 | */ 372 | public function isVectorInside(Vector3 $vector) : bool{ 373 | if($vector->x <= $this->minX or $vector->x >= $this->maxX){ 374 | return false; 375 | } 376 | if($vector->y <= $this->minY or $vector->y >= $this->maxY){ 377 | return false; 378 | } 379 | 380 | return $vector->z > $this->minZ and $vector->z < $this->maxZ; 381 | } 382 | 383 | /** 384 | * Returns the mean average of the AABB's X, Y and Z lengths. 385 | */ 386 | public function getAverageEdgeLength() : float{ 387 | return ($this->maxX - $this->minX + $this->maxY - $this->minY + $this->maxZ - $this->minZ) / 3; 388 | } 389 | 390 | public function getXLength() : float{ return $this->maxX - $this->minX; } 391 | 392 | public function getYLength() : float{ return $this->maxY - $this->minY; } 393 | 394 | public function getZLength() : float{ return $this->maxZ - $this->minZ; } 395 | 396 | public function isCube(float $epsilon = 0.000001) : bool{ 397 | [$xLen, $yLen, $zLen] = [$this->getXLength(), $this->getYLength(), $this->getZLength()]; 398 | return abs($xLen - $yLen) < $epsilon && abs($yLen - $zLen) < $epsilon; 399 | } 400 | 401 | /** 402 | * Returns the interior volume of the AABB. 403 | */ 404 | public function getVolume() : float{ 405 | return ($this->maxX - $this->minX) * ($this->maxY - $this->minY) * ($this->maxZ - $this->minZ); 406 | } 407 | 408 | /** 409 | * Returns whether the specified vector is within the Y and Z bounds of this AABB. 410 | */ 411 | public function isVectorInYZ(Vector3 $vector) : bool{ 412 | return $vector->y >= $this->minY and $vector->y <= $this->maxY and $vector->z >= $this->minZ and $vector->z <= $this->maxZ; 413 | } 414 | 415 | /** 416 | * Returns whether the specified vector is within the X and Z bounds of this AABB. 417 | */ 418 | public function isVectorInXZ(Vector3 $vector) : bool{ 419 | return $vector->x >= $this->minX and $vector->x <= $this->maxX and $vector->z >= $this->minZ and $vector->z <= $this->maxZ; 420 | } 421 | 422 | /** 423 | * Returns whether the specified vector is within the X and Y bounds of this AABB. 424 | */ 425 | public function isVectorInXY(Vector3 $vector) : bool{ 426 | return $vector->x >= $this->minX and $vector->x <= $this->maxX and $vector->y >= $this->minY and $vector->y <= $this->maxY; 427 | } 428 | 429 | /** 430 | * Performs a ray-trace and calculates the point on the AABB's edge nearest the start position that the ray-trace 431 | * collided with. Returns a RayTraceResult with colliding vector closest to the start position. 432 | * Returns null if no colliding point was found. 433 | */ 434 | public function calculateIntercept(Vector3 $pos1, Vector3 $pos2) : ?RayTraceResult{ 435 | $v1 = $pos1->getIntermediateWithXValue($pos2, $this->minX); 436 | $v2 = $pos1->getIntermediateWithXValue($pos2, $this->maxX); 437 | $v3 = $pos1->getIntermediateWithYValue($pos2, $this->minY); 438 | $v4 = $pos1->getIntermediateWithYValue($pos2, $this->maxY); 439 | $v5 = $pos1->getIntermediateWithZValue($pos2, $this->minZ); 440 | $v6 = $pos1->getIntermediateWithZValue($pos2, $this->maxZ); 441 | 442 | if($v1 !== null and !$this->isVectorInYZ($v1)){ 443 | $v1 = null; 444 | } 445 | 446 | if($v2 !== null and !$this->isVectorInYZ($v2)){ 447 | $v2 = null; 448 | } 449 | 450 | if($v3 !== null and !$this->isVectorInXZ($v3)){ 451 | $v3 = null; 452 | } 453 | 454 | if($v4 !== null and !$this->isVectorInXZ($v4)){ 455 | $v4 = null; 456 | } 457 | 458 | if($v5 !== null and !$this->isVectorInXY($v5)){ 459 | $v5 = null; 460 | } 461 | 462 | if($v6 !== null and !$this->isVectorInXY($v6)){ 463 | $v6 = null; 464 | } 465 | 466 | $vector = null; 467 | $distance = PHP_INT_MAX; 468 | $face = -1; 469 | 470 | foreach([ 471 | Facing::WEST => $v1, 472 | Facing::EAST => $v2, 473 | Facing::DOWN => $v3, 474 | Facing::UP => $v4, 475 | Facing::NORTH => $v5, 476 | Facing::SOUTH => $v6 477 | ] as $f => $v){ 478 | if($v !== null and ($d = $pos1->distanceSquared($v)) < $distance){ 479 | $vector = $v; 480 | $distance = $d; 481 | $face = $f; 482 | } 483 | } 484 | 485 | if($vector === null){ 486 | return null; 487 | } 488 | 489 | return new RayTraceResult($this, $face, $vector); 490 | } 491 | 492 | public function __toString() : string{ 493 | return "AxisAlignedBB({$this->minX}, {$this->minY}, {$this->minZ}, {$this->maxX}, {$this->maxY}, {$this->maxZ})"; 494 | } 495 | 496 | /** 497 | * Returns a 1x1x1 bounding box starting at grid position 0,0,0. 498 | */ 499 | public static function one() : AxisAlignedBB{ 500 | return new AxisAlignedBB(0, 0, 0, 1, 1, 1); 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /src/Facing.php: -------------------------------------------------------------------------------- 1 | [ 0, -1, 0], 61 | self::UP => [ 0, +1, 0], 62 | self::NORTH => [ 0, 0, -1], 63 | self::SOUTH => [ 0, 0, +1], 64 | self::WEST => [-1, 0, 0], 65 | self::EAST => [+1, 0, 0] 66 | ]; 67 | 68 | private const CLOCKWISE = [ 69 | Axis::Y => [ 70 | self::NORTH => self::EAST, 71 | self::EAST => self::SOUTH, 72 | self::SOUTH => self::WEST, 73 | self::WEST => self::NORTH 74 | ], 75 | Axis::Z => [ 76 | self::UP => self::EAST, 77 | self::EAST => self::DOWN, 78 | self::DOWN => self::WEST, 79 | self::WEST => self::UP 80 | ], 81 | Axis::X => [ 82 | self::UP => self::NORTH, 83 | self::NORTH => self::DOWN, 84 | self::DOWN => self::SOUTH, 85 | self::SOUTH => self::UP 86 | ] 87 | ]; 88 | 89 | /** 90 | * Returns the axis of the given direction. 91 | */ 92 | public static function axis(int $direction) : int{ 93 | return $direction >> 1; //shift off positive/negative bit 94 | } 95 | 96 | /** 97 | * Returns whether the direction is facing the positive of its axis. 98 | */ 99 | public static function isPositive(int $direction) : bool{ 100 | return ($direction & self::FLAG_AXIS_POSITIVE) === self::FLAG_AXIS_POSITIVE; 101 | } 102 | 103 | /** 104 | * Returns the opposite Facing of the specified one. 105 | * 106 | * @param int $direction 0-5 one of the Facing::* constants 107 | */ 108 | public static function opposite(int $direction) : int{ 109 | return $direction ^ self::FLAG_AXIS_POSITIVE; 110 | } 111 | 112 | /** 113 | * Rotates the given direction around the axis. 114 | * 115 | * @throws \InvalidArgumentException if not possible to rotate $direction around $axis 116 | */ 117 | public static function rotate(int $direction, int $axis, bool $clockwise) : int{ 118 | if(!isset(self::CLOCKWISE[$axis])){ 119 | throw new \InvalidArgumentException("Invalid axis $axis"); 120 | } 121 | if(!isset(self::CLOCKWISE[$axis][$direction])){ 122 | throw new \InvalidArgumentException("Cannot rotate facing \"" . self::toString($direction) . "\" around axis \"" . Axis::toString($axis) . "\""); 123 | } 124 | 125 | $rotated = self::CLOCKWISE[$axis][$direction]; 126 | return $clockwise ? $rotated : self::opposite($rotated); 127 | } 128 | 129 | /** 130 | * @throws \InvalidArgumentException 131 | */ 132 | public static function rotateY(int $direction, bool $clockwise) : int{ 133 | return self::rotate($direction, Axis::Y, $clockwise); 134 | } 135 | 136 | /** 137 | * @throws \InvalidArgumentException 138 | */ 139 | public static function rotateZ(int $direction, bool $clockwise) : int{ 140 | return self::rotate($direction, Axis::Z, $clockwise); 141 | } 142 | 143 | /** 144 | * @throws \InvalidArgumentException 145 | */ 146 | public static function rotateX(int $direction, bool $clockwise) : int{ 147 | return self::rotate($direction, Axis::X, $clockwise); 148 | } 149 | 150 | /** 151 | * Validates the given integer as a Facing direction. 152 | * 153 | * @throws \InvalidArgumentException if the argument is not a valid Facing constant 154 | */ 155 | public static function validate(int $facing) : void{ 156 | if(!in_array($facing, self::ALL, true)){ 157 | throw new \InvalidArgumentException("Invalid direction $facing"); 158 | } 159 | } 160 | 161 | /** 162 | * Returns a human-readable string representation of the given Facing direction. 163 | */ 164 | public static function toString(int $facing) : string{ 165 | return match($facing){ 166 | self::DOWN => "down", 167 | self::UP => "up", 168 | self::NORTH => "north", 169 | self::SOUTH => "south", 170 | self::WEST => "west", 171 | self::EAST => "east", 172 | default => throw new \InvalidArgumentException("Invalid facing $facing") 173 | }; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Math.php: -------------------------------------------------------------------------------- 1 | = $i ? $i : $i - 1; 42 | } 43 | 44 | /** 45 | * @param float $n 46 | */ 47 | public static function ceilFloat($n) : int{ 48 | $i = (int) $n; 49 | return $n <= $i ? $i : $i + 1; 50 | } 51 | 52 | /** 53 | * Solves a quadratic equation with the given coefficients and returns an array of up to two solutions. 54 | * 55 | * @return float[] 56 | */ 57 | public static function solveQuadratic(float $a, float $b, float $c) : array{ 58 | if($a === 0.0){ 59 | throw new \InvalidArgumentException("Coefficient a cannot be 0!"); 60 | } 61 | $discriminant = $b * $b - 4 * $a * $c; 62 | if($discriminant > 0){ //2 real roots 63 | $sqrtDiscriminant = sqrt($discriminant); 64 | return [ 65 | (-$b + $sqrtDiscriminant) / (2 * $a), 66 | (-$b - $sqrtDiscriminant) / (2 * $a) 67 | ]; 68 | }elseif($discriminant === 0.0){ //1 real root 69 | return [ 70 | -$b / (2 * $a) 71 | ]; 72 | }else{ //No real roots 73 | return []; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Matrix.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | class Matrix implements \ArrayAccess{ 34 | /** @var float[][] */ 35 | private array $matrix = []; 36 | private int $rows; 37 | private int $columns; 38 | 39 | public function offsetExists($offset) : bool{ 40 | return isset($this->matrix[(int) $offset]); 41 | } 42 | 43 | #[\ReturnTypeWillChange] 44 | public function offsetGet($offset){ 45 | return $this->matrix[(int) $offset]; 46 | } 47 | 48 | public function offsetSet($offset, $value) : void{ 49 | $this->matrix[(int) $offset] = $value; 50 | } 51 | 52 | public function offsetUnset($offset) : void{ 53 | unset($this->matrix[(int) $offset]); 54 | } 55 | 56 | /** 57 | * @param float[][] $set 58 | */ 59 | public function __construct(int $rows, int $columns, array $set = []){ 60 | $this->rows = max(1, $rows); 61 | $this->columns = max(1, $columns); 62 | $this->set($set); 63 | } 64 | 65 | /** 66 | * @param float[][] $m 67 | */ 68 | public function set(array $m) : void{ 69 | for($r = 0; $r < $this->rows; ++$r){ 70 | $this->matrix[$r] = []; 71 | for($c = 0; $c < $this->columns; ++$c){ 72 | $this->matrix[$r][$c] = $m[$r][$c] ?? 0; 73 | } 74 | } 75 | } 76 | 77 | public function getRows() : int{ 78 | return $this->rows; 79 | } 80 | 81 | public function getColumns() : int{ 82 | return $this->columns; 83 | } 84 | 85 | public function setElement(int $row, int $column, float $value) : void{ 86 | if($row > $this->rows or $row < 0 or $column > $this->columns or $column < 0){ 87 | throw new \InvalidArgumentException("Row or column out of bounds (have $this->rows rows $this->columns columns)"); 88 | } 89 | $this->matrix[$row][$column] = $value; 90 | } 91 | 92 | public function getElement(int $row, int $column) : float{ 93 | if($row > $this->rows or $row < 0 or $column > $this->columns or $column < 0){ 94 | throw new \InvalidArgumentException("Row or column out of bounds (have $this->rows rows $this->columns columns)"); 95 | } 96 | 97 | return $this->matrix[$row][$column]; 98 | } 99 | 100 | public function isSquare() : bool{ 101 | return $this->rows === $this->columns; 102 | } 103 | 104 | public function add(Matrix $matrix) : Matrix{ 105 | if($this->rows !== $matrix->getRows() or $this->columns !== $matrix->getColumns()){ 106 | throw new \InvalidArgumentException("Matrix does not have the same number of rows and/or columns"); 107 | } 108 | $result = new Matrix($this->rows, $this->columns); 109 | for($r = 0; $r < $this->rows; ++$r){ 110 | for($c = 0; $c < $this->columns; ++$c){ 111 | $element = $matrix->getElement($r, $c); 112 | $result->setElement($r, $c, $this->matrix[$r][$c] + $element); 113 | } 114 | } 115 | 116 | return $result; 117 | } 118 | 119 | public function subtract(Matrix $matrix) : Matrix{ 120 | if($this->rows !== $matrix->getRows() or $this->columns !== $matrix->getColumns()){ 121 | throw new \InvalidArgumentException("Matrix does not have the same number of rows and/or columns"); 122 | } 123 | $result = clone $this; 124 | for($r = 0; $r < $this->rows; ++$r){ 125 | for($c = 0; $c < $this->columns; ++$c){ 126 | $element = $matrix->getElement($r, $c); 127 | $result->setElement($r, $c, $this->matrix[$r][$c] - $element); 128 | } 129 | } 130 | 131 | return $result; 132 | } 133 | 134 | public function multiplyScalar(float $number) : Matrix{ 135 | $result = clone $this; 136 | for($r = 0; $r < $this->rows; ++$r){ 137 | for($c = 0; $c < $this->columns; ++$c){ 138 | $result->setElement($r, $c, $this->matrix[$r][$c] * $number); 139 | } 140 | } 141 | 142 | return $result; 143 | } 144 | 145 | public function divideScalar(float $number) : Matrix{ 146 | $result = clone $this; 147 | for($r = 0; $r < $this->rows; ++$r){ 148 | for($c = 0; $c < $this->columns; ++$c){ 149 | $result->setElement($r, $c, $this->matrix[$r][$c] / $number); 150 | } 151 | } 152 | 153 | return $result; 154 | } 155 | 156 | public function transpose() : Matrix{ 157 | $result = new Matrix($this->columns, $this->rows); 158 | for($r = 0; $r < $this->rows; ++$r){ 159 | for($c = 0; $c < $this->columns; ++$c){ 160 | $result->setElement($c, $r, $this->matrix[$r][$c]); 161 | } 162 | } 163 | 164 | return $result; 165 | } 166 | 167 | /** 168 | * Naive Matrix product, O(n^3) 169 | */ 170 | public function product(Matrix $matrix) : Matrix{ 171 | if($this->columns !== $matrix->getRows()){ 172 | throw new \InvalidArgumentException("Expected a matrix with $this->columns rows"); //???? 173 | } 174 | $c = $matrix->getColumns(); 175 | $result = new Matrix($this->rows, $c); 176 | for($i = 0; $i < $this->rows; ++$i){ 177 | for($j = 0; $j < $c; ++$j){ 178 | $sum = 0; 179 | for($k = 0; $k < $this->columns; ++$k){ 180 | $sum += $this->matrix[$i][$k] * $matrix->getElement($k, $j); 181 | } 182 | $result->setElement($i, $j, $sum); 183 | } 184 | } 185 | 186 | return $result; 187 | } 188 | 189 | /** 190 | * Computation of the determinant of 1x1, 2x2 and 3x3 matrices 191 | */ 192 | public function determinant() : float{ 193 | if($this->isSquare() !== true){ 194 | throw new \LogicException("Cannot calculate determinant of a non-square matrix"); 195 | } 196 | return match($this->rows){ 197 | 1 => $this->matrix[0][0], 198 | 2 => 199 | $this->matrix[0][0] * $this->matrix[1][1] - 200 | $this->matrix[0][1] * $this->matrix[1][0], 201 | 3 => 202 | $this->matrix[0][0] * $this->matrix[1][1] * $this->matrix[2][2] + 203 | $this->matrix[0][1] * $this->matrix[1][2] * $this->matrix[2][0] + 204 | $this->matrix[0][2] * $this->matrix[1][0] * $this->matrix[2][1] - 205 | $this->matrix[2][0] * $this->matrix[1][1] * $this->matrix[0][2] - 206 | $this->matrix[2][1] * $this->matrix[1][2] * $this->matrix[0][0] - 207 | $this->matrix[2][2] * $this->matrix[1][0] * $this->matrix[0][1], 208 | default => throw new \LogicException("Not implemented") 209 | }; 210 | } 211 | 212 | public function __toString() : string{ 213 | $s = ""; 214 | for($r = 0; $r < $this->rows; ++$r){ 215 | $s .= implode(",", $this->matrix[$r]) . ";"; 216 | } 217 | 218 | return "Matrix({$this->rows}x{$this->columns};" . substr($s, 0, -1) . ")"; 219 | } 220 | 221 | } 222 | -------------------------------------------------------------------------------- /src/RayTraceResult.php: -------------------------------------------------------------------------------- 1 | bb; 42 | } 43 | 44 | public function getHitFace() : int{ 45 | return $this->hitFace; 46 | } 47 | 48 | public function getHitVector() : Vector3{ 49 | return $this->hitVector; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Vector2.php: -------------------------------------------------------------------------------- 1 | x; 40 | } 41 | 42 | public function getY() : float{ 43 | return $this->y; 44 | } 45 | 46 | public function getFloorX() : int{ 47 | return (int) floor($this->x); 48 | } 49 | 50 | public function getFloorY() : int{ 51 | return (int) floor($this->y); 52 | } 53 | 54 | public function add(float $x, float $y) : Vector2{ 55 | return new Vector2($this->x + $x, $this->y + $y); 56 | } 57 | 58 | public function addVector(Vector2 $vector2) : Vector2{ 59 | return $this->add($vector2->x, $vector2->y); 60 | } 61 | 62 | public function subtract(float $x, float $y) : Vector2{ 63 | return $this->add(-$x, -$y); 64 | } 65 | 66 | public function subtractVector(Vector2 $vector2) : Vector2{ 67 | return $this->add(-$vector2->x, -$vector2->y); 68 | } 69 | 70 | public function ceil() : Vector2{ 71 | return new Vector2((int) ceil($this->x), (int) ceil($this->y)); 72 | } 73 | 74 | public function floor() : Vector2{ 75 | return new Vector2((int) floor($this->x), (int) floor($this->y)); 76 | } 77 | 78 | public function round() : Vector2{ 79 | return new Vector2(round($this->x), round($this->y)); 80 | } 81 | 82 | public function abs() : Vector2{ 83 | return new Vector2(abs($this->x), abs($this->y)); 84 | } 85 | 86 | public function multiply(float $number) : Vector2{ 87 | return new Vector2($this->x * $number, $this->y * $number); 88 | } 89 | 90 | public function divide(float $number) : Vector2{ 91 | return new Vector2($this->x / $number, $this->y / $number); 92 | } 93 | 94 | public function distance(Vector2 $pos) : float{ 95 | return sqrt($this->distanceSquared($pos)); 96 | } 97 | 98 | public function distanceSquared(Vector2 $pos) : float{ 99 | $dx = $this->x - $pos->x; 100 | $dy = $this->y - $pos->y; 101 | return ($dx * $dx) + ($dy * $dy); 102 | } 103 | 104 | public function length() : float{ 105 | return sqrt($this->lengthSquared()); 106 | } 107 | 108 | public function lengthSquared() : float{ 109 | return $this->x * $this->x + $this->y * $this->y; 110 | } 111 | 112 | public function normalize() : Vector2{ 113 | $len = $this->lengthSquared(); 114 | if($len > 0){ 115 | return $this->divide(sqrt($len)); 116 | } 117 | 118 | return new Vector2(0, 0); 119 | } 120 | 121 | public function dot(Vector2 $v) : float{ 122 | return $this->x * $v->x + $this->y * $v->y; 123 | } 124 | 125 | public function __toString(){ 126 | return "Vector2(x=" . $this->x . ",y=" . $this->y . ")"; 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/Vector3.php: -------------------------------------------------------------------------------- 1 | x; 51 | } 52 | 53 | public function getY() : float|int{ 54 | return $this->y; 55 | } 56 | 57 | public function getZ() : float|int{ 58 | return $this->z; 59 | } 60 | 61 | public function getFloorX() : int{ 62 | return (int) floor($this->x); 63 | } 64 | 65 | public function getFloorY() : int{ 66 | return (int) floor($this->y); 67 | } 68 | 69 | public function getFloorZ() : int{ 70 | return (int) floor($this->z); 71 | } 72 | 73 | public function add(float|int $x, float|int $y, float|int $z) : Vector3{ 74 | return new Vector3($this->x + $x, $this->y + $y, $this->z + $z); 75 | } 76 | 77 | public function addVector(Vector3 $v) : Vector3{ 78 | return $this->add($v->x, $v->y, $v->z); 79 | } 80 | 81 | public function subtract(float|int $x, float|int $y, float|int $z) : Vector3{ 82 | return $this->add(-$x, -$y, -$z); 83 | } 84 | 85 | public function subtractVector(Vector3 $v) : Vector3{ 86 | return $this->add(-$v->x, -$v->y, -$v->z); 87 | } 88 | 89 | public function multiply(float $number) : Vector3{ 90 | return new Vector3($this->x * $number, $this->y * $number, $this->z * $number); 91 | } 92 | 93 | public function divide(float $number) : Vector3{ 94 | return new Vector3($this->x / $number, $this->y / $number, $this->z / $number); 95 | } 96 | 97 | public function ceil() : Vector3{ 98 | return new Vector3((int) ceil($this->x), (int) ceil($this->y), (int) ceil($this->z)); 99 | } 100 | 101 | public function floor() : Vector3{ 102 | return new Vector3((int) floor($this->x), (int) floor($this->y), (int) floor($this->z)); 103 | } 104 | 105 | /** 106 | * @phpstan-param 1|2|3|4 $mode 107 | */ 108 | public function round(int $precision = 0, int $mode = PHP_ROUND_HALF_UP) : Vector3{ 109 | return $precision > 0 ? 110 | new Vector3(round($this->x, $precision, $mode), round($this->y, $precision, $mode), round($this->z, $precision, $mode)) : 111 | new Vector3((int) round($this->x, $precision, $mode), (int) round($this->y, $precision, $mode), (int) round($this->z, $precision, $mode)); 112 | } 113 | 114 | public function abs() : Vector3{ 115 | return new Vector3(abs($this->x), abs($this->y), abs($this->z)); 116 | } 117 | 118 | /** 119 | * @return Vector3 120 | */ 121 | public function getSide(int $side, int $step = 1){ 122 | [$offsetX, $offsetY, $offsetZ] = Facing::OFFSET[$side] ?? [0, 0, 0]; 123 | 124 | return $this->add($offsetX * $step, $offsetY * $step, $offsetZ * $step); 125 | } 126 | 127 | /** 128 | * @return Vector3 129 | */ 130 | public function down(int $step = 1){ 131 | return $this->getSide(Facing::DOWN, $step); 132 | } 133 | 134 | /** 135 | * @return Vector3 136 | */ 137 | public function up(int $step = 1){ 138 | return $this->getSide(Facing::UP, $step); 139 | } 140 | 141 | /** 142 | * @return Vector3 143 | */ 144 | public function north(int $step = 1){ 145 | return $this->getSide(Facing::NORTH, $step); 146 | } 147 | 148 | /** 149 | * @return Vector3 150 | */ 151 | public function south(int $step = 1){ 152 | return $this->getSide(Facing::SOUTH, $step); 153 | } 154 | 155 | /** 156 | * @return Vector3 157 | */ 158 | public function west(int $step = 1){ 159 | return $this->getSide(Facing::WEST, $step); 160 | } 161 | 162 | /** 163 | * @return Vector3 164 | */ 165 | public function east(int $step = 1){ 166 | return $this->getSide(Facing::EAST, $step); 167 | } 168 | 169 | /** 170 | * Yields vectors stepped out from this one in all directions. 171 | * 172 | * @param int $step Distance in each direction to shift the vector 173 | * 174 | * @return \Generator|Vector3[] 175 | * @phpstan-return \Generator 176 | */ 177 | public function sides(int $step = 1) : \Generator{ 178 | foreach(Facing::ALL as $facing){ 179 | yield $facing => $this->getSide($facing, $step); 180 | } 181 | } 182 | 183 | /** 184 | * Same as sides() but returns a pre-populated array instead of Generator. 185 | * 186 | * @return Vector3[] 187 | */ 188 | public function sidesArray(bool $keys = false, int $step = 1) : array{ 189 | return iterator_to_array($this->sides($step), $keys); 190 | } 191 | 192 | /** 193 | * Yields vectors stepped out from this one in directions except those on the given axis. 194 | * 195 | * @param int $axis Facing directions on this axis will be excluded 196 | * 197 | * @return \Generator|Vector3[] 198 | * @phpstan-return \Generator 199 | */ 200 | public function sidesAroundAxis(int $axis, int $step = 1) : \Generator{ 201 | foreach(Facing::ALL as $facing){ 202 | if(Facing::axis($facing) !== $axis){ 203 | yield $facing => $this->getSide($facing, $step); 204 | } 205 | } 206 | } 207 | 208 | /** 209 | * Return a Vector3 instance 210 | */ 211 | public function asVector3() : Vector3{ 212 | return new Vector3($this->x, $this->y, $this->z); 213 | } 214 | 215 | public function distance(Vector3 $pos) : float{ 216 | return sqrt($this->distanceSquared($pos)); 217 | } 218 | 219 | public function distanceSquared(Vector3 $pos) : float{ 220 | $dx = $this->x - $pos->x; 221 | $dy = $this->y - $pos->y; 222 | $dz = $this->z - $pos->z; 223 | return ($dx * $dx) + ($dy * $dy) + ($dz * $dz); 224 | } 225 | 226 | public function maxPlainDistance(Vector3|Vector2|float $x, float $z = 0) : float{ 227 | if($x instanceof Vector3){ 228 | return $this->maxPlainDistance($x->x, $x->z); 229 | }elseif($x instanceof Vector2){ 230 | return $this->maxPlainDistance($x->x, $x->y); 231 | }else{ 232 | return max(abs($this->x - $x), abs($this->z - $z)); 233 | } 234 | } 235 | 236 | public function length() : float{ 237 | return sqrt($this->lengthSquared()); 238 | } 239 | 240 | public function lengthSquared() : float{ 241 | return $this->x * $this->x + $this->y * $this->y + $this->z * $this->z; 242 | } 243 | 244 | public function normalize() : Vector3{ 245 | $len = $this->lengthSquared(); 246 | if($len > 0){ 247 | return $this->divide(sqrt($len)); 248 | } 249 | 250 | return new Vector3(0, 0, 0); 251 | } 252 | 253 | public function dot(Vector3 $v) : float{ 254 | return $this->x * $v->x + $this->y * $v->y + $this->z * $v->z; 255 | } 256 | 257 | public function cross(Vector3 $v) : Vector3{ 258 | return new Vector3( 259 | $this->y * $v->z - $this->z * $v->y, 260 | $this->z * $v->x - $this->x * $v->z, 261 | $this->x * $v->y - $this->y * $v->x 262 | ); 263 | } 264 | 265 | public function equals(Vector3 $v) : bool{ 266 | return 267 | floatval($this->x) === floatval($v->x) and 268 | floatval($this->y) === floatval($v->y) and 269 | floatval($this->z) === floatval($v->z); 270 | } 271 | 272 | /** 273 | * Returns a new vector with x value equal to the second parameter, along the line between this vector and the 274 | * passed in vector, or null if not possible. 275 | */ 276 | public function getIntermediateWithXValue(Vector3 $v, float $x) : ?Vector3{ 277 | $xDiff = $v->x - $this->x; 278 | if(($xDiff * $xDiff) < 0.0000001){ 279 | return null; 280 | } 281 | 282 | $f = ($x - $this->x) / $xDiff; 283 | 284 | if($f < 0 or $f > 1){ 285 | return null; 286 | }else{ 287 | return new Vector3($x, $this->y + ($v->y - $this->y) * $f, $this->z + ($v->z - $this->z) * $f); 288 | } 289 | } 290 | 291 | /** 292 | * Returns a new vector with y value equal to the second parameter, along the line between this vector and the 293 | * passed in vector, or null if not possible. 294 | */ 295 | public function getIntermediateWithYValue(Vector3 $v, float $y) : ?Vector3{ 296 | $yDiff = $v->y - $this->y; 297 | if(($yDiff * $yDiff) < 0.0000001){ 298 | return null; 299 | } 300 | 301 | $f = ($y - $this->y) / $yDiff; 302 | 303 | if($f < 0 or $f > 1){ 304 | return null; 305 | }else{ 306 | return new Vector3($this->x + ($v->x - $this->x) * $f, $y, $this->z + ($v->z - $this->z) * $f); 307 | } 308 | } 309 | 310 | /** 311 | * Returns a new vector with z value equal to the second parameter, along the line between this vector and the 312 | * passed in vector, or null if not possible. 313 | */ 314 | public function getIntermediateWithZValue(Vector3 $v, float $z) : ?Vector3{ 315 | $zDiff = $v->z - $this->z; 316 | if(($zDiff * $zDiff) < 0.0000001){ 317 | return null; 318 | } 319 | 320 | $f = ($z - $this->z) / $zDiff; 321 | 322 | if($f < 0 or $f > 1){ 323 | return null; 324 | }else{ 325 | return new Vector3($this->x + ($v->x - $this->x) * $f, $this->y + ($v->y - $this->y) * $f, $z); 326 | } 327 | } 328 | 329 | public function __toString(){ 330 | return "Vector3(x=" . $this->x . ",y=" . $this->y . ",z=" . $this->z . ")"; 331 | } 332 | 333 | /** 334 | * Returns a Vector3 with the provided components. If any of the components are null, the values from this 335 | * Vector3 will be filled in instead. 336 | * If no components are overridden (all components are null), the original vector will be returned unchanged. 337 | */ 338 | public function withComponents(float|int|null $x, float|int|null $y, float|int|null $z) : Vector3{ 339 | if($x !== null || $y !== null || $z !== null){ 340 | return new self($x ?? $this->x, $y ?? $this->y, $z ?? $this->z); 341 | } 342 | return $this; 343 | } 344 | 345 | /** 346 | * Returns a new Vector3 taking the maximum of each component in the input vectors. 347 | * 348 | * @param Vector3 ...$vectors 349 | */ 350 | public static function maxComponents(Vector3 $vector, Vector3 ...$vectors) : Vector3{ 351 | $x = $vector->x; 352 | $y = $vector->y; 353 | $z = $vector->z; 354 | foreach($vectors as $position){ 355 | $x = max($x, $position->x); 356 | $y = max($y, $position->y); 357 | $z = max($z, $position->z); 358 | } 359 | return new Vector3($x, $y, $z); 360 | } 361 | 362 | /** 363 | * Returns a new Vector3 taking the minimum of each component in the input vectors. 364 | * 365 | * @param Vector3 ...$vectors 366 | */ 367 | public static function minComponents(Vector3 $vector, Vector3 ...$vectors) : Vector3{ 368 | $x = $vector->x; 369 | $y = $vector->y; 370 | $z = $vector->z; 371 | foreach($vectors as $position){ 372 | $x = min($x, $position->x); 373 | $y = min($y, $position->y); 374 | $z = min($z, $position->z); 375 | } 376 | return new Vector3($x, $y, $z); 377 | } 378 | 379 | public static function sum(Vector3 ...$vector3s) : Vector3{ 380 | $x = $y = $z = 0; 381 | foreach($vector3s as $vector3){ 382 | $x += $vector3->x; 383 | $y += $vector3->y; 384 | $z += $vector3->z; 385 | } 386 | return new Vector3($x, $y, $z); 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /src/VectorMath.php: -------------------------------------------------------------------------------- 1 | 43 | */ 44 | public static function inDirection(Vector3 $start, Vector3 $directionVector, float $maxDistance) : \Generator{ 45 | return self::betweenPoints($start, $start->addVector($directionVector->multiply($maxDistance))); 46 | } 47 | 48 | /** 49 | * Performs a ray trace between the start and end coordinates. This returns a Generator which yields Vector3s 50 | * containing the coordinates of voxels it passes through. 51 | * 52 | * The first Vector3 is `$start->floor()`. 53 | * Every subsequent Vector3 has a taxicab distance of exactly 1 from the previous Vector3; 54 | * if the ray crosses the intersection of multiple axis boundaries directly, 55 | * the algorithm prefers crossing the boundaries in the order `Z -> Y -> X`. 56 | * 57 | * If `$end` is on an axis boundary, the final Vector3 may or may not cross that boundary. 58 | * Otherwise, the final Vector3 is equal to `$end->floor()`. 59 | * 60 | * This is an implementation of the algorithm described in the link below. 61 | * @link http://www.cse.yorku.ca/~amana/research/grid.pdf 62 | * 63 | * @return \Generator|Vector3[] 64 | * @phpstan-return \Generator 65 | * 66 | * @throws \InvalidArgumentException if $start and $end have zero distance. 67 | */ 68 | public static function betweenPoints(Vector3 $start, Vector3 $end) : \Generator{ 69 | $currentBlock = $start->floor(); 70 | 71 | $directionVector = $end->subtractVector($start)->normalize(); 72 | if($directionVector->lengthSquared() <= 0){ 73 | throw new \InvalidArgumentException("Start and end points are the same, giving a zero direction vector"); 74 | } 75 | 76 | $radius = $start->distance($end); 77 | 78 | $stepX = $directionVector->x <=> 0; 79 | $stepY = $directionVector->y <=> 0; 80 | $stepZ = $directionVector->z <=> 0; 81 | 82 | //Initialize the step accumulation variables depending how far into the current block the start position is. If 83 | //the start position is on the corner of the block, these will be zero. 84 | $tMaxX = self::distanceFactorToBoundary($start->x, $directionVector->x); 85 | $tMaxY = self::distanceFactorToBoundary($start->y, $directionVector->y); 86 | $tMaxZ = self::distanceFactorToBoundary($start->z, $directionVector->z); 87 | 88 | //The change in t on each axis when taking a step on that axis (always positive). 89 | $tDeltaX = floatval($directionVector->x) === 0.0 ? 0 : $stepX / $directionVector->x; 90 | $tDeltaY = floatval($directionVector->y) === 0.0 ? 0 : $stepY / $directionVector->y; 91 | $tDeltaZ = floatval($directionVector->z) === 0.0 ? 0 : $stepZ / $directionVector->z; 92 | 93 | while(true){ 94 | yield $currentBlock; 95 | 96 | // tMaxX stores the t-value at which we cross a cube boundary along the 97 | // X axis, and similarly for Y and Z. Therefore, choosing the least tMax 98 | // chooses the closest cube boundary. 99 | if($tMaxX < $tMaxY and $tMaxX < $tMaxZ){ 100 | if($tMaxX > $radius){ 101 | break; 102 | } 103 | $currentBlock = $currentBlock->add($stepX, 0, 0); 104 | $tMaxX += $tDeltaX; 105 | }elseif($tMaxY < $tMaxZ){ 106 | if($tMaxY > $radius){ 107 | break; 108 | } 109 | $currentBlock = $currentBlock->add(0, $stepY, 0); 110 | $tMaxY += $tDeltaY; 111 | }else{ 112 | if($tMaxZ > $radius){ 113 | break; 114 | } 115 | $currentBlock = $currentBlock->add(0, 0, $stepZ); 116 | $tMaxZ += $tDeltaZ; 117 | } 118 | } 119 | } 120 | 121 | /** 122 | * Used to decide which direction to move in first when beginning a ray trace. 123 | * 124 | * Examples: 125 | * s=0.25, ds=0.5 -> 0.25 + 1.5(0.5) = 1 -> returns 1.5 126 | * s=0.25, ds=-0.5 -> 0.25 + 0.5(-0.5) = 0 -> returns 0.5 127 | * s=1 ds=0.5 -> 1 + 2(0.5) = 2 -> returns 2 128 | * s=1 ds=-0.5 -> 1 + 0(-0.5) = 1 -> returns 0 (ds is negative and any subtraction will change 1 to 0.x) 129 | * 130 | * @param float $s Starting coordinate 131 | * @param float $ds Direction vector component of the relevant axis 132 | * 133 | * @return float Number of times $ds must be added to $s to change its whole-number component. 134 | */ 135 | private static function distanceFactorToBoundary(float $s, float $ds) : float{ 136 | if($ds === 0.0){ 137 | return INF; 138 | } 139 | 140 | return $ds < 0 ? 141 | ($s - floor($s)) / -$ds : 142 | (1 - ($s - floor($s))) / $ds; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/phpunit/FacingTest.php: -------------------------------------------------------------------------------- 1 | >, void, void> 96 | */ 97 | public static function sumProvider() : \Generator{ 98 | yield [[ 99 | new Vector3(1, 1, 1), 100 | new Vector3(-1, -1, -1) 101 | ]]; 102 | } 103 | 104 | /** 105 | * @param Vector3[] $vectors 106 | */ 107 | #[DataProvider("sumProvider")] 108 | public function testSum(array $vectors) : void{ 109 | $vec = new Vector3(0, 0, 0); 110 | foreach($vectors as $vector){ 111 | $vec = $vec->addVector($vector); 112 | } 113 | $vec2 = Vector3::sum(...$vectors); 114 | self::assertLessThan(0.000001, abs($vec->x - $vec2->x)); 115 | self::assertLessThan(0.000001, abs($vec->y - $vec2->y)); 116 | self::assertLessThan(0.000001, abs($vec->z - $vec2->z)); 117 | } 118 | 119 | /** 120 | * @phpstan-return \Generator 121 | */ 122 | public static function withComponentsProvider() : \Generator{ 123 | yield [new Vector3(0, 0, 0), 1, 1, 1, new Vector3(1, 1, 1)]; 124 | yield [new Vector3(0, 0, 0), null, 1, 1, new Vector3(0, 1, 1)]; 125 | yield [new Vector3(0, 0, 0), 1, null, 1, new Vector3(1, 0, 1)]; 126 | yield [new Vector3(0, 0, 0), 1, 1, null, new Vector3(1, 1, 0)]; 127 | yield [new Vector3(0, 0, 0), null, null, null, new Vector3(0, 0, 0)]; 128 | } 129 | 130 | #[DataProvider("withComponentsProvider")] 131 | public function testWithComponents(Vector3 $original, float|int|null $x, float|int|null $y, float|int|null $z, Vector3 $expected) : void{ 132 | $actual = $original->withComponents($x, $y, $z); 133 | self::assertTrue($actual->equals($expected)); 134 | } 135 | } 136 | --------------------------------------------------------------------------------