├── .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 | 
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 |
--------------------------------------------------------------------------------