├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build-figures.sh
├── canvaskit.wasm
├── cli-figure-to-apng.ts
├── collision-2d.js
├── docs
├── aabb-aabb-overlap.png
├── aabb-aabb-sweep1.png
├── aabb-aabb-sweep2.png
├── aabb-point-overlap.png
├── aabb-segment-overlap.png
├── aabb-segment-sweep1.png
├── aabb-segments-sweep1-indexed.png
├── cone-point-overlap.png
├── ray-plane-distance.png
├── ray-sphere-overlap.png
├── segment-point-overlap.png
├── segment-segment-overlap.png
├── segment-sphere-overlap.png
├── segments-segment-overlap.png
├── segments-sphere-sweep1.png
├── sphere-sphere-overlap.png
└── triangle-point-overlap.png
├── figures.html
├── figures
├── aabb-aabb-overlap.js
├── aabb-aabb-sweep1.js
├── aabb-aabb-sweep2.js
├── aabb-point-overlap.js
├── aabb-segment-overlap.js
├── aabb-segment-sweep1.js
├── aabb-segments-sweep1-indexed.js
├── common.js
├── cone-point-overlap.js
├── ray-plane-distance.js
├── ray-sphere-overlap.js
├── segment-point-overlap.js
├── segment-segment-overlap.js
├── segment-sphere-overlap.js
├── segments-segment-overlap.js
├── segments-sphere-sweep1.js
├── sphere-sphere-overlap.js
└── triangle-point-overlap.js
├── package-lock.json
├── package.json
├── src
├── AABB.js
├── Plane.js
├── Polygon.js
├── PolylinePath.js
├── TraceInfo.js
├── aabb-aabb-contain.js
├── aabb-aabb-overlap.js
├── aabb-aabb-sweep1.js
├── aabb-aabb-sweep2.js
├── aabb-point-overlap.js
├── aabb-segment-overlap.js
├── aabb-segment-sweep1.js
├── aabb-segments-sweep1-indexed.js
├── cone-point-overlap.js
├── contact-copy.js
├── contact.js
├── cpa-time.js
├── get-lowest-root.js
├── point-polygon-overlap.js
├── ray-sphere-overlap.js
├── segment-normal.js
├── segment-point-overlap.js
├── segment-segment-overlap.js
├── segment-sphere-overlap.js
├── segments-ellipsoid-sweep1-indexed.js
├── segments-segment-overlap-indexed.js
├── segments-segment-overlap.js
├── segments-sphere-sweep1-indexed.js
├── segments-sphere-sweep1.js
├── segseg-closest.js
├── sphere-point-overlap.js
├── sphere-sphere-collision-response.js
├── sphere-sphere-overlap.js
├── sphere-sphere-sweep2.js
├── toji-tris.js
├── triangle-area.js
├── triangle-get-center.js
└── triangle-point-overlap.js
└── test
├── _assert.js
├── aabb-point-overlap.js
├── aabb-segment-overlap.js
├── segment-segment-overlap.js
├── segments-ellipsoid-sweep1-indexed.js
└── trace-sphere-triangle.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | deno.lock
4 |
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 0.3.0
2 | * add Polygon-point checks
3 |
4 |
5 | # 0.2.0
6 | * add PolylinePath
7 |
8 |
9 | # 0.1.0
10 | * published to npm
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Michael Reinstein
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # collision-2d
2 |
3 | There are many javascript collision routines and libraries for 2d. None satisifed all of these criteria:
4 |
5 | * consistent vector/matrix/line representation
6 | * doesn't generate memory garbage
7 | * is data-oriented and functional
8 | * consistent API interface
9 | * collisions only - no gravity, rigid body handling, or complex solvers
10 | * pure es modules
11 |
12 | so here we are!
13 |
14 |
15 | Note: If you're looking for higher-level 2d collision handling routine for ellipsoids vs line segments, check out https://github.com/mreinstein/collide-and-slide-2d
16 |
17 |
18 | ## available collision checks
19 |
20 |
21 | ### aabb-aabb overlap
22 |
23 | 
24 |
25 | ```javascript
26 | import { aabbOverlap } from '@footgun/collision-2d'
27 |
28 | const collided = aabbOverlap(aabb, aabb2, contact)
29 | ```
30 |
31 | ### aabb-aabb contain
32 |
33 | ```javascript
34 | import { aabbContain } from '@footgun/collision-2d'
35 |
36 | // true when aabb1 fully contains aabb2 (2 is fully inside the bounds of 1)
37 | const contains = aabbContain(aabb1, aabb2)
38 | ```
39 |
40 |
41 | ### aabb-aabb sweep 1
42 |
43 | 
44 |
45 | ```javascript
46 | import { aabbSweep1 } from '@footgun/collision-2d'
47 |
48 | const collided = aabbSweep1(aabb, aabb2, delta, contact)
49 | ```
50 |
51 |
52 | ### aabb-aabb sweep 2
53 |
54 | 
55 |
56 | ```javascript
57 | import { aabbSweep2 } from '@footgun/collision-2d'
58 |
59 | const collided = aabbSweep2(aabb, delta, aabb2, delta2, contact)
60 | ```
61 |
62 |
63 | ### aabb-segment sweep
64 |
65 | 
66 |
67 | ```javascript
68 | import { aabbSegSweep1 } from '@footgun/collision-2d'
69 |
70 | const collided = aabbSegSweep1(line, aabb, delta, contact)
71 | ```
72 |
73 |
74 | ### aabb-segments sweep-indexed
75 |
76 | 
77 |
78 | ```javascript
79 | import { aabbSegsSweep1Indexed } from '@footgun/collision-2d'
80 |
81 | const collided = aabbSegsSweep1Indexed(segments, indices, segmentCount, aabb, delta, contact)
82 | ```
83 |
84 | if there is a collision, `contact.collider` will be an integer indicating the index of which segment in the `segments` array collided.
85 |
86 |
87 | ### aabb-point overlap
88 |
89 | 
90 |
91 | ```javascript
92 | import { aabbPointOverlap } from '@footgun/collision-2d'
93 |
94 | const collided = aabbPointOverlap(aabb, point, contact)
95 | ```
96 |
97 |
98 | ### aabb-segment overlap
99 |
100 | 
101 |
102 | ```javascript
103 | import { aabbSegOverlap } from '@footgun/collision-2d'
104 |
105 | const collided = aabbSegOverlap(aabb, pos, delta, paddingX, paddingY, contact)
106 | ```
107 |
108 |
109 |
110 | ### ray-plane-distance
111 |
112 | 
113 |
114 | ```javascript
115 | import { Plane } from '@footgun/collision-2d'
116 |
117 | const p = Plane.create()
118 | Plane.fromPlane(p, planeOrigin, planeNormal)
119 | const distance = Plane.rayDistance(p, rayOrigin, rayVector)
120 |
121 | ```
122 |
123 |
124 | ### ray-sphere overlap
125 |
126 | 
127 |
128 | ```javascript
129 | import { raySphereOverlap } from '@footgun/collision-2d'
130 |
131 |
132 | // declare 2 points that lie on an infinite ray
133 | const p1 = [ 100, 100 ]
134 | const p2 = [ 200, 100 ]
135 |
136 | const sphereCenter: [ 250, 100 ]
137 | const sphereRadius: 50
138 | const contact = { mu1: NaN, mu2: NaN }
139 | const overlaps = raySphereOverlap(p1, p2, sphereCenter, sphereRadius, contact)
140 |
141 | // mu1 and mu2 are the points along the line segment from p1 to p2 where the sphere intersection occurs:
142 | // intersection1 = p1 + contact.mu1 * (p2 - p1)
143 | // intersection2 = p1 + contact.mu2 * (p2 - p1)
144 | if (overlaps) {
145 | console.log('sphere intersection time 1:', contact.mu1)
146 | console.log('sphere intersection time 2', contact.mu2)
147 | }
148 | ```
149 |
150 |
151 | ### segment-sphere overlap
152 |
153 | 
154 |
155 | ```javascript
156 | import { segSphereOverlap } from '@footgun/collision-2d'
157 |
158 |
159 | // declare 2 points that lie on a line segment
160 | const p1 = [ 100, 100 ]
161 | const p2 = [ 200, 100 ]
162 |
163 | const sphereCenter: [ 250, 100 ]
164 | const sphereRadius: 50
165 | const contact = { intersectionCount: 0, mu1: NaN, mu2: NaN }
166 | const overlaps = segSphereOverlap(p1, p2, sphereCenter, sphereRadius, contact)
167 |
168 | // mu1 and mu2 are the points along the line segment from p1 to p2 where the sphere intersection occurs:
169 | // intersection1 = p1 + contact.mu1 * (p2 - p1)
170 | // intersection2 = p1 + contact.mu2 * (p2 - p1)
171 | if (overlaps) {
172 | // the segment interesects the sphere, intersectionCount is 1 or 2
173 | // either mu1 or mu2 will be NaN if there's not 2 intersections
174 | console.log('intersection count:', contact.intersectionCount)
175 | console.log('sphere intersection time 1:', contact.mu1)
176 | console.log('sphere intersection time 2', contact.mu2)
177 | } else {
178 | // no overlap, contact.intersectionCount is 0
179 | }
180 | ```
181 |
182 |
183 | ### segment-normal
184 |
185 | ```javascript
186 | import { segNormal } from '@footgun/collision-2d'
187 |
188 | const normal = segNormal(vec2.create(), pos1, pos2)
189 | ```
190 |
191 |
192 | ### segment-point-overlap
193 |
194 | 
195 |
196 | ```javascript
197 | import { segPointOverlap } from '@footgun/collision-2d'
198 |
199 | const collided = segPointOverlap(p, segPoint0, segPoint1) // true or false
200 | ```
201 |
202 |
203 | ### segment-segment-overlap
204 |
205 | 
206 |
207 | ```javascript
208 | import { segOverlap } from '@footgun/collision-2d'
209 |
210 | const intersectionPoint = vec2.create()
211 | if (segOverlap(seg1Point1, seg1Point2, seg2Point1, seg2Point2, intersectionPoint)) {
212 | // if we get here, intersectionPoint is filled in with where the 2 segments overlap
213 | }
214 | ```
215 |
216 |
217 | ### segments-segment-overlap
218 |
219 | 
220 |
221 | ```javascript
222 | import { segsSegOverlap } from '@footgun/collision-2d'
223 |
224 | const collided = segsSegOverlap(segments, start, delta, contact)
225 | ```
226 |
227 | if there is a collision, `contact.collider` will be an integer indicating the index of which segment in the `segments` array collided.
228 |
229 |
230 | ### segments-segment-overlap-indexed
231 |
232 | ```javascript
233 | import { segsSegOverlapIndexed } from '@footgun/collision-2d'
234 |
235 | const segs = [
236 | [ p0, p1 ],
237 | [ p2, p3 ],
238 | [ p4, p5 ]
239 | ]
240 | const indices = [ 0, 2 ] // indices into the segs array
241 |
242 | const segmentCount = 2 // numer of indices to include. only run the segmentsSegment intersection tests on [ p0, p1 ] and [ p4, p5]
243 |
244 | const collided = segsSegOverlapIndexed(segments, indices, segmentCount, start, delta, contact)
245 | ```
246 |
247 | if there is a collision, `contact.collider` will be an integer indicating the index of which segment in the `segments` array collided.
248 |
249 |
250 |
251 | ### segments-sphere-sweep 1
252 |
253 | 
254 |
255 |
256 | ```javascript
257 | import { segsSphereSweep1 } from '@footgun/collision-2d'
258 |
259 | const collided = segsSphereSweep1(segments, position, radius, delta, contact)
260 | ```
261 |
262 | if there is a collision, `contact.collider` will be an integer indicating the index of which segment in the `segments` array collided.
263 |
264 |
265 | ### segments-sphere-sweep-1-indexed
266 |
267 | ```javascript
268 | import { segsSphereSweep1Indexed } from '@footgun/collision-2d'
269 |
270 | const segs = [
271 | [ p0, p1 ],
272 | [ p2, p3 ],
273 | [ p4, p5 ]
274 | ]
275 | const indices = [ 0, 2 ] // indices into the segs array
276 |
277 | const segmentCount = 2 // only run the segmentsSphereSweep tests on [ p0, p1 ] and [ p4, p5 ]
278 |
279 | const collided = segsSphereSweep1Indexed(segments, indices, segmentCount, position, radius, delta, contact)
280 | ```
281 |
282 | if there is a collision, `contact.collider` will be an integer indicating the index of which segment in the `segments` array collided.
283 |
284 |
285 | ### sphere-sphere-overlap
286 |
287 | 
288 |
289 | ```javascript
290 | import { sphereOverlap } from '@footgun/collision-2d'
291 |
292 | const collided = sphereOverlap(centerA, radiusA, centerB, radiusB, contact) // collided is true or false
293 | ```
294 |
295 | if there is a collision, `contact.delta` is a vector that can be added to sphere A’s position to move them into a non-colliding state.
296 | `contact.position` is the point of contact of these 2 spheres
297 |
298 | Note: `contact` is an optional parameter. if you only want to determine if the 2 spheres overlap, omit `contact` which will be faster.
299 |
300 |
301 | ### sphere-sphere-sweep2
302 |
303 | ```javascript
304 | import { sphereSweep2 } from '@footgun/collision-2d'
305 |
306 | const collided = sphereSweep2(radiusA, A0, A1, radiusB, B0, B1, contact)
307 | ```
308 |
309 | * `A0` is the previous position of sphere A
310 | * `A1` is the new position of sphere A
311 | * `B0` is the previous position of sphere B
312 | * `B1` is the new position of sphere B
313 |
314 | If there is a collision `contact.position` will contain the point where the collision occurred. `contact.time` has the normalized time
315 | where the collision happened.
316 |
317 |
318 | ### cone-point-overlap
319 |
320 | 
321 |
322 | ```javascript
323 | import { conePointOverlap } from '@footgun/collision-2d'
324 |
325 | const collided = conePointOverlap(conePosition, coneRotation, coneFieldOfView, coneMinDistance, coneMaxDistance, point) // collided is true or false
326 | ```
327 |
328 |
329 | ### triangle-point-overlap
330 |
331 | 
332 |
333 | ```javascript
334 | import { trianglePointOverlap } from '@footgun/collision-2d'
335 |
336 | const collided = trianglePointOverlap(v0, v1, v2, point) // collided is true or false
337 | ```
338 |
339 |
340 | ## entities
341 |
342 | The collision routines all use these entity definitions
343 |
344 |
345 | ### point
346 |
347 | a point is a 2d vector, which is represented as an array with 2 values:
348 | ```javascript
349 |
350 | const position = [ 200, 150 ] // x: 200, y: 150
351 |
352 | ```
353 |
354 | We use the fantastic `gl-matrix` `vec2` for representing these.
355 |
356 |
357 | ### aabb
358 |
359 | an axially aligned bounding box
360 | ```javascript
361 | const aabb = {
362 | position: [ 200, 100 ], // center point of the AABB
363 | width: 50,
364 | height: 50
365 | }
366 | ```
367 |
368 | ### segment
369 |
370 | a line segment consists of 2 `point`s
371 | ```javascript
372 | const segment = [
373 | [ 0, 0 ], // starting point of line
374 | [ 100, 0 ] // ending point of line
375 | ]
376 | ```
377 |
378 |
379 | ### plane
380 |
381 | a 2d plane
382 |
383 | ```javascript
384 | {
385 | origin: vec2.create(),
386 | normal: vec2.create(),
387 | D: 0,
388 | }
389 | ```
390 |
391 |
392 | ### contact
393 |
394 | The data structure populated when a collision occurs
395 |
396 | ```javascript
397 | {
398 | // for segments-segment-overlap and segments-sphere-sweep1 this is set to the index
399 | // in the array of line segments passed into the collision routine
400 | // for all other routines, collider is a reference to the colliding object itself
401 | collider : null,
402 |
403 | position : [ 0, 0 ], // the exact position of the collision
404 | delta : [ 0, 0 ], // a vector that can be applied to get out of the colliding state
405 | normal : [ 0, 0 ], // the collision normal vector
406 | time : 0 // the time of the collision, from 0..1
407 | }
408 | ```
409 |
410 |
411 | ## conventions
412 |
413 | All collision checking functions return a boolean indicating if there was a collision. They also accept an optional `contact` argument, which gets filled in if there is an actual collision.
414 |
415 |
416 | "sweep" tests indicate at least 1 of the objects is moving. the number indicates how many objects are moving. e.g., `aabb-aabb-sweep2` means we are comparing 2 aabbs, both of which are moving.
417 |
418 | "overlap" tests don't take movement into account, and this is a static check to see if the 2 entities overlap.
419 |
420 | plural forms imply a collection. e.g., `segments-segment-ovelap` checks one line segment against a set of line segments. If there is more than one collision, the closest collision is set in the `contact` argument.
421 |
422 | "indexed" tests are the same as their non-indexed forms, except they take in an array of segment indices to use. These are nice in that you can avoid having to build large arrays of line segments every frame, if you have things like dynamic line segments (platforms) or have a spatial culling algorithm that selects line segments to include.
423 |
424 |
425 | ## credits
426 |
427 | Most of these collision checks were adapted from existing open source modules:
428 |
429 | * https://github.com/noonat/intersect
430 | * The diagrams are modified from noonat: https://noonat.github.io/intersect/
431 | * https://github.com/kevzettler/gl-swept-sphere-triangle
432 | * https://gist.github.com/toji/2802287
433 | * segment-point-overlap from https://gist.github.com/mattdesl/47412d930dcd8cd765c871a65532ffac
434 | * segment-segment overlap from https://github.com/tmpvar/segseg
435 | * http://www.gamasutra.com/view/feature/131790/simple_intersection_tests_for_games.php
436 | * http://geomalgorithms.com/a07-_distance.html#dist3D_Segment_to_Segment
437 | * https://observablehq.com/@kelleyvanevert/2d-point-in-triangle-test
438 | * aabb-segment sweep from https://gamedev.stackexchange.com/questions/29479/swept-aabb-vs-line-segment-2d
439 | * PolylinePath implementation from Craig Reynold's seminal work on autonomous steering https://opensteer.sourceforge.net/
440 |
--------------------------------------------------------------------------------
/build-figures.sh:
--------------------------------------------------------------------------------
1 | # generate animated pngs in docs/ from all of the examples in figures/
2 |
3 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/aabb-segments-sweep1-indexed.js --count 245 --output docs/aabb-segments-sweep1-indexed.png
4 |
5 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/aabb-segment-sweep1.js --count 245 --output docs/aabb-segment-sweep1.png
6 |
7 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/aabb-aabb-overlap.js --count 3000 --output docs/aabb-aabb-overlap.png
8 |
9 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/aabb-aabb-sweep1.js --count 245 --output docs/aabb-aabb-sweep1.png
10 |
11 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/aabb-aabb-sweep2.js --count 245 --output docs/aabb-aabb-sweep2.png
12 |
13 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/aabb-point-overlap.js --count 1200 --output docs/aabb-point-overlap.png
14 |
15 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/aabb-segment-overlap.js --count 1600 --output docs/aabb-segment-overlap.png
16 |
17 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/ray-plane-distance.js --count 1200 --output docs/ray-plane-distance.png
18 |
19 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/segment-point-overlap.js --count 1200 --output docs/segment-point-overlap.png
20 |
21 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/segment-segment-overlap.js --count 1200 --output docs/segment-segment-overlap.png
22 |
23 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/segments-segment-overlap.js --count 1200 --output docs/segments-segment-overlap.png
24 |
25 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/segments-sphere-sweep1.js --count 122 --output docs/segments-sphere-sweep1.png
26 |
27 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/sphere-sphere-overlap.js --count 245 --output docs/sphere-sphere-overlap.png
28 |
29 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/triangle-point-overlap.js --count 1200 --output docs/triangle-point-overlap.png
30 |
31 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/cone-point-overlap.js --count 1200 --output docs/cone-point-overlap.png
32 |
33 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/ray-sphere-overlap.js --count 1200 --output docs/ray-sphere-overlap.png
34 |
35 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/segment-sphere-overlap.js --count 1200 --output docs/segment-sphere-overlap.png
36 |
--------------------------------------------------------------------------------
/canvaskit.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/canvaskit.wasm
--------------------------------------------------------------------------------
/cli-figure-to-apng.ts:
--------------------------------------------------------------------------------
1 | import Canvas, { CanvasRenderingContext2D, dataURLtoFile } from 'https://deno.land/x/canvas@v1.4.2/mod.ts'
2 |
3 |
4 | let m, c, o;
5 |
6 | for (let i=0; i < Deno.args.length - 1; i++) {
7 | const a = Deno.args[i]
8 | if (a === '--module' || a === '-m')
9 | m = Deno.args[i+1]
10 | else if (a === '--count')
11 | c = parseInt(Deno.args[i+1], 10)
12 | else if (a === '--output')
13 | o = Deno.args[i+1]
14 | }
15 |
16 | if (!m) {
17 | console.error('module path not found, exiting')
18 | Deno.exit(1)
19 | }
20 |
21 | if (!c) {
22 | console.error('frame count not found, exiting')
23 | Deno.exit(1)
24 | }
25 |
26 | if (!o) {
27 | console.error('output not found, exiting')
28 | Deno.exit(1)
29 | }
30 |
31 | const CANVAS_WIDTH = 400
32 | const CANVAS_HEIGHT = 180
33 |
34 | const canvas = Canvas.MakeCanvas(CANVAS_WIDTH, CANVAS_HEIGHT)
35 | const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
36 |
37 | ctx.translate(0.5, 0.5)
38 |
39 |
40 | const figure = await import(`./${m}`)
41 |
42 |
43 | const ex = figure.default.init(ctx, CANVAS_WIDTH, CANVAS_HEIGHT)
44 |
45 | //const FPS = 60
46 |
47 | // setting this to 1/ 60 causes the sphere/segment collision test to not work
48 | // probably due to differences in rounding precision between deno and v8?
49 | // hardcoding this to a very close approximation seems to solve the problem
50 | const DT = 0.01666 //1 / FPS
51 |
52 | const padLength = ('' + c).length
53 |
54 | for (let i=0; i < c; i++) {
55 | figure.default.draw(ex, DT)
56 | const data = dataURLtoFile(canvas.toDataURL())
57 | const padded = ('' + i).padStart(padLength, '0')
58 | Deno.writeFileSync(`out_${padded}.png`, data)
59 | }
60 |
61 | await Deno.run({
62 | cmd: [ 'apngasm', '-o', o, 'out_*.png', '--delay=16', '--force' ]
63 | }).status()
64 |
65 |
66 | for (let i=0; i < c; i++) {
67 | const padded = ('' + i).padStart(padLength, '0')
68 | Deno.removeSync(`out_${padded}.png`)
69 | }
70 |
--------------------------------------------------------------------------------
/collision-2d.js:
--------------------------------------------------------------------------------
1 | export { default as aabbContain } from './src/aabb-aabb-contain.js'
2 | export { default as aabbOverlap } from './src/aabb-aabb-overlap.js'
3 | export { default as aabbSweep1 } from './src/aabb-aabb-sweep1.js'
4 | export { default as aabbSweep2 } from './src/aabb-aabb-sweep2.js'
5 | export { default as aabbPointOverlap } from './src/aabb-point-overlap.js'
6 | export { default as aabbSegOverlap } from './src/aabb-segment-overlap.js'
7 | export { default as aabbSegSweep1 } from './src/aabb-segment-sweep1.js'
8 | export { default as aabbSegsSweep1Indexed } from './src/aabb-segments-sweep1-indexed.js'
9 | export * as AABB from './src/AABB.js'
10 | export { default as conePointOverlap } from './src/cone-point-overlap.js'
11 | export { default as contactCopy } from './src/contact-copy.js'
12 | export { default as contact } from './src/contact.js'
13 | export { default as Plane } from './src/Plane.js'
14 | export { default as pointPolygonOverlap } from './src/point-polygon-overlap.js'
15 | export * as Polygon from './src/Polygon.js'
16 | export * as PolylinePath from './src/PolylinePath.js'
17 | export { default as raySphereOverlap } from './src/ray-sphere-overlap.js'
18 | export { default as segNormal } from './src/segment-normal.js'
19 | export { default as segPointOverlap } from './src/segment-point-overlap.js'
20 | export { default as segOverlap } from './src/segment-segment-overlap.js'
21 | export { default as segSphereOverlap } from './src/segment-sphere-overlap.js'
22 | export { default as segsEllipsoidSweep1Indexed }from './src/segments-ellipsoid-sweep1-indexed.js'
23 | export { default as segsSegOverlapIndexed } from './src/segments-segment-overlap-indexed.js'
24 | export { default as segsSegOverlap } from './src/segments-segment-overlap.js'
25 | export { default as segsSphereSweep1Indexed} from './src/segments-sphere-sweep1-indexed.js'
26 | export { default as segsSphereSweep1 } from './src/segments-sphere-sweep1.js'
27 | export { default as segClosest } from './src/segseg-closest.js'
28 | export { default as spherePointOverlap } from './src/sphere-point-overlap.js'
29 | export { default as sphereOverlap } from './src/sphere-sphere-overlap.js'
30 | export { default as sphereSweep2 } from './src/sphere-sphere-sweep2.js'
31 | export { default as triangleArea } from './src/triangle-area.js'
32 | export { default as triangleGetCenter } from './src/triangle-get-center.js'
33 | export { default as trianglePointOverlap } from './src/triangle-point-overlap.js'
34 |
--------------------------------------------------------------------------------
/docs/aabb-aabb-overlap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/aabb-aabb-overlap.png
--------------------------------------------------------------------------------
/docs/aabb-aabb-sweep1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/aabb-aabb-sweep1.png
--------------------------------------------------------------------------------
/docs/aabb-aabb-sweep2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/aabb-aabb-sweep2.png
--------------------------------------------------------------------------------
/docs/aabb-point-overlap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/aabb-point-overlap.png
--------------------------------------------------------------------------------
/docs/aabb-segment-overlap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/aabb-segment-overlap.png
--------------------------------------------------------------------------------
/docs/aabb-segment-sweep1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/aabb-segment-sweep1.png
--------------------------------------------------------------------------------
/docs/aabb-segments-sweep1-indexed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/aabb-segments-sweep1-indexed.png
--------------------------------------------------------------------------------
/docs/cone-point-overlap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/cone-point-overlap.png
--------------------------------------------------------------------------------
/docs/ray-plane-distance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/ray-plane-distance.png
--------------------------------------------------------------------------------
/docs/ray-sphere-overlap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/ray-sphere-overlap.png
--------------------------------------------------------------------------------
/docs/segment-point-overlap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/segment-point-overlap.png
--------------------------------------------------------------------------------
/docs/segment-segment-overlap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/segment-segment-overlap.png
--------------------------------------------------------------------------------
/docs/segment-sphere-overlap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/segment-sphere-overlap.png
--------------------------------------------------------------------------------
/docs/segments-segment-overlap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/segments-segment-overlap.png
--------------------------------------------------------------------------------
/docs/segments-sphere-sweep1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/segments-sphere-sweep1.png
--------------------------------------------------------------------------------
/docs/sphere-sphere-overlap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/sphere-sphere-overlap.png
--------------------------------------------------------------------------------
/docs/triangle-point-overlap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/triangle-point-overlap.png
--------------------------------------------------------------------------------
/figures.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | collision-2d examples
5 |
6 |
28 |
29 |
30 |
31 |
32 |
43 |
44 |
45 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/figures/aabb-aabb-overlap.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import contact from '../src/contact.js'
3 | import aabbAabbOverlap from '../src/aabb-aabb-overlap.js'
4 |
5 |
6 | function init (context, width, height) {
7 | return {
8 | angle: 0,
9 | box1: {
10 | position: [ 0, 0 ],
11 | width: 128,
12 | height: 32
13 | },
14 |
15 | box2: {
16 | position: [ 0, 0 ],
17 | width: 32,
18 | height: 32
19 | },
20 | ...common.init(context, width, height)
21 | }
22 | }
23 |
24 |
25 | function draw (data, dt) {
26 | common.clear(data)
27 |
28 | data.angle += 0.2 * Math.PI * dt
29 | data.box2.position[0] = Math.cos(data.angle) * 96
30 | data.box2.position[1] = Math.sin(data.angle * 2.4) * 24
31 |
32 |
33 | const c = contact()
34 | const hit = aabbAabbOverlap(data.box1, data.box2, c)
35 |
36 | common.drawAABB(data, data.box1, '#666')
37 |
38 | if (hit) {
39 | common.drawAABB(data, data.box2, '#f00')
40 | data.box2.position[0] += c.delta[0]
41 | data.box2.position[1] += c.delta[1]
42 | common.drawAABB(data, data.box2, '#ff0')
43 | common.drawPoint(data, c.position, '#ff0')
44 | common.drawRay(data, c.position, c.normal, 4, '#ff0', false)
45 | } else {
46 | common.drawAABB(data, data.box2, '#0f0')
47 | }
48 | }
49 |
50 |
51 | export default { init, draw }
52 |
--------------------------------------------------------------------------------
/figures/aabb-aabb-sweep1.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import contact from '../src/contact.js'
3 | import aabbAabbSweep1 from '../src/aabb-aabb-sweep1.js'
4 | import { vec2 } from 'gl-matrix'
5 |
6 |
7 | function init (context, width, height) {
8 |
9 | return {
10 | angle: 0,
11 | staticBox: {
12 | position: [ 0, 0 ],
13 | width: 224,
14 | height: 32
15 | },
16 |
17 | sweepBoxes: [
18 | {
19 | position: [ -152, 24 ],
20 | width: 32,
21 | height: 32
22 | },
23 | {
24 | position: [ 128, -48 ],
25 | width: 32,
26 | height: 32
27 | }
28 | ],
29 |
30 | sweepDeltas: [
31 | [ 64, -12 ],
32 | [ -32, 96 ]
33 | ],
34 |
35 | tempBox: {
36 | position: [ 0, 0 ],
37 | width: 32,
38 | height: 32
39 | },
40 | ...common.init(context, width, height)
41 | }
42 | }
43 |
44 |
45 | function draw (data, dt) {
46 | common.clear(data)
47 |
48 | data.angle += 0.5 * Math.PI * dt
49 |
50 | common.drawAABB(data, data.staticBox, '#666')
51 | const factor = (Math.cos(data.angle) + 1) * 0.5 || 1e-8
52 |
53 | common.drawAABB(data, data.staticBox, '#666')
54 |
55 | data.sweepBoxes.forEach((box, i) => {
56 | const delta = vec2.scale([], data.sweepDeltas[i], factor)
57 | const c = contact()
58 | const sweep = aabbAabbSweep1(data.staticBox, box, delta, c)
59 | const length = vec2.length(delta)
60 | const dir = vec2.normalize([], delta)
61 |
62 | common.drawAABB(data, box, '#666')
63 |
64 | if (sweep) {
65 | // Draw a red box at the point where it was trying to move to
66 | common.drawRay(data, box.position, dir, length, '#f00')
67 | data.tempBox.position[0] = box.position[0] + delta[0]
68 | data.tempBox.position[1] = box.position[1] + delta[1]
69 | common.drawAABB(data, data.tempBox, '#f00')
70 |
71 | // Draw a yellow box at the point it actually got to
72 | data.tempBox.position[0] = box.position[0] + delta[0] * c.time
73 | data.tempBox.position[1] = box.position[1] + delta[1] * c.time
74 | common.drawAABB(data, data.tempBox, '#ff0')
75 | common.drawPoint(data, c.position, '#ff0')
76 | common.drawRay(data, c.position, c.normal, 4, '#ff0', false)
77 |
78 | } else {
79 | data.tempBox.position[0] = box.position[0] + delta[0]
80 | data.tempBox.position[1] = box.position[1] + delta[1]
81 | common.drawAABB(data, data.tempBox, '#0f0')
82 | common.drawRay(data, box.position, dir, length, '#0f0')
83 | }
84 |
85 | })
86 |
87 | }
88 |
89 |
90 | export default { init, draw }
91 |
--------------------------------------------------------------------------------
/figures/aabb-aabb-sweep2.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import contact from '../src/contact.js'
3 | import aabbAabbSweep2 from '../src/aabb-aabb-sweep2.js'
4 | import { vec2 } from 'gl-matrix'
5 |
6 |
7 | function init (context, width, height) {
8 |
9 | return {
10 | angle: 0,
11 | sweepBoxes: [
12 | {
13 | position: [ -60, 24 ],
14 | width: 32,
15 | height: 32
16 | },
17 | {
18 | position: [ 60, -48 ],
19 | width: 32,
20 | height: 32
21 | }
22 | ],
23 |
24 | sweepDeltas: [
25 | [ 64, -20 ],
26 | [ -64,50 ]
27 | ],
28 |
29 | tempBox: {
30 | position: [ 0, 0 ],
31 | width: 32,
32 | height: 32
33 | },
34 | ...common.init(context, width, height)
35 | }
36 | }
37 |
38 |
39 | function draw (data, dt) {
40 | common.clear(data)
41 |
42 | data.angle += 0.5 * Math.PI * dt
43 |
44 | const factor = (Math.cos(data.angle) + 1) * 0.5 || 1e-8
45 |
46 | const vA = vec2.scale([], data.sweepDeltas[0], factor)
47 | const vB = vec2.scale([], data.sweepDeltas[1], factor)
48 |
49 | const [ box1, box2 ] = data.sweepBoxes
50 |
51 | const c = contact()
52 |
53 | const sweep = aabbAabbSweep2(data.sweepBoxes[0], vA, data.sweepBoxes[1], vB, c)
54 |
55 | if (sweep) {
56 | // Draw a red box at the point where it was trying to move to
57 | let length = vec2.length(vA)
58 | let dir = vec2.normalize([], vA)
59 | common.drawRay(data, box1.position, dir, length, '#f00')
60 | data.tempBox.position[0] = box1.position[0] + vA[0]
61 | data.tempBox.position[1] = box1.position[1] + vA[1]
62 | common.drawAABB(data, data.tempBox, '#f00')
63 |
64 | length = vec2.length(vB)
65 | dir = vec2.normalize([], vB)
66 | common.drawRay(data, box2.position, dir, length, '#f00')
67 | data.tempBox.position[0] = box2.position[0] + vB[0]
68 | data.tempBox.position[1] = box2.position[1] + vB[1]
69 | common.drawAABB(data, data.tempBox, '#f00')
70 |
71 |
72 | // Draw a yellow box at the point it actually got to
73 | data.tempBox.position[0] = box1.position[0] + vA[0] * c.time
74 | data.tempBox.position[1] = box1.position[1] + vA[1] * c.time
75 | common.drawAABB(data, data.tempBox, '#ff0')
76 |
77 | data.tempBox.position[0] = box2.position[0] + vB[0] * c.time
78 | data.tempBox.position[1] = box2.position[1] + vB[1] * c.time
79 | common.drawAABB(data, data.tempBox, '#ff0')
80 |
81 | // draw a ray at the contact location
82 | common.drawRay(data, c.position, c.normal, 4, '#ff0', false)
83 |
84 | } else {
85 |
86 | const dir = [ 0, 0 ]
87 | let length
88 |
89 | length = vec2.length(vA)
90 | vec2.normalize(dir, vA)
91 |
92 | vec2.add(data.tempBox.position, box1.position, vA)
93 | common.drawAABB(data, data.tempBox, '#0f0')
94 | common.drawRay(data, box1.position, dir, length, '#0f0')
95 |
96 |
97 | length = vec2.length(vB)
98 | vec2.normalize(dir, vB)
99 |
100 | vec2.add(data.tempBox.position, box2.position, vB)
101 | common.drawAABB(data, data.tempBox, '#0f0')
102 | common.drawRay(data, box2.position, dir, length, '#0f0')
103 | }
104 |
105 | }
106 |
107 |
108 | export default { init, draw }
109 |
--------------------------------------------------------------------------------
/figures/aabb-point-overlap.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import contact from '../src/contact.js'
3 | import aabbPointOverlap from '../src/aabb-point-overlap.js'
4 |
5 |
6 | function init (context, width, height) {
7 | return {
8 | angle: 0,
9 | pos: [ 0, 0 ],
10 | box: {
11 | position: [ 0, 0 ],
12 | width: 32,
13 | height: 32
14 | },
15 | ...common.init(context, width, height)
16 | }
17 | }
18 |
19 |
20 | function draw (data, dt) {
21 | common.clear(data)
22 |
23 | data.angle += 0.5 * Math.PI * dt
24 | data.pos[0] = Math.cos(data.angle * 0.4) * 32
25 | data.pos[1] = Math.sin(data.angle) * 12
26 |
27 | const c = contact()
28 | const hit = aabbPointOverlap(data.box, data.pos, c)
29 | common.drawAABB(data, data.box, '#666')
30 |
31 | const pointWidth = 1
32 |
33 | if (hit) {
34 | common.drawPoint(data, data.pos, '#f00', '', pointWidth)
35 | common.drawPoint(data, c.position, '#ff0', '', pointWidth)
36 | } else {
37 | common.drawPoint(data, data.pos, '#0f0', '', pointWidth)
38 | }
39 | }
40 |
41 |
42 | export default { init, draw }
43 |
--------------------------------------------------------------------------------
/figures/aabb-segment-overlap.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import contact from '../src/contact.js'
3 | import intersectSegment from '../src/aabb-segment-overlap.js'
4 | import { vec2 } from 'gl-matrix'
5 |
6 |
7 | function init (context, width, height) {
8 | return {
9 | angle: 0,
10 | box: {
11 | position: [ 0, 0 ],
12 | width: 32,
13 | height: 32
14 | },
15 | ...common.init(context, width, height)
16 | }
17 | }
18 |
19 |
20 | function draw (data, dt) {
21 | common.clear(data)
22 |
23 | data.angle += 0.5 * Math.PI * dt
24 |
25 | const pos1 = [
26 | Math.cos(data.angle) * 64,
27 | Math.sin(data.angle) * 64
28 | ]
29 |
30 | const pos2 = [
31 | Math.sin(data.angle) * 32,
32 | Math.cos(data.angle) * 32
33 | ]
34 |
35 | const delta = [ pos2[0] - pos1[0], pos2[1] - pos1[1] ]
36 |
37 | const c = contact()
38 | const paddingX = 0
39 | const paddingY = 0
40 | const hit = intersectSegment(data.box, pos1, delta, paddingX, paddingY, c)
41 |
42 | const length = vec2.length(delta)
43 | const dir = vec2.normalize([], delta) //[ delta[0], delta[1] ]
44 |
45 | common.drawAABB(data, data.box, '#666')
46 |
47 | if (hit) {
48 | common.drawRay(data, pos1, dir, length, '#f00')
49 | common.drawSegment(data, pos1, c.position, '#ff0')
50 | common.drawPoint(data, c.position, '#ff0')
51 | common.drawRay(data, c.position, c.normal, 6, '#ff0', false)
52 | } else {
53 | common.drawRay(data, pos1, dir, length, '#0f0')
54 | }
55 | }
56 |
57 |
58 | export default { init, draw }
59 |
--------------------------------------------------------------------------------
/figures/aabb-segment-sweep1.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import contact from '../src/contact.js'
3 | import aabbSegmentSweep from '../src/aabb-segment-sweep1.js'
4 | import { vec2 } from 'gl-matrix'
5 |
6 |
7 | function init (context, width, height) {
8 |
9 | return {
10 | angle: 0,
11 |
12 | staticLine: [
13 |
14 | [ 50, -50 ],
15 | [ -20, 0 ]
16 | ],
17 |
18 | sweepAABB: {
19 | position: [ -32, 60 ],
20 | width: 32,
21 | height: 32
22 | },
23 |
24 | sweepDelta: [
25 | 32, -96
26 | ],
27 |
28 | tempBox: {
29 | position: [ 0, 0 ],
30 | width: 32,
31 | height: 32
32 | },
33 | ...common.init(context, width, height)
34 | }
35 | }
36 |
37 |
38 | function draw (data, dt) {
39 | common.clear(data)
40 |
41 | common.drawSegment(data, data.staticLine[0], data.staticLine[1], '#666')
42 |
43 |
44 | data.angle += 0.5 * Math.PI * dt
45 |
46 | const factor = (Math.cos(data.angle) + 1) * 0.5 || 1e-8
47 |
48 | const delta = vec2.scale([], data.sweepDelta, factor)
49 | const c = contact()
50 |
51 | const box = data.sweepAABB
52 |
53 |
54 | const sweep = aabbSegmentSweep(data.staticLine, box, delta, c)
55 | const length = vec2.length(delta)
56 | const dir = vec2.normalize([], delta)
57 |
58 |
59 | if (sweep) {
60 | // Draw a red box at the point where it was trying to move to
61 | common.drawRay(data, box.position, dir, length, '#f00')
62 | data.tempBox.position[0] = box.position[0] + delta[0]
63 | data.tempBox.position[1] = box.position[1] + delta[1]
64 | common.drawAABB(data, data.tempBox, '#f00')
65 |
66 | // Draw a yellow box at the point it actually got to
67 | data.tempBox.position[0] = box.position[0] + delta[0] * c.time
68 | data.tempBox.position[1] = box.position[1] + delta[1] * c.time
69 | common.drawAABB(data, data.tempBox, '#ff0')
70 | common.drawPoint(data, c.position, '#ff0')
71 | common.drawRay(data, c.position, c.normal, 4, '#ff0', false)
72 |
73 | } else {
74 | data.tempBox.position[0] = box.position[0] + delta[0]
75 | data.tempBox.position[1] = box.position[1] + delta[1]
76 | common.drawAABB(data, data.tempBox, '#0f0')
77 | common.drawRay(data, box.position, dir, length, '#0f0')
78 | }
79 |
80 | }
81 |
82 |
83 | export default { init, draw }
84 |
--------------------------------------------------------------------------------
/figures/aabb-segments-sweep1-indexed.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import contact from '../src/contact.js'
3 | import aabbSegmentsSweep from '../src/aabb-segments-sweep1-indexed.js'
4 | import { vec2 } from 'gl-matrix'
5 |
6 |
7 | function init (context, width, height) {
8 |
9 | return {
10 | angle: 0,
11 |
12 | staticLines: [
13 | [ [96,28], [-22,-15] ],
14 | [ [100, 11], [-30,-40] ],
15 | [ [100, -15], [-30,-60] ]
16 | ],
17 |
18 | indices: [ 0, 1, 2 ],
19 | lineCount: 3,
20 |
21 | sweepAABB: {
22 | position: [ -32, 60 ],
23 | width: 32,
24 | height: 32
25 | },
26 |
27 | sweepDelta: [
28 | 32, -96
29 | ],
30 |
31 | tempBox: {
32 | position: [ 0, 0 ],
33 | width: 32,
34 | height: 32
35 | },
36 | ...common.init(context, width, height)
37 | }
38 | }
39 |
40 |
41 | function draw (data, dt) {
42 | common.clear(data)
43 |
44 | for (const line of data.staticLines) {
45 | common.drawSegment(data, line[0], line[1], '#666')
46 | }
47 |
48 | data.angle += 0.5 * Math.PI * dt
49 |
50 | const factor = (Math.cos(data.angle) + 1) * 0.5 || 1e-8
51 |
52 | const delta = vec2.scale([], data.sweepDelta, factor)
53 | const c = contact()
54 |
55 | const box = data.sweepAABB
56 |
57 |
58 | const sweep = aabbSegmentsSweep(data.staticLines, data.indices, data.lineCount, box, delta, c)
59 | const length = vec2.length(delta)
60 | const dir = vec2.normalize([], delta)
61 |
62 | if (sweep) {
63 | // Draw a red box at the point where it was trying to move to
64 | common.drawRay(data, box.position, dir, length, '#f00')
65 | data.tempBox.position[0] = box.position[0] + delta[0]
66 | data.tempBox.position[1] = box.position[1] + delta[1]
67 | common.drawAABB(data, data.tempBox, '#f00')
68 |
69 | // Draw a yellow box at the point it actually got to
70 | data.tempBox.position[0] = box.position[0] + delta[0] * c.time
71 | data.tempBox.position[1] = box.position[1] + delta[1] * c.time
72 | common.drawAABB(data, data.tempBox, '#ff0')
73 | common.drawPoint(data, c.position, '#ff0')
74 | common.drawRay(data, c.position, c.normal, 4, '#ff0', false)
75 |
76 | } else {
77 | data.tempBox.position[0] = box.position[0] + delta[0]
78 | data.tempBox.position[1] = box.position[1] + delta[1]
79 | common.drawAABB(data, data.tempBox, '#0f0')
80 | common.drawRay(data, box.position, dir, length, '#0f0')
81 | }
82 |
83 | }
84 |
85 |
86 | export default { init, draw }
87 |
--------------------------------------------------------------------------------
/figures/common.js:
--------------------------------------------------------------------------------
1 | import { toRadians } from '@footgun/math-gap'
2 |
3 |
4 | function drawAABB (data, box, color='#fff', thickness=1) {
5 | const { origin, context } = data
6 |
7 | const x1 = Math.floor(origin[0] + box.position[0] - box.width/2)
8 | const y1 = Math.floor(origin[1] + box.position[1] - box.height/2)
9 | const x2 = Math.floor(origin[0] + box.position[0] + box.width/2)
10 | const y2 = Math.floor(origin[1] + box.position[1] + box.height/2)
11 | context.beginPath()
12 | context.moveTo(x1, y1)
13 | context.lineTo(x2, y1)
14 | context.lineTo(x2, y2)
15 | context.lineTo(x1, y2)
16 | context.lineTo(x1, y1)
17 | context.closePath()
18 | context.lineWidth = thickness
19 | context.strokeStyle = color
20 | context.stroke()
21 | }
22 |
23 |
24 | function drawCircle (data, center, radius, color='#fff', thickness=1) {
25 | const { origin, context } = data
26 |
27 | const x = Math.floor(origin[0] + center[0])
28 | const y = Math.floor(origin[1] + center[1])
29 | context.beginPath()
30 | context.arc(x, y, radius, 0, 2 * Math.PI, true)
31 | context.closePath()
32 | context.lineWidth = thickness
33 | context.strokeStyle = color
34 | context.stroke()
35 | }
36 |
37 |
38 | function drawCone (data, conePosition, coneRotation, coneFieldOfView, minDistance, maxDistance) {
39 | const { origin, context } = data
40 |
41 | const x = Math.floor(origin[0] + conePosition[0])
42 | const y = Math.floor(origin[1] + conePosition[1])
43 |
44 | //console.log('max:', maxDistance, 'min:', minDistance)
45 | //console.log('fov:', coneFieldOfView, ' rot:', coneRotation)
46 | context.fillStyle = '#333'
47 | context.beginPath()
48 | context.moveTo(origin[0], origin[1])
49 | context.arc(x, y, maxDistance, coneRotation-toRadians(coneFieldOfView/2), coneRotation+toRadians(coneFieldOfView/2), false)
50 | context.fill()
51 |
52 | if (minDistance > 0) {
53 | context.fillStyle = '#202020'
54 | context.beginPath()
55 | context.arc(x, y, minDistance, 0, Math.PI*2, false)
56 | context.fill()
57 | }
58 | }
59 |
60 |
61 | function drawPoint (data, point, color='#fff', text='', thickness=1) {
62 | const { origin, context } = data
63 |
64 | const x = Math.floor(origin[0] + point[0] - thickness / 2)
65 | const y = Math.floor(origin[1] + point[1] - thickness / 2)
66 | context.lineWidth = thickness
67 | context.fillStyle = color
68 | context.strokeStyle = color
69 | context.fillRect(x, y, thickness, thickness)
70 | context.strokeRect(x, y, thickness, thickness)
71 | if (text)
72 | context.fillText(text, x + thickness * 4, y + thickness * 2)
73 | }
74 |
75 |
76 | function drawRay (data, pos, dir, length, color='#fff', arrow=true, thickness=1) {
77 | const { context } = data
78 |
79 | const pos2 = [
80 | pos[0] + dir[0] * length,
81 | pos[1] + dir[1] * length
82 | ]
83 |
84 | drawSegment(data, pos, pos2, color, thickness)
85 |
86 | if (arrow) {
87 | pos = [ pos2[0], pos2[1] ]
88 | pos2[0] = pos[0] - dir[0] * 4 + dir[1] * 4
89 | pos2[1] = pos[1] - dir[1] * 4 - dir[0] * 4
90 | drawSegment(data, pos, pos2, color, thickness)
91 | pos2[0] = pos[0] - dir[0] * 4 - dir[1] * 4
92 | pos2[1] = pos[1] - dir[1] * 4 + dir[0] * 4
93 | drawSegment(data, pos, pos2, color, thickness)
94 | }
95 | }
96 |
97 |
98 | function drawSegment (data, point1, point2, color='#fff', thickness=1, dashed=false) {
99 | const { origin, context } = data
100 |
101 | if (dashed)
102 | context.setLineDash([4, 6])
103 |
104 | const x1 = Math.floor(origin[0] + point1[0])
105 | const y1 = Math.floor(origin[1] + point1[1])
106 | const x2 = Math.floor(origin[0] + point2[0])
107 | const y2 = Math.floor(origin[1] + point2[1])
108 | context.beginPath()
109 | context.moveTo(x1, y1)
110 | context.lineTo(x2, y2)
111 |
112 | context.closePath()
113 | context.lineWidth = thickness
114 | context.strokeStyle = color
115 | context.stroke()
116 |
117 | if (dashed)
118 | context.setLineDash([])
119 | }
120 |
121 |
122 | function drawTriangle (data, point0, point1, point2, color='#fff', thickness=1, dashed=false) {
123 | drawSegment(data, point0, point1, color, thickness, dashed)
124 | drawSegment(data, point1, point2, color, thickness, dashed)
125 | drawSegment(data, point2, point0, color, thickness, dashed)
126 | }
127 |
128 |
129 | function init (context, width, height) {
130 | return {
131 | context,
132 | width,
133 | height,
134 | origin: [ width * 0.5, height * 0.5 ],
135 | infiniteLength: Math.sqrt(width * width + height * height)
136 | }
137 | }
138 |
139 |
140 | function clear (data) {
141 | const { context, width, height } = data
142 | context.fillStyle = '#202020'
143 | context.fillRect(0, 0, width, height)
144 | }
145 |
146 |
147 | export default { drawAABB, drawCircle, drawCone, drawPoint, drawRay, drawSegment, drawTriangle, init, clear }
148 |
--------------------------------------------------------------------------------
/figures/cone-point-overlap.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import conePointOverlap from '../src/cone-point-overlap.js'
3 |
4 |
5 | function init (context, width, height) {
6 | return {
7 | angle: 0,
8 | pos: [ 0, 0 ],
9 | tri: [
10 | [ 0, -20 ],
11 | [ -20, 20 ],
12 | [ 20, 20 ]
13 | ],
14 | cone: {
15 | position: [ 0, 0 ],
16 | rotation: 0,
17 | fieldOfView: 80,
18 | minDistance: 10,
19 | maxDistance: 40
20 | },
21 | ...common.init(context, width, height)
22 | }
23 | }
24 |
25 |
26 | function draw (data, dt) {
27 | common.clear(data)
28 |
29 | data.angle += 0.5 * Math.PI * dt
30 | data.pos[0] = 30 + Math.cos(data.angle * 0.4) * 32
31 | data.pos[1] = Math.sin(data.angle) * 12
32 |
33 | common.drawCone(data, data.cone.position, data.cone.rotation, data.cone.fieldOfView, data.cone.minDistance, data.cone.maxDistance)
34 |
35 | const pointWidth = 1
36 |
37 | /*
38 | const overlapping = triPointOverlap(data.tri[0], data.tri[1], data.tri[2], data.pos)
39 |
40 | common.drawTriangle(data, data.tri[0], data.tri[1], data.tri[2], '#666')
41 |
42 | if (overlapping)
43 | common.drawPoint(data, data.pos, '#f00', '', pointWidth)
44 | else
45 | common.drawPoint(data, data.pos, '#0f0', '', pointWidth)
46 | */
47 |
48 | const pc = conePointOverlap(data.cone.position, data.cone.rotation, data.cone.fieldOfView, data.cone.minDistance, data.cone.maxDistance, data.pos) ? '#0f0' : '#f00'
49 |
50 | common.drawPoint(data, data.pos, pc, '', pointWidth)
51 |
52 | //data.cone.rotation += 0.02
53 | }
54 |
55 |
56 | export default { init, draw }
57 |
--------------------------------------------------------------------------------
/figures/ray-plane-distance.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import plane from '../src/plane.js'
3 | import { vec2 } from 'gl-matrix'
4 | import segmentNormal from '../src/segment-normal.js'
5 | import setLength from 'https://cdn.jsdelivr.net/gh/mreinstein/vec2-gap/set-length.js'
6 |
7 |
8 | function init (context, width, height) {
9 | const line = [
10 | [ -200, 200 ],
11 | [ 200, -200 ]
12 | ]
13 |
14 | const planeNormal = segmentNormal([], line[0], line[1])
15 | const planeOrigin = line[0]
16 |
17 | return {
18 | angle: 2.6,
19 | line,
20 | planeNormal,
21 | planeOrigin,
22 | ...common.init(context, width, height)
23 | }
24 | }
25 |
26 |
27 |
28 | function draw (data, dt) {
29 | common.clear(data)
30 |
31 | //window.d = data
32 | data.angle += 0.5 * Math.PI * dt
33 | //data.angle = 6.8
34 |
35 | const pos1 = [
36 | Math.cos(data.angle) * 64,
37 | Math.sin(data.angle) * 64
38 | ]
39 |
40 | const pos2 = [
41 | Math.sin(data.angle) * 32,
42 | Math.cos(data.angle) * 32
43 | ]
44 |
45 | const delta = vec2.subtract([], pos2, pos1)
46 |
47 | common.drawSegment(data, data.line[0], data.line[1], '#666', 1, true)
48 |
49 | const dir = vec2.normalize([], delta)
50 |
51 | const p = plane.create()
52 | plane.fromPlane(p, data.planeOrigin, data.planeNormal)
53 |
54 | const distance = plane.rayDistance(p, pos1, dir)
55 |
56 |
57 | //const len = vec2.length(delta)
58 | //common.drawRay(data, pos1, dir, len, '#0f0')
59 |
60 | //const rr = setLength([], dir, distance)
61 | common.drawRay(data, pos1, dir, distance, '#ff0')
62 |
63 | /*
64 | if (segmentSegmentOverlap(pos1, pos2, data.line[0], data.line[1], intersection)) {
65 | common.drawPoint(data, intersection, '#ff0', '', 2)
66 | common.drawSegment(data, pos1, intersection, '#ff0')
67 | common.drawSegment(data, pos2, intersection, '#f00')
68 | }
69 | else {
70 | common.drawSegment(data, pos1, pos2, '#0f0')
71 | }
72 | */
73 |
74 | }
75 |
76 |
77 | export default { init, draw }
78 |
--------------------------------------------------------------------------------
/figures/ray-sphere-overlap.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import raySphereOverlap from '../src/ray-sphere-overlap.js'
3 | import { vec2 } from 'gl-matrix'
4 |
5 |
6 | function init (context, width, height) {
7 | return {
8 | angle: 0,
9 | box: {
10 | position: [ 0, 0 ],
11 | width: 32,
12 | height: 32
13 | },
14 | ...common.init(context, width, height)
15 | }
16 | }
17 |
18 |
19 | function draw (data, dt) {
20 | common.clear(data)
21 |
22 | data.angle += 0.3 * Math.PI * dt
23 |
24 | const pos1 = [
25 | Math.cos(data.angle) * 64,
26 | Math.sin(data.angle) * 64
27 | ]
28 |
29 | const pos2 = [
30 | Math.sin(data.angle) * 32,
31 | Math.cos(data.angle) * 32
32 | ]
33 |
34 |
35 | const delta = vec2.subtract(vec2.create(), pos2, pos1)
36 | //const delta = [ pos2[0] - pos1[0], pos2[1] - pos1[1] ]
37 |
38 | const sphereCenter = [ 0, 0 ]
39 | const sphereRadius = 22
40 | const contact = { mu1: NaN, mu2: NaN }
41 |
42 | const hit = raySphereOverlap(pos1, pos2, sphereCenter, sphereRadius, contact)
43 |
44 |
45 | common.drawCircle(data, sphereCenter, sphereRadius, '#666')
46 |
47 |
48 | const normal = vec2.normalize(vec2.create(), delta)
49 | const startP = vec2.scaleAndAdd(vec2.create(), pos1, normal, -200)
50 | const endP = vec2.scaleAndAdd(vec2.create(), pos1, normal, 200)
51 |
52 | if (hit) {
53 | common.drawSegment(data, startP, endP, '#f00', 1, true)
54 | common.drawSegment(data, pos1, pos2, '#f00', 1)
55 |
56 | const p1 = vec2.scaleAndAdd(vec2.create(), pos1, delta, contact.mu1)
57 | const p2 = vec2.scaleAndAdd(vec2.create(), pos1, delta, contact.mu2)
58 |
59 | common.drawPoint(data, p1, 'yellow', 'μ1', 2)
60 | common.drawPoint(data, p2, 'yellow', 'μ2', 2)
61 |
62 | } else {
63 | common.drawSegment(data, startP, endP, '#0f0', 1, true)
64 | common.drawSegment(data, pos1, pos2, '#0f0', 1)
65 | }
66 |
67 | }
68 |
69 |
70 | export default { init, draw }
71 |
--------------------------------------------------------------------------------
/figures/segment-point-overlap.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import contact from '../src/contact.js'
3 | import segmentPointOverlap from '../src/segment-point-overlap.js'
4 | import { vec2 } from 'gl-matrix'
5 |
6 |
7 | function init (context, width, height) {
8 | return {
9 | angle: 0,
10 | line: [
11 | [ 50, -50],
12 | [ -50, 50 ]
13 | ],
14 | point: [ 0, 0 ],
15 | ...common.init(context, width, height)
16 | }
17 | }
18 |
19 |
20 | function draw (data, dt) {
21 | common.clear(data)
22 |
23 | data.angle += 0.5 * Math.PI * dt
24 | vec2.set(data.point, Math.cos(data.angle * 0.4) * 32, Math.sin(data.angle) * 12)
25 |
26 | common.drawSegment(data, data.line[0], data.line[1], '#666')
27 |
28 | const overlaps = segmentPointOverlap(data.point, data.line[0], data.line[1])
29 |
30 | common.drawPoint(data, data.point, overlaps ? '#ff0' : '#0f0', '', 2)
31 | }
32 |
33 |
34 | export default { init, draw }
35 |
--------------------------------------------------------------------------------
/figures/segment-segment-overlap.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import segmentSegmentOverlap from '../src/segment-segment-overlap.js'
3 | import { vec2 } from 'gl-matrix'
4 |
5 |
6 | function init (context, width, height) {
7 | return {
8 | angle: 2.6,
9 | line: [
10 | [ 50, -50 ],
11 | [ -50, 50 ]
12 | ],
13 | ...common.init(context, width, height)
14 | }
15 | }
16 |
17 |
18 |
19 | function draw (data, dt) {
20 | common.clear(data)
21 |
22 | data.angle += 0.5 * Math.PI * dt
23 |
24 | const pos1 = [
25 | Math.cos(data.angle) * 64,
26 | Math.sin(data.angle) * 64
27 | ]
28 |
29 | const pos2 = [
30 | Math.sin(data.angle) * 32,
31 | Math.cos(data.angle) * 32
32 | ]
33 |
34 | common.drawSegment(data, data.line[0], data.line[1], '#666')
35 |
36 |
37 | const intersection = [ 0, 0 ]
38 | if (segmentSegmentOverlap(pos1, pos2, data.line[0], data.line[1], intersection)) {
39 | common.drawPoint(data, intersection, '#ff0', '', 2)
40 | common.drawSegment(data, pos1, intersection, '#ff0')
41 | common.drawSegment(data, pos2, intersection, '#f00')
42 | }
43 | else {
44 | common.drawSegment(data, pos1, pos2, '#0f0')
45 | }
46 |
47 | }
48 |
49 |
50 | export default { init, draw }
51 |
--------------------------------------------------------------------------------
/figures/segment-sphere-overlap.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import segmentSphereOverlap from '../src/segment-sphere-overlap.js'
3 | import { vec2 } from 'gl-matrix'
4 |
5 |
6 | function init (context, width, height) {
7 | return {
8 | angle: 0,
9 | ...common.init(context, width, height)
10 | }
11 | }
12 |
13 |
14 | function draw (data, dt) {
15 | common.clear(data)
16 |
17 | data.angle += 0.3 * Math.PI * dt
18 |
19 | const pos1 = [
20 | Math.cos(data.angle) * 124,
21 | Math.sin(data.angle) * 24
22 | ]
23 |
24 | const pos2 = [
25 | Math.sin(data.angle) * 32,
26 | Math.cos(data.angle) * 32
27 | ]
28 |
29 |
30 | const delta = vec2.subtract(vec2.create(), pos2, pos1)
31 |
32 | const sphereCenter = [ 0, 0 ]
33 | const sphereRadius = 31
34 | const contact = { mu1: NaN, mu2: NaN }
35 |
36 | const hit = segmentSphereOverlap(pos1, pos2, sphereCenter, sphereRadius, contact)
37 |
38 |
39 | common.drawCircle(data, sphereCenter, sphereRadius, '#666')
40 |
41 | if (hit) {
42 | common.drawSegment(data, pos1, pos2, '#f00', 1)
43 |
44 | const p1 = vec2.scaleAndAdd(vec2.create(), pos1, delta, contact.mu1)
45 | const p2 = vec2.scaleAndAdd(vec2.create(), pos1, delta, contact.mu2)
46 |
47 | common.drawPoint(data, p1, 'yellow', 'μ1', 2)
48 | common.drawPoint(data, p2, 'yellow', 'μ2', 2)
49 |
50 | } else {
51 | common.drawSegment(data, pos1, pos2, '#0f0', 1)
52 | }
53 |
54 | }
55 |
56 |
57 | export default { init, draw }
58 |
--------------------------------------------------------------------------------
/figures/segments-segment-overlap.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import contact from '../src/contact.js'
3 | import segmentsSegmentOverlap from '../src/segments-segment-overlap.js'
4 | import { vec2 } from 'gl-matrix'
5 |
6 |
7 | function init (context, width, height) {
8 |
9 | const lines = [[[4,23],[6,31]],[[-22,-15],[96,28]],[[-67,76],[5,-77]],[[-26,-23],[-69,-37]],[[-11,59],[12,38]]]
10 |
11 | return {
12 | angle: 2.6,
13 | lines,
14 | ...common.init(context, width, height)
15 | }
16 | }
17 |
18 |
19 | function draw (data, dt) {
20 | common.clear(data)
21 |
22 | data.angle += 0.4 * Math.PI * dt
23 |
24 |
25 | const pos1 = [
26 | Math.cos(data.angle) * 64,
27 | Math.sin(data.angle) * 64
28 | ]
29 |
30 | const pos2 = [
31 | Math.sin(data.angle) * 32,
32 | Math.cos(data.angle) * 32
33 | ]
34 |
35 | const delta = vec2.subtract([], pos2, pos1)
36 | let len = vec2.length(delta)
37 | const dir = vec2.normalize([], delta)
38 |
39 |
40 | for (const line of data.lines)
41 | common.drawSegment(data, line[0], line[1], '#666')
42 |
43 | const c = contact()
44 |
45 | if (segmentsSegmentOverlap(data.lines, pos1, delta, c)) {
46 | vec2.subtract(dir, c.position, pos1)
47 | len = vec2.length(dir)
48 | vec2.normalize(dir, dir)
49 | common.drawRay(data, pos1, dir, len, '#ff0')
50 |
51 | vec2.subtract(dir, pos2, c.position)
52 | len = vec2.length(dir)
53 | vec2.normalize(dir, dir)
54 | common.drawRay(data, c.position, dir, len, '#f00')
55 |
56 | common.drawPoint(data, c.position, '#ff0', '', 2)
57 | } else {
58 | common.drawRay(data, pos1, dir, len, '#0f0')
59 | }
60 |
61 | }
62 |
63 |
64 | export default { init, draw }
65 |
--------------------------------------------------------------------------------
/figures/segments-sphere-sweep1.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import contact from '../src/contact.js'
3 | import segmentsSphereSweep1 from '../src/segments-sphere-sweep1.js'
4 | import { vec2 } from 'gl-matrix'
5 | import randomInt from 'https://cdn.jsdelivr.net/gh/mreinstein/random-gap/int.js'
6 |
7 |
8 | function init (context, width, height) {
9 |
10 | const lines = [
11 | [ [-50, -50], [ -50, 50] ],
12 | [ [ 50, 50], [ 50, -50] ],
13 | //[ [ 50, -50 ], [ -50, -50 ] ],
14 | //[ [ -50, 50 ], [ 50, 50 ] ]
15 | ]
16 |
17 | const velocity = vec2.random([], 60)
18 |
19 | return {
20 | angle: 2.6,
21 | radius: 10,
22 | position: [ 0, 0 ],
23 | velocity: [ 80, 0],
24 | dx: 80,
25 | lines,
26 | ...common.init(context, width, height)
27 | }
28 | }
29 |
30 |
31 | function draw (data, dt) {
32 | common.clear(data)
33 |
34 | const delta = vec2.scale([], data.velocity, dt)
35 |
36 | for (const line of data.lines)
37 | common.drawSegment(data, line[0], line[1], '#666')
38 |
39 | const c = contact()
40 |
41 | if (segmentsSphereSweep1(data.lines, data.position, data.radius, delta, c)) {
42 | common.drawCircle(data, data.position, data.radius, '#ff0')
43 | common.drawPoint(data, c.position, '#ff0', '', 2)
44 |
45 | // bounce
46 | vec2.scale(data.velocity, c.normal, data.dx)
47 |
48 | } else {
49 | vec2.add(data.position, data.position, delta)
50 | common.drawCircle(data, data.position, data.radius, '#0f0')
51 | }
52 |
53 | }
54 |
55 |
56 | export default { init, draw }
57 |
--------------------------------------------------------------------------------
/figures/sphere-sphere-overlap.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import sphereSphereOverlap from '../src/sphere-sphere-overlap.js'
3 | import { vec2 } from 'gl-matrix'
4 |
5 |
6 | function init (context, width, height) {
7 | return {
8 | angle: 0,
9 | sweepBoxes: [
10 | {
11 | position: [ -60, 24 ],
12 | radius: 32
13 | },
14 | {
15 | position: [ 60, -48 ],
16 | radius: 32
17 | }
18 | ],
19 |
20 | sweepDeltas: [
21 | [ 64, -20 ],
22 | [ -64,50 ]
23 | ],
24 |
25 | ...common.init(context, width, height)
26 | }
27 | }
28 |
29 |
30 | function draw (data, dt) {
31 | common.clear(data)
32 |
33 | data.angle += 0.5 * Math.PI * dt
34 |
35 | const factor = (Math.cos(data.angle) + 1) * 0.5 || 1e-8
36 |
37 | const vA = vec2.scale([], data.sweepDeltas[0], factor)
38 | const vB = vec2.scale([], data.sweepDeltas[1], factor)
39 |
40 | const centerA = data.sweepBoxes[0].position
41 | const radiusA = data.sweepBoxes[0].radius
42 |
43 | const centerB = data.sweepBoxes[1].position
44 | const radiusB = data.sweepBoxes[1].radius
45 |
46 | const tmp1 = vec2.add([], centerA, vA)
47 | const tmp2 = vec2.add([], centerB, vB)
48 |
49 |
50 | const overlapping = sphereSphereOverlap(tmp1, radiusA, tmp2, radiusB)
51 |
52 | if (overlapping) {
53 | common.drawCircle(data, tmp1, radiusA, '#f00')
54 | common.drawCircle(data, tmp2, radiusB, '#f00')
55 | } else {
56 | common.drawCircle(data, tmp1, radiusA, '#0f0')
57 | common.drawCircle(data, tmp2, radiusB, '#0f0')
58 | }
59 |
60 | }
61 |
62 |
63 | export default { init, draw }
64 |
--------------------------------------------------------------------------------
/figures/triangle-point-overlap.js:
--------------------------------------------------------------------------------
1 | import common from './common.js'
2 | import triPointOverlap from '../src/triangle-point-overlap.js'
3 |
4 |
5 | function init (context, width, height) {
6 | return {
7 | angle: 0,
8 | pos: [ 0, 0 ],
9 | tri: [
10 | [ 0, -20 ],
11 | [ -20, 20 ],
12 | [ 20, 20 ]
13 | ],
14 | ...common.init(context, width, height)
15 | }
16 | }
17 |
18 |
19 | function draw (data, dt) {
20 | common.clear(data)
21 |
22 | data.angle += 0.5 * Math.PI * dt
23 | data.pos[0] = Math.cos(data.angle * 0.4) * 32
24 | data.pos[1] = Math.sin(data.angle) * 12
25 |
26 | const overlapping = triPointOverlap(data.tri[0], data.tri[1], data.tri[2], data.pos)
27 |
28 | common.drawTriangle(data, data.tri[0], data.tri[1], data.tri[2], '#666')
29 |
30 | const pointWidth = 1
31 |
32 | if (overlapping)
33 | common.drawPoint(data, data.pos, '#f00', '', pointWidth)
34 | else
35 | common.drawPoint(data, data.pos, '#0f0', '', pointWidth)
36 | }
37 |
38 |
39 | export default { init, draw }
40 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@footgun/collision-2d",
3 | "version": "0.2.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "@footgun/collision-2d",
9 | "version": "0.2.0",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@footgun/math-gap": "^0.2.0",
13 | "clamp": "^1.0.1",
14 | "gl-matrix": "^3.4.3",
15 | "point-to-segment-2d": "^1.0.0",
16 | "robust-point-in-polygon": "^1.0.3",
17 | "segseg": "^1.0.0",
18 | "wgpu-matrix": "^3.4.0"
19 | }
20 | },
21 | "node_modules/@footgun/math-gap": {
22 | "version": "0.2.0",
23 | "resolved": "https://registry.npmjs.org/@footgun/math-gap/-/math-gap-0.2.0.tgz",
24 | "integrity": "sha512-sGhfGo5OOEGAv4j4eHvECDwgnX2IDdx1Xd2MJeiFAyP9b/htmopRd2PNFQzQETxEuMBj+I5M5Odeky+AA+CFjQ==",
25 | "license": "MIT"
26 | },
27 | "node_modules/clamp": {
28 | "version": "1.0.1",
29 | "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz",
30 | "integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==",
31 | "license": "MIT"
32 | },
33 | "node_modules/gl-matrix": {
34 | "version": "3.4.3",
35 | "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
36 | "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==",
37 | "license": "MIT"
38 | },
39 | "node_modules/point-to-segment-2d": {
40 | "version": "1.0.0",
41 | "resolved": "https://registry.npmjs.org/point-to-segment-2d/-/point-to-segment-2d-1.0.0.tgz",
42 | "integrity": "sha512-qjMaW51jOWDjvNSEntnxiGCvfWKu64Iiu6FVRaCWB5hBlZG7adxhx1AG3sXEwgf7v7e+o0y7TpO65OgumpDhfg==",
43 | "license": "MIT",
44 | "engines": {
45 | "node": ">=12"
46 | }
47 | },
48 | "node_modules/robust-orientation": {
49 | "version": "1.2.1",
50 | "resolved": "https://registry.npmjs.org/robust-orientation/-/robust-orientation-1.2.1.tgz",
51 | "integrity": "sha512-FuTptgKwY6iNuU15nrIJDLjXzCChWB+T4AvksRtwPS/WZ3HuP1CElCm1t+OBfgQKfWbtZIawip+61k7+buRKAg==",
52 | "license": "MIT",
53 | "dependencies": {
54 | "robust-scale": "^1.0.2",
55 | "robust-subtract": "^1.0.0",
56 | "robust-sum": "^1.0.0",
57 | "two-product": "^1.0.2"
58 | }
59 | },
60 | "node_modules/robust-point-in-polygon": {
61 | "version": "1.0.3",
62 | "resolved": "https://registry.npmjs.org/robust-point-in-polygon/-/robust-point-in-polygon-1.0.3.tgz",
63 | "integrity": "sha512-pPzz7AevOOcPYnFv4Vs5L0C7BKOq6C/TfAw5EUE58CylbjGiPyMjAnPLzzSuPZ2zftUGwWbmLWPOjPOz61tAcA==",
64 | "license": "MIT",
65 | "dependencies": {
66 | "robust-orientation": "^1.0.2"
67 | }
68 | },
69 | "node_modules/robust-scale": {
70 | "version": "1.0.2",
71 | "resolved": "https://registry.npmjs.org/robust-scale/-/robust-scale-1.0.2.tgz",
72 | "integrity": "sha512-jBR91a/vomMAzazwpsPTPeuTPPmWBacwA+WYGNKcRGSh6xweuQ2ZbjRZ4v792/bZOhRKXRiQH0F48AvuajY0tQ==",
73 | "license": "MIT",
74 | "dependencies": {
75 | "two-product": "^1.0.2",
76 | "two-sum": "^1.0.0"
77 | }
78 | },
79 | "node_modules/robust-subtract": {
80 | "version": "1.0.0",
81 | "resolved": "https://registry.npmjs.org/robust-subtract/-/robust-subtract-1.0.0.tgz",
82 | "integrity": "sha512-xhKUno+Rl+trmxAIVwjQMiVdpF5llxytozXJOdoT4eTIqmqsndQqFb1A0oiW3sZGlhMRhOi6pAD4MF1YYW6o/A==",
83 | "license": "MIT"
84 | },
85 | "node_modules/robust-sum": {
86 | "version": "1.0.0",
87 | "resolved": "https://registry.npmjs.org/robust-sum/-/robust-sum-1.0.0.tgz",
88 | "integrity": "sha512-AvLExwpaqUqD1uwLU6MwzzfRdaI6VEZsyvQ3IAQ0ZJ08v1H+DTyqskrf2ZJyh0BDduFVLN7H04Zmc+qTiahhAw==",
89 | "license": "MIT"
90 | },
91 | "node_modules/segseg": {
92 | "version": "1.0.0",
93 | "resolved": "https://registry.npmjs.org/segseg/-/segseg-1.0.0.tgz",
94 | "integrity": "sha512-2yXmyV1AJN9F9CFlUaQ0XLo0zh8C/BgB91ae8oouDw2yvH6uCXkxOvRocbvgAJmrxssiUn3PgfzNv+0yA4OmYA==",
95 | "license": "MIT",
96 | "engines": {
97 | "node": ">=12"
98 | }
99 | },
100 | "node_modules/two-product": {
101 | "version": "1.0.2",
102 | "resolved": "https://registry.npmjs.org/two-product/-/two-product-1.0.2.tgz",
103 | "integrity": "sha512-vOyrqmeYvzjToVM08iU52OFocWT6eB/I5LUWYnxeAPGXAhAxXYU/Yr/R2uY5/5n4bvJQL9AQulIuxpIsMoT8XQ==",
104 | "license": "MIT"
105 | },
106 | "node_modules/two-sum": {
107 | "version": "1.0.0",
108 | "resolved": "https://registry.npmjs.org/two-sum/-/two-sum-1.0.0.tgz",
109 | "integrity": "sha512-phP48e8AawgsNUjEY2WvoIWqdie8PoiDZGxTDv70LDr01uX5wLEQbOgSP7Z/B6+SW5oLtbe8qaYX2fKJs3CGTw==",
110 | "license": "MIT"
111 | },
112 | "node_modules/wgpu-matrix": {
113 | "version": "3.4.0",
114 | "resolved": "https://registry.npmjs.org/wgpu-matrix/-/wgpu-matrix-3.4.0.tgz",
115 | "integrity": "sha512-kXHrbAPKEn9A32Wf4wVldyx9MmnzwhuB5p8GCqoJP3ItU5+iDT4J3aTQwPZWkfb153hwGtqZtUwR2M+ipJKadg==",
116 | "license": "MIT"
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@footgun/collision-2d",
3 | "version": "0.3.0",
4 | "description": "A collection of collision detection and response functions for 2D",
5 | "main": "collision-2d.js",
6 | "type": "module",
7 | "directories": {
8 | "doc": "docs",
9 | "test": "test"
10 | },
11 | "scripts": {
12 | "build:figures": "./build-figures.sh",
13 | "prepublishOnly": "npm test",
14 | "test": "node --test ./test/*.js"
15 | },
16 | "keywords": [
17 | "collision",
18 | "2d",
19 | "physics",
20 | "geometry"
21 | ],
22 | "author": "Michael Reinstein",
23 | "license": "MIT",
24 | "repository": {
25 | "type": "git",
26 | "url": "git@github.com:mreinstein/collision-2d.git"
27 | },
28 | "dependencies": {
29 | "@footgun/math-gap": "^0.2.0",
30 | "clamp": "^1.0.1",
31 | "gl-matrix": "^3.4.3",
32 | "point-to-segment-2d": "^1.0.0",
33 | "robust-point-in-polygon": "^1.0.3",
34 | "segseg": "^1.0.0",
35 | "wgpu-matrix": "^3.4.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/AABB.js:
--------------------------------------------------------------------------------
1 | // generate a bounding AABB from another primitive
2 |
3 |
4 | // create an AABB containing all 2D points
5 | export function fromPoints (points) {
6 | if (!points.length)
7 | throw new Error('no points specified')
8 |
9 | let minX, maxX, minY, maxY
10 |
11 | minX = maxX = points[0][0]
12 | minY = maxY = points[0][1]
13 |
14 | for (let i=1; i < points.length; i++) {
15 | minX = Math.min(minX, points[i][0])
16 | maxX = Math.max(maxX, points[i][0])
17 | minY = Math.min(minY, points[i][1])
18 | maxY = Math.max(maxY, points[i][1])
19 | }
20 |
21 | const width = maxX - minX
22 | const height = maxY - minY
23 |
24 | return {
25 | position: [ minX + (width / 2), minY + (height / 2)],
26 | width,
27 | height
28 | }
29 | }
30 |
31 |
32 | // create an AABB containing all line segments
33 | export function fromSegments (segments, indices) {
34 | if (!segments.length)
35 | throw new Error('no segments specified')
36 |
37 | if (!indices.length)
38 | throw new Error('no indices specified')
39 |
40 | let minX, maxX, minY, maxY
41 |
42 | const seg = segments[indices[0]]
43 | minX = maxX = seg[0][0]
44 | minY = maxY = seg[0][1]
45 |
46 | for (const idx of indices) {
47 | const seg = segments[idx]
48 | minX = Math.min(minX, seg[0][0])
49 | minX = Math.min(minX, seg[1][0])
50 |
51 | maxX = Math.max(maxX, seg[0][0])
52 | maxX = Math.max(maxX, seg[1][0])
53 |
54 | minY = Math.min(minY, seg[0][1])
55 | minY = Math.min(minY, seg[1][1])
56 |
57 | maxY = Math.max(maxY, seg[0][1])
58 | maxY = Math.max(maxY, seg[1][1])
59 | }
60 |
61 | const width = maxX - minX
62 | const height = maxY - minY
63 |
64 | return {
65 | position: [ minX + (width / 2), minY + (height / 2)],
66 | width,
67 | height
68 | }
69 | }
70 |
71 |
72 | // create an AABB containing all triangle vertices
73 | export function fromTriangle (p0, p1, p2) {
74 | const minX = Math.min(p0[0], p1[0], p2[0])
75 | const maxX = Math.max(p0[0], p1[0], p2[0])
76 |
77 | const minY = Math.min(p0[1], p1[1], p2[1])
78 | const maxY = Math.max(p0[1], p1[1], p2[1])
79 |
80 | const width = maxX - minX
81 | const height = maxY - minY
82 |
83 | return {
84 | position: [ minX + (width / 2), minY + (height / 2)],
85 | width,
86 | height
87 | }
88 | }
89 |
90 |
91 | // create an AABB containing a sphere
92 | export function fromSphere (center, radius) {
93 | return {
94 | position: [ center[0], center[1] ],
95 | width: radius * 2,
96 | height: radius * 2
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Plane.js:
--------------------------------------------------------------------------------
1 | // plane implementation adapted from Appendix B of http://www.peroxide.dk/papers/collision/collision.pdf
2 | import segmentNormal from './segment-normal.js'
3 | import { vec2 } from 'gl-matrix'
4 |
5 |
6 | function create () {
7 | return {
8 | origin: vec2.create(),
9 | normal: vec2.create(),
10 | D: 0,
11 | }
12 | }
13 |
14 |
15 | // create a new plane from a normal and origin (a point on the plane)
16 | function fromPlane (out, origin, normal) {
17 | vec2.copy(out.origin, origin)
18 | vec2.copy(out.normal, normal)
19 | out.D = -(normal[0] * origin[0] + normal[1] * origin[1])
20 | return out
21 | }
22 |
23 |
24 | // create a new plane from a line segment
25 | function fromSegment (out, p0, p1) {
26 | vec2.copy(out.origin, p0)
27 | segmentNormal(out.normal, p0, p1)
28 | out.D = -(out.normal[0] * out.origin[0] + out.normal[1] * out.origin[1])
29 | return out
30 | }
31 |
32 |
33 | function isFrontFacingTo (plane, dir) {
34 | const dot = vec2.dot(plane.normal, dir)
35 | return dot <= 0
36 | }
37 |
38 |
39 | // returns the distance from the ray origin to the plane along the ray
40 | // @param vec2 rOrigin starting point of ray
41 | // @param vec2 rVector must be normalized
42 | // @return Number the distance from the ray origin to the plane along the ray
43 | function rayDistance (plane, rOrigin, rVector) {
44 | const numer = vec2.dot(plane.normal, rOrigin) + plane.D
45 | const denom = vec2.dot(plane.normal, rVector)
46 | if (denom === 0) // normal is orthogonal to vector, cant intersect
47 | return -1
48 |
49 | return -(numer / denom)
50 | }
51 |
52 |
53 | // determine how far a point is from a plane
54 | // returns a signed number indicating plane distance (negative numbers means behind the plane)
55 | function signedDistanceTo (plane, point) {
56 | return vec2.dot(point, plane.normal) + plane.D
57 | }
58 |
59 |
60 | export default { create, fromPlane, fromSegment, isFrontFacingTo, rayDistance, signedDistanceTo }
61 |
--------------------------------------------------------------------------------
/src/Polygon.js:
--------------------------------------------------------------------------------
1 | import pointInPoly from 'robust-point-in-polygon'
2 | import aabbPointOverlap from './aabb-point-overlap.js'
3 | import { vec2 } from 'wgpu-matrix'
4 |
5 |
6 | export function isPointInsidePolygon (point, polygon) {
7 | if (!aabbPointOverlap(polygon.aabb, point))
8 | return false
9 |
10 | const OUTSIDE_POLYGON = 1
11 | if (pointInPoly(polygon.points, point) === OUTSIDE_POLYGON)
12 | return false
13 |
14 | return true
15 | }
16 |
17 |
18 | // find the closest point on the edge of a polygon to a given point.
19 | // assumes the point is outside of the polygon
20 | export function closestPointOnPolygon (point, polygon) {
21 | let closest
22 |
23 | for (let i=0; i < polygon.points.length-1; i++) {
24 | const c = _getClosestPoint(polygon.points[i], polygon.points[i+1], point)
25 | if (!closest || vec2.distance(c, point) < vec2.distance(closest, point))
26 | closest = c
27 | }
28 |
29 | return closest
30 | }
31 |
32 |
33 | // given a line defined by [A, B] and a point P, find the closest point on that line to P
34 | // set 'segmentClamp' to true if you want the closest point on the segment, not just the line.
35 | // from https://www.gamedev.net/forums/topic/444154-closest-point-on-a-line/3941160/
36 | //
37 | // This one is unused but maybe good inspiration if the prev link's logic doesn't work out:
38 | // //https://math.stackexchange.com/questions/2193720/find-a-point-on-a-line-segment-which-is-the-closest-to-other-point-not-on-the-li
39 | function _getClosestPoint (A, B, P, segmentClamp=true) {
40 | const AP = vec2.subtract(P, A)
41 | const AB = vec2.subtract(B, A)
42 |
43 | const ab2 = AB[0]*AB[0] + AB[1]*AB[1]
44 | const ap_ab = AP[0]*AB[0] + AP[1]*AB[1]
45 | let t = ap_ab / ab2
46 |
47 | if (segmentClamp) {
48 | if (t < 0.0)
49 | t = 0.0
50 | else if (t > 1.0)
51 | t = 1.0
52 | }
53 |
54 | return vec2.addScaled(A, AB, t)
55 | }
56 |
57 | ///////////////////////////
--------------------------------------------------------------------------------
/src/PolylinePath.js:
--------------------------------------------------------------------------------
1 | import { vec2 } from 'gl-matrix'
2 |
3 |
4 | // this module was ported from OpenSteer's Pathway.cpp implementation
5 |
6 | // static temp variables used to avoid mem allocations
7 | const tmpSegDistance = {
8 | segmentProjection: 0,
9 | distance: 0,
10 | chosen: vec2.create()
11 | }
12 |
13 | const segmentNormal = vec2.create()
14 |
15 | const local = vec2.create()
16 |
17 |
18 | // a simple implementation of the Pathway protocol. The path is a "polyline" a series of line
19 | // segments between specified points. A radius defines a volume for the path which is the union
20 | // of a sphere at each point and a cylinder along each segment.
21 | export function create (options={}) {
22 | // construct a PolylinePathway given the number of points (vertices),
23 | // an array of points, and a path radius.
24 | let { points, pointCount, radius, cyclic } = options
25 |
26 | if (cyclic)
27 | pointCount++
28 |
29 | const normals = [ ]
30 | const lengths = [ ]
31 | let totalPathLength = 0
32 |
33 | // loop over all points
34 | for (let i = 0; i < pointCount; i++) {
35 | // copy in point locations, closing cycle when appropriate
36 | const closeCycle = cyclic && (i == pointCount-1)
37 | const j = closeCycle ? 0 : i
38 | points[i] = points[j]
39 |
40 | // for the end of each segment
41 | if (i > 0) {
42 | // compute the segment length
43 | normals[i] = vec2.subtract(vec2.create(), points[i], points[i-1])
44 | lengths[i] = vec2.length(normals[i])
45 |
46 | // find the normalized vector parallel to the segment
47 | vec2.normalize(normals[i], normals[i])
48 |
49 | // keep running total of segment lengths
50 | totalPathLength += lengths[i]
51 | }
52 | }
53 |
54 | return {
55 | pointCount,
56 | points,
57 | radius,
58 | cyclic,
59 |
60 | // lengths of each line segment between the points
61 | lengths,
62 |
63 | // unit vectors of each line segment between the points
64 | normals,
65 |
66 | totalPathLength
67 | }
68 | }
69 |
70 |
71 | // Given an arbitrary point ("A"), returns the nearest point ("P") on
72 | // this path. Also returns, via output arguments, the path tangent at
73 | // P and a measure of how far A is outside the Pathway's "tube". Note
74 | // that a negative distance indicates A is inside the Pathway.
75 | export function mapPointToPath (out, path, point) {
76 | let d, minDistance
77 |
78 | // loop over all segments, find the one nearest to the given point
79 | for (let i = 1; i < path.pointCount; i++) {
80 | pointToSegmentDistance(tmpSegDistance, point, path.points[i-1], path.points[i])
81 | d = tmpSegDistance.distance
82 | if ((minDistance === undefined) || d < minDistance) {
83 | minDistance = d
84 | vec2.copy(out.onPath, tmpSegDistance.chosen) // set the point on the path
85 |
86 | vec2.subtract(out.tangent, path.points[i], path.points[i-1])
87 | vec2.normalize(out.tangent, out.tangent)
88 | }
89 | }
90 |
91 | // measure how far original point is outside the Pathway's "tube"
92 | out.outside = vec2.distance(out.onPath, point) - path.radius
93 |
94 | return out
95 | }
96 |
97 |
98 | // given an arbitrary point, convert it to a distance along the path
99 | export function mapPointToPathDistance (path, point) {
100 | let d, minDistance
101 | let segmentLengthTotal = 0
102 | let pathDistance = 0
103 |
104 | for (let i = 1; i < path.pointCount; i++) {
105 | pointToSegmentDistance(tmpSegDistance, point, path.points[i-1], path.points[i])
106 | d = tmpSegDistance.distance
107 | const segmentLength = path.lengths[i]
108 |
109 | if ((minDistance === undefined) || d < minDistance) {
110 | minDistance = d
111 | pathDistance = segmentLengthTotal + tmpSegDistance.segmentProjection
112 | }
113 | segmentLengthTotal += segmentLength
114 | }
115 |
116 | // return distance along path of onPath point
117 | return pathDistance
118 | }
119 |
120 |
121 | // given a distance along the path, convert it to a point on the path
122 | export function mapPathDistanceToPoint (out, path, pathDistance) {
123 | // clip or wrap given path distance according to cyclic flag
124 | let remaining = pathDistance
125 |
126 | if (path.cyclic) {
127 | remaining = pathDistance % path.totalPathLength //fmod(pathDistance, totalPathLength)
128 | } else {
129 | if (pathDistance < 0)
130 | return vec2.copy(out, path.points[0])
131 | if (pathDistance >= path.totalPathLength)
132 | return vec2.copy(out, path.points[path.pointCount-1])
133 | }
134 |
135 | // step through segments, subtracting off segment lengths until
136 | // locating the segment that contains the original pathDistance.
137 | // Interpolate along that segment to find 3d point value to return.
138 | for (let i = 1; i < path.pointCount; i++) {
139 | const segmentLength = path.lengths[i]
140 | if (segmentLength < remaining) {
141 | remaining -= segmentLength
142 | } else {
143 | const ratio = remaining / segmentLength
144 | //result = interpolate(ratio, points[i-1], points[i])
145 | vec2.lerp(out, path.points[i-1], path.points[i], ratio)
146 | break
147 | }
148 | }
149 | return out
150 | }
151 |
152 |
153 | // TODO: compare this against https://www.npmjs.com/package/point-to-segment-2d
154 | // They claim to do the same things. Is that true? Is one better than the other?
155 | //
156 | // computes distance from a point to a line segment
157 | function pointToSegmentDistance (out, point, ep0, ep1) {
158 |
159 | vec2.subtract(segmentNormal, ep1, ep0)
160 | vec2.normalize(segmentNormal, segmentNormal)
161 |
162 | const segmentLength = vec2.distance(ep0, ep1)
163 |
164 | // convert the test point to be "local" to ep0
165 | vec2.subtract(local, point, ep0)
166 |
167 | // find the projection of "local" onto "segmentNormal"
168 | const segmentProjection = vec2.dot(segmentNormal, local)
169 |
170 | // handle boundary cases: when projection is not on segment, the
171 | // nearest point is one of the endpoints of the segment
172 | if (segmentProjection < 0) {
173 | vec2.copy(out.chosen, ep0)
174 | out.segmentProjection = 0
175 | out.distance = vec2.distance(point, ep0)
176 | return out
177 | }
178 |
179 | if (segmentProjection > segmentLength) {
180 | vec2.copy(out.chosen, ep1)
181 | out.segmentProjection = segmentLength
182 | out.distance = vec2.distance(point, ep1)
183 | return out
184 | }
185 |
186 | // otherwise nearest point is projection point on segment
187 | vec2.scale(out.chosen, segmentNormal, segmentProjection)
188 | vec2.add(out.chosen, out.chosen, ep0)
189 |
190 | out.segmentProjection = segmentProjection
191 | out.distance = vec2.distance(point, out.chosen)
192 | return out
193 | }
194 |
--------------------------------------------------------------------------------
/src/TraceInfo.js:
--------------------------------------------------------------------------------
1 | import { vec2 } from 'gl-matrix'
2 |
3 |
4 | export default function TraceInfo() {
5 | this.start = vec2.create()
6 | this.end = vec2.create()
7 | this.scaledStart = vec2.create()
8 | this.radius = 0
9 | this.invRadius = 0
10 | this.vel = vec2.create()
11 | this.scaledVel = vec2.create()
12 | this.velLength = 0
13 | this.normVel = vec2.create()
14 | this.collision = false
15 |
16 | this.t = 0 // time of collision from 0..1 when collision is true
17 | this.intersectPoint = vec2.create() // point on the triangle where the sphere collided
18 |
19 | this.tmp = vec2.create()
20 | this.tmpTri = [ vec2.create(), vec2.create() ]
21 | this.intersectTri = [ vec2.create(), vec2.create() ]
22 |
23 | this.tmpTriNorm = vec2.create()
24 | this.intersectTriNorm = vec2.create()
25 | }
26 |
27 |
28 | TraceInfo.prototype.resetTrace = function(start, end, radius) {
29 | this.invRadius = 1/radius
30 | this.radius = radius
31 |
32 | vec2.copy(this.start, start)
33 | vec2.copy(this.end, end)
34 | vec2.subtract(this.vel, end, start)
35 | vec2.normalize(this.normVel, this.vel)
36 |
37 | vec2.scale(this.scaledStart, start, this.invRadius)
38 | vec2.scale(this.scaledVel, this.vel, this.invRadius)
39 |
40 | this.velLength = vec2.length(this.vel)
41 |
42 | this.collision = false
43 | this.t = 1.0
44 | }
45 |
46 |
47 | TraceInfo.prototype.setCollision = function(t, point) {
48 | this.collision = true
49 | vec2.copy(this.intersectTri[0], this.tmpTri[0])
50 | vec2.copy(this.intersectTri[1], this.tmpTri[1])
51 | vec2.copy(this.intersectTriNorm, this.tmpTriNorm)
52 | if (t < this.t) {
53 | this.t = t
54 | vec2.scale(this.intersectPoint, point, this.radius)
55 | }
56 | }
57 |
58 |
59 | // position of the sphere when it collided with the closest triangle
60 | TraceInfo.prototype.getTraceEndpoint = function(end) {
61 | vec2.scale(this.tmp, this.vel, this.t)
62 | vec2.add(end, this.start, this.tmp)
63 | return end
64 | }
65 |
66 |
67 | TraceInfo.prototype.getTraceDistance = function() {
68 | vec2.scale(this.tmp, this.vel, this.t)
69 | return vec2.length(this.tmp)
70 | }
71 |
--------------------------------------------------------------------------------
/src/aabb-aabb-contain.js:
--------------------------------------------------------------------------------
1 | // returns true if A fully contains B (B is fully inside of the bounds of A)
2 | export default function aabbAABBContain (a, b) {
3 | // B extends to the left of A
4 | if (b.position[0] - (b.width/2) < a.position[0] - (a.width/2))
5 | return false
6 |
7 | // B extends to the right of A
8 | if (b.position[0] + (b.width/2) > a.position[0] + (a.width/2))
9 | return false
10 |
11 | // B extends above A
12 | if (b.position[1] - (b.height/2) < a.position[1] - (a.height/2))
13 | return false
14 |
15 | // B extends below A
16 | if (b.position[1] + (b.height/2) > a.position[1] + (a.height/2))
17 | return false
18 |
19 | return true
20 | }
21 |
--------------------------------------------------------------------------------
/src/aabb-aabb-overlap.js:
--------------------------------------------------------------------------------
1 | import { sign } from '@footgun/math-gap'
2 | import { vec2 } from 'gl-matrix'
3 |
4 |
5 | /*
6 | https://noonat.github.io/intersect/#aabb-vs-aabb
7 |
8 | This test uses a separating axis test, which checks for overlaps between the
9 | two boxes on each axis. If either axis is not overlapping, the boxes aren’t
10 | colliding.
11 |
12 | The function returns a Hit object, or null if the two static boxes do not
13 | overlap, and gives the axis of least overlap as the contact point. That is, it
14 | sets hit.delta so that the colliding box will be pushed out of the nearest edge
15 | This can cause weird behavior for moving boxes, so you should use sweepAABB
16 | instead for moving boxes.
17 |
18 | @param Object contact if specified, filled with collision details
19 | */
20 | export default function aabbAABBOverlap (rect, rect2, contact=null) {
21 |
22 | const dx = rect2.position[0] - rect.position[0]
23 | const px = (rect.width / 2 + rect2.width / 2) - Math.abs(dx)
24 |
25 | if (px <= 0)
26 | return false
27 |
28 | const dy = rect2.position[1] - rect.position[1]
29 | const py = (rect.height / 2 + rect2.height / 2) - Math.abs(dy)
30 |
31 | if (py <= 0)
32 | return false
33 |
34 | // if we don't have to provide details on the collision, it's sufficient to
35 | // return true, indicating the rectangles do intersect
36 | if (!contact)
37 | return true
38 |
39 | /*
40 | pos is the point of contact between the two objects (or an estimation of it, in some sweep tests).
41 | normal is the surface normal at the point of contact.
42 | delta is the overlap between the two objects, and is a vector that can be added to the colliding object’s position to move it back to a non-colliding state.
43 | */
44 | contact.collider = rect
45 | vec2.set(contact.delta, 0, 0)
46 | vec2.set(contact.normal, 0, 0)
47 | contact.time = 0 // boxes overlap
48 |
49 | if (px < py) {
50 | const sx = sign(dx)
51 | contact.delta[0] = px * sx
52 | contact.normal[0] = sx
53 | contact.position[0] = rect.position[0] + (rect.width / 2 * sx)
54 | contact.position[1] = rect2.position[1]
55 | } else {
56 | const sy = sign(dy)
57 | contact.delta[1] = py * sy
58 | contact.normal[1] = sy
59 | contact.position[0] = rect2.position[0]
60 | contact.position[1] = rect.position[1] + (rect.height / 2 * sy)
61 | }
62 |
63 | return true
64 | }
65 |
--------------------------------------------------------------------------------
/src/aabb-aabb-sweep1.js:
--------------------------------------------------------------------------------
1 | import clamp from 'clamp'
2 | import intersectAABB from './aabb-aabb-overlap.js'
3 | import intersectLine from './aabb-segment-overlap.js'
4 | import { vec2 } from 'gl-matrix'
5 |
6 |
7 | const EPSILON = 1e-8
8 |
9 | /*
10 | finds the intersection of this box and another moving box, where the delta
11 | argument is the displacement vector of the moving box
12 | https://noonat.github.io/intersect/#aabb-vs-swept-aabb
13 |
14 | @param aabb the static box
15 | @param aabb2 the moving box
16 | @param delta the displacement vector of aabb2
17 | @param contact the contact object. filled if collision occurs
18 | @return bool true if the two AABBs collide, false otherwise
19 | */
20 | export default function aabbAABBSweep1 (aabb, aabb2, delta, contact) {
21 | if (delta[0] === 0 && delta[1] === 0)
22 | return intersectAABB(aabb, aabb2, contact)
23 |
24 | const result = intersectLine(aabb, aabb2.position, delta, aabb2.width/2, aabb2.height/2, contact)
25 | if (result) {
26 | contact.time = clamp(contact.time - EPSILON, 0, 1)
27 |
28 | //contact.position[0] = aabb2.position[0] + delta[0] * contact.time
29 | //contact.position[1] = aabb2.position[1] + delta[1] * contact.time
30 |
31 | const direction = vec2.normalize([], delta)
32 |
33 | const halfX2 = aabb2.width / 2
34 | const halfY2 = aabb2.height / 2
35 |
36 | const halfX = aabb.width / 2
37 | const halfY = aabb.height / 2
38 |
39 | contact.position[0] = clamp(contact.position[0] + direction[0] * halfX2, aabb.position[0] - halfX, aabb.position[0] + halfX);
40 | contact.position[1] = clamp(contact.position[1] + direction[1] * halfY2, aabb.position[1] - halfY, aabb.position[1] + halfY);
41 | }
42 |
43 | return result
44 | }
45 |
--------------------------------------------------------------------------------
/src/aabb-aabb-sweep2.js:
--------------------------------------------------------------------------------
1 | import aabbAABBOverlap from './aabb-aabb-overlap.js'
2 | import aabbAabbSweep1 from './aabb-aabb-sweep1.js'
3 | import { vec2 } from 'gl-matrix'
4 |
5 |
6 | const pos2 = vec2.create()
7 | const dir = vec2.create()
8 | const amt = vec2.create()
9 |
10 |
11 | // Sweep two moving AABBs to see if and when they first and last were overlapping
12 | // https://www.gamedeveloper.com/disciplines/simple-intersection-tests-for-games
13 | //
14 | // A previous state of AABB A
15 | // B previous state of AABB B
16 | // va displacment vector of A
17 | // vb displacement vector of B
18 | // contact
19 | export default function aabbAABBSweep2 (A, va, B, vb, contact) {
20 | const delta = vec2.subtract([], vb, va)
21 | const hit = aabbAabbSweep1(A, B, delta, contact)
22 |
23 | if (hit) {
24 | // tweak collision position
25 |
26 | contact.position[0] = A.position[0] + va[0] * contact.time
27 | contact.position[1] = A.position[1] + va[1] * contact.time
28 |
29 | pos2[0] = B.position[0] + vb[0] * contact.time
30 | pos2[1] = B.position[1] + vb[1] * contact.time
31 |
32 | vec2.subtract(dir, pos2, contact.position)
33 | vec2.scale(amt, dir, 0.5)
34 |
35 | contact.position[0] += amt[0]
36 | contact.position[1] += amt[1]
37 | }
38 |
39 | return hit
40 | }
41 |
--------------------------------------------------------------------------------
/src/aabb-point-overlap.js:
--------------------------------------------------------------------------------
1 | import { sign } from '@footgun/math-gap'
2 | import { vec2 } from 'gl-matrix'
3 |
4 |
5 | /*
6 | https://noonat.github.io/intersect/#aabb-vs-point
7 |
8 | If a point is behind all of the edges of the box, it’s colliding.
9 | The function returns true if the point is in the aabb, false otherwise
10 |
11 | contact.position and contact.delta will be set to the nearest edge of the box.
12 |
13 | This code first finds the overlap on the X and Y axis. If the overlap is less than zero for either,
14 | a collision is not possible. Otherwise, we find the axis with the smallest overlap and use that to
15 | create an intersection point on the edge of the box.
16 |
17 | @param Object contact if specified, filled with collision details
18 | */
19 | export default function aabbPointOverlap (aabb, point, contact=null) {
20 | const halfX = aabb.width / 2
21 |
22 | const dx = point[0] - aabb.position[0]
23 | const px = halfX - Math.abs(dx)
24 | if (px <= 0)
25 | return false
26 |
27 | const halfY = aabb.height / 2
28 | const dy = point[1] - aabb.position[1]
29 | const py = halfY - Math.abs(dy)
30 | if (py <= 0)
31 | return false
32 |
33 | if (contact) {
34 | if (px < py) {
35 | const sx = sign(dx)
36 | vec2.set(contact.delta, px * sx, 0)
37 | vec2.set(contact.normal, sx, 0)
38 | contact.position[0] = aabb.position[0] + (halfX * sx)
39 | contact.position[1] = point[1]
40 | } else {
41 | const sy = sign(dy)
42 | vec2.set(contact.delta, 0, py * sy)
43 | vec2.set(contact.normal, 0, sy)
44 | contact.position[0] = point[0]
45 | contact.position[1] = aabb.position[1] + (halfY * sy)
46 | }
47 |
48 | contact.time = 1
49 | contact.collider = point
50 | }
51 |
52 | return true
53 | }
54 |
--------------------------------------------------------------------------------
/src/aabb-segment-overlap.js:
--------------------------------------------------------------------------------
1 | import clamp from 'clamp'
2 | import { sign } from '@footgun/math-gap'
3 |
4 |
5 | /*
6 | determine if a line segment intersects a bounding box
7 | https://noonat.github.io/intersect/#aabb-vs-segment
8 |
9 | @param object rect bounding box to check, with { position, width, height }
10 | @param vec2 pos line segment origin/start position
11 | @param vec2 delta line segment move/displacement vector
12 | @param number paddingX added to the radius of the bounding box
13 | @param number paddingY added to the radius of the bounding box
14 | @param object contact physics contact descriptor. filled when argument isn't null and a collision occurs
15 | @return bool true if they intersect, false otherwise
16 | */
17 | export default function aabbSegmentOverlap (rect, pos, delta, paddingX, paddingY, contact=null) {
18 | // if x or y is 0, result will be javascript Infinity
19 | let scaleX = 1.0 / delta[0]
20 | let scaleY = 1.0 / delta[1]
21 |
22 | let signX = sign(scaleX)
23 | let signY = sign(scaleY)
24 | let halfx = rect.width / 2
25 | let halfy = rect.height / 2
26 | let posx = rect.position[0]
27 | let posy = rect.position[1]
28 | let nearTimeX = (posx - signX * (halfx + paddingX) - pos[0]) * scaleX
29 | let nearTimeY = (posy - signY * (halfy + paddingY) - pos[1]) * scaleY
30 | let farTimeX = (posx + signX * (halfx + paddingX) - pos[0]) * scaleX
31 | let farTimeY = (posy + signY * (halfy + paddingY) - pos[1]) * scaleY
32 |
33 | if (Number.isNaN(nearTimeY))
34 | nearTimeY = Infinity
35 |
36 | if (Number.isNaN(farTimeY))
37 | farTimeY = Infinity
38 |
39 | if (nearTimeX > farTimeY || nearTimeY > farTimeX)
40 | return false
41 |
42 | const nearTime = nearTimeX > nearTimeY ? nearTimeX : nearTimeY
43 | const farTime = farTimeX < farTimeY ? farTimeX : farTimeY
44 |
45 | if (nearTime >= 1 || farTime <= 0)
46 | return false
47 |
48 |
49 | // if we don't have to provide details on the collision, it's sufficient to
50 | // return true, indicating the rectangles do intersect
51 | if (!contact)
52 | return true
53 |
54 | /*
55 | position is the point of contact between the two objects (or an estimation of it, in some sweep tests).
56 | normal is the surface normal at the point of contact.
57 | delta is the overlap between the two objects, and is a vector that can be added to the colliding object’s position to move it back to a non-colliding state.
58 | time is a fraction from 0 to 1 indicating how far along the line the collision occurred. (This is the t value for the line equation L(t) = A + t * (B - A))
59 | */
60 | contact.collider = rect
61 | contact.time = clamp(nearTime, 0, 1)
62 | if (nearTimeX > nearTimeY) {
63 | contact.normal[0] = -signX
64 | contact.normal[1] = 0
65 | } else {
66 | contact.normal[0] = 0
67 | contact.normal[1] = -signY
68 | }
69 |
70 | // NEW
71 | contact.delta[0] = (1.0 - contact.time) * -delta[0];
72 | contact.delta[1] = (1.0 - contact.time) * -delta[1];
73 | contact.position[0] = pos[0] + delta[0] * contact.time;
74 | contact.position[1] = pos[1] + delta[1] * contact.time;
75 |
76 | return true
77 | }
78 |
--------------------------------------------------------------------------------
/src/aabb-segment-sweep1.js:
--------------------------------------------------------------------------------
1 | // based on https://gamedev.stackexchange.com/questions/29479/swept-aabb-vs-line-segment-2d
2 |
3 | import segmentNormal from './segment-normal.js'
4 | import { sign } from '@footgun/math-gap'
5 | import { vec2 } from 'gl-matrix'
6 |
7 |
8 | const aabbCenter = vec2.create()
9 | const aabbMin = vec2.create()
10 | const aabbMax = vec2.create()
11 | const lineNormal = vec2.create()
12 | const lineDir = vec2.create()
13 | const lineMin = vec2.create()
14 | const lineMax = vec2.create()
15 | const lineAabbDist = vec2.create()
16 | const hitNormal = vec2.create()
17 | const normalizedDelta = vec2.create()
18 |
19 | const PADDING = 0.005
20 |
21 | // sweep a moving aabb against a line segment
22 | //
23 | //@param line non-moving line segment
24 | //@param aabb moving box
25 | //@param vec2 delta movement vector of the aabb
26 | //@param object contact optional contact data structure filled on hit
27 | //@return boolean true when the box hits the segment
28 | export default function aabbSegmentSweep1 (line, aabb, delta, contact) {
29 |
30 | vec2.copy(aabbCenter, aabb.position)
31 | vec2.set(aabbMin, aabb.position[0] - aabb.width/2, aabb.position[1] - aabb.height/2)
32 | vec2.set(aabbMax, aabb.position[0] + aabb.width/2, aabb.position[1] + aabb.height/2)
33 |
34 | vec2.normalize(normalizedDelta, delta)
35 |
36 | // calculate line bounds
37 | vec2.subtract(lineDir,line[1],line[0])
38 | if (lineDir[0] > 0) {
39 | // right
40 | lineMin[0] = line[0][0]
41 | lineMax[0] = line[1][0]
42 |
43 | } else {
44 | // left
45 | lineMin[0] = line[1][0]
46 | lineMax[0] = line[0][0]
47 | }
48 |
49 | if (lineDir[1] > 0) {
50 | // down
51 | lineMin[1] = line[0][1]
52 | lineMax[1] = line[1][1]
53 |
54 | } else {
55 | // up
56 | lineMin[1] = line[1][1]
57 | lineMax[1] = line[0][1]
58 | }
59 |
60 | // get aabb's center to line[0] distance
61 | vec2.subtract(lineAabbDist, line[0], aabbCenter)
62 |
63 | // get the line's normal
64 | // if the dot product of it and the delta is larger than 0,
65 | // it means the line's normal is facing away from the sweep
66 | segmentNormal(lineNormal, line[0], line[1])
67 | vec2.copy(hitNormal, lineNormal)
68 |
69 | let hitTime = 0 // first overlap time
70 | let outTime = 1 // last overlap time
71 |
72 | // calculate the radius of the box in respect to the line normal
73 | let r = (aabb.width/2) * Math.abs(lineNormal[0]) + (aabb.height/2) * Math.abs(lineNormal[1])
74 |
75 | // distance from box to line in respect to the line normal
76 | const boxProj = vec2.dot(lineAabbDist, lineNormal)
77 |
78 | // velocity, projected on the line normal
79 | const velProj = vec2.dot(delta, lineNormal)
80 |
81 | // inverse the radius if required
82 | if (velProj < 0)
83 | r *= -1
84 |
85 | // calculate first and last overlap times,
86 | // as if we're dealing with a line rather than a segment
87 | hitTime = Math.max((boxProj - r) / velProj, hitTime)
88 | outTime = Math.min((boxProj + r) / velProj, outTime)
89 |
90 | // run standard AABBvsAABB sweep
91 | // against an AABB constructed from the extents of the line segment
92 | // X axis overlap
93 | if (delta[0] < 0) {
94 | // sweeping left
95 | if (aabbMax[0] < lineMin[0])
96 | return false
97 |
98 | const hit = (lineMax[0] - aabbMin[0]) / delta[0]
99 | const out = (lineMin[0] - aabbMax[0]) / delta[0]
100 | outTime = Math.min(out, outTime)
101 | if (hit >= hitTime && hit <= outTime) {
102 | // box is hitting the line on its end:
103 | // adjust the normal accordingly
104 | vec2.set(hitNormal, 1, 0)
105 | }
106 | hitTime = Math.max(hit, hitTime)
107 |
108 | } else if (delta[0] > 0) {
109 | // sweeping right
110 | if (aabbMin[0] > lineMax[0])
111 | return false
112 |
113 | const hit = (lineMin[0] - aabbMax[0]) / delta[0]
114 | const out = (lineMax[0] - aabbMin[0]) / delta[0]
115 | outTime = Math.min(out, outTime)
116 | if (hit >= hitTime && hit <= outTime)
117 | vec2.set(hitNormal, -1, 0)
118 |
119 | hitTime = Math.max(hit, hitTime)
120 |
121 | } else if (lineMin[0] > aabbMax[0] || lineMax[0] < aabbMin[0]) {
122 | return false
123 | }
124 |
125 | if ( hitTime > outTime )
126 | return false
127 |
128 | // Y axis overlap
129 | if (delta[1] < 0) {
130 | // sweeping up
131 | if (aabbMax[1] < lineMin[1])
132 | return false
133 |
134 | const hit = (lineMax[1] - aabbMin[1]) / delta[1]
135 | const out = (lineMin[1] - aabbMax[1]) / delta[1]
136 | outTime = Math.min(out, outTime)
137 | if (hit >= hitTime && hit <= outTime)
138 | vec2.set(hitNormal, 0, 1)
139 |
140 | hitTime = Math.max(hit, hitTime)
141 |
142 | } else if (delta[1] > 0) {
143 | // sweeping down
144 | if (aabbMin[1] > lineMax[1])
145 | return false
146 |
147 | const hit = (lineMin[1] - aabbMax[1]) / delta[1]
148 | const out = (lineMax[1] - aabbMin[1]) / delta[1]
149 | outTime = Math.min(out, outTime)
150 | if (hit >= hitTime && hit <= outTime)
151 | vec2.set(hitNormal, 0, -1)
152 |
153 | hitTime = Math.max(hit, hitTime)
154 |
155 | } else if (lineMin[1] > aabbMax[1] || lineMax[1] < aabbMin[1]) {
156 | return false
157 | }
158 |
159 | if (hitTime > outTime)
160 | return false
161 |
162 | // ignore this line if its normal is facing away from the sweep delta
163 | // check for this only at this point to account for a possibly changed hitNormal
164 | // from a hit on the line's end
165 | //
166 | // also ignore this line its normal is facing away from the adjusted hitNormal
167 | if (vec2.dot(normalizedDelta, hitNormal) > 0 || vec2.dot(lineNormal, hitNormal) < 0)
168 | return false
169 |
170 | if (contact) {
171 | const deltaX = delta[0] * hitTime + (PADDING * hitNormal[0])
172 | const deltaY = delta[1] * hitTime + (PADDING * hitNormal[1])
173 | vec2.set(contact.delta, deltaX, deltaY)
174 | vec2.add(contact.position, aabb.position, contact.delta)
175 | vec2.copy(contact.normal, hitNormal)
176 | contact.collider = line
177 | contact.time = hitTime
178 | }
179 |
180 | return true
181 | }
182 |
--------------------------------------------------------------------------------
/src/aabb-segments-sweep1-indexed.js:
--------------------------------------------------------------------------------
1 | // based on https://gamedev.stackexchange.com/questions/29479/swept-aabb-vs-line-segment-2d
2 | import aabbSegmentSweep1 from './aabb-segment-sweep1.js'
3 | import contact from './contact.js'
4 | import copyContact from './contact-copy.js'
5 | import { vec2 } from 'gl-matrix'
6 |
7 |
8 | const _tmpContact = contact()
9 |
10 |
11 | export default function aabbSegmentSweep1Indexed (lines, indices, lineCount, aabb, delta, contact) {
12 |
13 | let colliderIndex = -1
14 | let resHitTime
15 |
16 | for (let i=0; i= 0
32 | }
33 |
--------------------------------------------------------------------------------
/src/cone-point-overlap.js:
--------------------------------------------------------------------------------
1 | import { toRadians } from '@footgun/math-gap'
2 | import { vec2 } from 'gl-matrix'
3 |
4 |
5 | // static temp variables to avoid creating new ones each invocation
6 | const v1 = vec2.create(), v2 = vec2.create()
7 |
8 |
9 | /*
10 | Determine if a point is within a cone
11 |
12 | @param Vec2 conePosition position of cone origin
13 | @param number coneRotation observer rotation in radians
14 | @param number coneFieldOfView angle of cone in degrees
15 | @param number coneMinDistance min distance from cone position
16 | @param number coneMaxDistance max distance from cone position
17 | @param Object point vec2 position of item being checked against cone
18 | @return Bool true if point is in cone, false otherwise
19 | */
20 | export default function conePointOverlap (conePosition, coneRotation, coneFieldOfView, coneMinDistance, coneMaxDistance, point) {
21 | const dist = vec2.distance(conePosition, point)
22 | if (dist > coneMaxDistance)
23 | return false
24 |
25 | if (dist < coneMinDistance)
26 | return false
27 |
28 | if (coneFieldOfView >= 360)
29 | return true
30 |
31 | vec2.subtract(v1, point, conePosition)
32 | vec2.set(v2, Math.cos(coneRotation), Math.sin(coneRotation))
33 | const angle = vec2.angle(v1, v2)
34 | return angle <= toRadians(coneFieldOfView/2)
35 | }
36 |
--------------------------------------------------------------------------------
/src/contact-copy.js:
--------------------------------------------------------------------------------
1 | import { vec2 } from 'gl-matrix'
2 |
3 |
4 | export default function copyContact (out, contact) {
5 | out.time = contact.time
6 | out.collider = contact.collider
7 | vec2.copy(out.position, contact.position)
8 | vec2.copy(out.delta, contact.delta)
9 | vec2.copy(out.normal, contact.normal)
10 | return out
11 | }
12 |
--------------------------------------------------------------------------------
/src/contact.js:
--------------------------------------------------------------------------------
1 | import { vec2 } from 'gl-matrix'
2 |
3 |
4 | /*
5 | information related to a physics system collision.
6 | position is the point of contact between the two objects (or an estimation of it, in some sweep tests).
7 | normal is the surface normal at the point of contact.
8 | delta is the overlap between the two objects, and is a vector that can be added to the colliding object’s position to move it back to a non-colliding state.
9 | time is a fraction from 0 to 1 indicating how far along the line the collision occurred. (This is the t value for the line equation L(t) = A + t * (B - A))
10 | */
11 | export default function contact () {
12 | return {
13 | // for segments-segment-overlap and segments-sphere-sweep1 this is set to the index in the array of line segments passed into the collision routine
14 | collider : null,
15 | position : vec2.create(),
16 | delta : vec2.create(),
17 | normal : vec2.create(),
18 | time : 0
19 | }
20 | }
--------------------------------------------------------------------------------
/src/cpa-time.js:
--------------------------------------------------------------------------------
1 | import { vec2 } from 'gl-matrix'
2 |
3 |
4 | const EPSILON = 0.00000001
5 |
6 | const v1 = vec2.create()
7 | const v2 = vec2.create()
8 | const dv = vec2.create()
9 | const w0 = vec2.create()
10 |
11 | // compute the time of closest point of approach (CPA) for two tracks
12 | // a track consists of a starting point and a delta vector
13 | //
14 | // Input: two tracks Tr1 and Tr2
15 | // Return: the time at which the two tracks are closest from 0..1
16 | export default function cpaTime (Tr1, Tr2) {
17 | vec2.subtract(v1, Tr1[1], Tr1[0])
18 | vec2.subtract(v2, Tr2[1], Tr2[0])
19 |
20 | vec2.subtract(dv, v1, v2)
21 |
22 | const dv2 = vec2.dot(dv, dv)
23 |
24 | if (dv2 < EPSILON) // the tracks are almost parallel
25 | return 0.0; // any time is ok. Use time 0.
26 |
27 | vec2.subtract(w0, Tr1[0], Tr2[0])
28 |
29 | //Vector w0 = Tr1.P0 - Tr2.P0;
30 |
31 | const cpatime = -vec2.dot(w0, dv) / dv2
32 |
33 | //float cpatime = -dot(w0,dv) / dv2;
34 |
35 | return cpatime; // time of CPA
36 | }
37 |
--------------------------------------------------------------------------------
/src/get-lowest-root.js:
--------------------------------------------------------------------------------
1 | // TODO: Don't like the duality of returning a null or float, probably doesn't optimize nicely
2 | export default function getLowestRoot (a, b, c, maxR) {
3 | // check if a solution exists
4 | const det = b * b - 4.0 * a * c
5 |
6 | // if determinant is negative it means no solutions.
7 | if (det < 0)
8 | return null
9 |
10 | // calculate the two roots: (if determinant == 0 then
11 | // x1==x2 but let’s disregard that slight optimization)
12 | const sqrtDet = Math.sqrt(det)
13 | let r1 = (-b - sqrtDet) / (2.0*a)
14 | let r2 = (-b + sqrtDet) / (2.0*a)
15 |
16 | // sort so x1 <= x2
17 | if (r1 > r2) {
18 | const tmp = r2
19 | r2 = r1
20 | r1 = tmp
21 | }
22 |
23 | // get lowest root:
24 | if (r1 > 0 && r1 < maxR)
25 | return r1
26 |
27 | // it is possible that we want x2 - this can happen if x1 < 0
28 | if (r2 > 0 && r2 < maxR)
29 | return r2
30 |
31 | // No (valid) solutions
32 | return null
33 | }
34 |
--------------------------------------------------------------------------------
/src/point-polygon-overlap.js:
--------------------------------------------------------------------------------
1 | import { vec2 } from 'gl-matrix'
2 |
3 |
4 | const DEFAULT_OFFSET = vec2.create()
5 |
6 |
7 | // determine if a point is inside of a polygon
8 | export default function pointPolygonOverlap (point, segments, polygonOffset=DEFAULT_OFFSET) {
9 |
10 | for (const segment of segments) {
11 | const dx = segment[1][0] - segment[0][0]
12 | const dy = segment[1][1] - segment[0][1]
13 | const startX = segment[0][0] + polygonOffset[0]
14 | const startY = segment[0][1] + polygonOffset[1]
15 |
16 | if (dx === 0) {
17 | const dir = Math.sign(dy)
18 | if ((point[0] - startX) * dir < 0)
19 | return false
20 | } else {
21 | const dir = Math.sign(dx)
22 | const slope = dy / dx
23 | if ((point[1] - (startY + slope * (point[0] - startX))) * dir > 0)
24 | return false
25 | }
26 | }
27 |
28 | return true
29 | }
30 |
--------------------------------------------------------------------------------
/src/ray-sphere-overlap.js:
--------------------------------------------------------------------------------
1 | import { vec2 } from 'gl-matrix'
2 |
3 |
4 | // from http://paulbourke.net/geometry/circlesphere/raysphere.c
5 |
6 | const EPSILON = 1e-8
7 |
8 | const dp = [ 0, 0 ]
9 |
10 |
11 | // Calculate the intersection of an infinite ray and a sphere
12 | // When collision occurs, there are potentially two points of intersection given by
13 | // p = p1 + contact.mu1 (p2 - p1)
14 | // p = p1 + contact.mu2 (p2 - p1)
15 | //
16 | // @param Object p1 vec2 1 point of the segment on the infinite ray
17 | // @param Object p2 vec2 1 point of the segment on the infinite ray
18 | // @param Object sc vec2 sphere center point
19 | // @param Number r sphere radius
20 | // @param Object contact filled in when a collision occurs. e.g., { mu1: 0.34885, mu2: -0.233891 }
21 | // @returns Boolean true if there's an intersection, false otherwise
22 | export default function raySphereOverlap (p1, p2, sc, r, contact) {
23 |
24 | vec2.subtract(dp, p2, p1)
25 |
26 | const a = dp[0] * dp[0] + dp[1] * dp[1]
27 | const b = 2 * (dp[0] * (p1[0] - sc[0]) + dp[1] * (p1[1] - sc[1]))
28 | let c = sc[0] * sc[0] + sc[1] * sc[1]
29 | c += p1[0] * p1[0] + p1[1] * p1[1]
30 | c -= 2 * (sc[0] * p1[0] + sc[1] * p1[1])
31 | c -= r * r;
32 |
33 | const bb4ac = b * b - 4 * a * c
34 |
35 | if (Math.abs(a) < EPSILON || bb4ac < 0) {
36 | contact.mu1 = 0
37 | contact.mu2 = 0
38 | return false
39 | }
40 |
41 | contact.mu1 = (-b + Math.sqrt(bb4ac)) / (2 * a)
42 | contact.mu2 = (-b - Math.sqrt(bb4ac)) / (2 * a)
43 |
44 | return true
45 | }
46 |
--------------------------------------------------------------------------------
/src/segment-normal.js:
--------------------------------------------------------------------------------
1 | import { vec2 } from 'gl-matrix'
2 |
3 |
4 | export default function segmentNormal (out, pos1, pos2) {
5 | let dx = pos2[0] - pos1[0]
6 | let dy = pos2[1] - pos1[1]
7 |
8 | if (dx !== 0)
9 | dx = -dx
10 |
11 | vec2.set(out, dy, dx) // normals: [ -dy, dx ] [ dy, -dx ]
12 | vec2.normalize(out, out)
13 |
14 | return out
15 | }
16 |
--------------------------------------------------------------------------------
/src/segment-point-overlap.js:
--------------------------------------------------------------------------------
1 | import dist from 'point-to-segment-2d'
2 |
3 |
4 | // p - point
5 | // t0 - start point of segment
6 | // t1 - end point of segment
7 | // return boolean indicating if p is on the segment
8 | export default function segmentPointOverlap (p, t0, t1) {
9 | return dist(p, t0, t1) <= 0
10 | }
11 |
--------------------------------------------------------------------------------
/src/segment-segment-overlap.js:
--------------------------------------------------------------------------------
1 | import segseg from 'segseg'
2 |
3 |
4 | export default function segmentSegmentOverlap (pos1, pos2, pos3, pos4, intersection) {
5 | return segseg(intersection, pos1, pos2, pos3, pos4) === 1
6 | }
7 |
--------------------------------------------------------------------------------
/src/segment-sphere-overlap.js:
--------------------------------------------------------------------------------
1 | import raySphereOverlap from './ray-sphere-overlap.js'
2 | import { vec2 } from 'gl-matrix'
3 |
4 |
5 | // Calculate the intersection of a line segment and a sphere
6 | // When collision occurs, there are potentially zero to two points of intersection given by
7 | // p = p1 + contact.mu1 (p2 - p1)
8 | // p = p1 + contact.mu2 (p2 - p1)
9 | //
10 | // @param Object p1 vec2 1 point of the segment
11 | // @param Object p2 vec2 1 point of the segment
12 | // @param Object sc vec2 sphere center point
13 | // @param Number r sphere radius
14 | // @param Object contact filled in when a collision occurs. e.g., { intersectionCount: 2, mu1: 0.34885, mu2: -0.233891 }
15 | // @returns Boolean true if there's an intersection, false otherwise
16 | export default function segmentSphereOverlap (p1, p2, sc, r, contact) {
17 |
18 | const hit = raySphereOverlap(p1, p2, sc, r, contact)
19 |
20 | contact.intersectionCount = 0
21 |
22 | if (hit) {
23 | if (contact.mu1 >= 0 && contact.mu1 <= 1)
24 | contact.intersectionCount++
25 | else
26 | contact.mu1 = NaN
27 |
28 | if (contact.mu2 >= 0 && contact.mu2 <= 1)
29 | contact.intersectionCount++
30 | else
31 | contact.mu2 = NaN
32 | }
33 |
34 | return contact.intersectionCount > 0
35 | }
36 |
--------------------------------------------------------------------------------
/src/segments-ellipsoid-sweep1-indexed.js:
--------------------------------------------------------------------------------
1 | import TraceInfo from './TraceInfo.js'
2 | import toji from './toji-tris.js'
3 | import { vec2 } from 'gl-matrix'
4 |
5 |
6 | const traceInfo = new TraceInfo()
7 | const p0 = vec2.create()
8 | const p1 = vec2.create()
9 | const endPoint = vec2.create()
10 |
11 |
12 | export default function segmentsEllipsoid1Indexed (lines, indices, lineCount, position, ellipsoid, delta, contact) {
13 |
14 | vec2.add(endPoint, position, delta)
15 |
16 | const radius = 1
17 | traceInfo.resetTrace(position, endPoint, radius)
18 |
19 | let collider = -1
20 | for (let i=0; i < lineCount; i++) {
21 | const idx = indices[i]
22 | const line = lines[idx]
23 | const oldT = traceInfo.t
24 |
25 | vec2.divide(p0, line[0], ellipsoid)
26 | vec2.divide(p1, line[1], ellipsoid)
27 |
28 | toji.traceSphereTriangle(p0, p1, traceInfo)
29 | if (traceInfo.collision && oldT !== traceInfo.t)
30 | collider = idx
31 | }
32 |
33 | if (traceInfo.collision) {
34 | contact.time = traceInfo.t
35 |
36 | vec2.copy(contact.position, traceInfo.intersectPoint)
37 | vec2.copy(contact.normal, traceInfo.intersectTriNorm)
38 |
39 | vec2.negate(contact.delta, delta)
40 | vec2.scale(contact.delta, contact.delta, 1-contact.time)
41 |
42 | contact.collider = collider
43 | }
44 |
45 | return traceInfo.collision
46 | }
47 |
--------------------------------------------------------------------------------
/src/segments-segment-overlap-indexed.js:
--------------------------------------------------------------------------------
1 | import lineNormal from './segment-normal.js'
2 | import segseg from './segment-segment-overlap.js'
3 | import { vec2 } from 'gl-matrix'
4 |
5 |
6 | const EPSILON = 1e-8
7 |
8 | const isect = vec2.create() // the intersection if there is one
9 | const end = vec2.create()
10 |
11 |
12 | export default function segmentsSegmentOverlapIndexed (lines, indices, lineCount, start, delta, contact) {
13 | let nearest, nearestTime = 0, nearestIdx = -1
14 |
15 | vec2.set(end, start[0] + delta[0], start[1] + delta[1])
16 |
17 | for (let i=0; i < lineCount; i++) {
18 | const idx = indices[i]
19 | const line = lines[idx]
20 |
21 | if (segseg(start, end, line[0], line[1], isect)) {
22 | const dist = vec2.distance(start, isect)
23 | if (!nearest || dist < nearestTime) {
24 | nearestTime = dist
25 | nearest = line
26 | nearestIdx = idx
27 | }
28 | }
29 | }
30 |
31 | let nearTime = nearestTime / vec2.length(delta)
32 | if (nearTime > 1)
33 | return false
34 |
35 | if (nearTime <= EPSILON)
36 | nearTime = 0
37 |
38 | if (nearest) {
39 | vec2.scaleAndAdd(contact.position, start, delta, nearTime)
40 | contact.collider = nearestIdx
41 |
42 | // determine which normal is on the right side of the plane for the intersection
43 | lineNormal(contact.normal, nearest[0], nearest[1])
44 |
45 | // if dot product is less than 0, flip the normal 180 degrees
46 | const dot = vec2.dot(delta, contact.normal)
47 | if (dot > 0)
48 | vec2.negate(contact.normal, contact.normal)
49 |
50 | contact.time = nearTime
51 | }
52 |
53 | return !!nearest
54 | }
55 |
--------------------------------------------------------------------------------
/src/segments-segment-overlap.js:
--------------------------------------------------------------------------------
1 | import lineNormal from './segment-normal.js'
2 | import segseg from './segment-segment-overlap.js'
3 | import { vec2 } from 'gl-matrix'
4 |
5 |
6 | const EPSILON = 1e-8
7 | const isect = vec2.create() // the intersection if there is one
8 | const end = vec2.create()
9 |
10 |
11 | export default function segmentsSegmentOverlap (lines, start, delta, contact) {
12 | let nearest, nearestTime = 0, nearestIdx = -1
13 |
14 | vec2.set(end, start[0] + delta[0], start[1] + delta[1])
15 |
16 | for (let i=0; i < lines.length; i++) {
17 | const line = lines[i];
18 | if (segseg(start, end, line[0], line[1], isect)) {
19 | const dist = vec2.distance(start, isect)
20 | if (!nearest || dist < nearestTime) {
21 | nearestTime = dist
22 | nearest = line
23 | nearestIdx = i
24 | }
25 | }
26 | }
27 |
28 | let nearTime = nearestTime / vec2.length(delta)
29 | if (nearTime > 1)
30 | return false
31 |
32 | if (nearTime <= EPSILON)
33 | nearTime = 0
34 |
35 | if (nearest) {
36 | vec2.scaleAndAdd(contact.position, start, delta, nearTime)
37 | contact.collider = nearestIdx
38 |
39 | // determine which normal is on the right side of the plane for the intersection
40 | lineNormal(contact.normal, nearest[0], nearest[1])
41 |
42 | // if dot product is less than 0, flip the normal 180 degrees
43 | const dot = vec2.dot(delta, contact.normal)
44 | if (dot > 0)
45 | vec2.negate(contact.normal, contact.normal)
46 |
47 | contact.time = nearTime
48 | }
49 |
50 | return !!nearest
51 | }
52 |
--------------------------------------------------------------------------------
/src/segments-sphere-sweep1-indexed.js:
--------------------------------------------------------------------------------
1 | import TraceInfo from './TraceInfo.js'
2 | import toji from './toji-tris.js'
3 | import { vec2 } from 'gl-matrix'
4 |
5 |
6 | const traceInfo = new TraceInfo()
7 | const endPoint = vec2.create()
8 |
9 |
10 | export default function segmentsSphereSweep1Indexed (lines, indices, lineCount, position, radius, delta, contact) {
11 |
12 | vec2.add(endPoint, position, delta)
13 |
14 | traceInfo.resetTrace(position, endPoint, radius)
15 |
16 | let collider = -1
17 | for (let i=0; i < lineCount; i++) {
18 | const idx = indices[i]
19 | const line = lines[idx]
20 | const oldT = traceInfo.t
21 | toji.traceSphereTriangle(line[0], line[1], traceInfo)
22 | if (traceInfo.collision && oldT !== traceInfo.t)
23 | collider = idx
24 | }
25 |
26 | if (traceInfo.collision) {
27 | contact.time = traceInfo.t
28 |
29 | vec2.copy(contact.position, traceInfo.intersectPoint)
30 | vec2.copy(contact.normal, traceInfo.intersectTriNorm)
31 |
32 | vec2.negate(contact.delta, delta)
33 | vec2.scale(contact.delta, contact.delta, 1-contact.time)
34 |
35 | contact.collider = collider
36 | }
37 |
38 | return traceInfo.collision
39 | }
40 |
--------------------------------------------------------------------------------
/src/segments-sphere-sweep1.js:
--------------------------------------------------------------------------------
1 | import TraceInfo from './TraceInfo.js'
2 | import toji from './toji-tris.js'
3 | import { vec2 } from 'gl-matrix'
4 |
5 |
6 | const traceInfo = new TraceInfo()
7 | const endPoint = vec2.create()
8 |
9 |
10 | export default function segmentsSphereSweep1 (lines, position, radius, delta, contact) {
11 |
12 | vec2.add(endPoint, position, delta)
13 |
14 | traceInfo.resetTrace(position, endPoint, radius)
15 |
16 | let collider = -1
17 | for (let i=0; i < lines.length; i++) {
18 | const line = lines[i]
19 | const oldT = traceInfo.t
20 | toji.traceSphereTriangle(line[0], line[1], traceInfo)
21 | if (traceInfo.collision && oldT !== traceInfo.t)
22 | collider = i
23 | }
24 |
25 | if (traceInfo.collision) {
26 | contact.time = traceInfo.t
27 |
28 | vec2.copy(contact.position, traceInfo.intersectPoint)
29 | vec2.copy(contact.normal, traceInfo.intersectTriNorm)
30 |
31 | vec2.negate(contact.delta, delta)
32 | vec2.scale(contact.delta, contact.delta, 1-contact.time)
33 |
34 | contact.collider = collider
35 | }
36 |
37 | return traceInfo.collision
38 | }
39 |
--------------------------------------------------------------------------------
/src/segseg-closest.js:
--------------------------------------------------------------------------------
1 | // determine the closest point between 2 line segments.
2 | // from http://geomalgorithms.com/a07-_distance.html#dist3D_Segment_to_Segment
3 | import { vec2 } from 'gl-matrix'
4 |
5 |
6 | const SMALL_NUM = 0.00000001 // anything that avoids division overflow
7 |
8 | const u = vec2.create()
9 | const v = vec2.create()
10 | const w = vec2.create()
11 | const s1tmp = vec2.create()
12 | const s2tmp = vec2.create()
13 | const diff = vec2.create()
14 | const dP = vec2.create()
15 |
16 |
17 | // get the 2D minimum distance between 2 segments
18 | // Input: two 2D line segments S1 and S2
19 | // Return: the shortest distance between S1 and S2
20 |
21 | export default function segSegClosest (S1, S2, detail) {
22 | vec2.subtract(u, S1[1], S1[0])
23 | vec2.subtract(v, S2[1], S2[0])
24 | vec2.subtract(w, S1[0], S2[0])
25 |
26 | //Vector u = S1[1] - S1[0];
27 | //Vector v = S2[1] - S2[0];
28 | //Vector w = S1[0] - S2[0];
29 |
30 |
31 | const a = vec2.dot(u, u)
32 | const b = vec2.dot(u, v)
33 | const c = vec2.dot(v, v)
34 | const d = vec2.dot(u, w)
35 | const e = vec2.dot(v, w)
36 | const D = a*c - b*b
37 |
38 | //float a = dot(u,u); // always >= 0
39 | //float b = dot(u,v);
40 | //float c = dot(v,v); // always >= 0
41 | //float d = dot(u,w);
42 | //float e = dot(v,w);
43 | //float D = a*c - b*b; // always >= 0
44 |
45 |
46 | let sN, sD = D
47 | let tN, tD = D
48 |
49 | //float sc, sN, sD = D; // sc = sN / sD, default sD = D >= 0
50 | //float tc, tN, tD = D; // tc = tN / tD, default tD = D >= 0
51 |
52 |
53 | // compute the line parameters of the two closest points
54 | if (D < SMALL_NUM) { // the lines are almost parallel
55 | sN = 0.0; // force using point P0 on segment S1
56 | sD = 1.0; // to prevent possible division by 0.0 later
57 | tN = e;
58 | tD = c;
59 | }
60 | else { // get the closest points on the infinite lines
61 | sN = (b*e - c*d);
62 | tN = (a*e - b*d);
63 | if (sN < 0.0) { // sc < 0 => the s=0 edge is visible
64 | sN = 0.0;
65 | tN = e;
66 | tD = c;
67 | }
68 | else if (sN > sD) { // sc > 1 => the s=1 edge is visible
69 | sN = sD;
70 | tN = e + b;
71 | tD = c;
72 | }
73 | }
74 |
75 | if (tN < 0.0) { // tc < 0 => the t=0 edge is visible
76 | tN = 0.0;
77 | // recompute sc for this edge
78 | if (-d < 0.0)
79 | sN = 0.0;
80 | else if (-d > a)
81 | sN = sD;
82 | else {
83 | sN = -d;
84 | sD = a;
85 | }
86 | }
87 | else if (tN > tD) { // tc > 1 => the t=1 edge is visible
88 | tN = tD;
89 | // recompute sc for this edge
90 | if ((-d + b) < 0.0)
91 | sN = 0;
92 | else if ((-d + b) > a)
93 | sN = sD;
94 | else {
95 | sN = (-d + b);
96 | sD = a;
97 | }
98 | }
99 |
100 | // do the division to get sc and tc
101 | // these are the distances on the lines from 0..1 where the lines are closest
102 | // sc is for line 1 (S1) and tc is for line 2 (S2)
103 | const sc = (Math.abs(sN) < SMALL_NUM ? 0.0 : sN / sD)
104 | const tc = (Math.abs(tN) < SMALL_NUM ? 0.0 : tN / tD)
105 |
106 | if (sc > 1)
107 | console.warn('WARNING: sc > 1:', sc)
108 |
109 | if (tc > 1)
110 | console.warn('WARNING: tc > 1:', tc)
111 |
112 | vec2.scale(s1tmp, u, sc)
113 |
114 | vec2.scale(s2tmp, v, tc)
115 |
116 | vec2.subtract(diff, s1tmp, s2tmp)
117 |
118 | vec2.add(dP, w, diff)
119 |
120 | const closestDistance = vec2.length(dP)
121 |
122 | if (detail) {
123 | detail.sc = sc
124 | detail.tc = tc
125 | detail.closestDistance = closestDistance
126 | }
127 |
128 | // get the difference of the two closest points
129 | //Vector dP = w + (sc * u) - (tc * v); // = S1(sc) - S2(tc)
130 |
131 | return closestDistance
132 |
133 | //return norm(dP); // return the closest distance sqrt(dot(v,v))
134 | }
135 |
--------------------------------------------------------------------------------
/src/sphere-point-overlap.js:
--------------------------------------------------------------------------------
1 | import { vec2 } from 'gl-matrix'
2 |
3 |
4 | // determine if a point is inside a sphere
5 | export default function spherePointOverlap (point, centerA, radiusA) {
6 | return vec2.distance(point, centerA) <= radiusA
7 | }
8 |
--------------------------------------------------------------------------------
/src/sphere-sphere-collision-response.js:
--------------------------------------------------------------------------------
1 | import contact from './contact.js'
2 | import sphereOverlap from './sphere-sphere-overlap.js'
3 | import { vec2 } from 'wgpu-matrix'
4 |
5 |
6 | // TODO: make this function actually work. Right now the data structure is hardcoded to
7 | // use crossroads entity data structures that have no analog in collision-2d (rigidBody, transform components)
8 | /*
9 | const mtd = vec2.create()
10 | const scaled = vec2.create()
11 | const v = vec2.create()
12 | const normalized = vec2.create()
13 | const impulse = vec2.create()
14 | const tmpContact = contact()
15 |
16 |
17 | // collide 2 spherical rigid bodies, updating their positions and velocities
18 | export default function sphereSphereCollisionResponse (sphere1, sphere2, restitution=0.85) {
19 | const body1 = sphere1.rigidBody
20 | const body2 = sphere2.rigidBody
21 |
22 | const overlap = sphereOverlap(sphere1.transform.position,
23 | body1.radius,
24 | sphere2.transform.position,
25 | body2.radius,
26 | tmpContact)
27 |
28 | if (!overlap)
29 | return
30 |
31 | vec2.copy(tmpContact.delta, mtd)
32 |
33 | // resolve intersection
34 | const im1 = (body1.mass !== 0) ? 1 / body1.mass : 1 / 500
35 | const im2 = (body2.mass !== 0) ? 1 / body2.mass : 1 / 500
36 |
37 | // push-pull them apart
38 | // 0 mass means static body unmoved by collisions (doors, machines, etc.)
39 | if (body1.mass !== 0)
40 | vec2.addScaled(sphere1.transform.position, mtd, im1 / (im1 + im2), sphere1.transform.position)
41 |
42 | if (body2.mass !== 0) {
43 | vec2.scale(mtd, im2 / (im1 + im2), scaled)
44 | vec2.subtract(sphere2.transform.position, scaled, sphere2.transform.position)
45 | }
46 |
47 | // impact speed
48 | vec2.subtract(body1.velocity, body2.velocity, v)
49 | vec2.normalize(mtd, normalized)
50 | const vn = vec2.dot(v, normalized)
51 |
52 | // sphere intersecting but moving away from each other already
53 | if (vn > 0.0)
54 | return
55 |
56 | // collision impulse
57 | const i = (-(1.0 + restitution) * vn) / (im1 + im2)
58 | vec2.scale(mtd, i, impulse)
59 |
60 | // change in momentum
61 | if (body1.mass !== 0)
62 | vec2.addScaled(body1.velocity, impulse, im1, body1.velocity)
63 |
64 | if (body2.mass !== 0)
65 | vec2.addScaled(body2.velocity, impulse, -im2, body2.velocity)
66 | }
67 | */
68 |
--------------------------------------------------------------------------------
/src/sphere-sphere-overlap.js:
--------------------------------------------------------------------------------
1 | import contact from './contact.js'
2 | import { vec2 } from 'gl-matrix'
3 |
4 |
5 | const _delta = vec2.create()
6 | const _mtd = vec2.create()
7 |
8 |
9 | // determine if 2 spheres overlap, and provide contact details
10 | export default function sphereSphereOverlap (centerA, radiusA, centerB, radiusB, contact) {
11 | if (!contact)
12 | return vec2.squaredDistance(centerA, centerB) <= ((radiusA + radiusB) ** 2)
13 |
14 | vec2.subtract(_delta, centerA, centerB)
15 |
16 | const r = radiusA + radiusB
17 |
18 | const dist2 = vec2.dot(_delta, _delta)
19 |
20 | if (dist2 > r*r)
21 | return false // they aren't colliding
22 |
23 | let d = vec2.length(_delta)
24 |
25 | if (d !== 0.0) {
26 | // minimum translation distance to push balls apart after intersecting
27 | vec2.scale(_mtd, _delta, ((radiusA + radiusB)-d)/d)
28 | } else {
29 | // Special case. spheres are exactly on top of each other. Prevent divide by zero.
30 | d = r - 1.0
31 | vec2.set(_delta, r, 0.0)
32 | vec2.scale(_mtd, _delta, (r-d)/d)
33 | }
34 |
35 | // delta is the overlap between the two spheres, and is a vector that can be added to
36 | // sphere A’s position to move them into a non-colliding state.
37 | vec2.copy(contact.delta, _mtd)
38 |
39 | // position is the point of contact of these 2 spheres (assuming they no longer penetrate)
40 | vec2.normalize(contact.position, _delta)
41 | vec2.scaleAndAdd(contact.position, centerA, contact.position, -radiusA)
42 |
43 | return true
44 | }
45 |
--------------------------------------------------------------------------------
/src/sphere-sphere-sweep2.js:
--------------------------------------------------------------------------------
1 | import getLowestRoot from './get-lowest-root.js'
2 | import { vec2 } from 'gl-matrix'
3 |
4 |
5 | // from https://web.archive.org/web/20100629145557/http://www.gamasutra.com/view/feature/3383/simple_intersection_tests_for_games.php?page=2
6 |
7 | // tmp vars to avoid garbage collection
8 | const va = vec2.create()
9 | const vb = vec2.create()
10 | const AB = vec2.create()
11 | const vab = vec2.create()
12 |
13 |
14 | /*
15 | const SCALAR ra, //radius of sphere A
16 | const VECTOR& A0, //previous position of sphere A
17 | const VECTOR& A1, //current position of sphere A
18 | const SCALAR rb, //radius of sphere B
19 | const VECTOR& B0, //previous position of sphere B
20 | const VECTOR& B1, //current position of sphere B
21 | SCALAR& u0, //normalized time of first collision
22 | SCALAR& u1 //normalized time of second collision
23 | */
24 | export default function sphereSphereSweep2 (ra, A0, A1, rb, B0, B1, contact) {
25 |
26 | vec2.subtract(va, A1, A0)
27 |
28 | vec2.subtract(vb, B1, B0)
29 |
30 | vec2.subtract(AB, B0, A0)
31 |
32 | vec2.subtract(vab, vb, va) // relative velocity (in normalized time)
33 |
34 |
35 | const rab = ra + rb
36 |
37 | const a = vec2.dot(vab, vab) // u*u coefficient
38 |
39 |
40 | const b = 2 * vec2.dot(vab, AB) // u coefficient
41 |
42 | const c = vec2.dot(AB, AB) - rab*rab // constant term
43 |
44 | // check if they're currently overlapping
45 | if (vec2.dot(AB, AB) <= rab*rab) {
46 | const t = 0
47 | fillContactDeets(ra, A0, A1, rb, B0, B1, t, contact)
48 | return true
49 | }
50 |
51 | // check if they hit each other during the frame
52 | const maxVal = 1
53 | const t = getLowestRoot(a, b, c, maxVal)
54 |
55 | if (t !== null) {
56 | fillContactDeets(ra, A0, A1, rb, B0, B1, t, contact)
57 | return true
58 | }
59 |
60 | return false
61 | }
62 |
63 |
64 | const _delta = vec2.create()
65 | const _pos1 = vec2.create()
66 | const _pos2 = vec2.create()
67 |
68 | function fillContactDeets (ra, A0, A1, rb, B0, B1, t, contact) {
69 | contact.time = t
70 |
71 | // final sphereA position
72 | vec2.subtract(_delta, A1, A0)
73 | vec2.scaleAndAdd(_pos1, A0, _delta, contact.time)
74 |
75 | // final sphereB position
76 | vec2.subtract(_delta, B1, B0)
77 | vec2.scaleAndAdd(_pos2, B0, _delta, contact.time)
78 |
79 | vec2.subtract(contact.position, _pos1, _pos2)
80 | vec2.normalize(contact.position, contact.position)
81 | vec2.scaleAndAdd(contact.position, _pos2, contact.position, rb)
82 |
83 | vec2.subtract(contact.normal, contact.position, _pos2)
84 | vec2.normalize(contact.normal, contact.normal)
85 | }
86 |
87 |
--------------------------------------------------------------------------------
/src/toji-tris.js:
--------------------------------------------------------------------------------
1 | // from https://github.com/kevzettler/gl-swept-sphere-triangle
2 | import clamp from 'clamp'
3 | import TraceInfo from './TraceInfo.js'
4 | import getLowestRoot from './get-lowest-root.js'
5 | import lineNormal from './segment-normal.js'
6 | import segmentPointOverlap from './segment-point-overlap.js'
7 | import { vec2 } from 'gl-matrix'
8 |
9 |
10 | const ta = vec2.create()
11 | const tb = vec2.create()
12 |
13 | const norm = vec2.create()
14 |
15 | const v = vec2.create()
16 | const edge = vec2.create()
17 |
18 | const planeIntersect = vec2.create()
19 |
20 |
21 | function testVertex (p, velSqrLen, t, start, vel, trace) {
22 | vec2.subtract(v, start, p)
23 | const b = 2.0 * vec2.dot(vel, v)
24 | const c = vec2.squaredLength(v) - 1.0
25 | const newT = getLowestRoot(velSqrLen, b, c, t)
26 | if (newT !== null) {
27 | trace.setCollision(newT, p)
28 | return newT
29 | }
30 | return t
31 | }
32 |
33 |
34 | function testEdge (pa, pb, velSqrLen, t, start, vel, trace) {
35 | vec2.subtract(edge, pb, pa)
36 | vec2.subtract(v, pa, start)
37 |
38 | const edgeSqrLen = vec2.squaredLength(edge)
39 | const edgeDotVel = vec2.dot(edge, vel)
40 | const edgeDotSphereVert = vec2.dot(edge, v)
41 |
42 | const a = edgeSqrLen*-velSqrLen + edgeDotVel*edgeDotVel
43 | const b = edgeSqrLen*(2.0*vec2.dot(vel, v))-2.0*edgeDotVel*edgeDotSphereVert
44 | const c = edgeSqrLen*(1.0-vec2.squaredLength(v))+edgeDotSphereVert*edgeDotSphereVert
45 |
46 | // Check for intersection against infinite line
47 | const newT = getLowestRoot(a, b, c, t)
48 | if (newT !== null && newT < trace.t) {
49 | // Check if intersection against the line segment:
50 | const f = (edgeDotVel*newT-edgeDotSphereVert)/edgeSqrLen
51 | if (f >= 0.0 && f <= 1.0) {
52 | vec2.scale(v, edge, f)
53 | vec2.add(v, pa, v)
54 | trace.setCollision(newT, v)
55 | return newT
56 | }
57 | }
58 | return t
59 | }
60 |
61 |
62 | /**
63 | * @param {vec2} a First line vertex
64 | * @param {vec2} b Second line vertex
65 | * @param {TraceInfo} trace TraceInfo containing the sphere path to trace
66 | */
67 | function traceSphereTriangle (a, b, trace) {
68 | vec2.copy(trace.tmpTri[0], a)
69 | vec2.copy(trace.tmpTri[0], b)
70 |
71 | const invRadius = trace.invRadius
72 | const vel = trace.scaledVel
73 | const start = trace.scaledStart
74 |
75 | // Scale the triangle points so that we're colliding against a unit-radius sphere.
76 | vec2.scale(ta, a, invRadius)
77 | vec2.scale(tb, b, invRadius)
78 |
79 | lineNormal(norm, ta, tb)
80 |
81 | vec2.copy(trace.tmpTriNorm, norm)
82 | const planeD = -(norm[0]*ta[0]+norm[1] *ta[1])
83 |
84 | // Colliding against the backface of the triangle
85 | if (vec2.dot(norm, trace.normVel) >= 0) {
86 | // Two choices at this point:
87 |
88 | // 1) Negate the normal so that it always points towards the start point
89 | // This feels kludgy, but I'm not sure if there's a better alternative
90 | /*vec2.negate(norm, norm)
91 | planeD = -planeD*/
92 |
93 | // 2) Or allow it to pass through
94 | return
95 | }
96 |
97 | // Get interval of plane intersection:
98 | let t0, t1
99 | let embedded = false
100 |
101 | // Calculate the signed distance from sphere
102 | // position to triangle plane
103 | const distToPlane = vec2.dot(start, norm) + planeD
104 |
105 | // cache this as we’re going to use it a few times below:
106 | const normDotVel = vec2.dot(norm, vel)
107 |
108 | if (normDotVel === 0.0) {
109 | // Sphere is travelling parrallel to the plane:
110 | if (Math.abs(distToPlane) >= 1.0) {
111 | // Sphere is not embedded in plane, No collision possible
112 | return
113 | } else {
114 | // Sphere is completely embedded in plane.
115 | // It intersects in the whole range [0..1]
116 | embedded = true
117 | t0 = 0.0
118 | t1 = 1.0
119 | }
120 | } else {
121 |
122 | // Calculate intersection interval:
123 | t0 = (-1.0-distToPlane) / normDotVel
124 | t1 = ( 1.0-distToPlane) / normDotVel
125 | // Swap so t0 < t1
126 | if (t0 > t1) {
127 | const temp = t1
128 | t1 = t0
129 | t0 = temp
130 | }
131 | // Check that at least one result is within range:
132 | if (t0 > 1.0 || t1 < 0.0) {
133 | // No collision possible
134 | return
135 | }
136 |
137 | t0 = clamp(t0, 0.0, 1.0)
138 | t1 = clamp(t1, 0.0, 1.0)
139 | }
140 |
141 | // If the closest possible collision point is further away
142 | // than an already detected collision then there's no point
143 | // in testing further.
144 | //
145 | // this is a cheaper way to sort time of intersections, by only
146 | // keeping the closest one.
147 | if (t0 >= trace.t)
148 | return
149 |
150 | // t0 and t1 now represent the range of the sphere movement
151 | // during which it intersects with the triangle plane.
152 | // Collisions cannot happen outside that range.
153 |
154 | // Check for collision againt the triangle face:
155 | if (!embedded) {
156 | // Calculate the intersection point with the plane
157 | vec2.subtract(planeIntersect, start, norm)
158 | vec2.scale(v, vel, t0)
159 | vec2.add(planeIntersect, v, planeIntersect)
160 |
161 | // Is that point inside the triangle?
162 | if (segmentPointOverlap(planeIntersect, ta, tb)) {
163 | trace.setCollision(t0, planeIntersect)
164 | // Collisions against the face will always be closer than vertex or edge collisions
165 | // so we can stop checking now.
166 | return
167 | }
168 | }
169 |
170 | const velSqrLen = vec2.squaredLength(vel)
171 | let t = trace.t
172 |
173 | // Check for collision againt the triangle vertices:
174 | t = testVertex(ta, velSqrLen, t, start, vel, trace)
175 | t = testVertex(tb, velSqrLen, t, start, vel, trace)
176 |
177 | // Check for collision against the triangle edges:
178 | t = testEdge(ta, tb, velSqrLen, t, start, vel, trace)
179 | }
180 |
181 |
182 | export default { TraceInfo, traceSphereTriangle }
183 |
--------------------------------------------------------------------------------
/src/triangle-area.js:
--------------------------------------------------------------------------------
1 | // compute the area of a triangle given it's 3 vertices
2 | export default function triangleArea (a, b, c) {
3 | const ax = b[0] - a[0]
4 | const ay = b[1] - a[1]
5 | const bx = c[0] - a[0]
6 | const by = c[1] - a[1]
7 | return bx*ay - ax*by
8 | }
9 |
--------------------------------------------------------------------------------
/src/triangle-get-center.js:
--------------------------------------------------------------------------------
1 | export default function getTriangleCenter (out, v0, v1, v2) {
2 | out[0] = (v0[0] + v1[0] + v2[0]) / 3
3 | out[1] = (v0[1] + v1[1] + v2[1]) / 3
4 | return out
5 | }
6 |
--------------------------------------------------------------------------------
/src/triangle-point-overlap.js:
--------------------------------------------------------------------------------
1 | import { vec2 } from 'gl-matrix'
2 |
3 |
4 | // static temp variables to avoid creating new ones each invocation
5 | const c = vec2.create()
6 | const b = vec2.create()
7 | const p = vec2.create()
8 |
9 |
10 | /*
11 | Determine if a point is inside a triangle
12 |
13 | https://observablehq.com/@kelleyvanevert/2d-point-in-triangle-test
14 |
15 | @param Array v0, v1, v2 3 points of the triangle expressed as vec2
16 | @param Array point to check for containment within the triangle
17 | @returns bool true if the point is in the triangle, false otherwise
18 | */
19 | export default function trianglePointOverlap (v0, v1, v2, point) {
20 | // compute vectors
21 | vec2.sub(c, v2, v0)
22 | vec2.sub(b, v1, v0)
23 | vec2.sub(p, point, v0)
24 |
25 | // compute dot products
26 | const cc = vec2.dot(c, c)
27 | const bc = vec2.dot(b, c)
28 | const pc = vec2.dot(c, p)
29 | const bb = vec2.dot(b, b)
30 | const pb = vec2.dot(b, p)
31 |
32 | // compute barycentric coordinates
33 | const denom = cc * bb - bc * bc
34 | const u = (bb*pc - bc*pb) / denom
35 | const v = (cc*pb - bc*pc) / denom
36 |
37 | return (u >= 0) && (v >= 0) && (u + v < 1)
38 | }
39 |
--------------------------------------------------------------------------------
/test/_assert.js:
--------------------------------------------------------------------------------
1 |
2 | function equal (a, b) {
3 | if (a !== b)
4 | throw new Error(`${a} is not equal to ${b}`)
5 | }
6 |
7 |
8 | function deepEqual (a, b) {
9 | if (a.length !== b.length)
10 | throw new Error(`${a} is not equal to ${b}`)
11 |
12 | for (let i=0; i < a.length; i++)
13 | if (a[i] !== b[i])
14 | throw new Error(`${a} is not equal to ${b}`)
15 | }
16 |
17 |
18 | function almostEqual (actual, expected, epsilon=1e-8) {
19 | if (Math.abs(actual - expected) > epsilon)
20 | equal(actual, expected)
21 | }
22 |
23 |
24 | function notNull (value) {
25 | if (value === null)
26 | throw new Error('value is unexpectedly null')
27 |
28 | return value
29 | }
30 |
31 |
32 | export default { almostEqual, notNull, equal, deepEqual }
33 |
--------------------------------------------------------------------------------
/test/aabb-point-overlap.js:
--------------------------------------------------------------------------------
1 | import assert from './_assert.js'
2 | import contact from '../src/contact.js'
3 | import intersectPoint from '../src/aabb-point-overlap.js'
4 |
5 |
6 | // should return null when not colliding
7 | let aabb = {
8 | position: [ -8, -8 ],
9 | width: 16,
10 | height: 16
11 | }
12 |
13 | const points = [
14 | [ -16, -16 ],
15 | [ 0, -16 ],
16 | [ 16, -16 ],
17 | [ 16, 0 ],
18 | [ 16, 16 ],
19 | [ 0, 16 ],
20 | [ -16, 16 ],
21 | [ -16, 0 ]
22 | ]
23 |
24 | const hit = contact()
25 |
26 | points.forEach(point => {
27 | assert.equal(intersectPoint(aabb, point), false);
28 | })
29 |
30 |
31 | // should return hit when colliding
32 | aabb.position = [ 0, 0 ]
33 | assert.equal(intersectPoint(aabb, [ 4, 4 ]), true)
34 |
35 |
36 | /*
37 | // should set hit pos and normal to nearest edge of box
38 | const aabb = new AABB(new Point(0, 0), new Point(8, 8));
39 | let hit = assert.notNull(aabb.intersectPoint(new Point(-4, -2)));
40 | assert.almostEqual(hit.pos.x, -8);
41 | assert.almostEqual(hit.pos.y, -2);
42 | assert.almostEqual(hit.delta.x, -4);
43 | assert.almostEqual(hit.delta.y, 0);
44 | assert.almostEqual(hit.normal.x, -1);
45 | assert.almostEqual(hit.normal.y, 0);
46 |
47 | hit = assert.notNull(aabb.intersectPoint(new Point(4, -2)));
48 | assert.almostEqual(hit.pos.x, 8);
49 | assert.almostEqual(hit.pos.y, -2);
50 | assert.almostEqual(hit.delta.x, 4);
51 | assert.almostEqual(hit.delta.y, 0);
52 | assert.almostEqual(hit.normal.x, 1);
53 | assert.almostEqual(hit.normal.y, 0);
54 |
55 | hit = assert.notNull(aabb.intersectPoint(new Point(2, -4)));
56 | assert.almostEqual(hit.pos.x, 2);
57 | assert.almostEqual(hit.pos.y, -8);
58 | assert.almostEqual(hit.delta.x, 0);
59 | assert.almostEqual(hit.delta.y, -4);
60 | assert.almostEqual(hit.normal.x, 0);
61 | assert.almostEqual(hit.normal.y, -1);
62 |
63 | hit = assert.notNull(aabb.intersectPoint(new Point(2, 4)));
64 | assert.almostEqual(hit.pos.x, 2);
65 | assert.almostEqual(hit.pos.y, 8);
66 | assert.almostEqual(hit.delta.x, 0);
67 | assert.almostEqual(hit.delta.y, 4);
68 | assert.almostEqual(hit.normal.x, 0);
69 | assert.almostEqual(hit.normal.y, 1);
70 | */
71 |
--------------------------------------------------------------------------------
/test/aabb-segment-overlap.js:
--------------------------------------------------------------------------------
1 | import assert from './_assert.js'
2 | import aabbSegOverlap from '../src/aabb-segment-overlap.js'
3 |
4 |
5 | const aabb = {
6 | position: [ 2624.20556640625, 1062 ],
7 | width: 10,
8 | height: 24
9 | }
10 |
11 | const seg = [
12 | [3152, 1072],
13 | [3248, 1072]
14 | ]
15 |
16 | const delta = [ 96, 0 ]
17 |
18 |
19 | {
20 | let paddingX = 0
21 | let paddingY = -2
22 |
23 | assert.equal(aabbSegOverlap(aabb, seg[0], delta, paddingX, paddingY), false)
24 | }
25 |
26 |
27 | {
28 | aabb.position = [ 907.5, 318.5 ]
29 | aabb.width = 15
30 | aabb.height = 35
31 |
32 |
33 | seg[0] = [ 531, 301 ]
34 | delta[0] = 16
35 |
36 | assert.equal(aabbSegOverlap(aabb, seg[0], delta), false)
37 | }
38 |
--------------------------------------------------------------------------------
/test/segment-segment-overlap.js:
--------------------------------------------------------------------------------
1 | import t from './_assert.js'
2 | import segseg from '../src/segment-segment-overlap.js'
3 |
4 |
5 | const result = [ 0, 0 ]
6 |
7 |
8 | /*
9 | Basic intersection
10 |
11 | (0, 5)
12 | o
13 | |
14 | (-10, 0) o--------+-------o (10, 0)
15 | |
16 | o
17 | (0, -5)
18 |
19 | */
20 |
21 | t.equal(segseg([-10, 0], [10, 0], [0, 5], [0, -5], result), true)
22 | t.deepEqual(result, [0, 0])
23 |
24 |
25 |
26 | /*
27 | Basic intersection
28 |
29 | (5, 5)
30 | o------o (10, 5)
31 | |
32 | |
33 | o
34 | (5, 0)
35 |
36 | */
37 | t.equal(segseg([ 5, 5], [5, 0], [5, 5], [10, 5], result), true)
38 | t.deepEqual(result, [5,5])
39 |
40 |
41 | /*
42 | Colinear
43 | (-2, 0) (2, 0)
44 | (-10, 0) o----o--------o-----o (10, 0)
45 |
46 | */
47 | t.equal(segseg([-10, 0], [10, 0], [-2, 0], [2, 0], result), false)
48 |
49 |
50 | /*
51 | No intersection (parallel)
52 |
53 | (-10, 5) o-------------o (10, 5)
54 |
55 | (-10, 0) o-------------o (10, 0)
56 |
57 | */
58 | t.equal(segseg([-10, 0], [10, 0], [-10, 5], [10, 5], result), false)
59 |
60 |
61 | /*
62 | No intersection
63 |
64 | (-2, 5) o
65 | \
66 | (-10, 0) o----o o (2, 0)
67 | (0, 0)
68 |
69 | */
70 | t.equal(segseg([-10, 0], [0, 0], [-2, 5], [2, 0], result), false)
71 |
72 |
73 | /*
74 | No intersection
75 |
76 | (-2, 5) o
77 | |
78 | o (-2, 1)
79 | (-10, 0) o----o
80 | (0, 0)
81 |
82 | */
83 | t.equal(segseg([ -10, 0], [0, 0], [-2, 5], [-2, 1], result), false)
84 |
85 |
86 | /*
87 | No intersection
88 |
89 | (-5, 5) o
90 | /
91 | / (-10, 0)
92 | /o-----------o
93 | o (0, 0)
94 | (-25, -5)
95 |
96 | */
97 | t.equal(segseg([-10, 0], [0, 0], [-5, 5], [-25, -5], result), false)
98 |
--------------------------------------------------------------------------------
/test/segments-ellipsoid-sweep1-indexed.js:
--------------------------------------------------------------------------------
1 | import Contact from '../src/contact.js'
2 | import assert from './_assert.js'
3 | import sweep from '../src/segments-ellipsoid-sweep1-indexed.js'
4 | import { vec2 } from 'gl-matrix'
5 |
6 |
7 | const lines = [
8 | [ [64.0, 128.0], [ 64.0, 0.0 ] ],
9 | [ [0,0], [0, 128] ]
10 | ]
11 | const indices = [ 0, 1 ]
12 |
13 | const ellipsoid = [ 5, 10 ]
14 | const position = [ 32, 64 ]
15 | const delta = [ 120, 0 ]
16 | const lineCount = 2
17 | const contact = Contact()
18 |
19 |
20 | // convert the start and end positions into R3 ellipsoid space
21 | vec2.divide(position, position, ellipsoid)
22 | vec2.divide(delta, delta, ellipsoid)
23 |
24 | const collision = sweep(lines, indices, lineCount, position, ellipsoid, delta, contact)
25 |
26 | // convert the intersection point back to R3 (non-ellipsoid) space
27 | vec2.multiply(contact.position, contact.position, ellipsoid)
28 |
29 | assert.equal(collision, true)
30 | assert.equal(contact.collider, 0)
31 | assert.almostEqual(contact.time, 0.225)
32 |
33 | const EPSILON = 0.000001
34 | assert.almostEqual(contact.position[0], 64, EPSILON)
35 | assert.almostEqual(contact.position[1], 64, EPSILON)
36 |
37 | assert.deepEqual(contact.normal, [ -1, 0 ])
38 |
--------------------------------------------------------------------------------
/test/trace-sphere-triangle.js:
--------------------------------------------------------------------------------
1 | import assert from './_assert.js'
2 | import toji from '../src/toji-tris.js'
3 |
4 |
5 | const start = [ 0, 32 ]
6 | const end = [ 128, 32 ]
7 | const radius = 16
8 |
9 | const ti = new toji.TraceInfo()
10 | ti.resetTrace(start, end, radius)
11 |
12 | const b = [ 128, 0 ]
13 | const a = [ 128, 128 ]
14 |
15 | toji.traceSphereTriangle(a, b, ti);
16 |
17 | assert.equal(ti.collision, true)
18 | assert.equal(ti.t, 0.875)
19 | assert.deepEqual(ti.intersectTriNorm, [ -1, 0 ])
20 | assert.deepEqual(ti.intersectPoint, [ 128, 32 ])
21 |
22 |
23 | // when the sphere is 0 distance from a line segment, collide with it
24 | {
25 | const start = [ 112, 64 ]
26 | const end = [ 140, 64 ]
27 | const radius = 16
28 |
29 | const ti = new toji.TraceInfo()
30 | ti.resetTrace(start, end, radius)
31 |
32 | toji.traceSphereTriangle(a, b, ti);
33 | assert.equal(ti.collision, true)
34 | assert.equal(ti.t, 0.0)
35 | }
36 |
--------------------------------------------------------------------------------