dacha 0.18.0-alpha.1 → 0.18.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/build/contrib/components/collider/index.d.ts +2 -2
  2. package/build/contrib/systems/physics-system/subsystems/collision-detection/aabb-builders/build-capsule-aabb.d.ts +2 -0
  3. package/build/contrib/systems/physics-system/subsystems/collision-detection/aabb-builders/build-capsule-aabb.js +13 -0
  4. package/build/contrib/systems/physics-system/subsystems/collision-detection/aabb-builders/build-segment-aabb.js +1 -1
  5. package/build/contrib/systems/physics-system/subsystems/collision-detection/aabb-builders/index.js +2 -0
  6. package/build/contrib/systems/physics-system/subsystems/collision-detection/constants.d.ts +1 -0
  7. package/build/contrib/systems/physics-system/subsystems/collision-detection/constants.js +1 -0
  8. package/build/contrib/systems/physics-system/subsystems/collision-detection/dispersion-calculator/index.js +18 -4
  9. package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-capsule-geometry.d.ts +3 -0
  10. package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-capsule-geometry.js +34 -0
  11. package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/index.d.ts +2 -0
  12. package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/index.js +2 -0
  13. package/build/contrib/systems/physics-system/subsystems/collision-detection/index.js +1 -1
  14. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-box/check-boxes-intersection.js +1 -1
  15. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-box/utils.d.ts +13 -2
  16. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-box/utils.js +30 -8
  17. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-capsule/check-box-and-capsule-intersection.d.ts +20 -0
  18. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-capsule/check-box-and-capsule-intersection.js +32 -0
  19. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-capsule/utils.d.ts +15 -0
  20. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-capsule/utils.js +135 -0
  21. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-circle/check-box-and-circle-intersection.js +5 -5
  22. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-segment/check-box-and-segment-intersection.js +4 -5
  23. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-segment/utils.d.ts +0 -1
  24. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/box-segment/utils.js +22 -17
  25. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/capsule-capsule/check-capsules-intersection.d.ts +14 -0
  26. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/capsule-capsule/check-capsules-intersection.js +26 -0
  27. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/circle-capsule/check-circle-and-capsule-intersection.d.ts +11 -0
  28. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/circle-capsule/check-circle-and-capsule-intersection.js +40 -0
  29. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/circle-circle/check-circles-intersection.js +1 -1
  30. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/circle-segment/check-circle-and-segment-intersection.js +5 -5
  31. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/common/capsule.d.ts +12 -0
  32. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/common/capsule.js +37 -0
  33. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/common/normals.d.ts +3 -0
  34. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/common/normals.js +8 -0
  35. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/common/points.d.ts +4 -0
  36. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/common/points.js +8 -0
  37. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/common/projections.d.ts +9 -0
  38. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/common/projections.js +32 -0
  39. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/common/segment-distance.d.ts +17 -0
  40. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/common/segment-distance.js +63 -0
  41. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/index.js +30 -3
  42. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/point-box/check-point-and-box-intersection.js +5 -5
  43. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/point-capsule/check-point-and-capsule-intersection.d.ts +10 -0
  44. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/point-capsule/check-point-and-capsule-intersection.js +33 -0
  45. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/point-circle/check-point-and-circle-intersection.js +4 -5
  46. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/point-segment/check-point-and-segment-intersection.js +5 -5
  47. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/ray-box/check-ray-and-box-intersection.js +3 -4
  48. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/ray-capsule/check-ray-and-capsule-intersection.d.ts +11 -0
  49. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/ray-capsule/check-ray-and-capsule-intersection.js +126 -0
  50. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/ray-circle/check-ray-and-circle-intersection.js +3 -4
  51. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/ray-segment/check-ray-and-segment-intersection.js +3 -4
  52. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/segment-capsule/check-segment-and-capsule-intersection.d.ts +12 -0
  53. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/segment-capsule/check-segment-and-capsule-intersection.js +24 -0
  54. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/tests/helpers.d.ts +3 -2
  55. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/tests/helpers.js +34 -20
  56. package/build/contrib/systems/physics-system/subsystems/collision-detection/types.d.ts +4 -1
  57. package/build/engine/math-lib/vector/ops.js +8 -6
  58. package/package.json +1 -1
  59. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/utils.d.ts +0 -9
  60. package/build/contrib/systems/physics-system/subsystems/collision-detection/intersection-checkers/utils.js +0 -23
@@ -1,7 +1,7 @@
1
1
  import { Component } from '../../../engine/component';
2
2
  import type { Point } from '../../../engine/math-lib';
