-
Notifications
You must be signed in to change notification settings - Fork 39
Expand file tree
/
Copy pathgeometry_c.js
More file actions
1284 lines (1078 loc) · 42.4 KB
/
geometry_c.js
File metadata and controls
1284 lines (1078 loc) · 42.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* This file includes code that is:
*
* - Copyright 2023 Erin Catto, released under the MIT license.
* - Copyright 2024 Phaser Studio Inc, released under the MIT license.
*/
import { B2_MAX_POLYGON_VERTICES, b2Capsule, b2CastOutput, b2Circle, b2DistanceCache, b2DistanceInput, b2MassData, b2Polygon, b2ShapeCastPairInput } from './include/collision_h.js';
import {
b2AABB,
b2Add,
b2ClampFloat,
b2Cross,
b2CrossVS,
b2DistanceSquared,
b2Dot,
b2GetLengthAndNormalize,
b2IsValid,
b2Length,
b2Lerp,
b2Max,
b2Min,
b2MulAdd,
b2MulSV,
b2MulSub,
b2Neg,
b2Normalize,
b2RightPerp,
b2Rot,
b2RotateVector,
b2Sub,
b2Transform,
b2TransformPoint,
b2Vec2,
b2Vec2_IsValid,
eps
} from './include/math_functions_h.js';
import { b2MakeProxy, b2ShapeCast, b2ShapeDistance } from './include/distance_h.js';
import { B2_HUGE } from './include/core_h.js';
import { b2ValidateHull } from './include/hull_h.js';
/**
* @namespace Geometry
*/
/**
* @import {b2Hull, b2RayCastInput, b2Segment, b2ShapeCastInput} from './include/collision_h.js'
*/
/**
* Validates a ray cast input structure.
* @function b2IsValidRay
* @param {b2RayCastInput} input - The ray cast input to validate, containing:
* - origin: b2Vec2 - Starting point of the ray
* - translation: b2Vec2 - Direction and length of the ray
* - maxFraction: number - Maximum fraction of translation to check
* @returns {boolean} True if the ray cast input is valid, false otherwise.
* @description
* Checks if a ray cast input is valid by verifying:
* - The origin vector is valid
* - The translation vector is valid
* - The maxFraction is a valid number
* - The maxFraction is between 0 and B2_HUGE(exclusive)
*/
export function b2IsValidRay(input)
{
const isValid = b2Vec2_IsValid(input.origin) && b2Vec2_IsValid(input.translation) && b2IsValid(input.maxFraction) &&
0.0 <= input.maxFraction && input.maxFraction < B2_HUGE;
return isValid;
}
function b2ComputePolygonCentroid(vertices, count)
{
let center = new b2Vec2(0.0, 0.0);
let area = 0.0;
// Get a reference point for forming triangles.
// Use the first vertex to reduce round-off errors.
const origin = vertices[0];
const inv3 = 1.0 / 3.0;
for (let i = 1; i < count - 1; ++i)
{
// Triangle edges
const e1 = b2Sub(vertices[i], origin);
const e2 = b2Sub(vertices[i + 1], origin);
const a = 0.5 * b2Cross(e1, e2);
// Area weighted centroid
center = b2MulAdd(center, a * inv3, b2Add(e1, e2));
area += a;
}
// Centroid
console.assert(area > eps);
const invArea = 1.0 / area;
center.x *= invArea;
center.y *= invArea;
// Restore offset
center = b2Add(origin, center);
return center;
}
// default to forceCheck: true - turn this off at your own peril, it validates the hull
/**
* @function b2MakePolygon
* @summary Creates a polygon shape from a hull with rounded corners.
* @param {b2Hull} hull - A convex hull structure containing points that define the polygon vertices
* @param {number} radius - The radius used to round the corners of the polygon
* @param {boolean} [forceCheck=true] - Whether to enforce hull validation
* @returns {b2Polygon} A new polygon shape with computed vertices, normals, and centroid
* @throws {Error} Throws an assertion error if the hull is invalid
* @description
* Creates a b2Polygon from a convex hull. If the hull has fewer than 3 points, returns a
* square shape. The function computes the polygon's vertices, edge normals, and centroid.
* Each edge normal is a unit vector perpendicular to the corresponding edge.
*/
export function b2MakePolygon(hull, radius, forceCheck = true)
{
if (forceCheck && !b2ValidateHull(hull))
{
console.warn("Invalid hull.");
return null;
}
if (hull.count < 3)
{
// Handle a bad hull when assertions are disabled
return b2MakeSquare(0.5);
}
const shape = new b2Polygon();
shape.count = hull.count;
shape.radius = radius;
// Copy vertices
for (let i = 0; i < shape.count; ++i)
{
shape.vertices[i] = hull.points[i];
}
// Compute normals. Ensure the edges have non-zero length.
for (let i = 0; i < shape.count; ++i)
{
const i1 = i;
const i2 = i + 1 < shape.count ? i + 1 : 0;
const edge = b2Sub(shape.vertices[i2], shape.vertices[i1]);
console.assert(b2Dot(edge, edge) > eps * eps);
shape.normals[i] = b2Normalize(b2CrossVS(edge, 1.0));
}
shape.centroid = b2ComputePolygonCentroid(shape.vertices, shape.count);
return shape;
}
// default to forceCheck: true - turn this off at your own peril, it validates the hull
/**
* @function b2MakeOffsetPolygon
* @description Creates a polygon shape from a hull with specified radius and transform
* @param {b2Hull} hull - The input hull to create the polygon from
* @param {number} radius - The radius to offset the polygon vertices
* @param {b2Transform} transform - Transform to apply to the hull points
* @param {boolean} [forceCheck=true] - Whether to force validation check of the hull
* @returns {b2Polygon} A new polygon shape with transformed vertices, computed normals and centroid
* @throws {Error} Throws assertion error if hull validation fails
* @note Returns a square polygon of size 0.5 if hull has less than 3 points
*/
export function b2MakeOffsetPolygon(hull, radius, transform, forceCheck = true)
{
console.assert(forceCheck && b2ValidateHull(hull), "Invalid hull.");
if (hull.count < 3)
{
// Handle a bad hull when assertions are disabled
return b2MakeSquare(0.5);
}
const shape = new b2Polygon();
shape.count = hull.count;
shape.radius = radius;
// Copy vertices
for (let i = 0; i < shape.count; ++i)
{
shape.vertices[i] = b2TransformPoint(transform, hull.points[i]);
}
// Compute normals. Ensure the edges have non-zero length.
for (let i = 0; i < shape.count; ++i)
{
const i1 = i;
const i2 = i + 1 < shape.count ? i + 1 : 0;
const edge = b2Sub(shape.vertices[i2], shape.vertices[i1]);
console.assert(b2Dot(edge, edge) > eps * eps);
shape.normals[i] = b2Normalize(b2CrossVS(edge, 1.0));
}
shape.centroid = b2ComputePolygonCentroid(shape.vertices, shape.count);
return shape;
}
/**
* Creates a square polygon with equal width and height.
* @function b2MakeSquare
* @param {number} h - The half-width and half-height of the square.
* @returns {b2Polygon} A polygon object representing a square centered at the origin.
* @description
* Creates a square polygon by calling b2MakeBox with equal dimensions.
* The square is centered at the origin with sides of length 2h.
*/
export function b2MakeSquare(h)
{
return b2MakeBox(h, h);
}
/**
* @function b2MakeBox
* @description
* Creates a rectangular polygon shape centered at the origin with specified half-widths.
* The vertices are arranged counter-clockwise starting from the bottom-left corner.
* The shape includes pre-computed normals for each edge.
* @param {number} hx - Half-width of the box in the x-direction (must be positive)
* @param {number} hy - Half-height of the box in the y-direction (must be positive)
* @returns {b2Polygon} A polygon shape representing a rectangle with:
* - 4 vertices at (-hx,-hy), (hx,-hy), (hx,hy), (-hx,hy)
* - 4 normals pointing outward from each edge
* - radius of 0
* - centroid at (0,0)
* @throws {Error} Throws an assertion error if hx or hy are not valid positive numbers
*/
export function b2MakeBox(hx, hy)
{
console.assert(b2IsValid(hx) && hx > 0.0);
console.assert(b2IsValid(hy) && hy > 0.0);
const shape = new b2Polygon();
shape.count = 4;
shape.vertices[0] = new b2Vec2(-hx, -hy);
shape.vertices[1] = new b2Vec2(hx, -hy);
shape.vertices[2] = new b2Vec2(hx, hy);
shape.vertices[3] = new b2Vec2(-hx, hy);
shape.normals[0] = new b2Vec2(0.0, -1.0);
shape.normals[1] = new b2Vec2(1.0, 0.0);
shape.normals[2] = new b2Vec2(0.0, 1.0);
shape.normals[3] = new b2Vec2(-1.0, 0.0);
shape.radius = 0.0;
shape.centroid = new b2Vec2(0,0);
return shape;
}
/**
* Creates a rounded box shape by generating a box with specified dimensions and corner radius.
* @function b2MakeRoundedBox
* @param {number} hx - Half-width of the box along the x-axis
* @param {number} hy - Half-height of the box along the y-axis
* @param {number} radius - Radius of the rounded corners
* @returns {b2Polygon} A polygon shape representing a rounded box
*/
export function b2MakeRoundedBox(hx, hy, radius)
{
const shape = b2MakeBox(hx, hy);
shape.radius = radius;
return shape;
}
/**
* Creates a rectangular polygon shape with specified dimensions, position, and rotation.
* @function b2MakeOffsetBox
* @param {number} hx - Half-width of the box along the x-axis
* @param {number} hy - Half-height of the box along the y-axis
* @param {b2Vec2} center - The center position of the box
* @param {b2Rot} rotation - The 2D rotation of the box
* @returns {b2Polygon} A polygon shape representing the box with 4 vertices and normals
* @description
* Creates a b2Polygon representing a rectangle with the given dimensions. The box is centered
* at the specified position and rotated by the given angle. The resulting polygon includes
* 4 vertices, 4 normals, and has its centroid set to the center position.
*/
export function b2MakeOffsetBox(hx, hy, center, rotation)
{
const xf = new b2Transform();
xf.p = center;
xf.q = rotation;
const shape = new b2Polygon();
shape.count = 4;
shape.vertices[0] = b2TransformPoint(xf, new b2Vec2(-hx, -hy));
shape.vertices[1] = b2TransformPoint(xf, new b2Vec2(hx, -hy));
shape.vertices[2] = b2TransformPoint(xf, new b2Vec2(hx, hy));
shape.vertices[3] = b2TransformPoint(xf, new b2Vec2(-hx, hy));
shape.normals[0] = b2RotateVector(xf.q, new b2Vec2(0.0, -1.0));
shape.normals[1] = b2RotateVector(xf.q, new b2Vec2(1.0, 0.0));
shape.normals[2] = b2RotateVector(xf.q, new b2Vec2(0.0, 1.0));
shape.normals[3] = b2RotateVector(xf.q, new b2Vec2(-1.0, 0.0));
shape.radius = 0.0;
shape.centroid = center;
return shape;
}
/**
* @function b2TransformPolygon
* @summary Transforms a polygon by applying a rigid body transformation.
* @param {b2Transform} transform - The transformation to apply, consisting of a position vector and rotation.
* @param {b2Polygon} polygon - The polygon to transform, containing vertices, normals and centroid.
* @returns {b2Polygon} The transformed polygon with updated vertices, normals and centroid.
* @description
* Applies a rigid body transformation to a polygon by:
* 1. Transforming each vertex using the full transform
* 2. Rotating each normal vector using only the rotation component
* 3. Transforming the centroid using the full transform
*/
export function b2TransformPolygon(transform, polygon)
{
const p = polygon;
for (let i = 0; i < p.count; ++i)
{
p.vertices[i] = b2TransformPoint(transform, p.vertices[i]);
p.normals[i] = b2RotateVector(transform.q, p.normals[i]);
}
p.centroid = b2TransformPoint(transform, p.centroid);
return p;
}
/**
* @function b2ComputeCircleMass
* @summary Computes mass properties for a circle shape.
* @param {b2Circle} shape - A circle shape object containing radius and center properties
* @param {number} density - The density of the circle in mass per unit area
* @returns {b2MassData} An object containing:
* - mass: The total mass of the circle
* - center: The center of mass (copied from shape.center)
* - rotationalInertia: The rotational inertia about the center of mass
* @description
* Calculates the mass, center of mass, and rotational inertia for a circle shape
* with given density. The rotational inertia is computed about the center of mass
* using the parallel axis theorem when the circle's center is offset from the origin.
*/
export function b2ComputeCircleMass(shape, density)
{
const rr = shape.radius * shape.radius;
const massData = new b2MassData();
massData.mass = density * Math.PI * rr;
massData.center = shape.center.clone();
// inertia about the local origin
massData.rotationalInertia = massData.mass * (0.5 * rr + b2Dot(shape.center, shape.center));
return massData;
}
/**
* @function b2ComputeCapsuleMass
* @description
* Computes mass properties for a capsule shape, including total mass, center of mass,
* and rotational inertia. A capsule consists of a rectangle with semicircles at both ends.
* @param {b2Capsule} shape - A capsule shape defined by two centers (center1, center2) and a radius
* @param {number} density - The density of the capsule in mass per unit area
* @returns {b2MassData} An object containing:
* - mass: The total mass of the capsule
* - center: A b2Vec2 representing the center of mass
* - rotationalInertia: The moment of inertia about the center of mass
*/
export function b2ComputeCapsuleMass(shape, density)
{
const radius = shape.radius;
const rr = radius * radius;
const p1 = shape.center1;
const p2 = shape.center2;
const length = b2Length(b2Sub(p2, p1));
const ll = length * length;
const circleMass = density * Math.PI * rr;
const boxMass = density * (2.0 * radius * length);
const massData = new b2MassData();
massData.mass = circleMass + boxMass;
massData.center = new b2Vec2(0.5 * (p1.x + p2.x), 0.5 * (p1.y + p2.y));
// two offset half circles, both halves add up to full circle and each half is offset by half length
// semi-circle centroid = 4 r / 3 pi
// Need to apply parallel-axis theorem twice:
// 1. shift semi-circle centroid to origin
// 2. shift semi-circle to box end
// m * ((h + lc)^2 - lc^2) = m * (h^2 + 2 * h * lc)
// See = https://en.wikipedia.org/wiki/Parallel_axis_theorem
// I verified this formula by computing the convex hull of a 128 vertex capsule
// half circle centroid
const lc = 4.0 * radius / (3.0 * Math.PI);
// half length of rectangular portion of capsule
const h = 0.5 * length;
const circleInertia = circleMass * (0.5 * rr + h * h + 2.0 * h * lc);
const boxInertia = boxMass * (4.0 * rr + ll) / 12.0;
massData.rotationalInertia = circleInertia + boxInertia;
// inertia about the local origin
massData.rotationalInertia += massData.mass * b2Dot(massData.center, massData.center);
return massData;
}
/**
* @function b2ComputePolygonMass
* @description
* Computes mass properties for a polygon shape, including mass, center of mass, and rotational inertia.
* Handles special cases for 1-vertex (circle) and 2-vertex (capsule) polygons.
* For polygons with 3 or more vertices, calculates properties using triangulation.
* @param {b2Polygon} shape - The polygon shape containing vertices, normals, count, and radius
* @param {number} density - The density of the shape in mass per unit area
* @returns {b2MassData} Object containing:
* - mass: Total mass of the shape
* - center: Center of mass as b2Vec2
* - rotationalInertia: Moment of inertia about the center of mass
* @throws {Error} Throws assertion error if shape.count is 0 or exceeds B2_MAX_POLYGON_VERTICES
*/
export function b2ComputePolygonMass(shape, density)
{
console.assert(shape.count > 0);
if (shape.count == 1)
{
const circle = new b2Circle();
circle.center = shape.vertices[0].clone();
circle.radius = shape.radius;
return b2ComputeCircleMass(circle, density);
}
if (shape.count == 2)
{
const capsule = new b2Capsule();
capsule.center1 = shape.vertices[0].clone();
capsule.center2 = shape.vertices[1].clone();
capsule.radius = shape.radius;
return b2ComputeCapsuleMass(capsule, density);
}
const vertices = new Array(B2_MAX_POLYGON_VERTICES);
const count = shape.count;
const radius = shape.radius;
console.assert(count <= B2_MAX_POLYGON_VERTICES); // PJB: ragdolls crash at 8, maxPolygonVertices == 8, coincidence?
if (radius > 0.0)
{
// Approximate mass of rounded polygons by pushing out the vertices.
const sqrt2 = 1.412;
for (let i = 0; i < count; ++i)
{
const j = i == 0 ? count - 1 : i - 1;
const n1 = shape.normals[j];
const n2 = shape.normals[i];
const mid = b2Normalize(b2Add(n1, n2));
vertices[i] = b2MulAdd(shape.vertices[i], sqrt2 * radius, mid);
}
}
else
{
for (let i = 0; i < count; ++i)
{
vertices[i] = shape.vertices[i];
}
}
let center = new b2Vec2(0.0, 0.0);
let area = 0.0;
let rotationalInertia = 0.0;
// Get a reference point for forming triangles.
// Use the first vertex to reduce round-off errors.
const r = vertices[0];
const inv3 = 1.0 / 3.0;
for (let i = 1; i < count - 1; ++i)
{
// Triangle edges
const e1 = b2Sub(vertices[i], r);
const e2 = b2Sub(vertices[i + 1], r);
const D = b2Cross(e1, e2);
const triangleArea = 0.5 * D;
area += triangleArea;
// Area weighted centroid, r at origin
center = b2MulAdd(center, triangleArea * inv3, b2Add(e1, e2));
const ex1 = e1.x,
ey1 = e1.y;
const ex2 = e2.x,
ey2 = e2.y;
const intx2 = ex1 * ex1 + ex2 * ex1 + ex2 * ex2;
const inty2 = ey1 * ey1 + ey2 * ey1 + ey2 * ey2;
rotationalInertia += (0.25 * inv3 * D) * (intx2 + inty2);
}
const massData = new b2MassData();
// Total mass
massData.mass = density * area;
// Center of mass, shift back from origin at r
console.assert(area > eps);
const invArea = 1.0 / area;
center.x *= invArea;
center.y *= invArea;
massData.center = b2Add(r, center);
// Inertia tensor relative to the local origin (point s).
massData.rotationalInertia = density * rotationalInertia;
// Shift to center of mass then to original body origin.
massData.rotationalInertia += massData.mass * (b2Dot(massData.center, massData.center) - b2Dot(center, center));
return massData;
}
/**
* @function b2ComputeCircleAABB
* @summary Computes an Axis-Aligned Bounding Box (AABB) for a circle shape after applying a transform.
* @param {b2Circle} shape - The circle shape containing center point and radius.
* @param {b2Transform} xf - The transform to be applied, consisting of a position (p) and rotation (q).
* @returns {b2AABB} An AABB object defined by minimum and maximum points that bound the transformed circle.
* @description
* Calculates the AABB by transforming the circle's center point using the provided transform
* and extending the bounds by the circle's radius in each direction.
*/
export function b2ComputeCircleAABB(shape, xf)
{
// let p = b2TransformPoint(xf, shape.center);
const pX = (xf.q.c * shape.center.x - xf.q.s * shape.center.y) + xf.p.x;
const pY = (xf.q.s * shape.center.x + xf.q.c * shape.center.y) + xf.p.y;
const r = shape.radius;
const aabb = new b2AABB(pX - r, pY - r, pX + r, pY + r);
return aabb;
}
/**
* @function b2ComputeCapsuleAABB
* @summary Computes an Axis-Aligned Bounding Box (AABB) for a capsule shape.
* @param {b2Capsule} shape - A capsule shape defined by two centers and a radius.
* @param {b2Transform} xf - A transform containing position and rotation to be applied to the capsule.
* @returns {b2AABB} An AABB that encompasses the transformed capsule shape.
* @description
* Calculates the minimum and maximum bounds of a capsule after applying a transform.
* The AABB is computed by transforming the capsule's center points and extending
* the bounds by the capsule's radius in all directions.
*/
export function b2ComputeCapsuleAABB(shape, xf)
{
const v1 = b2TransformPoint(xf, shape.center1);
const v2 = b2TransformPoint(xf, shape.center2);
const lowerX = Math.min(v1.x, v2.x) - shape.radius;
const lowerY = Math.min(v1.y, v2.y) - shape.radius;
const upperX = Math.max(v1.x, v2.x) + shape.radius;
const upperY = Math.max(v1.y, v2.y) + shape.radius;
const aabb = new b2AABB(lowerX, lowerY, upperX, upperY);
return aabb;
}
/**
* @function b2ComputePolygonAABB
* @description
* Computes the Axis-Aligned Bounding Box (AABB) for a polygon shape after applying a transform.
* The AABB includes the polygon's radius in its calculations.
* @param {b2Polygon} shape - The polygon shape containing vertices and radius
* @param {b2Transform} xf - The transform to apply, consisting of position (p) and rotation (q)
* @returns {b2AABB} An AABB object with lower and upper bounds that encompass the transformed polygon
*/
export function b2ComputePolygonAABB(shape, xf)
{
// let lower = b2TransformPoint(xf, shape.vertices[0]);
const sv = shape.vertices[0];
let lowerX = (xf.q.c * sv.x - xf.q.s * sv.y) + xf.p.x;
let lowerY = (xf.q.s * sv.x + xf.q.c * sv.y) + xf.p.y;
let upperX = lowerX,
upperY = lowerY;
for (let i = 1; i < shape.count; ++i)
{
// let v = b2TransformPoint(xf, shape.vertices[i]);
const sv = shape.vertices[i];
const vx = (xf.q.c * sv.x - xf.q.s * sv.y) + xf.p.x;
const vy = (xf.q.s * sv.x + xf.q.c * sv.y) + xf.p.y;
// lower = b2Min(lower, v);
lowerX = Math.min(lowerX, vx);
lowerY = Math.min(lowerY, vy);
// upper = b2Max(upper, v);
upperX = Math.max(upperX, vx);
upperY = Math.max(upperY, vy);
}
const r = shape.radius;
// lower = b2Sub(lower, r);
lowerX -= r;
lowerY -= r;
// upper = b2Add(upper, r);
upperX += r;
upperY += r;
const aabb = new b2AABB(lowerX, lowerY, upperX, upperY);
return aabb;
}
/**
* @summary Computes an Axis-Aligned Bounding Box (AABB) for a line segment.
* @function b2ComputeSegmentAABB
* @param {b2Segment} shape - A line segment defined by two points (point1 and point2)
* @param {b2Transform} xf - A transform containing position and rotation to be applied to the segment
* @returns {b2AABB} An AABB that contains the transformed line segment
* @description
* Transforms the segment's endpoints using the provided transform, then creates an AABB
* that encompasses the transformed segment by finding the minimum and maximum coordinates
* of the transformed endpoints.
*/
export function b2ComputeSegmentAABB(shape, xf)
{
const v1 = b2TransformPoint(xf, shape.point1);
const v2 = b2TransformPoint(xf, shape.point2);
const lower = b2Min(v1, v2);
const upper = b2Max(v1, v2);
const aabb = new b2AABB(lower.x, lower.y, upper.x, upper.y);
return aabb;
}
/**
* @summary Determines if a point lies within a circle.
* @function b2PointInCircle
* @param {b2Vec2} point - The point to test, represented as a 2D vector.
* @param {b2Circle} shape - The circle to test against, containing center and radius properties.
* @returns {boolean} True if the point lies within or on the circle's boundary, false otherwise.
* @description
* Tests if a point lies within a circle by comparing the squared distance between
* the point and circle's center against the circle's squared radius.
*/
export function b2PointInCircle(point, shape)
{
const center = shape.center;
return b2DistanceSquared(point, center) <= shape.radius * shape.radius;
}
/**
* @function b2PointInCapsule
* @summary Tests if a point lies inside a capsule shape.
* @param {b2Vec2} point - The point to test
* @param {b2Capsule} shape - The capsule shape defined by two centers and a radius
* @returns {boolean} True if the point lies inside or on the capsule, false otherwise
* @description
* A capsule is defined by two end centers (center1 and center2) and a radius.
* The function calculates if the given point lies within the capsule by:
* 1. Testing if the capsule has zero length (centers are identical)
* 2. If not, finding the closest point on the line segment between centers
* 3. Checking if the distance from the test point to the closest point is within the radius
*/
export function b2PointInCapsule(point, shape)
{
const rr = shape.radius * shape.radius;
const p1 = shape.center1;
const p2 = shape.center2;
const d = b2Sub(p2, p1);
const dd = b2Dot(d, d);
if (dd == 0.0)
{
// Capsule is really a circle
return b2DistanceSquared(point, p1) <= rr;
}
// Get closest point on capsule segment
// c = p1 + t * d
// dot(point - c, d) = 0
// dot(point - p1 - t * d, d) = 0
// t = dot(point - p1, d) / dot(d, d)
let t = b2Dot(b2Sub(point, p1), d) / dd;
t = b2ClampFloat(t, 0.0, 1.0);
const c = b2MulAdd(p1, t, d);
// Is query point within radius around closest point?
return b2DistanceSquared(point, c) <= rr;
}
/**
* @function b2PointInPolygon
* @description
* Tests if a point lies inside a polygon shape by calculating the minimum distance
* between the point and the polygon.
* @param {b2Vec2} point - The point to test
* @param {b2Polygon} shape - The polygon shape to test against
* @returns {boolean} True if the point is inside or on the polygon (within shape.radius), false otherwise
*/
export function b2PointInPolygon(point, shape)
{
const input = new b2DistanceInput();
input.proxyA = b2MakeProxy(shape.vertices, shape.count, 0.0);
input.proxyB = b2MakeProxy([ point ], 1, 0.0);
input.transformA = new b2Transform(new b2Vec2(0, 0), new b2Rot(1, 0));
input.transformB = new b2Transform(new b2Vec2(0, 0), new b2Rot(1, 0));
input.useRadii = false;
const cache = new b2DistanceCache();
const output = b2ShapeDistance(cache, input, null, 0);
return output.distance <= shape.radius;
}
const rayPoint = new b2Vec2(0, 0);
const rayNormal = new b2Vec2(0, 1);
/**
* @function b2RayCastCircle
* @summary Performs a ray cast against a circle shape.
* @param {b2RayCastInput} input - The ray cast input parameters containing:
* - origin: b2Vec2 starting point of the ray
* - translation: b2Vec2 direction and length of the ray
* - maxFraction: number maximum intersection distance as a fraction of ray length
* @param {b2Circle} shape - The circle shape to test against, containing:
* - center: b2Vec2 position of circle center
* - radius: number radius of the circle
* @returns {b2CastOutput} The ray cast results containing:
* - hit: boolean whether the ray intersects the circle
* - point: b2Vec2 point of intersection if hit is true
* - normal: b2Vec2 surface normal at intersection point if hit is true
* - fraction: number intersection distance as a fraction of ray length if hit is true
* @description
* Calculates the intersection point between a ray and a circle shape.
* Returns early with no hit if the ray length is 0 or if the ray passes outside the circle radius.
*/
export function b2RayCastCircle(input, shape)
{
console.assert(b2IsValidRay(input));
const p = shape.center.clone();
const output = new b2CastOutput(rayNormal, rayPoint);
// Shift ray so circle center is the origin
const s = b2Sub(input.origin, p);
const res = b2GetLengthAndNormalize(input.translation);
const length = res.length;
if (length == 0.0)
{
// zero length ray
return output;
}
const d = res.normal;
// Find closest point on ray to origin
// solve = dot(s + t * d, d) = 0
const t = -b2Dot(s, d);
// c is the closest point on the line to the origin
const c = b2MulAdd(s, t, d);
const cc = b2Dot(c, c);
const r = shape.radius;
const rr = r * r;
if (cc > rr)
{
// closest point is outside the circle
return output;
}
// Pythagoras
const h = Math.sqrt(rr - cc);
const fraction = t - h;
if (fraction < 0.0 || input.maxFraction * length < fraction)
{
// outside the range of the ray segment
return output;
}
const hitPoint = b2MulAdd(s, fraction, d);
output.fraction = fraction / length;
output.normal = b2Normalize(hitPoint);
output.point = b2MulAdd(p, shape.radius, output.normal);
output.hit = true;
return output;
}
/**
* @function b2RayCastCapsule
* @description
* Performs a ray cast against a capsule shape. A capsule is defined by two centers and a radius.
* If the capsule length is near zero, it degrades to a circle ray cast.
* @param {b2RayCastInput} input - Contains ray cast parameters including:
* - origin: b2Vec2 starting point of the ray
* - translation: b2Vec2 direction and length of the ray
* - maxFraction: number maximum intersection distance as a fraction of ray length
* @param {b2Capsule} shape - The capsule to test against, containing:
* - center1: b2Vec2 first endpoint of capsule centerline
* - center2: b2Vec2 second endpoint of capsule centerline
* - radius: number radius of the capsule
* @returns {b2CastOutput} Contains the ray cast results:
* - fraction: number intersection distance as a fraction of ray length
* - point: b2Vec2 point of intersection
* - normal: b2Vec2 surface normal at intersection
* - hit: boolean whether an intersection occurred
*/
export function b2RayCastCapsule(input, shape)
{
console.assert(b2IsValidRay(input));
const output = new b2CastOutput(rayNormal, rayPoint);
const v1 = shape.center1;
const v2 = shape.center2;
const e = b2Sub(v2, v1);
const res = b2GetLengthAndNormalize(e);
const capsuleLength = res.length;
const a = res.normal;
if (capsuleLength < eps)
{
// Capsule is really a circle
const circle = new b2Circle();
circle.center = v1;
circle.radius = shape.radius;
return b2RayCastCircle(input, circle);
}
const p1 = input.origin;
const d = input.translation;
// Ray from capsule start to ray start
const q = b2Sub(p1, v1);
const qa = b2Dot(q, a);
// Vector to ray start that is perpendicular to capsule axis
const qp = b2MulAdd(q, -qa, a);
const radius = shape.radius;
// Does the ray start within the infinite length capsule?
if (b2Dot(qp, qp) < radius * radius)
{
if (qa < 0.0)
{
// start point behind capsule segment
const circle = new b2Circle();
circle.center = v1;
circle.radius = shape.radius;
return b2RayCastCircle(input, circle);
}
if (qa > 1.0)
{
// start point ahead of capsule segment
const circle = new b2Circle();
circle.center = v2;
circle.radius = shape.radius;
return b2RayCastCircle(input, circle);
}
// ray starts inside capsule -> no hit
return output;
}
// Perpendicular to capsule axis, pointing right
let n = new b2Vec2(a.y, -a.x);
const res0 = b2GetLengthAndNormalize(d);
const rayLength = res0.length;
const u = res0.normal;
// Intersect ray with infinite length capsule
// v1 + radius * n + s1 * a = p1 + s2 * u
// v1 - radius * n + s1 * a = p1 + s2 * u
// s1 * a - s2 * u = b
// b = q - radius * ap
// or
// b = q + radius * ap
// Cramer's rule [a -u]
const den = -a.x * u.y + u.x * a.y;
if (-eps < den && den < eps)
{
// Ray is parallel to capsule and outside infinite length capsule
return output;
}
const b1 = b2MulSub(q, radius, n);
const b2 = b2MulAdd(q, radius, n);
const invDen = 1.0 / den;
// Cramer's rule [a b1]
const s21 = (a.x * b1.y - b1.x * a.y) * invDen;
// Cramer's rule [a b2]
const s22 = (a.x * b2.y - b2.x * a.y) * invDen;
let s2, b;
if (s21 < s22)
{
s2 = s21;
b = b1;
}
else
{
s2 = s22;
b = b2;
n = b2Neg(n);
}
if (s2 < 0.0 || input.maxFraction * rayLength < s2)
{
return output;
}
// Cramer's rule [b -u]
const s1 = (-b.x * u.y + u.x * b.y) * invDen;
if (s1 < 0.0)
{
// ray passes behind capsule segment
const circle = new b2Circle();
circle.center = v1;
circle.radius = shape.radius;
return b2RayCastCircle(input, circle);
}
else if (capsuleLength < s1)
{
// ray passes ahead of capsule segment
const circle = new b2Circle();
circle.center = v2;
circle.radius = shape.radius;
return b2RayCastCircle(input, circle);
}
else
{
// ray hits capsule side
output.fraction = s2 / rayLength;
output.point = b2Add(b2Lerp(v1, v2, s1 / capsuleLength), b2MulSV(shape.radius, n));
output.normal = n;
output.hit = true;
return output;
}
}
/**
* @function b2RayCastSegment
* @description
* Performs a ray cast against a line segment, determining if and where the ray intersects the segment.
* For one-sided segments, intersections are only detected from one side based on a cross product test.
* @param {b2RayCastInput} input - Contains origin point, translation vector, and max fraction for the ray