@codexo/exojs-physics 0.15.2 → 0.16.0
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.
- package/README.md +51 -23
- package/dist/esm/Aabb.d.ts +6 -10
- package/dist/esm/Aabb.d.ts.map +1 -0
- package/dist/esm/Aabb.js +9 -5
- package/dist/esm/Aabb.js.map +1 -1
- package/dist/esm/Collider.d.ts +62 -10
- package/dist/esm/Collider.d.ts.map +1 -0
- package/dist/esm/Collider.js +263 -157
- package/dist/esm/Collider.js.map +1 -1
- package/dist/esm/ContactGraph.d.ts +51 -5
- package/dist/esm/ContactGraph.d.ts.map +1 -0
- package/dist/esm/ContactGraph.js +282 -223
- package/dist/esm/ContactGraph.js.map +1 -1
- package/dist/esm/ContactModifier.d.ts +85 -0
- package/dist/esm/ContactModifier.d.ts.map +1 -0
- package/dist/esm/ContactModifier.js +49 -0
- package/dist/esm/ContactModifier.js.map +1 -0
- package/dist/esm/PhysicsBody.d.ts +56 -22
- package/dist/esm/PhysicsBody.d.ts.map +1 -0
- package/dist/esm/PhysicsBody.js +461 -443
- package/dist/esm/PhysicsBody.js.map +1 -1
- package/dist/esm/PhysicsWorld.d.ts +276 -43
- package/dist/esm/PhysicsWorld.d.ts.map +1 -0
- package/dist/esm/PhysicsWorld.js +890 -626
- package/dist/esm/PhysicsWorld.js.map +1 -1
- package/dist/esm/TimeStepper.d.ts +3 -2
- package/dist/esm/TimeStepper.d.ts.map +1 -0
- package/dist/esm/TimeStepper.js +56 -67
- package/dist/esm/TimeStepper.js.map +1 -1
- package/dist/esm/backend/NativePhysicsBackend.d.ts +8 -4
- package/dist/esm/backend/NativePhysicsBackend.d.ts.map +1 -0
- package/dist/esm/backend/NativePhysicsBackend.js +48 -41
- package/dist/esm/backend/NativePhysicsBackend.js.map +1 -1
- package/dist/esm/backend/PhysicsBackend.d.ts +9 -2
- package/dist/esm/backend/PhysicsBackend.d.ts.map +1 -0
- package/dist/esm/binding/BindingRegistry.d.ts +25 -4
- package/dist/esm/binding/BindingRegistry.d.ts.map +1 -0
- package/dist/esm/binding/BindingRegistry.js +70 -36
- package/dist/esm/binding/BindingRegistry.js.map +1 -1
- package/dist/esm/binding/PhysicsBinding.d.ts +22 -5
- package/dist/esm/binding/PhysicsBinding.d.ts.map +1 -0
- package/dist/esm/binding/PhysicsBinding.js +46 -21
- package/dist/esm/binding/PhysicsBinding.js.map +1 -1
- package/dist/esm/broadphase/AabbTreeBroadPhase.d.ts +68 -0
- package/dist/esm/broadphase/AabbTreeBroadPhase.d.ts.map +1 -0
- package/dist/esm/broadphase/AabbTreeBroadPhase.js +149 -0
- package/dist/esm/broadphase/AabbTreeBroadPhase.js.map +1 -0
- package/dist/esm/broadphase/BroadPhase.d.ts +7 -4
- package/dist/esm/broadphase/BroadPhase.d.ts.map +1 -0
- package/dist/esm/broadphase/index.d.ts +2 -1
- package/dist/esm/broadphase/index.d.ts.map +1 -0
- package/dist/esm/collision/CollisionProxy.d.ts +3 -2
- package/dist/esm/collision/CollisionProxy.d.ts.map +1 -0
- package/dist/esm/collision/Manifold.d.ts +5 -4
- package/dist/esm/collision/Manifold.d.ts.map +1 -0
- package/dist/esm/collision/Manifold.js +34 -25
- package/dist/esm/collision/Manifold.js.map +1 -1
- package/dist/esm/collision/chainAdjacency.d.ts +29 -0
- package/dist/esm/collision/chainAdjacency.d.ts.map +1 -0
- package/dist/esm/collision/chainAdjacency.js +44 -0
- package/dist/esm/collision/chainAdjacency.js.map +1 -0
- package/dist/esm/collision/dispatch.d.ts +35 -0
- package/dist/esm/collision/dispatch.d.ts.map +1 -0
- package/dist/esm/collision/dispatch.js +58 -0
- package/dist/esm/collision/dispatch.js.map +1 -0
- package/dist/esm/collision/index.d.ts +2 -0
- package/dist/esm/collision/index.d.ts.map +1 -0
- package/dist/esm/collision/narrowphase.d.ts +5 -2
- package/dist/esm/collision/narrowphase.d.ts.map +1 -0
- package/dist/esm/collision/narrowphase.js +524 -399
- package/dist/esm/collision/narrowphase.js.map +1 -1
- package/dist/esm/collision/segments.d.ts +19 -0
- package/dist/esm/collision/segments.d.ts.map +1 -0
- package/dist/esm/collision/segments.js +36 -0
- package/dist/esm/collision/segments.js.map +1 -0
- package/dist/esm/collision/sweep.d.ts +35 -0
- package/dist/esm/collision/sweep.d.ts.map +1 -0
- package/dist/esm/collision/sweep.js +437 -0
- package/dist/esm/collision/sweep.js.map +1 -0
- package/dist/esm/debug/PhysicsDebugDraw.d.ts +23 -4
- package/dist/esm/debug/PhysicsDebugDraw.d.ts.map +1 -0
- package/dist/esm/debug/PhysicsDebugDraw.js +230 -188
- package/dist/esm/debug/PhysicsDebugDraw.js.map +1 -1
- package/dist/esm/debug/index.d.ts +1 -0
- package/dist/esm/debug/index.d.ts.map +1 -0
- package/dist/esm/debug/index.js +3 -2
- package/dist/esm/events.d.ts +1 -0
- package/dist/esm/events.d.ts.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +24 -19
- package/dist/esm/joints/DistanceJoint.d.ts +4 -3
- package/dist/esm/joints/DistanceJoint.d.ts.map +1 -0
- package/dist/esm/joints/DistanceJoint.js +160 -172
- package/dist/esm/joints/DistanceJoint.js.map +1 -1
- package/dist/esm/joints/Joint.d.ts +5 -4
- package/dist/esm/joints/Joint.d.ts.map +1 -0
- package/dist/esm/joints/Joint.js +23 -21
- package/dist/esm/joints/Joint.js.map +1 -1
- package/dist/esm/joints/MouseJoint.d.ts +8 -7
- package/dist/esm/joints/MouseJoint.d.ts.map +1 -0
- package/dist/esm/joints/MouseJoint.js +132 -133
- package/dist/esm/joints/MouseJoint.js.map +1 -1
- package/dist/esm/joints/PrismaticJoint.d.ts +5 -4
- package/dist/esm/joints/PrismaticJoint.d.ts.map +1 -0
- package/dist/esm/joints/PrismaticJoint.js +211 -240
- package/dist/esm/joints/PrismaticJoint.js.map +1 -1
- package/dist/esm/joints/RevoluteJoint.d.ts +4 -3
- package/dist/esm/joints/RevoluteJoint.d.ts.map +1 -0
- package/dist/esm/joints/RevoluteJoint.js +191 -213
- package/dist/esm/joints/RevoluteJoint.js.map +1 -1
- package/dist/esm/joints/WeldJoint.d.ts +3 -2
- package/dist/esm/joints/WeldJoint.d.ts.map +1 -0
- package/dist/esm/joints/WeldJoint.js +156 -152
- package/dist/esm/joints/WeldJoint.js.map +1 -1
- package/dist/esm/joints/WheelJoint.d.ts +4 -3
- package/dist/esm/joints/WheelJoint.d.ts.map +1 -0
- package/dist/esm/joints/WheelJoint.js +228 -255
- package/dist/esm/joints/WheelJoint.js.map +1 -1
- package/dist/esm/math.d.ts +6 -9
- package/dist/esm/math.d.ts.map +1 -0
- package/dist/esm/math.js +37 -39
- package/dist/esm/math.js.map +1 -1
- package/dist/esm/physicsBuildInfo.d.ts +1 -0
- package/dist/esm/physicsBuildInfo.d.ts.map +1 -0
- package/dist/esm/physicsBuildInfo.js +6 -4
- package/dist/esm/physicsBuildInfo.js.map +1 -1
- package/dist/esm/public.d.ts +8 -3
- package/dist/esm/public.d.ts.map +1 -0
- package/dist/esm/query/QueryEngine.d.ts +56 -12
- package/dist/esm/query/QueryEngine.d.ts.map +1 -0
- package/dist/esm/query/QueryEngine.js +494 -237
- package/dist/esm/query/QueryEngine.js.map +1 -1
- package/dist/esm/query/SpatialIndex.d.ts +34 -0
- package/dist/esm/query/SpatialIndex.d.ts.map +1 -0
- package/dist/esm/shapes/AnyShape.d.ts +9 -4
- package/dist/esm/shapes/AnyShape.d.ts.map +1 -0
- package/dist/esm/shapes/BoxShape.d.ts +2 -1
- package/dist/esm/shapes/BoxShape.d.ts.map +1 -0
- package/dist/esm/shapes/BoxShape.js +37 -25
- package/dist/esm/shapes/BoxShape.js.map +1 -1
- package/dist/esm/shapes/CapsuleShape.d.ts +30 -0
- package/dist/esm/shapes/CapsuleShape.d.ts.map +1 -0
- package/dist/esm/shapes/CapsuleShape.js +88 -0
- package/dist/esm/shapes/CapsuleShape.js.map +1 -0
- package/dist/esm/shapes/ChainEdgeShape.d.ts +30 -0
- package/dist/esm/shapes/ChainEdgeShape.d.ts.map +1 -0
- package/dist/esm/shapes/ChainEdgeShape.js +66 -0
- package/dist/esm/shapes/ChainEdgeShape.js.map +1 -0
- package/dist/esm/shapes/ChainShape.d.ts +52 -0
- package/dist/esm/shapes/ChainShape.d.ts.map +1 -0
- package/dist/esm/shapes/ChainShape.js +85 -0
- package/dist/esm/shapes/ChainShape.js.map +1 -0
- package/dist/esm/shapes/CircleShape.d.ts +3 -4
- package/dist/esm/shapes/CircleShape.d.ts.map +1 -0
- package/dist/esm/shapes/CircleShape.js +27 -26
- package/dist/esm/shapes/CircleShape.js.map +1 -1
- package/dist/esm/shapes/PolygonShape.d.ts +7 -8
- package/dist/esm/shapes/PolygonShape.d.ts.map +1 -0
- package/dist/esm/shapes/PolygonShape.js +131 -162
- package/dist/esm/shapes/PolygonShape.js.map +1 -1
- package/dist/esm/shapes/SegmentShape.d.ts +31 -0
- package/dist/esm/shapes/SegmentShape.d.ts.map +1 -0
- package/dist/esm/shapes/SegmentShape.js +62 -0
- package/dist/esm/shapes/SegmentShape.js.map +1 -0
- package/dist/esm/shapes/Shape.d.ts +28 -17
- package/dist/esm/shapes/Shape.d.ts.map +1 -0
- package/dist/esm/shapes/Shape.js +12 -13
- package/dist/esm/shapes/Shape.js.map +1 -1
- package/dist/esm/shapes/convexParts.d.ts +21 -0
- package/dist/esm/shapes/convexParts.d.ts.map +1 -0
- package/dist/esm/shapes/convexParts.js +26 -0
- package/dist/esm/shapes/convexParts.js.map +1 -0
- package/dist/esm/shapes/decompose.d.ts +24 -0
- package/dist/esm/shapes/decompose.d.ts.map +1 -0
- package/dist/esm/shapes/decompose.js +272 -0
- package/dist/esm/shapes/decompose.js.map +1 -0
- package/dist/esm/shapes/index.d.ts +4 -0
- package/dist/esm/shapes/index.d.ts.map +1 -0
- package/dist/esm/solver/ContactSolver.d.ts +12 -5
- package/dist/esm/solver/ContactSolver.d.ts.map +1 -0
- package/dist/esm/solver/ContactSolver.js +425 -471
- package/dist/esm/solver/ContactSolver.js.map +1 -1
- package/dist/esm/solver/tolerances.d.ts +34 -0
- package/dist/esm/solver/tolerances.d.ts.map +1 -0
- package/dist/esm/solver/tolerances.js +38 -0
- package/dist/esm/solver/tolerances.js.map +1 -0
- package/dist/esm/sort.d.ts +5 -4
- package/dist/esm/sort.d.ts.map +1 -0
- package/dist/esm/sort.js +41 -51
- package/dist/esm/sort.js.map +1 -1
- package/dist/esm/types.d.ts +4 -8
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +15 -17
- package/dist/esm/types.js.map +1 -1
- package/package.json +6 -6
- package/dist/esm/broadphase/SweepAndPrune.d.ts +0 -15
- package/dist/esm/broadphase/SweepAndPrune.js +0 -77
- package/dist/esm/broadphase/SweepAndPrune.js.map +0 -1
- package/dist/esm/debug/index.js.map +0 -1
- package/dist/esm/index.js.map +0 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Shape } from "./Shape.js";
|
|
2
|
+
|
|
3
|
+
//#region src/shapes/SegmentShape.ts
|
|
4
|
+
/** Segments shorter than this (px) have no well-defined orientation. */
|
|
5
|
+
const minimumLength = .001;
|
|
6
|
+
/**
|
|
7
|
+
* A zero-thickness boundary between two local-space endpoints: level edges,
|
|
8
|
+
* one-off walls, the geometry a body may not cross.
|
|
9
|
+
*
|
|
10
|
+
* It has no interior, so {@link Shape.massProperties} is `null` and the shape
|
|
11
|
+
* contributes collision only - a `dynamic` body needs at least one mass-bearing
|
|
12
|
+
* collider alongside it.
|
|
13
|
+
*
|
|
14
|
+
* A segment blocks from **both** sides. There is no inside for it to be outside
|
|
15
|
+
* of, and a one-sided segment would be a gameplay rule in a geometry costume:
|
|
16
|
+
* express that with the world's `contactModifier`, which decides per contact and
|
|
17
|
+
* per step whether it is solved.
|
|
18
|
+
*
|
|
19
|
+
* Being zero-thickness makes it the worst case for discrete detection - a body
|
|
20
|
+
* that crosses it entirely within one fixed step misses it. Continuous collision
|
|
21
|
+
* covers a fast body against a segment; a fast segment is not swept.
|
|
22
|
+
*/
|
|
23
|
+
var SegmentShape = class SegmentShape extends Shape {
|
|
24
|
+
type = "segment";
|
|
25
|
+
/** Local endpoints, flattened - the same layout a polygon uses. */
|
|
26
|
+
vertices;
|
|
27
|
+
/** The two opposite outward unit normals, flattened. */
|
|
28
|
+
normals;
|
|
29
|
+
length;
|
|
30
|
+
boundingRadius;
|
|
31
|
+
/** Always `null`: a boundary has no interior and therefore no mass. */
|
|
32
|
+
massProperties = null;
|
|
33
|
+
constructor(x0, y0, x1, y1) {
|
|
34
|
+
super();
|
|
35
|
+
if (!Number.isFinite(x0) || !Number.isFinite(y0) || !Number.isFinite(x1) || !Number.isFinite(y1)) throw new RangeError(`SegmentShape: endpoints must be finite, received (${x0}, ${y0}) and (${x1}, ${y1}).`);
|
|
36
|
+
const ex = x1 - x0;
|
|
37
|
+
const ey = y1 - y0;
|
|
38
|
+
const length = Math.hypot(ex, ey);
|
|
39
|
+
if (length < minimumLength) throw new RangeError(`SegmentShape: the endpoints are coincident (length ${length}).`);
|
|
40
|
+
const nx = ey / length;
|
|
41
|
+
const ny = -ex / length;
|
|
42
|
+
this.length = length;
|
|
43
|
+
this.vertices = Object.freeze([
|
|
44
|
+
x0,
|
|
45
|
+
y0,
|
|
46
|
+
x1,
|
|
47
|
+
y1
|
|
48
|
+
]);
|
|
49
|
+
this.normals = Object.freeze([
|
|
50
|
+
nx,
|
|
51
|
+
ny,
|
|
52
|
+
-nx,
|
|
53
|
+
-ny
|
|
54
|
+
]);
|
|
55
|
+
this.boundingRadius = Math.max(Math.hypot(x0, y0), Math.hypot(x1, y1));
|
|
56
|
+
if (new.target === SegmentShape) Object.freeze(this);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
export { SegmentShape };
|
|
62
|
+
//# sourceMappingURL=SegmentShape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SegmentShape.js","names":[],"sources":["../../../src/shapes/SegmentShape.ts"],"sourcesContent":["import { Shape } from './Shape';\n\n/** Segments shorter than this (px) have no well-defined orientation. */\nconst minimumLength = 1e-3;\n\n/**\n * A zero-thickness boundary between two local-space endpoints: level edges,\n * one-off walls, the geometry a body may not cross.\n *\n * It has no interior, so {@link Shape.massProperties} is `null` and the shape\n * contributes collision only - a `dynamic` body needs at least one mass-bearing\n * collider alongside it.\n *\n * A segment blocks from **both** sides. There is no inside for it to be outside\n * of, and a one-sided segment would be a gameplay rule in a geometry costume:\n * express that with the world's `contactModifier`, which decides per contact and\n * per step whether it is solved.\n *\n * Being zero-thickness makes it the worst case for discrete detection - a body\n * that crosses it entirely within one fixed step misses it. Continuous collision\n * covers a fast body against a segment; a fast segment is not swept.\n */\nexport class SegmentShape extends Shape {\n public readonly type = 'segment' as const;\n\n /** Local endpoints, flattened - the same layout a polygon uses. */\n public readonly vertices: readonly number[];\n\n /** The two opposite outward unit normals, flattened. */\n public readonly normals: readonly number[];\n\n public readonly length: number;\n public readonly boundingRadius: number;\n\n /** Always `null`: a boundary has no interior and therefore no mass. */\n public readonly massProperties = null;\n\n public constructor(x0: number, y0: number, x1: number, y1: number) {\n super();\n\n if (!Number.isFinite(x0) || !Number.isFinite(y0) || !Number.isFinite(x1) || !Number.isFinite(y1)) {\n throw new RangeError(`SegmentShape: endpoints must be finite, received (${x0}, ${y0}) and (${x1}, ${y1}).`);\n }\n\n const ex = x1 - x0;\n const ey = y1 - y0;\n const length = Math.hypot(ex, ey);\n\n if (length < minimumLength) {\n throw new RangeError(`SegmentShape: the endpoints are coincident (length ${length}).`);\n }\n\n // Same convention as PolygonShape: the outward normal of the counter-clockwise\n // edge i → i+1 is (eY, -eX). A two-vertex ring has two opposite edges.\n const nx = ey / length;\n const ny = -ex / length;\n\n this.length = length;\n this.vertices = Object.freeze([x0, y0, x1, y1]);\n this.normals = Object.freeze([nx, ny, -nx, -ny]);\n this.boundingRadius = Math.max(Math.hypot(x0, y0), Math.hypot(x1, y1));\n\n // A subclass still has its own fields to write, so it freezes itself once\n // its constructor is done.\n if (new.target === SegmentShape) {\n Object.freeze(this);\n }\n }\n}\n"],"mappings":";;;;AAGA,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;AAmBtB,IAAa,eAAb,MAAa,qBAAqB,MAAM;CACtC,AAAgB,OAAO;;CAGvB,AAAgB;;CAGhB,AAAgB;CAEhB,AAAgB;CAChB,AAAgB;;CAGhB,AAAgB,iBAAiB;CAEjC,AAAO,YAAY,IAAY,IAAY,IAAY,IAAY;EACjE,MAAM;EAEN,IAAI,CAAC,OAAO,SAAS,EAAE,KAAK,CAAC,OAAO,SAAS,EAAE,KAAK,CAAC,OAAO,SAAS,EAAE,KAAK,CAAC,OAAO,SAAS,EAAE,GAC7F,MAAM,IAAI,WAAW,qDAAqD,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,GAAG,GAAG;EAG5G,MAAM,KAAK,KAAK;EAChB,MAAM,KAAK,KAAK;EAChB,MAAM,SAAS,KAAK,MAAM,IAAI,EAAE;EAEhC,IAAI,SAAS,eACX,MAAM,IAAI,WAAW,sDAAsD,OAAO,GAAG;EAKvF,MAAM,KAAK,KAAK;EAChB,MAAM,KAAK,CAAC,KAAK;EAEjB,KAAK,SAAS;EACd,KAAK,WAAW,OAAO,OAAO;GAAC;GAAI;GAAI;GAAI;EAAE,CAAC;EAC9C,KAAK,UAAU,OAAO,OAAO;GAAC;GAAI;GAAI,CAAC;GAAI,CAAC;EAAE,CAAC;EAC/C,KAAK,iBAAiB,KAAK,IAAI,KAAK,MAAM,IAAI,EAAE,GAAG,KAAK,MAAM,IAAI,EAAE,CAAC;EAIrE,IAAI,eAAe,cACjB,OAAO,OAAO,IAAI;CAEtB;AACF"}
|
|
@@ -1,26 +1,37 @@
|
|
|
1
1
|
/** Discriminant for the concrete shape kinds supported in the MVP. */
|
|
2
|
-
export type ShapeType = 'circle' | 'polygon';
|
|
2
|
+
export type ShapeType = 'circle' | 'capsule' | 'segment' | 'polygon' | 'chain';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
* Area-based mass properties of a solid shape, from which a {@link Collider}'s
|
|
5
|
+
* density derives the owning body's mass and rotational inertia. Computed once
|
|
6
|
+
* at shape construction and frozen.
|
|
7
|
+
*/
|
|
8
|
+
export interface ShapeMassProperties {
|
|
9
|
+
/** Surface area in px². `mass = density × area`. */
|
|
10
|
+
readonly area: number;
|
|
11
|
+
/** X of the area centroid in local space. */
|
|
12
|
+
readonly centroidX: number;
|
|
13
|
+
/** Y of the area centroid in local space. */
|
|
14
|
+
readonly centroidY: number;
|
|
15
|
+
/**
|
|
16
|
+
* Second moment of area about the centroid (∫ r² dA). Multiplying by density
|
|
17
|
+
* yields the shape's rotational inertia contribution about its centroid.
|
|
18
|
+
*/
|
|
19
|
+
readonly unitInertia: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Immutable local-space collision geometry. A `Shape` carries no transform - a
|
|
23
|
+
* {@link Collider} positions it in a body.
|
|
9
24
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
25
|
+
* Having an interior is a capability, not a given: solid shapes expose
|
|
26
|
+
* {@link massProperties}, while boundary geometry (a segment, a chain of edges)
|
|
27
|
+
* reports `null` and contributes nothing to the owning body's mass. Read the
|
|
28
|
+
* property rather than assuming an area exists.
|
|
13
29
|
*/
|
|
14
30
|
export declare abstract class Shape {
|
|
15
31
|
abstract readonly type: ShapeType;
|
|
16
32
|
/** Radius of the smallest circle about the local origin enclosing the shape. */
|
|
17
33
|
abstract readonly boundingRadius: number;
|
|
18
|
-
/**
|
|
19
|
-
abstract readonly
|
|
20
|
-
/** X of the area centroid in local space. */
|
|
21
|
-
abstract readonly centroidX: number;
|
|
22
|
-
/** Y of the area centroid in local space. */
|
|
23
|
-
abstract readonly centroidY: number;
|
|
24
|
-
/** Second moment of area (∫ r² dA) about the centroid; `inertia = density × unitInertia`. */
|
|
25
|
-
abstract readonly unitInertia: number;
|
|
34
|
+
/** Area-based mass properties, or `null` for boundary geometry with no interior. */
|
|
35
|
+
abstract readonly massProperties: ShapeMassProperties | null;
|
|
26
36
|
}
|
|
37
|
+
//# sourceMappingURL=Shape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Shape.d.ts","sourceRoot":"","sources":["../../../src/shapes/Shape.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE/E;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6CAA6C;IAC7C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,6CAA6C;IAC7C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,8BAAsB,KAAK;IACzB,kBAAyB,IAAI,EAAE,SAAS,CAAC;IAEzC,gFAAgF;IAChF,kBAAyB,cAAc,EAAE,MAAM,CAAC;IAEhD,oFAAoF;IACpF,kBAAyB,cAAc,EAAE,mBAAmB,GAAG,IAAI,CAAC;CACrE"}
|
package/dist/esm/shapes/Shape.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
+
//#region src/shapes/Shape.ts
|
|
1
2
|
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*/
|
|
12
|
-
class Shape {
|
|
13
|
-
}
|
|
3
|
+
* Immutable local-space collision geometry. A `Shape` carries no transform - a
|
|
4
|
+
* {@link Collider} positions it in a body.
|
|
5
|
+
*
|
|
6
|
+
* Having an interior is a capability, not a given: solid shapes expose
|
|
7
|
+
* {@link massProperties}, while boundary geometry (a segment, a chain of edges)
|
|
8
|
+
* reports `null` and contributes nothing to the owning body's mass. Read the
|
|
9
|
+
* property rather than assuming an area exists.
|
|
10
|
+
*/
|
|
11
|
+
var Shape = class {};
|
|
14
12
|
|
|
13
|
+
//#endregion
|
|
15
14
|
export { Shape };
|
|
16
|
-
//# sourceMappingURL=Shape.js.map
|
|
15
|
+
//# sourceMappingURL=Shape.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Shape.js","sources":["
|
|
1
|
+
{"version":3,"file":"Shape.js","names":[],"sources":["../../../src/shapes/Shape.ts"],"sourcesContent":["/** Discriminant for the concrete shape kinds supported in the MVP. */\nexport type ShapeType = 'circle' | 'capsule' | 'segment' | 'polygon' | 'chain';\n\n/**\n * Area-based mass properties of a solid shape, from which a {@link Collider}'s\n * density derives the owning body's mass and rotational inertia. Computed once\n * at shape construction and frozen.\n */\nexport interface ShapeMassProperties {\n /** Surface area in px². `mass = density × area`. */\n readonly area: number;\n /** X of the area centroid in local space. */\n readonly centroidX: number;\n /** Y of the area centroid in local space. */\n readonly centroidY: number;\n /**\n * Second moment of area about the centroid (∫ r² dA). Multiplying by density\n * yields the shape's rotational inertia contribution about its centroid.\n */\n readonly unitInertia: number;\n}\n\n/**\n * Immutable local-space collision geometry. A `Shape` carries no transform - a\n * {@link Collider} positions it in a body.\n *\n * Having an interior is a capability, not a given: solid shapes expose\n * {@link massProperties}, while boundary geometry (a segment, a chain of edges)\n * reports `null` and contributes nothing to the owning body's mass. Read the\n * property rather than assuming an area exists.\n */\nexport abstract class Shape {\n public abstract readonly type: ShapeType;\n\n /** Radius of the smallest circle about the local origin enclosing the shape. */\n public abstract readonly boundingRadius: number;\n\n /** Area-based mass properties, or `null` for boundary geometry with no interior. */\n public abstract readonly massProperties: ShapeMassProperties | null;\n}\n"],"mappings":";;;;;;;;;;AA+BA,IAAsB,QAAtB,MAA4B,CAQ5B"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PointLike } from '@codexo/exojs';
|
|
2
|
+
import { PolygonShape } from './PolygonShape';
|
|
3
|
+
/**
|
|
4
|
+
* Turn one possibly-concave outline into the convex solver shapes that
|
|
5
|
+
* represent it. Attach all of them to a single body: together they are the
|
|
6
|
+
* original area, and a `PolygonShape` stays exactly one convex shape rather
|
|
7
|
+
* than secretly standing for several.
|
|
8
|
+
*
|
|
9
|
+
* The number of shapes and their order follow from the current decomposition
|
|
10
|
+
* and are not a stable contract; the preserved properties are the area, the
|
|
11
|
+
* centre of mass and the rotational inertia of the compound.
|
|
12
|
+
*
|
|
13
|
+
* An already-convex outline yields exactly one shape, so a caller that does not
|
|
14
|
+
* know whether its input is convex can route everything through here.
|
|
15
|
+
*
|
|
16
|
+
* @throws RangeError on a non-finite coordinate, on fewer than three distinct
|
|
17
|
+
* non-collinear vertices, on a self-intersecting outline, or when a part comes
|
|
18
|
+
* out too degenerate for {@link PolygonShape}.
|
|
19
|
+
*/
|
|
20
|
+
export declare const toConvexPolygonShapes: (vertices: ReadonlyArray<Readonly<PointLike>>) => PolygonShape[];
|
|
21
|
+
//# sourceMappingURL=convexParts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convexParts.d.ts","sourceRoot":"","sources":["../../../src/shapes/convexParts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAG/C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,qBAAqB,GAAI,UAAU,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,KAAG,YAAY,EAC3B,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PolygonShape } from "./PolygonShape.js";
|
|
2
|
+
import { decomposeToConvexParts } from "./decompose.js";
|
|
3
|
+
|
|
4
|
+
//#region src/shapes/convexParts.ts
|
|
5
|
+
/**
|
|
6
|
+
* Turn one possibly-concave outline into the convex solver shapes that
|
|
7
|
+
* represent it. Attach all of them to a single body: together they are the
|
|
8
|
+
* original area, and a `PolygonShape` stays exactly one convex shape rather
|
|
9
|
+
* than secretly standing for several.
|
|
10
|
+
*
|
|
11
|
+
* The number of shapes and their order follow from the current decomposition
|
|
12
|
+
* and are not a stable contract; the preserved properties are the area, the
|
|
13
|
+
* centre of mass and the rotational inertia of the compound.
|
|
14
|
+
*
|
|
15
|
+
* An already-convex outline yields exactly one shape, so a caller that does not
|
|
16
|
+
* know whether its input is convex can route everything through here.
|
|
17
|
+
*
|
|
18
|
+
* @throws RangeError on a non-finite coordinate, on fewer than three distinct
|
|
19
|
+
* non-collinear vertices, on a self-intersecting outline, or when a part comes
|
|
20
|
+
* out too degenerate for {@link PolygonShape}.
|
|
21
|
+
*/
|
|
22
|
+
const toConvexPolygonShapes = (vertices) => decomposeToConvexParts(vertices).map((part) => new PolygonShape(part));
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
export { toConvexPolygonShapes };
|
|
26
|
+
//# sourceMappingURL=convexParts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convexParts.js","names":[],"sources":["../../../src/shapes/convexParts.ts"],"sourcesContent":["import type { PointLike } from '@codexo/exojs';\n\nimport { decomposeToConvexParts } from './decompose';\nimport { PolygonShape } from './PolygonShape';\n\n/**\n * Turn one possibly-concave outline into the convex solver shapes that\n * represent it. Attach all of them to a single body: together they are the\n * original area, and a `PolygonShape` stays exactly one convex shape rather\n * than secretly standing for several.\n *\n * The number of shapes and their order follow from the current decomposition\n * and are not a stable contract; the preserved properties are the area, the\n * centre of mass and the rotational inertia of the compound.\n *\n * An already-convex outline yields exactly one shape, so a caller that does not\n * know whether its input is convex can route everything through here.\n *\n * @throws RangeError on a non-finite coordinate, on fewer than three distinct\n * non-collinear vertices, on a self-intersecting outline, or when a part comes\n * out too degenerate for {@link PolygonShape}.\n */\nexport const toConvexPolygonShapes = (vertices: ReadonlyArray<Readonly<PointLike>>): PolygonShape[] =>\n decomposeToConvexParts(vertices).map(part => new PolygonShape(part));\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAsBA,MAAa,yBAAyB,aACpC,uBAAuB,QAAQ,CAAC,CAAC,KAAI,SAAQ,IAAI,aAAa,IAAI,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PointLike } from '@codexo/exojs';
|
|
2
|
+
/**
|
|
3
|
+
* Split a simple polygon into convex parts.
|
|
4
|
+
*
|
|
5
|
+
* The input may be concave, wound either way, and may carry duplicate or
|
|
6
|
+
* collinear vertices; it must not intersect itself. The result is a list of
|
|
7
|
+
* strictly convex, counter-clockwise parts whose union is the input and whose
|
|
8
|
+
* interiors do not overlap. An already-convex input yields exactly one part.
|
|
9
|
+
*
|
|
10
|
+
* Deterministic: the same cleaned input always produces the same parts. The
|
|
11
|
+
* number of parts and their order are an artefact of the current algorithm and
|
|
12
|
+
* must not be relied on - assert convexity, preserved area and preserved mass
|
|
13
|
+
* properties instead.
|
|
14
|
+
*
|
|
15
|
+
* Cost is O(n²) in the vertex count (self-intersection check and ear clipping),
|
|
16
|
+
* which suits import-time geometry rather than a per-step path.
|
|
17
|
+
*
|
|
18
|
+
* @throws RangeError if a coordinate is not finite, if fewer than three
|
|
19
|
+
* distinct non-collinear vertices remain after cleanup, or if the outline
|
|
20
|
+
* intersects itself.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export declare const decomposeToConvexParts: (vertices: ReadonlyArray<Readonly<PointLike>>) => PointLike[][];
|
|
24
|
+
//# sourceMappingURL=decompose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decompose.d.ts","sourceRoot":"","sources":["../../../src/shapes/decompose.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAmB/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,sBAAsB,GAAI,UAAU,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,KAAG,SAAS,EAAE,EAqChG,CAAC"}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { triangulate } from "@codexo/exojs";
|
|
2
|
+
|
|
3
|
+
//#region src/shapes/decompose.ts
|
|
4
|
+
/** Vertices closer than this (px) are welded into one. */
|
|
5
|
+
const weldEpsilon = 1e-4;
|
|
6
|
+
/**
|
|
7
|
+
* Sine-of-turn threshold below which three consecutive vertices count as
|
|
8
|
+
* collinear. Compared against the normalised cross product so the test is
|
|
9
|
+
* scale-free: a 10 px polygon and a 10 000 px one accept the same shapes.
|
|
10
|
+
*/
|
|
11
|
+
const collinearEpsilon = 1e-7;
|
|
12
|
+
/**
|
|
13
|
+
* Fraction of the outline's area below which a triangulation face counts as
|
|
14
|
+
* empty. Ear clipping emits its last triangle without an orientation test, so a
|
|
15
|
+
* polygon whose final three vertices end up collinear yields one zero-area face
|
|
16
|
+
* - it covers nothing and must not become a collider.
|
|
17
|
+
*/
|
|
18
|
+
const degenerateAreaFraction = 1e-9;
|
|
19
|
+
/**
|
|
20
|
+
* Split a simple polygon into convex parts.
|
|
21
|
+
*
|
|
22
|
+
* The input may be concave, wound either way, and may carry duplicate or
|
|
23
|
+
* collinear vertices; it must not intersect itself. The result is a list of
|
|
24
|
+
* strictly convex, counter-clockwise parts whose union is the input and whose
|
|
25
|
+
* interiors do not overlap. An already-convex input yields exactly one part.
|
|
26
|
+
*
|
|
27
|
+
* Deterministic: the same cleaned input always produces the same parts. The
|
|
28
|
+
* number of parts and their order are an artefact of the current algorithm and
|
|
29
|
+
* must not be relied on - assert convexity, preserved area and preserved mass
|
|
30
|
+
* properties instead.
|
|
31
|
+
*
|
|
32
|
+
* Cost is O(n²) in the vertex count (self-intersection check and ear clipping),
|
|
33
|
+
* which suits import-time geometry rather than a per-step path.
|
|
34
|
+
*
|
|
35
|
+
* @throws RangeError if a coordinate is not finite, if fewer than three
|
|
36
|
+
* distinct non-collinear vertices remain after cleanup, or if the outline
|
|
37
|
+
* intersects itself.
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
const decomposeToConvexParts = (vertices) => {
|
|
41
|
+
const points = normalize(vertices);
|
|
42
|
+
const count = points.length / 2;
|
|
43
|
+
assertSimple(points);
|
|
44
|
+
if (isStrictlyConvex(points)) return [toPoints(points, sequence(count))];
|
|
45
|
+
const indices = triangulate(points);
|
|
46
|
+
if (indices.length !== (count - 2) * 3) throw new RangeError(`decomposeToConvexParts: could not triangulate the outline (${indices.length / 3} of ${count - 2} triangles); check it for near-degenerate geometry.`);
|
|
47
|
+
const sourceArea = Math.abs(signedArea(points));
|
|
48
|
+
const cycles = mergeTriangles(points, indices, sourceArea * degenerateAreaFraction);
|
|
49
|
+
const parts = cycles.map((cycle) => toPoints(points, cycle));
|
|
50
|
+
const partArea = cycles.reduce((sum, cycle) => sum + Math.abs(cycleArea(points, cycle)), 0);
|
|
51
|
+
if (parts.length === 0 || Math.abs(partArea - sourceArea) > sourceArea * 1e-6) throw new RangeError(`decomposeToConvexParts: the parts cover ${partArea.toFixed(4)} of the outline's ${sourceArea.toFixed(4)}; the outline is too degenerate to decompose.`);
|
|
52
|
+
return parts;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Weld coincident vertices, drop collinear ones and canonicalise to
|
|
56
|
+
* counter-clockwise, returning a flat `[x0, y0, x1, y1, ...]` array.
|
|
57
|
+
*/
|
|
58
|
+
const normalize = (vertices) => {
|
|
59
|
+
const points = [];
|
|
60
|
+
for (const vertex of vertices) {
|
|
61
|
+
if (!Number.isFinite(vertex.x) || !Number.isFinite(vertex.y)) throw new RangeError(`decomposeToConvexParts: vertex has a non-finite component (${vertex.x}, ${vertex.y}).`);
|
|
62
|
+
if (points.length === 0) {
|
|
63
|
+
points.push(vertex.x, vertex.y);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (Math.hypot(vertex.x - points[points.length - 2], vertex.y - points[points.length - 1]) >= weldEpsilon) points.push(vertex.x, vertex.y);
|
|
67
|
+
}
|
|
68
|
+
if (points.length >= 4 && Math.hypot(points[0] - points[points.length - 2], points[1] - points[points.length - 1]) < weldEpsilon) points.length -= 2;
|
|
69
|
+
if (signedArea(points) < 0) reverseWinding(points);
|
|
70
|
+
dropCollinear(points);
|
|
71
|
+
if (points.length < 6) throw new RangeError(`decomposeToConvexParts: needs at least 3 distinct, non-collinear vertices, received ${points.length / 2} after cleanup.`);
|
|
72
|
+
if (Math.abs(signedArea(points)) <= weldEpsilon) throw new RangeError("decomposeToConvexParts: the outline encloses no area.");
|
|
73
|
+
return points;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Remove every vertex whose two edges are collinear. Repeats until stable: one
|
|
77
|
+
* removal can make its neighbour collinear in turn.
|
|
78
|
+
*/
|
|
79
|
+
const dropCollinear = (points) => {
|
|
80
|
+
let removed = true;
|
|
81
|
+
while (removed && points.length >= 6) {
|
|
82
|
+
removed = false;
|
|
83
|
+
for (let i = 0; i < points.length / 2; i++) {
|
|
84
|
+
const count = points.length / 2;
|
|
85
|
+
if (count < 3) break;
|
|
86
|
+
const p = (i + count - 1) % count;
|
|
87
|
+
const n = (i + 1) % count;
|
|
88
|
+
if (isCollinear(points, p, i, n)) {
|
|
89
|
+
points.splice(i * 2, 2);
|
|
90
|
+
removed = true;
|
|
91
|
+
i--;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const isCollinear = (points, a, b, c) => {
|
|
97
|
+
const e1x = points[b * 2] - points[a * 2];
|
|
98
|
+
const e1y = points[b * 2 + 1] - points[a * 2 + 1];
|
|
99
|
+
const e2x = points[c * 2] - points[b * 2];
|
|
100
|
+
const e2y = points[c * 2 + 1] - points[b * 2 + 1];
|
|
101
|
+
const lengths = Math.hypot(e1x, e1y) * Math.hypot(e2x, e2y);
|
|
102
|
+
return lengths === 0 || Math.abs(e1x * e2y - e1y * e2x) <= collinearEpsilon * lengths;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Reject an outline that crosses or touches itself. Every pair of non-adjacent
|
|
106
|
+
* edges must be disjoint - a shared point between them is exactly what makes a
|
|
107
|
+
* polygon non-simple, and both ear clipping and the solver assume it cannot
|
|
108
|
+
* happen.
|
|
109
|
+
*/
|
|
110
|
+
const assertSimple = (points) => {
|
|
111
|
+
const count = points.length / 2;
|
|
112
|
+
for (let i = 0; i < count; i++) {
|
|
113
|
+
const iNext = (i + 1) % count;
|
|
114
|
+
for (let j = i + 1; j < count; j++) {
|
|
115
|
+
const jNext = (j + 1) % count;
|
|
116
|
+
if (i === j || iNext === j || jNext === i) continue;
|
|
117
|
+
if (segmentsIntersect(points, i, iNext, j, jNext)) throw new RangeError("decomposeToConvexParts: the outline intersects itself; only simple polygons can be decomposed.");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/** `true` when segments `a→b` and `c→d` share any point, touching included. */
|
|
122
|
+
const segmentsIntersect = (points, a, b, c, d) => {
|
|
123
|
+
const o1 = orientation(points, a, b, c);
|
|
124
|
+
const o2 = orientation(points, a, b, d);
|
|
125
|
+
const o3 = orientation(points, c, d, a);
|
|
126
|
+
const o4 = orientation(points, c, d, b);
|
|
127
|
+
if (o1 !== o2 && o3 !== o4) return true;
|
|
128
|
+
return o1 === 0 && onSegment(points, a, b, c) || o2 === 0 && onSegment(points, a, b, d) || o3 === 0 && onSegment(points, c, d, a) || o4 === 0 && onSegment(points, c, d, b);
|
|
129
|
+
};
|
|
130
|
+
/** Sign of the turn `a → b → c`: `1` left, `-1` right, `0` collinear. */
|
|
131
|
+
const orientation = (points, a, b, c) => {
|
|
132
|
+
const cross = (points[b * 2] - points[a * 2]) * (points[c * 2 + 1] - points[a * 2 + 1]) - (points[b * 2 + 1] - points[a * 2 + 1]) * (points[c * 2] - points[a * 2]);
|
|
133
|
+
const scale = Math.hypot(points[b * 2] - points[a * 2], points[b * 2 + 1] - points[a * 2 + 1]) * Math.hypot(points[c * 2] - points[a * 2], points[c * 2 + 1] - points[a * 2 + 1]);
|
|
134
|
+
if (scale === 0 || Math.abs(cross) <= collinearEpsilon * scale) return 0;
|
|
135
|
+
return cross > 0 ? 1 : -1;
|
|
136
|
+
};
|
|
137
|
+
/** `true` when `p` lies within the bounding box of the collinear segment `a→b`. */
|
|
138
|
+
const onSegment = (points, a, b, p) => points[p * 2] >= Math.min(points[a * 2], points[b * 2]) - weldEpsilon && points[p * 2] <= Math.max(points[a * 2], points[b * 2]) + weldEpsilon && points[p * 2 + 1] >= Math.min(points[a * 2 + 1], points[b * 2 + 1]) - weldEpsilon && points[p * 2 + 1] <= Math.max(points[a * 2 + 1], points[b * 2 + 1]) + weldEpsilon;
|
|
139
|
+
/**
|
|
140
|
+
* Hertel-Mehlhorn: start from the triangulation and drop every internal
|
|
141
|
+
* diagonal whose two faces merge into a still-convex polygon. Diagonals are
|
|
142
|
+
* visited in a fixed key order, so the outcome depends only on the cleaned
|
|
143
|
+
* input.
|
|
144
|
+
*/
|
|
145
|
+
const mergeTriangles = (points, indices, degenerateArea) => {
|
|
146
|
+
const cycles = /* @__PURE__ */ new Map();
|
|
147
|
+
const owners = /* @__PURE__ */ new Map();
|
|
148
|
+
const parent = [];
|
|
149
|
+
const find = (id) => {
|
|
150
|
+
let root = id;
|
|
151
|
+
while (parent[root] !== root) root = parent[root];
|
|
152
|
+
return root;
|
|
153
|
+
};
|
|
154
|
+
const stride = points.length / 2;
|
|
155
|
+
const diagonalKey = (a, b) => a < b ? a * stride + b : b * stride + a;
|
|
156
|
+
for (let t = 0; t < indices.length; t += 3) {
|
|
157
|
+
const id = t / 3;
|
|
158
|
+
const cycle = [
|
|
159
|
+
indices[t],
|
|
160
|
+
indices[t + 1],
|
|
161
|
+
indices[t + 2]
|
|
162
|
+
];
|
|
163
|
+
if (Math.abs(cycleArea(points, cycle)) <= degenerateArea) continue;
|
|
164
|
+
parent[id] = id;
|
|
165
|
+
cycles.set(id, cycle);
|
|
166
|
+
for (let e = 0; e < 3; e++) {
|
|
167
|
+
const key = diagonalKey(cycle[e], cycle[(e + 1) % 3]);
|
|
168
|
+
const owner = owners.get(key);
|
|
169
|
+
if (owner === void 0) owners.set(key, [id]);
|
|
170
|
+
else owner.push(id);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const key of [...owners.keys()].sort((x, y) => x - y)) {
|
|
174
|
+
const owner = owners.get(key);
|
|
175
|
+
if (owner.length !== 2) continue;
|
|
176
|
+
const rootA = find(owner[0]);
|
|
177
|
+
const rootB = find(owner[1]);
|
|
178
|
+
if (rootA === rootB) continue;
|
|
179
|
+
const cycleA = cycles.get(rootA);
|
|
180
|
+
const cycleB = cycles.get(rootB);
|
|
181
|
+
const merged = mergeAcross(points, cycleA, cycleB, Math.floor(key / stride), key % stride);
|
|
182
|
+
if (merged === null) continue;
|
|
183
|
+
const keep = rootA < rootB ? rootA : rootB;
|
|
184
|
+
const drop = rootA < rootB ? rootB : rootA;
|
|
185
|
+
cycles.set(keep, merged);
|
|
186
|
+
cycles.delete(drop);
|
|
187
|
+
parent[drop] = keep;
|
|
188
|
+
}
|
|
189
|
+
return [...cycles.values()];
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Join two convex cycles across the shared edge between `u` and `v`, or `null`
|
|
193
|
+
* when the union would not be strictly convex. Rejecting a merely collinear
|
|
194
|
+
* junction is deliberate: the resulting part is handed to `PolygonShape`, which
|
|
195
|
+
* refuses collinear edges.
|
|
196
|
+
*/
|
|
197
|
+
const mergeAcross = (points, cycleA, cycleB, u, v) => {
|
|
198
|
+
let a = u;
|
|
199
|
+
let b = v;
|
|
200
|
+
let start = directedEdgeIndex(cycleA, a, b);
|
|
201
|
+
if (start === -1) {
|
|
202
|
+
a = v;
|
|
203
|
+
b = u;
|
|
204
|
+
start = directedEdgeIndex(cycleA, a, b);
|
|
205
|
+
}
|
|
206
|
+
const back = start === -1 ? -1 : directedEdgeIndex(cycleB, b, a);
|
|
207
|
+
if (start === -1 || back === -1) return null;
|
|
208
|
+
const merged = [];
|
|
209
|
+
for (let i = 0; i < cycleA.length; i++) merged.push(cycleA[(start + 1 + i) % cycleA.length]);
|
|
210
|
+
for (let i = 2; i < cycleB.length; i++) merged.push(cycleB[(back + i) % cycleB.length]);
|
|
211
|
+
return isCycleStrictlyConvex(points, merged) ? merged : null;
|
|
212
|
+
};
|
|
213
|
+
/** Index of `a` in `cycle` when `b` immediately follows it, else `-1`. */
|
|
214
|
+
const directedEdgeIndex = (cycle, a, b) => {
|
|
215
|
+
for (let i = 0; i < cycle.length; i++) if (cycle[i] === a && cycle[(i + 1) % cycle.length] === b) return i;
|
|
216
|
+
return -1;
|
|
217
|
+
};
|
|
218
|
+
const isCycleStrictlyConvex = (points, cycle) => {
|
|
219
|
+
for (let i = 0; i < cycle.length; i++) {
|
|
220
|
+
const a = cycle[i];
|
|
221
|
+
const b = cycle[(i + 1) % cycle.length];
|
|
222
|
+
const c = cycle[(i + 2) % cycle.length];
|
|
223
|
+
if (orientation(points, a, b, c) <= 0) return false;
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
};
|
|
227
|
+
const isStrictlyConvex = (points) => isCycleStrictlyConvex(points, sequence(points.length / 2));
|
|
228
|
+
const sequence = (count) => {
|
|
229
|
+
const out = [];
|
|
230
|
+
for (let i = 0; i < count; i++) out.push(i);
|
|
231
|
+
return out;
|
|
232
|
+
};
|
|
233
|
+
const toPoints = (points, cycle) => cycle.map((index) => ({
|
|
234
|
+
x: points[index * 2],
|
|
235
|
+
y: points[index * 2 + 1]
|
|
236
|
+
}));
|
|
237
|
+
/** Shoelace signed area of an index cycle over `points`. */
|
|
238
|
+
const cycleArea = (points, cycle) => {
|
|
239
|
+
let sum = 0;
|
|
240
|
+
for (let i = 0; i < cycle.length; i++) {
|
|
241
|
+
const a = cycle[i];
|
|
242
|
+
const b = cycle[(i + 1) % cycle.length];
|
|
243
|
+
sum += points[a * 2] * points[b * 2 + 1] - points[b * 2] * points[a * 2 + 1];
|
|
244
|
+
}
|
|
245
|
+
return sum / 2;
|
|
246
|
+
};
|
|
247
|
+
/** Shoelace signed area of a flat point ring; positive is counter-clockwise. */
|
|
248
|
+
const signedArea = (points) => {
|
|
249
|
+
const count = points.length / 2;
|
|
250
|
+
let sum = 0;
|
|
251
|
+
for (let i = 0; i < count; i++) {
|
|
252
|
+
const j = (i + 1) % count;
|
|
253
|
+
sum += points[i * 2] * points[j * 2 + 1] - points[j * 2] * points[i * 2 + 1];
|
|
254
|
+
}
|
|
255
|
+
return sum / 2;
|
|
256
|
+
};
|
|
257
|
+
const reverseWinding = (points) => {
|
|
258
|
+
const count = points.length / 2;
|
|
259
|
+
for (let i = 0; i < Math.floor(count / 2); i++) {
|
|
260
|
+
const j = count - 1 - i;
|
|
261
|
+
const ix = points[i * 2];
|
|
262
|
+
const iy = points[i * 2 + 1];
|
|
263
|
+
points[i * 2] = points[j * 2];
|
|
264
|
+
points[i * 2 + 1] = points[j * 2 + 1];
|
|
265
|
+
points[j * 2] = ix;
|
|
266
|
+
points[j * 2 + 1] = iy;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
//#endregion
|
|
271
|
+
export { decomposeToConvexParts };
|
|
272
|
+
//# sourceMappingURL=decompose.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decompose.js","names":[],"sources":["../../../src/shapes/decompose.ts"],"sourcesContent":["import type { PointLike } from '@codexo/exojs';\nimport { triangulate } from '@codexo/exojs';\n\n/** Vertices closer than this (px) are welded into one. */\nconst weldEpsilon = 1e-4;\n/**\n * Sine-of-turn threshold below which three consecutive vertices count as\n * collinear. Compared against the normalised cross product so the test is\n * scale-free: a 10 px polygon and a 10 000 px one accept the same shapes.\n */\nconst collinearEpsilon = 1e-7;\n/**\n * Fraction of the outline's area below which a triangulation face counts as\n * empty. Ear clipping emits its last triangle without an orientation test, so a\n * polygon whose final three vertices end up collinear yields one zero-area face\n * - it covers nothing and must not become a collider.\n */\nconst degenerateAreaFraction = 1e-9;\n\n/**\n * Split a simple polygon into convex parts.\n *\n * The input may be concave, wound either way, and may carry duplicate or\n * collinear vertices; it must not intersect itself. The result is a list of\n * strictly convex, counter-clockwise parts whose union is the input and whose\n * interiors do not overlap. An already-convex input yields exactly one part.\n *\n * Deterministic: the same cleaned input always produces the same parts. The\n * number of parts and their order are an artefact of the current algorithm and\n * must not be relied on - assert convexity, preserved area and preserved mass\n * properties instead.\n *\n * Cost is O(n²) in the vertex count (self-intersection check and ear clipping),\n * which suits import-time geometry rather than a per-step path.\n *\n * @throws RangeError if a coordinate is not finite, if fewer than three\n * distinct non-collinear vertices remain after cleanup, or if the outline\n * intersects itself.\n * @internal\n */\nexport const decomposeToConvexParts = (vertices: ReadonlyArray<Readonly<PointLike>>): PointLike[][] => {\n const points = normalize(vertices);\n const count = points.length / 2;\n\n assertSimple(points);\n\n if (isStrictlyConvex(points)) {\n return [toPoints(points, sequence(count))];\n }\n\n const indices = triangulate(points);\n\n // Ear clipping stops early rather than throwing when it cannot find an ear,\n // so a short index list is how a polygon that survived the checks above but is\n // still numerically degenerate shows up. Accepting it would silently drop area.\n if (indices.length !== (count - 2) * 3) {\n throw new RangeError(\n `decomposeToConvexParts: could not triangulate the outline (${indices.length / 3} of ${count - 2} triangles); check it for near-degenerate geometry.`,\n );\n }\n\n const sourceArea = Math.abs(signedArea(points));\n const cycles = mergeTriangles(points, indices, sourceArea * degenerateAreaFraction);\n const parts = cycles.map(cycle => toPoints(points, cycle));\n\n // Post-condition rather than trust: ear clipping and the merge pass both work\n // on floating-point turns, and any future change that started losing area\n // would otherwise show up as a silently smaller collider.\n const partArea = cycles.reduce((sum, cycle) => sum + Math.abs(cycleArea(points, cycle)), 0);\n\n if (parts.length === 0 || Math.abs(partArea - sourceArea) > sourceArea * 1e-6) {\n throw new RangeError(\n `decomposeToConvexParts: the parts cover ${partArea.toFixed(4)} of the outline's ${sourceArea.toFixed(4)}; the outline is too degenerate to decompose.`,\n );\n }\n\n return parts;\n};\n\n/**\n * Weld coincident vertices, drop collinear ones and canonicalise to\n * counter-clockwise, returning a flat `[x0, y0, x1, y1, ...]` array.\n */\nconst normalize = (vertices: ReadonlyArray<Readonly<PointLike>>): number[] => {\n const points: number[] = [];\n\n for (const vertex of vertices) {\n if (!Number.isFinite(vertex.x) || !Number.isFinite(vertex.y)) {\n throw new RangeError(`decomposeToConvexParts: vertex has a non-finite component (${vertex.x}, ${vertex.y}).`);\n }\n\n if (points.length === 0) {\n points.push(vertex.x, vertex.y);\n\n continue;\n }\n\n if (Math.hypot(vertex.x - points[points.length - 2]!, vertex.y - points[points.length - 1]!) >= weldEpsilon) {\n points.push(vertex.x, vertex.y);\n }\n }\n\n // A trailing vertex coinciding with the first closes the ring explicitly; the\n // ring is implicit here.\n if (points.length >= 4 && Math.hypot(points[0]! - points[points.length - 2]!, points[1]! - points[points.length - 1]!) < weldEpsilon) {\n points.length -= 2;\n }\n\n if (signedArea(points) < 0) {\n reverseWinding(points);\n }\n\n dropCollinear(points);\n\n if (points.length < 6) {\n throw new RangeError(`decomposeToConvexParts: needs at least 3 distinct, non-collinear vertices, received ${points.length / 2} after cleanup.`);\n }\n\n if (Math.abs(signedArea(points)) <= weldEpsilon) {\n throw new RangeError('decomposeToConvexParts: the outline encloses no area.');\n }\n\n return points;\n};\n\n/**\n * Remove every vertex whose two edges are collinear. Repeats until stable: one\n * removal can make its neighbour collinear in turn.\n */\nconst dropCollinear = (points: number[]): void => {\n let removed = true;\n\n while (removed && points.length >= 6) {\n removed = false;\n\n for (let i = 0; i < points.length / 2; i++) {\n const count = points.length / 2;\n\n if (count < 3) {\n break;\n }\n\n const p = (i + count - 1) % count;\n const n = (i + 1) % count;\n\n if (isCollinear(points, p, i, n)) {\n points.splice(i * 2, 2);\n removed = true;\n i--;\n }\n }\n }\n};\n\nconst isCollinear = (points: number[], a: number, b: number, c: number): boolean => {\n const e1x = points[b * 2]! - points[a * 2]!;\n const e1y = points[b * 2 + 1]! - points[a * 2 + 1]!;\n const e2x = points[c * 2]! - points[b * 2]!;\n const e2y = points[c * 2 + 1]! - points[b * 2 + 1]!;\n const lengths = Math.hypot(e1x, e1y) * Math.hypot(e2x, e2y);\n\n return lengths === 0 || Math.abs(e1x * e2y - e1y * e2x) <= collinearEpsilon * lengths;\n};\n\n/**\n * Reject an outline that crosses or touches itself. Every pair of non-adjacent\n * edges must be disjoint - a shared point between them is exactly what makes a\n * polygon non-simple, and both ear clipping and the solver assume it cannot\n * happen.\n */\nconst assertSimple = (points: number[]): void => {\n const count = points.length / 2;\n\n for (let i = 0; i < count; i++) {\n const iNext = (i + 1) % count;\n\n for (let j = i + 1; j < count; j++) {\n const jNext = (j + 1) % count;\n\n // Edges sharing a vertex always \"touch\" at it; only non-adjacent pairs\n // carry information.\n if (i === j || iNext === j || jNext === i) {\n continue;\n }\n\n if (segmentsIntersect(points, i, iNext, j, jNext)) {\n throw new RangeError('decomposeToConvexParts: the outline intersects itself; only simple polygons can be decomposed.');\n }\n }\n }\n};\n\n/** `true` when segments `a→b` and `c→d` share any point, touching included. */\nconst segmentsIntersect = (points: number[], a: number, b: number, c: number, d: number): boolean => {\n const o1 = orientation(points, a, b, c);\n const o2 = orientation(points, a, b, d);\n const o3 = orientation(points, c, d, a);\n const o4 = orientation(points, c, d, b);\n\n if (o1 !== o2 && o3 !== o4) {\n return true;\n }\n\n return (\n (o1 === 0 && onSegment(points, a, b, c)) ||\n (o2 === 0 && onSegment(points, a, b, d)) ||\n (o3 === 0 && onSegment(points, c, d, a)) ||\n (o4 === 0 && onSegment(points, c, d, b))\n );\n};\n\n/** Sign of the turn `a → b → c`: `1` left, `-1` right, `0` collinear. */\nconst orientation = (points: number[], a: number, b: number, c: number): number => {\n const cross =\n (points[b * 2]! - points[a * 2]!) * (points[c * 2 + 1]! - points[a * 2 + 1]!) -\n (points[b * 2 + 1]! - points[a * 2 + 1]!) * (points[c * 2]! - points[a * 2]!);\n const scale =\n Math.hypot(points[b * 2]! - points[a * 2]!, points[b * 2 + 1]! - points[a * 2 + 1]!) *\n Math.hypot(points[c * 2]! - points[a * 2]!, points[c * 2 + 1]! - points[a * 2 + 1]!);\n\n if (scale === 0 || Math.abs(cross) <= collinearEpsilon * scale) {\n return 0;\n }\n\n return cross > 0 ? 1 : -1;\n};\n\n/** `true` when `p` lies within the bounding box of the collinear segment `a→b`. */\nconst onSegment = (points: number[], a: number, b: number, p: number): boolean =>\n points[p * 2]! >= Math.min(points[a * 2]!, points[b * 2]!) - weldEpsilon &&\n points[p * 2]! <= Math.max(points[a * 2]!, points[b * 2]!) + weldEpsilon &&\n points[p * 2 + 1]! >= Math.min(points[a * 2 + 1]!, points[b * 2 + 1]!) - weldEpsilon &&\n points[p * 2 + 1]! <= Math.max(points[a * 2 + 1]!, points[b * 2 + 1]!) + weldEpsilon;\n\n/**\n * Hertel-Mehlhorn: start from the triangulation and drop every internal\n * diagonal whose two faces merge into a still-convex polygon. Diagonals are\n * visited in a fixed key order, so the outcome depends only on the cleaned\n * input.\n */\nconst mergeTriangles = (points: number[], indices: Uint32Array, degenerateArea: number): number[][] => {\n const cycles = new Map<number, number[]>();\n const owners = new Map<number, number[]>();\n // Union-find over part ids: a merged part keeps the lower id, and every\n // diagonal recorded against the absorbed one resolves through here.\n const parent: number[] = [];\n\n const find = (id: number): number => {\n let root = id;\n\n while (parent[root] !== root) {\n root = parent[root]!;\n }\n\n return root;\n };\n\n const stride = points.length / 2;\n const diagonalKey = (a: number, b: number): number => (a < b ? a * stride + b : b * stride + a);\n\n for (let t = 0; t < indices.length; t += 3) {\n const id = t / 3;\n const cycle = [indices[t]!, indices[t + 1]!, indices[t + 2]!];\n\n if (Math.abs(cycleArea(points, cycle)) <= degenerateArea) {\n continue;\n }\n\n parent[id] = id;\n cycles.set(id, cycle);\n\n for (let e = 0; e < 3; e++) {\n const key = diagonalKey(cycle[e]!, cycle[(e + 1) % 3]!);\n const owner = owners.get(key);\n\n if (owner === undefined) {\n owners.set(key, [id]);\n } else {\n owner.push(id);\n }\n }\n }\n\n for (const key of [...owners.keys()].sort((x, y) => x - y)) {\n const owner = owners.get(key)!;\n\n if (owner.length !== 2) {\n continue;\n }\n\n const rootA = find(owner[0]!);\n const rootB = find(owner[1]!);\n\n if (rootA === rootB) {\n continue;\n }\n\n const cycleA = cycles.get(rootA)!;\n const cycleB = cycles.get(rootB)!;\n const merged = mergeAcross(points, cycleA, cycleB, Math.floor(key / stride), key % stride);\n\n if (merged === null) {\n continue;\n }\n\n const keep = rootA < rootB ? rootA : rootB;\n const drop = rootA < rootB ? rootB : rootA;\n\n cycles.set(keep, merged);\n cycles.delete(drop);\n parent[drop] = keep;\n }\n\n return [...cycles.values()];\n};\n\n/**\n * Join two convex cycles across the shared edge between `u` and `v`, or `null`\n * when the union would not be strictly convex. Rejecting a merely collinear\n * junction is deliberate: the resulting part is handed to `PolygonShape`, which\n * refuses collinear edges.\n */\nconst mergeAcross = (points: number[], cycleA: number[], cycleB: number[], u: number, v: number): number[] | null => {\n // Both cycles are counter-clockwise and share the undirected edge, so exactly\n // one of them traverses it as `a → b` and the other as `b → a`.\n let a = u;\n let b = v;\n let start = directedEdgeIndex(cycleA, a, b);\n\n if (start === -1) {\n a = v;\n b = u;\n start = directedEdgeIndex(cycleA, a, b);\n }\n\n const back = start === -1 ? -1 : directedEdgeIndex(cycleB, b, a);\n\n if (start === -1 || back === -1) {\n return null;\n }\n\n // Walk A once starting at b (so it ends on a), then splice in B's vertices\n // strictly between a and b.\n const merged: number[] = [];\n\n for (let i = 0; i < cycleA.length; i++) {\n merged.push(cycleA[(start + 1 + i) % cycleA.length]!);\n }\n\n for (let i = 2; i < cycleB.length; i++) {\n merged.push(cycleB[(back + i) % cycleB.length]!);\n }\n\n return isCycleStrictlyConvex(points, merged) ? merged : null;\n};\n\n/** Index of `a` in `cycle` when `b` immediately follows it, else `-1`. */\nconst directedEdgeIndex = (cycle: number[], a: number, b: number): number => {\n for (let i = 0; i < cycle.length; i++) {\n if (cycle[i] === a && cycle[(i + 1) % cycle.length] === b) {\n return i;\n }\n }\n\n return -1;\n};\n\nconst isCycleStrictlyConvex = (points: number[], cycle: number[]): boolean => {\n for (let i = 0; i < cycle.length; i++) {\n const a = cycle[i]!;\n const b = cycle[(i + 1) % cycle.length]!;\n const c = cycle[(i + 2) % cycle.length]!;\n\n if (orientation(points, a, b, c) <= 0) {\n return false;\n }\n }\n\n return true;\n};\n\nconst isStrictlyConvex = (points: number[]): boolean => isCycleStrictlyConvex(points, sequence(points.length / 2));\n\nconst sequence = (count: number): number[] => {\n const out: number[] = [];\n\n for (let i = 0; i < count; i++) {\n out.push(i);\n }\n\n return out;\n};\n\nconst toPoints = (points: number[], cycle: number[]): PointLike[] => cycle.map(index => ({ x: points[index * 2]!, y: points[index * 2 + 1]! }));\n\n/** Shoelace signed area of an index cycle over `points`. */\nconst cycleArea = (points: number[], cycle: number[]): number => {\n let sum = 0;\n\n for (let i = 0; i < cycle.length; i++) {\n const a = cycle[i]!;\n const b = cycle[(i + 1) % cycle.length]!;\n\n sum += points[a * 2]! * points[b * 2 + 1]! - points[b * 2]! * points[a * 2 + 1]!;\n }\n\n return sum / 2;\n};\n\n/** Shoelace signed area of a flat point ring; positive is counter-clockwise. */\nconst signedArea = (points: number[]): number => {\n const count = points.length / 2;\n let sum = 0;\n\n for (let i = 0; i < count; i++) {\n const j = (i + 1) % count;\n\n sum += points[i * 2]! * points[j * 2 + 1]! - points[j * 2]! * points[i * 2 + 1]!;\n }\n\n return sum / 2;\n};\n\nconst reverseWinding = (points: number[]): void => {\n const count = points.length / 2;\n\n for (let i = 0; i < Math.floor(count / 2); i++) {\n const j = count - 1 - i;\n const ix = points[i * 2]!;\n const iy = points[i * 2 + 1]!;\n\n points[i * 2] = points[j * 2]!;\n points[i * 2 + 1] = points[j * 2 + 1]!;\n points[j * 2] = ix;\n points[j * 2 + 1] = iy;\n }\n};\n"],"mappings":"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"}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export type { AnyShape } from './AnyShape';
|
|
2
2
|
export { BoxShape } from './BoxShape';
|
|
3
|
+
export { CapsuleShape } from './CapsuleShape';
|
|
4
|
+
export { ChainShape, type ChainShapeOptions } from './ChainShape';
|
|
3
5
|
export { CircleShape } from './CircleShape';
|
|
4
6
|
export { PolygonShape } from './PolygonShape';
|
|
7
|
+
export { SegmentShape } from './SegmentShape';
|
|
5
8
|
export { Shape, type ShapeType } from './Shape';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shapes/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,KAAK,EAAE,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -11,7 +11,7 @@ import type { ContactRecord } from '../ContactGraph';
|
|
|
11
11
|
*
|
|
12
12
|
* The position constraint is folded into the velocity solve as a **soft bias**
|
|
13
13
|
* (a damped-spring push-out whose stiffness is decoupled from the iteration
|
|
14
|
-
* count), recomputed each pass from the live per-body delta position/rotation
|
|
14
|
+
* count), recomputed each pass from the live per-body delta position/rotation -
|
|
15
15
|
* so there is no separate NGS geometric pass. The contact anchors are rotated by
|
|
16
16
|
* the live sub-step rotation each pass, so a tilting stack's restoring torque is
|
|
17
17
|
* computed against its current orientation (without this the tilt grows instead
|
|
@@ -19,8 +19,9 @@ import type { ContactRecord } from '../ContactGraph';
|
|
|
19
19
|
* 2×2 block LCP** (Box2D-style), which propagates stack loads far better than
|
|
20
20
|
* solving the points sequentially; the block path carries the soft push-out as
|
|
21
21
|
* its velocity target (hard mass scale), while single-point contacts use the full
|
|
22
|
-
* soft mass/impulse scaling
|
|
23
|
-
*
|
|
22
|
+
* soft mass/impulse scaling for as long as their push-out target stays inside
|
|
23
|
+
* the cap, and the hard scale once it is clamped. Accumulated impulses are
|
|
24
|
+
* written back to each {@link ContactRecord} for warm-starting the next frame.
|
|
24
25
|
*
|
|
25
26
|
* Internal to {@link NativePhysicsBackend}; not part of the public surface.
|
|
26
27
|
*/
|
|
@@ -64,6 +65,13 @@ export declare class ContactSolver {
|
|
|
64
65
|
* pushes out the penetration and bleeds a little stored impulse (the damped
|
|
65
66
|
* spring). The relax pass passes `useBias=false`, collapsing it to the hard
|
|
66
67
|
* `−normalMass·vn` (no bias, full mass, no impulse decay).
|
|
68
|
+
*
|
|
69
|
+
* A point whose push-out target was clamped to `maxBiasVelocity` is solved
|
|
70
|
+
* with the hard scale as well: the soft scaling models a spring, and once the
|
|
71
|
+
* target is capped the point would otherwise reach only a fraction of an
|
|
72
|
+
* already limited push-out. That deficit grows with the load, so past the
|
|
73
|
+
* acceleration at which the cap binds the contact would sink without bound
|
|
74
|
+
* instead of settling into a slightly deeper limit cycle.
|
|
67
75
|
*/
|
|
68
76
|
private _solveNormalSequential;
|
|
69
77
|
/**
|
|
@@ -74,8 +82,6 @@ export declare class ContactSolver {
|
|
|
74
82
|
* soft push-out as its target but solves with a hard mass scale.
|
|
75
83
|
*/
|
|
76
84
|
private _solveNormalBlock;
|
|
77
|
-
/** Apply the two-point block impulse deltas `(d1, d2)` along the normal at both contact points. */
|
|
78
|
-
private _applyBlock;
|
|
79
85
|
private _acquire;
|
|
80
86
|
/**
|
|
81
87
|
* Pooled constraint at `ci`. Every caller indexes within `0..this._count-1`
|
|
@@ -85,3 +91,4 @@ export declare class ContactSolver {
|
|
|
85
91
|
*/
|
|
86
92
|
private _at;
|
|
87
93
|
}
|
|
94
|
+
//# sourceMappingURL=ContactSolver.d.ts.map
|