3
3
  export interface ColliderConfig {
4
- type: 'box' | 'circle' | 'segment';
4
+ type: 'box' | 'circle' | 'segment' | 'capsule';
5
5
  centerX: number;
6
6
  centerY: number;
7
7
  sizeX?: number;
@@ -48,7 +48,7 @@ export interface ColliderConfig {
48
48
  * @category Components
49
49
  */
50
50
  export declare class Collider extends Component {
51
- type: 'box' | 'circle' | 'segment';
51
+ type: 'box' | 'circle' | 'segment' | 'capsule';
52
52
  centerX: number;
53
53
  centerY: number;
54
54
  sizeX?: number;
@@ -0,0 +1,2 @@
1
+ import type { AABB, Geometry } from '../types';
2
+ export declare const buildCapsuleAABB: (geometry: Geometry) => AABB;
@@ -0,0 +1,13 @@
1
+ export const buildCapsuleAABB = (geometry) => {
2
+ const { point1, point2, radius } = geometry;
3
+ return {
4
+ min: {
5
+ x: Math.min(point1.x, point2.x) - radius,
6
+ y: Math.min(point1.y, point2.y) - radius,
7
+ },
8
+ max: {
9
+ x: Math.max(point1.x, point2.x) + radius,
10
+ y: Math.max(point1.y, point2.y) + radius,
11
+ },
12
+ };
13
+ };
@@ -1,4 +1,4 @@
1
- import { INTERSECTION_EPSILON } from '../intersection-checkers/utils';
1
+ import { INTERSECTION_EPSILON } from '../constants';
2
2
  export const buildSegmentAABB = (geometry) => {
3
3
  const { point1, point2 } = geometry;
4
4
  return {
@@ -1,10 +1,12 @@
1
1
  import { buildBoxAABB } from './build-box-aabb';
2
+ import { buildCapsuleAABB } from './build-capsule-aabb';
2
3
  import { buildCircleAABB } from './build-circle-aabb';
3
4
  import { buildPointAABB } from './build-point-aabb';
4
5
  import { buildRayAABB } from './build-ray-aabb';
5
6
  import { buildSegmentAABB } from './build-segment-aabb';
6
7
  export const aabbBuilders = {
7
8
  box: buildBoxAABB,
9
+ capsule: buildCapsuleAABB,
8
10
  circle: buildCircleAABB,
9
11
  segment: buildSegmentAABB,
10
12
  point: buildPointAABB,
@@ -0,0 +1 @@
1
+ export declare const INTERSECTION_EPSILON = 0.000001;
@@ -0,0 +1 @@
1
+ export const INTERSECTION_EPSILON = 1e-6;
@@ -16,12 +16,26 @@ export class DispersionCalculator {
16
16
  this.sampleSize += 1;
17
17
  }
18
18
  removeFromSample(aabb) {
19
- const average = (aabb.min[this.axis] + aabb.max[this.axis]) * 0.5;
20
- this.sum -= average;
21
- this.squaredSum -= average ** 2;
19
+ if (this.sampleSize === 0) {
20
+ return;
21
+ }
22
22
  this.sampleSize -= 1;
23
+ if (this.sampleSize === 0) {
24
+ this.sum = 0;
25
+ this.squaredSum = 0;
26
+ }
27
+ else {
28
+ const average = (aabb.min[this.axis] + aabb.max[this.axis]) * 0.5;
29
+ this.sum -= average;
30
+ this.squaredSum -= average ** 2;
31
+ }
23
32
  }
24
33
  getDispersion() {
25
- return (this.squaredSum / this.sampleSize) - (this.sum / this.sampleSize) ** 2;
34
+ if (this.sampleSize <= 1) {
35
+ return 0;
36
+ }
37
+ const average = this.sum / this.sampleSize;
38
+ const dispersion = this.squaredSum / this.sampleSize - average ** 2;
39
+ return Math.max(0, dispersion);
26
40
  }
27
41
  }
@@ -0,0 +1,3 @@
1
+ import type { Collider, Transform } from '../../../../../components';
2
+ import type { CapsuleGeometry } from '../types';
3
+ export declare function buildCapsuleGeometry(collider: Collider, transform: Transform): CapsuleGeometry;
@@ -0,0 +1,34 @@
1
+ import { VectorOps } from '../../../../../../engine/math-lib';
2
+ export function buildCapsuleGeometry(collider, transform) {
3
+ const centerX = collider.centerX + transform.world.position.x;
4
+ const centerY = collider.centerY + transform.world.position.y;
5
+ const point1X = collider.point1?.x ?? 0;
6
+ const point1Y = collider.point1?.y ?? 0;
7
+ const point2X = collider.point2?.x ?? 0;
8
+ const point2Y = collider.point2?.y ?? 0;
9
+ const rotation = transform.world.rotation;
10
+ const scaleX = transform.world.scale.x;
11
+ const scaleY = transform.world.scale.y;
12
+ const cos = Math.cos(rotation);
13
+ const sin = Math.sin(rotation);
14
+ const buildPoint = (x, y) => {
15
+ const scaledX = x * scaleX;
16
+ const scaledY = y * scaleY;
17
+ return {
18
+ x: scaledX * cos - scaledY * sin + centerX,
19
+ y: scaledX * sin + scaledY * cos + centerY,
20
+ };
21
+ };
22
+ const point1 = buildPoint(point1X, point1Y);
23
+ const point2 = buildPoint(point2X, point2Y);
24
+ return {
25
+ center: {
26
+ x: (point1.x + point2.x) / 2,
27
+ y: (point1.y + point2.y) / 2,
28
+ },
29
+ point1,
30
+ point2,
31
+ normal: VectorOps.getNormal(point1.x, point2.x, point1.y, point2.y),
32
+ radius: (collider.radius ?? 0) * Math.max(scaleX, scaleY),
33
+ };
34
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Collider, Transform } from '../../../../../components';
2
2
  import type { Geometry } from '../types';
3
3
  import { buildBoxGeometry } from './build-box-geometry';
4
+ import { buildCapsuleGeometry } from './build-capsule-geometry';
4
5
  import { buildCircleGeometry } from './build-circle-geometry';
5
6
  import { buildPointGeometry } from './build-point-geometry';
6
7
  import { buildRayGeometry } from './build-ray-geometry';
@@ -8,6 +9,7 @@ import { buildSegmentGeometry } from './build-segment-geometry';
8
9
  export type BuildGeometryFn = (collider: Collider, transform: Transform) => Geometry;
9
10
  export declare const geometryBuilders: {
10
11
  box: typeof buildBoxGeometry;
12
+ capsule: typeof buildCapsuleGeometry;
11
13
  circle: typeof buildCircleGeometry;
12
14
  segment: typeof buildSegmentGeometry;
13
15
  point: typeof buildPointGeometry;
@@ -1,10 +1,12 @@
1
1
  import { buildBoxGeometry } from './build-box-geometry';
2
+ import { buildCapsuleGeometry } from './build-capsule-geometry';
2
3
  import { buildCircleGeometry } from './build-circle-geometry';
3
4
  import { buildPointGeometry } from './build-point-geometry';
4
5
  import { buildRayGeometry } from './build-ray-geometry';
5
6
  import { buildSegmentGeometry } from './build-segment-geometry';
6
7
  export const geometryBuilders = {
7
8
  box: buildBoxGeometry,
9
+ capsule: buildCapsuleGeometry,
8
10
  circle: buildCircleGeometry,
9
11
  segment: buildSegmentGeometry,
10
12
  point: buildPointGeometry,
@@ -183,7 +183,7 @@ export class CollisionDetectionSubsystem {
183
183
  testAABB(proxy1, proxy2, axis) {
184
184
  const aabb1 = proxy1.aabb;
185
185
  const aabb2 = proxy2.aabb;
186
- return (aabb1.max[axis] > aabb2.min[axis] && aabb1.min[axis] < aabb2.max[axis]);
186
+ return (aabb1.max[axis] >= aabb2.min[axis] && aabb1.min[axis] <= aabb2.max[axis]);
187
187
  }
188
188
  testCollisionLayers(proxy1, proxy2) {
189
189
  return this.collisionMatrix[proxy1.layer]?.[proxy2.layer] ?? true;
@@ -1,4 +1,4 @@
1
- import { orientNormal } from '../utils';
1
+ import { orientNormal } from '../common/normals';
2
2
  import { findMinBoxesOverlap, buildContactPoints } from './utils';
3
3
  /**
4
4
  * Checks box colliders for intersection.
@@ -6,12 +6,23 @@ export interface AxisOverlap {
6
6
  axis: Vector2;
7
7
  overlap: number;
8
8
  }
9
+ /**
10
+ * Runs the SAT pass for one box's face normals against another box.
11
+ *
12
+ * Each face normal is a potential separating axis. If any projection pair does
13
+ * not overlap, the boxes cannot intersect. Otherwise, the axis with the
14
+ * smallest overlap is the least expensive direction to separate the boxes, so
15
+ * it becomes the candidate collision normal for this polygon.
16
+ */
9
17
  export declare const findMinBoxesOverlap: (geometry1: BoxGeometry, geometry2: BoxGeometry) => AxisOverlap | false;
10
18
  /**
11
19
  * Builds up to two box-vs-box contact points by clipping the incident edge
12
20
  * against the side planes of the reference edge.
13
21
  *
14
- * The returned points lie on the reference face plane, which makes them
15
- * suitable to use as world-space contact points in a simple impulse solver.
22
+ * The process mirrors the standard contact manifold construction for SAT:
23
+ * choose a reference face, choose the incident face most opposed to it, clip
24
+ * the incident segment by the reference face's tangent bounds, then project
25
+ * surviving points back onto the reference face plane. The projected points are
26
+ * suitable as world-space contacts for impulse resolution.
16
27
  */
17
28
  export declare const buildContactPoints: (referencePolygon: BoxGeometry, referenceNormal: Vector2, incidentPolygon: BoxGeometry) => Point[];
@@ -1,20 +1,27 @@
1
1
  import { Vector2, VectorOps } from '../../../../../../../engine/math-lib';
2
- import { INTERSECTION_EPSILON, projectPolygon } from '../utils';
2
+ import { getProjectionOverlap, projectPolygon } from '../common/projections';
3
3
  export const CONTACT_EPSILON = 1e-4;
4
4
  export const MAX_CONTACT_POINTS = 2;
5
+ /**
6
+ * Runs the SAT pass for one box's face normals against another box.
7
+ *
8
+ * Each face normal is a potential separating axis. If any projection pair does
9
+ * not overlap, the boxes cannot intersect. Otherwise, the axis with the
10
+ * smallest overlap is the least expensive direction to separate the boxes, so
11
+ * it becomes the candidate collision normal for this polygon.
12
+ */
5
13
  export const findMinBoxesOverlap = (geometry1, geometry2) => {
6
14
  let minOverlap = Infinity;
7
15
  let bestAxis = geometry1.edges[0].normal;
8
- for (const edge of geometry1.edges) {
16
+ for (let i = 0; i < 2; i += 1) {
17
+ const edge = geometry1.edges[i];
9
18
  const axis = edge.normal;
10
19
  const projection1 = projectPolygon(geometry1.points, axis);
11
20
  const projection2 = projectPolygon(geometry2.points, axis);
12
- const distance1 = projection1.min - projection2.max;
13
- const distance2 = projection2.min - projection1.max;
14
- if (distance1 > INTERSECTION_EPSILON || distance2 > INTERSECTION_EPSILON) {
21
+ const overlap = getProjectionOverlap(projection1.min, projection1.max, projection2.min, projection2.max);
22
+ if (overlap === false) {
15
23
  return false;
16
24
  }
17
- const overlap = Math.min(Math.abs(distance1), Math.abs(distance2));
18
25
  if (overlap < minOverlap) {
19
26
  minOverlap = overlap;
20
27
  bestAxis = axis;
@@ -46,6 +53,12 @@ const clipSegmentToLine = (vertices, normal, offset) => {
46
53
  }
47
54
  return clipped;
48
55
  };
56
+ /**
57
+ * Selects the incident edge for clipping.
58
+ *
59
+ * The incident edge should face into the reference face, so it is the edge
60
+ * whose normal points most strongly opposite the reference collision normal.
61
+ */
49
62
  const getMostAntiParallelEdge = (polygon, normal) => {
50
63
  let bestEdge = polygon.edges[0];
51
64
  let minDot = VectorOps.dotProduct(bestEdge.normal, normal);
@@ -58,6 +71,12 @@ const getMostAntiParallelEdge = (polygon, normal) => {
58
71
  }
59
72
  return bestEdge;
60
73
  };
74
+ /**
75
+ * Selects the reference edge that owns the collision plane.
76
+ *
77
+ * The reference edge is the edge whose normal most closely matches the chosen
78
+ * collision normal. Its side planes bound the incident edge during clipping.
79
+ */
61
80
  const getMostParallelEdge = (polygon, normal) => {
62
81
  let bestEdge = polygon.edges[0];
63
82
  let maxDot = VectorOps.dotProduct(bestEdge.normal, normal);
@@ -88,8 +107,11 @@ const dedupePoints = (points) => {
88
107
  * Builds up to two box-vs-box contact points by clipping the incident edge
89
108
  * against the side planes of the reference edge.
90
109
  *
91
- * The returned points lie on the reference face plane, which makes them
92
- * suitable to use as world-space contact points in a simple impulse solver.
110
+ * The process mirrors the standard contact manifold construction for SAT:
111
+ * choose a reference face, choose the incident face most opposed to it, clip
112
+ * the incident segment by the reference face's tangent bounds, then project
113
+ * surviving points back onto the reference face plane. The projected points are
114
+ * suitable as world-space contacts for impulse resolution.
93
115
  */
94
116
  export const buildContactPoints = (referencePolygon, referenceNormal, incidentPolygon) => {
95
117
  const referenceEdge = getMostParallelEdge(referencePolygon, referenceNormal);
@@ -0,0 +1,20 @@
1
+ import type { Intersection, Proxy } from '../../types';
2
+ /**
3
+ * Checks a box against a capsule.
4
+ *
5
+ * A capsule is treated as a line segment swept by a radius. The fast rejection
6
+ * path finds the closest distance between each box edge and the capsule axis;
7
+ * if that distance is greater than the capsule radius, the shapes are
8
+ * separated.
9
+ *
10
+ * When the capsule axis is already inside the box, SAT is used with
11
+ * the box face normals plus the capsule axis normal, with the capsule
12
+ * projection expanded by its radius.
13
+ *
14
+ * For deep axis overlaps, the capsule axis
15
+ * is clipped by the box and then shifted by the capsule radius along the
16
+ * collision normal so contacts lie on the capsule surface.
17
+ *
18
+ * Side and corner contacts use the nearest box point.
19
+ */
20
+ export declare const checkBoxAndCapsuleIntersection: (arg1: Proxy, arg2: Proxy) => Intersection | false;
@@ -0,0 +1,32 @@
1
+ import { orientNormal } from '../common/normals';
2
+ import { buildBoxCapsuleIntersection } from './utils';
3
+ /**
4
+ * Checks a box against a capsule.
5
+ *
6
+ * A capsule is treated as a line segment swept by a radius. The fast rejection
7
+ * path finds the closest distance between each box edge and the capsule axis;
8
+ * if that distance is greater than the capsule radius, the shapes are
9
+ * separated.
10
+ *
11
+ * When the capsule axis is already inside the box, SAT is used with
12
+ * the box face normals plus the capsule axis normal, with the capsule
13
+ * projection expanded by its radius.
14
+ *
15
+ * For deep axis overlaps, the capsule axis
16
+ * is clipped by the box and then shifted by the capsule radius along the
17
+ * collision normal so contacts lie on the capsule surface.
18
+ *
19
+ * Side and corner contacts use the nearest box point.
20
+ */
21
+ export const checkBoxAndCapsuleIntersection = (arg1, arg2) => {
22
+ const box = arg1.geometry;
23
+ const capsule = arg2.geometry;
24
+ const intersection = buildBoxCapsuleIntersection(box, capsule);
25
+ if (!intersection) {
26
+ return false;
27
+ }
28
+ return {
29
+ ...intersection,
30
+ normal: orientNormal(intersection.normal, box.center, capsule.center),
31
+ };
32
+ };
@@ -0,0 +1,15 @@
1
+ import type { BoxGeometry, CapsuleGeometry, Intersection } from '../../types';
2
+ /**
3
+ * Builds a box-vs-capsule manifold using the cheapest valid path for the
4
+ * contact shape.
5
+ *
6
+ * Edge-to-axis distance handles the common side/corner case directly: if the
7
+ * closest box edge and capsule axis are farther apart than the capsule radius,
8
+ * the shapes are separated; otherwise the closest box point is already a good
9
+ * contact. When the axis penetrates or lies inside the box, distance alone
10
+ * cannot pick a stable separation direction, so the function falls back to SAT
11
+ * over the box face normals plus the capsule axis normal. The chosen SAT axis
12
+ * gives penetration depth, and clipped axis contacts produce one or two
13
+ * surface points for deeper face-like overlaps.
14
+ */
15
+ export declare const buildBoxCapsuleIntersection: (box: BoxGeometry, capsule: CapsuleGeometry) => Intersection | false;
@@ -0,0 +1,135 @@
1
+ import { MathOps, Vector2, VectorOps, } from '../../../../../../../engine/math-lib';
2
+ import { INTERSECTION_EPSILON } from '../../constants';
3
+ import { orientNormal } from '../common/normals';
4
+ import { arePointsEqual, lerpPoint } from '../common/points';
5
+ import { getProjectionOverlap, projectPolygon, projectSegment, } from '../common/projections';
6
+ import { getClosestPointsBetweenSegments } from '../common/segment-distance';
7
+ /**
8
+ * Clips the capsule axis to the portion that lies inside the box.
9
+ *
10
+ * A capsule is a segment swept by a radius, so when the best collision axis is
11
+ * a face-like axis we first find the interval of the center segment that
12
+ * survives all box half-spaces. The resulting endpoint(s) are then shifted by
13
+ * the capsule radius opposite the contact normal so the manifold lies on the
14
+ * capsule surface rather than on its center line.
15
+ */
16
+ const buildAxisContactPoints = (box, capsule, normal) => {
17
+ let start = 0;
18
+ let end = 1;
19
+ for (const edge of box.edges) {
20
+ const offset = VectorOps.dotProduct(edge.point1, edge.normal);
21
+ const point1Distance = VectorOps.dotProduct(capsule.point1, edge.normal) - offset;
22
+ const point2Distance = VectorOps.dotProduct(capsule.point2, edge.normal) - offset;
23
+ if (point1Distance > INTERSECTION_EPSILON &&
24
+ point2Distance > INTERSECTION_EPSILON) {
25
+ return [];
26
+ }
27
+ if (point1Distance <= INTERSECTION_EPSILON &&
28
+ point2Distance <= INTERSECTION_EPSILON) {
29
+ continue;
30
+ }
31
+ const denominator = point1Distance - point2Distance;
32
+ const t = denominator === 0 ? 0 : point1Distance / denominator;
33
+ if (point1Distance > INTERSECTION_EPSILON) {
34
+ start = Math.max(start, t);
35
+ }
36
+ else {
37
+ end = Math.min(end, t);
38
+ }
39
+ if (start > end + INTERSECTION_EPSILON) {
40
+ return [];
41
+ }
42
+ }
43
+ const contact1 = lerpPoint(capsule.point1, capsule.point2, MathOps.clamp(start, 0, 1));
44
+ contact1.x -= normal.x * capsule.radius;
45
+ contact1.y -= normal.y * capsule.radius;
46
+ if (Math.abs(end - start) <= INTERSECTION_EPSILON) {
47
+ return [contact1];
48
+ }
49
+ const contact2 = lerpPoint(capsule.point1, capsule.point2, MathOps.clamp(end, 0, 1));
50
+ contact2.x -= normal.x * capsule.radius;
51
+ contact2.y -= normal.y * capsule.radius;
52
+ return arePointsEqual(contact1, contact2) ? [contact1] : [contact1, contact2];
53
+ };
54
+ const getAxisOverlap = (box, capsule, axis) => {
55
+ const boxProjection = projectPolygon(box.points, axis);
56
+ const segmentProjection = projectSegment(capsule.point1, capsule.point2, axis);
57
+ const capsuleMin = segmentProjection.min - capsule.radius;
58
+ const capsuleMax = segmentProjection.max + capsule.radius;
59
+ return getProjectionOverlap(capsuleMin, capsuleMax, boxProjection.min, boxProjection.max);
60
+ };
61
+ /**
62
+ * Builds a box-vs-capsule manifold using the cheapest valid path for the
63
+ * contact shape.
64
+ *
65
+ * Edge-to-axis distance handles the common side/corner case directly: if the
66
+ * closest box edge and capsule axis are farther apart than the capsule radius,
67
+ * the shapes are separated; otherwise the closest box point is already a good
68
+ * contact. When the axis penetrates or lies inside the box, distance alone
69
+ * cannot pick a stable separation direction, so the function falls back to SAT
70
+ * over the box face normals plus the capsule axis normal. The chosen SAT axis
71
+ * gives penetration depth, and clipped axis contacts produce one or two
72
+ * surface points for deeper face-like overlaps.
73
+ */
74
+ export const buildBoxCapsuleIntersection = (box, capsule) => {
75
+ let closestBoxPoint = box.points[0];
76
+ let closestCapsulePoint = capsule.point1;
77
+ let minDistance = Infinity;
78
+ for (const edge of box.edges) {
79
+ const closest = getClosestPointsBetweenSegments(edge, capsule);
80
+ if (closest.distance < minDistance) {
81
+ minDistance = closest.distance;
82
+ closestBoxPoint = closest.point1;
83
+ closestCapsulePoint = closest.point2;
84
+ }
85
+ }
86
+ if (VectorOps.isPointInPolygon(capsule.point1, box.edges)) {
87
+ minDistance = 0;
88
+ closestBoxPoint = capsule.point1;
89
+ closestCapsulePoint = capsule.point1;
90
+ }
91
+ else if (VectorOps.isPointInPolygon(capsule.point2, box.edges)) {
92
+ minDistance = 0;
93
+ closestBoxPoint = capsule.point2;
94
+ closestCapsulePoint = capsule.point2;
95
+ }
96
+ if (minDistance > capsule.radius + INTERSECTION_EPSILON) {
97
+ return false;
98
+ }
99
+ if (minDistance > INTERSECTION_EPSILON) {
100
+ const normal = new Vector2(closestCapsulePoint.x - closestBoxPoint.x, closestCapsulePoint.y - closestBoxPoint.y).normalize();
101
+ return {
102
+ normal,
103
+ penetration: Math.max(0, capsule.radius - minDistance),
104
+ contactPoints: [closestBoxPoint],
105
+ };
106
+ }
107
+ let minOverlap = Infinity;
108
+ let bestAxis = box.edges[0].normal;
109
+ for (let i = 0; i < 2; i += 1) {
110
+ const edge = box.edges[i];
111
+ const overlap = getAxisOverlap(box, capsule, edge.normal);
112
+ if (overlap === false) {
113
+ return false;
114
+ }
115
+ if (overlap < minOverlap) {
116
+ minOverlap = overlap;
117
+ bestAxis = edge.normal;
118
+ }
119
+ }
120
+ const capsuleAxisOverlap = getAxisOverlap(box, capsule, capsule.normal);
121
+ if (capsuleAxisOverlap === false) {
122
+ return false;
123
+ }
124
+ if (capsuleAxisOverlap < minOverlap) {
125
+ minOverlap = capsuleAxisOverlap;
126
+ bestAxis = capsule.normal;
127
+ }
128
+ const contactNormal = orientNormal(bestAxis.clone(), box.center, capsule.center);
129
+ const contactPoints = buildAxisContactPoints(box, capsule, contactNormal);
130
+ return {
131
+ normal: bestAxis.clone(),
132
+ penetration: minOverlap,
133
+ contactPoints: contactPoints.length > 0 ? contactPoints : [closestBoxPoint],
134
+ };
135
+ };
@@ -1,5 +1,6 @@
1
1
  import { MathOps, Vector2, VectorOps, } from '../../../../../../../engine/math-lib';
2
- import { orientNormal, INTERSECTION_EPSILON } from '../utils';
2
+ import { INTERSECTION_EPSILON } from '../../constants';
3
+ import { orientNormal } from '../common/normals';
3
4
  const buildNormal = (circle, closestEdge, closestPoint) => {
4
5
  let normal = new Vector2(circle.center.x - closestPoint.x, circle.center.y - closestPoint.y);
5
6
  if (normal.magnitude === 0) {
@@ -15,9 +16,8 @@ const buildNormal = (circle, closestEdge, closestPoint) => {
15
16
  * collision direction and penetration is measured to that face.
16
17
  */
17
18
  export const checkBoxAndCircleIntersection = (arg1, arg2) => {
18
- const isBoxFirst = 'radius' in arg2.geometry;
19
- const box = (isBoxFirst ? arg1.geometry : arg2.geometry);
20
- const circle = (isBoxFirst ? arg2.geometry : arg1.geometry);
19
+ const box = arg1.geometry;
20
+ const circle = arg2.geometry;
21
21
  let closestEdge = box.edges[0];
22
22
  let closestPoint = box.points[0];
23
23
  let minDistance = Infinity;
@@ -39,7 +39,7 @@ export const checkBoxAndCircleIntersection = (arg1, arg2) => {
39
39
  ? circle.radius + minDistance
40
40
  : Math.max(0, circle.radius - minDistance);
41
41
  return {
42
- normal: orientNormal(buildNormal(circle, closestEdge, closestPoint), arg1.geometry.center, arg2.geometry.center),
42
+ normal: orientNormal(buildNormal(circle, closestEdge, closestPoint), box.center, circle.center),
43
43
  penetration,
44
44
  contactPoints: [closestPoint],
45
45
  };
@@ -1,5 +1,5 @@
1
1
  import { buildBoxSegmentContactPoints, findMinBoxSegmentOverlap } from './utils';
2
- import { orientNormal } from '../utils';
2
+ import { orientNormal } from '../common/normals';
3
3
  /**
4
4
  * Checks a box against a segment.
5
5
  *
@@ -9,9 +9,8 @@ import { orientNormal } from '../utils';
9
9
  * segment, and explicit edge/segment intersections.
10
10
  */
11
11
  export const checkBoxAndSegmentIntersection = (arg1, arg2) => {
12
- const isBoxFirst = 'edges' in arg1.geometry;
13
- const box = (isBoxFirst ? arg1.geometry : arg2.geometry);
14
- const segment = (isBoxFirst ? arg2.geometry : arg1.geometry);
12
+ const box = arg1.geometry;
13
+ const segment = arg2.geometry;
15
14
  const overlap = findMinBoxSegmentOverlap(box, segment);
16
15
  if (overlap === false) {
17
16
  return false;
@@ -21,7 +20,7 @@ export const checkBoxAndSegmentIntersection = (arg1, arg2) => {
21
20
  return false;
22
21
  }
23
22
  return {
24
- normal: orientNormal(overlap.axis.clone(), arg1.geometry.center, arg2.geometry.center),
23
+ normal: orientNormal(overlap.axis.clone(), box.center, segment.center),
25
24
  penetration: overlap.overlap,
26
25
  contactPoints,
27
26
  };
@@ -1,6 +1,5 @@
1
1
  import { type Vector2 } from '../../../../../../../engine/math-lib';
2
2
  import type { BoxGeometry, Point, SegmentGeometry } from '../../types';
3
- export declare const lerpPoint: (point1: Point, point2: Point, value: number) => Point;
4
3
  export declare const findMinBoxSegmentOverlap: (box: BoxGeometry, segment: SegmentGeometry) => {
5
4
  axis: Vector2;
6
5
  overlap: number;
@@ -1,29 +1,34 @@
1
1
  import { MathOps, VectorOps, } from '../../../../../../../engine/math-lib';
2
- import { INTERSECTION_EPSILON, projectPolygon } from '../utils';
3
- const arePointsEqual = (point1, point2) => Math.abs(point1.x - point2.x) <= INTERSECTION_EPSILON &&
4
- Math.abs(point1.y - point2.y) <= INTERSECTION_EPSILON;
5
- export const lerpPoint = (point1, point2, value) => ({
6
- x: point1.x + (point2.x - point1.x) * value,
7
- y: point1.y + (point2.y - point1.y) * value,
8
- });
2
+ import { arePointsEqual, lerpPoint } from '../common/points';
3
+ import { INTERSECTION_EPSILON } from '../../constants';
4
+ import { getProjectionOverlap, projectPolygon, projectSegment, } from '../common/projections';
5
+ const getBoxSegmentAxisOverlap = (box, segment, axis) => {
6
+ const boxProjection = projectPolygon(box.points, axis);
7
+ const segmentProjection = projectSegment(segment.point1, segment.point2, axis);
8
+ return getProjectionOverlap(segmentProjection.min, segmentProjection.max, boxProjection.min, boxProjection.max);
9
+ };
9
10
  export const findMinBoxSegmentOverlap = (box, segment) => {
10
- const axes = [...box.edges.map((edge) => edge.normal), segment.normal];
11
11
  let minOverlap = Infinity;
12
- let bestAxis = axes[0];
13
- for (const axis of axes) {
14
- const boxProjection = projectPolygon(box.points, axis);
15
- const segmentProjection = projectPolygon([segment.point1, segment.point2], axis);
16
- const distance1 = segmentProjection.min - boxProjection.max;
17
- const distance2 = boxProjection.min - segmentProjection.max;
18
- if (distance1 > INTERSECTION_EPSILON || distance2 > INTERSECTION_EPSILON) {
12
+ let bestAxis = box.edges[0].normal;
13
+ for (let i = 0; i < 2; i += 1) {
14
+ const edge = box.edges[i];
15
+ const overlap = getBoxSegmentAxisOverlap(box, segment, edge.normal);
16
+ if (overlap === false) {
19
17
  return false;
20
18
  }
21
- const overlap = Math.min(Math.abs(distance1), Math.abs(distance2));
22
19
  if (overlap < minOverlap) {
23
20
  minOverlap = overlap;
24
- bestAxis = axis;
21
+ bestAxis = edge.normal;
25
22
  }
26
23
  }
24
+ const segmentAxisOverlap = getBoxSegmentAxisOverlap(box, segment, segment.normal);
25
+ if (segmentAxisOverlap === false) {
26
+ return false;
27
+ }
28
+ if (segmentAxisOverlap < minOverlap) {
29
+ minOverlap = segmentAxisOverlap;
30
+ bestAxis = segment.normal;
31
+ }
27
32
  return {
28
33
  axis: bestAxis,
29
34
  overlap: minOverlap,
@@ -0,0 +1,14 @@
1
+ import type { Intersection, Proxy } from '../../types';
2
+ /**
3
+ * Checks two capsules for intersection.
4
+ *
5
+ * Each capsule is represented by its center segment swept by a radius. The
6
+ * closest points between the two finite center segments define the collision
7
+ * direction. The capsules overlap when that segment distance is less than or
8
+ * equal to the sum of both radii.
9
+ *
10
+ * The contact point is placed on the surface
11
+ * of the second capsule along the separating direction, matching the manifold
12
+ * orientation used by the other pair checkers.
13
+ */
14
+ export declare const checkCapsulesIntersection: (arg1: Proxy, arg2: Proxy) => Intersection | false;