@forgeax/engine-physics 0.1.27 → 0.1.28
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 +115 -1
- package/dist/__tests__/derived-physics.unit.test.d.ts +2 -0
- package/dist/__tests__/derived-physics.unit.test.d.ts.map +1 -0
- package/dist/derived-physics.d.ts +218 -0
- package/dist/derived-physics.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +415 -2
- package/dist/index.mjs.map +1 -1
- package/dist/physics-world.d.ts +60 -2
- package/dist/physics-world.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/__tests__/derived-physics.unit.test.ts +156 -0
- package/src/derived-physics.ts +740 -0
- package/src/index.ts +31 -0
- package/src/physics-world.ts +79 -2
package/dist/physics-world.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Vec2, Vec3 } from '@forgeax/engine-math';
|
|
2
|
+
import type { Result } from '@forgeax/engine-types';
|
|
3
|
+
import type { DerivedPhysicsCandidate, DerivedPhysicsCandidateInput, DerivedPhysicsError, DerivedPhysicsFailure, DerivedPhysicsMotion, DerivedPhysicsPublication, DerivedPhysicsSnapshot, DerivedShapeState, PhysicsConstraintInput, PhysicsContactObservation } from './derived-physics';
|
|
2
4
|
/**
|
|
3
5
|
* Raycast hit result — returned by `PhysicsWorld.raycast()`.
|
|
4
6
|
*
|
|
@@ -49,7 +51,7 @@ export interface PhysicsWorld {
|
|
|
49
51
|
* (unlike `world.set(entity, Transform, { translation: ... })` on
|
|
50
52
|
* dynamic bodies, which would cause a velocity spike).
|
|
51
53
|
*
|
|
52
|
-
* @param entity - the entity
|
|
54
|
+
* @param entity - the entity with a registered native body.
|
|
53
55
|
* @param position - new world-space position.
|
|
54
56
|
*/
|
|
55
57
|
teleport(entity: number, position: Vec3): void;
|
|
@@ -85,7 +87,7 @@ export interface PhysicsWorld {
|
|
|
85
87
|
*
|
|
86
88
|
* Returns `true` after `ensureBody` has created a Rapier body for the entity
|
|
87
89
|
* (which happens asynchronously via WASM fire-and-forget load + tick pipeline).
|
|
88
|
-
*
|
|
90
|
+
* A RigidBody-only 3D entity is a native body even before derived shapes arrive.
|
|
89
91
|
*
|
|
90
92
|
* AI-user contract: before calling `moveAndSlide` inside a per-frame driver,
|
|
91
93
|
* guard with `if (!pw.hasBody(entity)) return;` to avoid `body-not-found`
|
|
@@ -93,6 +95,62 @@ export interface PhysicsWorld {
|
|
|
93
95
|
* `physicsSyncBackend` tick that builds the body.
|
|
94
96
|
*/
|
|
95
97
|
hasBody(entity: number): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Prepare a replacement set of local derived shapes without making it
|
|
100
|
+
* queryable. The Rapier 3D implementation backs these shapes with one
|
|
101
|
+
* native world and one ECS body; 2D backends may omit this optional seam.
|
|
102
|
+
*/
|
|
103
|
+
prepareDerivedShapeCandidate?: (input: DerivedPhysicsCandidateInput) => Result<DerivedPhysicsCandidate, DerivedPhysicsError>;
|
|
104
|
+
/**
|
|
105
|
+
* Queue a prepared candidate. The optional synchronous geometry commit runs
|
|
106
|
+
* after fallible native preparation, before step/publication. It must either
|
|
107
|
+
* commit its complete ECS binding or return failure without changing it.
|
|
108
|
+
* Returned failure restores old physics; a thrown callback has uncertain ECS
|
|
109
|
+
* writes and requires rebuild. Reentrant physics access is refused.
|
|
110
|
+
*/
|
|
111
|
+
admitDerivedShapeCandidate?: (candidate: DerivedPhysicsCandidate, commitGeometry?: () => Result<void, Error>) => Result<DerivedPhysicsCandidate, DerivedPhysicsError>;
|
|
112
|
+
/** Borrowed ordering proof, present only inside the paired geometry commit. */
|
|
113
|
+
getDerivedAdmission?: (entity?: number) => {
|
|
114
|
+
readonly entity: number;
|
|
115
|
+
readonly revision: number;
|
|
116
|
+
readonly fixedStep: number;
|
|
117
|
+
} | undefined;
|
|
118
|
+
/** Cancel a candidate that has not been published. */
|
|
119
|
+
cancelDerivedShapeCandidate?: (candidate: DerivedPhysicsCandidate) => Result<void, DerivedPhysicsError>;
|
|
120
|
+
/** Invalidate pending candidates without touching the last committed state. */
|
|
121
|
+
invalidateDerivedShapeCandidates?: (reason?: string) => void;
|
|
122
|
+
/** Read the most recent fixed-step publication for one body. */
|
|
123
|
+
getDerivedPublication?: (entity: number) => DerivedPhysicsPublication | undefined;
|
|
124
|
+
/** Read the structured failure from the latest rejected admission, if any. */
|
|
125
|
+
getDerivedFailure?: (entity: number) => DerivedPhysicsFailure | undefined;
|
|
126
|
+
/** Read the committed body type without exposing a native Rapier body. */
|
|
127
|
+
getDerivedBodyType?: (entity: number) => 'static' | 'dynamic' | 'kinematic' | undefined;
|
|
128
|
+
/** Read whether a native admission failure requires a full World rebuild. */
|
|
129
|
+
getDerivedRecoveryState?: () => 'ready' | 'rebuild-required';
|
|
130
|
+
/** Read detached mass evidence without exposing a native body handle. */
|
|
131
|
+
getDerivedBodyMass?: (entity: number) => number | undefined;
|
|
132
|
+
/** Read the committed world-space COM and velocities for one derived body. */
|
|
133
|
+
getDerivedMotion?: (entity: number) => DerivedPhysicsMotion | undefined;
|
|
134
|
+
/** Read the currently committed local shape identities for one body. */
|
|
135
|
+
getDerivedShapes?: (entity: number) => readonly DerivedShapeState[];
|
|
136
|
+
/** Read detached contact observations from the latest fixed-step drain. */
|
|
137
|
+
getContactObservations?: () => readonly PhysicsContactObservation[];
|
|
138
|
+
/** Complete the fixed-step publication after ECS writeback/contact sync. */
|
|
139
|
+
finalizeDerivedFixedStep?: () => void;
|
|
140
|
+
/** Capture portable, committed input for explicit rebuild/recovery. */
|
|
141
|
+
captureDerivedPhysicsState?: () => DerivedPhysicsSnapshot;
|
|
142
|
+
/** Restore a previously captured input through normal candidate admission. */
|
|
143
|
+
restoreDerivedPhysicsState?: (snapshot: DerivedPhysicsSnapshot) => Result<readonly DerivedPhysicsCandidate[], DerivedPhysicsError>;
|
|
144
|
+
/** Create/update/remove constraints in the same solver as ordinary bodies. */
|
|
145
|
+
createDerivedConstraint?: (input: PhysicsConstraintInput) => Result<{
|
|
146
|
+
readonly id: string;
|
|
147
|
+
readonly revision: number;
|
|
148
|
+
}, DerivedPhysicsError>;
|
|
149
|
+
updateDerivedConstraint?: (input: PhysicsConstraintInput) => Result<{
|
|
150
|
+
readonly id: string;
|
|
151
|
+
readonly revision: number;
|
|
152
|
+
}, DerivedPhysicsError>;
|
|
153
|
+
removeDerivedConstraint?: (id: string) => Result<void, DerivedPhysicsError>;
|
|
96
154
|
}
|
|
97
155
|
/** 2D raycast hit result. */
|
|
98
156
|
export interface RaycastHit2D {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"physics-world.d.ts","sourceRoot":"","sources":["../src/physics-world.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"physics-world.d.ts","sourceRoot":"","sources":["../src/physics-world.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-physics",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -26,21 +26,21 @@
|
|
|
26
26
|
"LICENSE"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@forgeax/engine-ecs": "0.1.
|
|
30
|
-
"@forgeax/engine-math": "0.1.
|
|
31
|
-
"@forgeax/engine-plugin": "0.1.
|
|
32
|
-
"@forgeax/engine-types": "0.1.
|
|
29
|
+
"@forgeax/engine-ecs": "0.1.28",
|
|
30
|
+
"@forgeax/engine-math": "0.1.28",
|
|
31
|
+
"@forgeax/engine-plugin": "0.1.28",
|
|
32
|
+
"@forgeax/engine-types": "0.1.28"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@forgeax/engine-assets-runtime": "0.1.
|
|
36
|
-
"@forgeax/engine-physics-rapier2d": "0.1.
|
|
37
|
-
"@forgeax/engine-physics-rapier3d": "0.1.
|
|
38
|
-
"@forgeax/engine-runtime": "0.1.
|
|
39
|
-
"@forgeax/engine-shader": "0.1.
|
|
35
|
+
"@forgeax/engine-assets-runtime": "0.1.28",
|
|
36
|
+
"@forgeax/engine-physics-rapier2d": "0.1.28",
|
|
37
|
+
"@forgeax/engine-physics-rapier3d": "0.1.28",
|
|
38
|
+
"@forgeax/engine-runtime": "0.1.28",
|
|
39
|
+
"@forgeax/engine-shader": "0.1.28"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@forgeax/engine-physics-rapier2d": "0.1.
|
|
43
|
-
"@forgeax/engine-physics-rapier3d": "0.1.
|
|
42
|
+
"@forgeax/engine-physics-rapier2d": "0.1.28",
|
|
43
|
+
"@forgeax/engine-physics-rapier3d": "0.1.28"
|
|
44
44
|
},
|
|
45
45
|
"peerDependenciesMeta": {
|
|
46
46
|
"@forgeax/engine-physics-rapier2d": {
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
cloneDerivedPhysicsInput,
|
|
4
|
+
normalizeVoxelShapeInput,
|
|
5
|
+
preserveCenterOfMassVelocity,
|
|
6
|
+
validateMassProperties,
|
|
7
|
+
} from '../derived-physics';
|
|
8
|
+
|
|
9
|
+
const shape = (id = 'left', voxelSize: readonly [number, number, number] = [1, 1, 1]) => ({
|
|
10
|
+
id,
|
|
11
|
+
revision: 1,
|
|
12
|
+
cells: new Int32Array([0, 0, 0, -1, 0, 0]),
|
|
13
|
+
voxelSize,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('derived physics data contract', () => {
|
|
17
|
+
it('copies cells and normalizes a local orientation before native preparation', () => {
|
|
18
|
+
const input = shape();
|
|
19
|
+
const result = normalizeVoxelShapeInput({ ...input, rotation: [0, 0, 0, 2] });
|
|
20
|
+
expect(result.ok).toBe(true);
|
|
21
|
+
if (!result.ok) return;
|
|
22
|
+
input.cells[0] = 99;
|
|
23
|
+
expect(result.value.cells[0]).toBe(0);
|
|
24
|
+
expect(result.value.rotation[3]).toBeCloseTo(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('rejects mismatched voxel seams and degenerate explicit mass', () => {
|
|
28
|
+
const seam = cloneDerivedPhysicsInput({
|
|
29
|
+
entity: 7,
|
|
30
|
+
revision: 1,
|
|
31
|
+
sourceKey: 'body:7',
|
|
32
|
+
shapes: [shape('left'), shape('right', [2, 1, 1])],
|
|
33
|
+
seams: [{ shapeA: 'left', shapeB: 'right', offset: [1, 0, 0] }],
|
|
34
|
+
});
|
|
35
|
+
expect(seam.ok).toBe(false);
|
|
36
|
+
if (!seam.ok) expect(seam.error.code).toBe('derived-seam-invalid');
|
|
37
|
+
|
|
38
|
+
const mass = validateMassProperties({
|
|
39
|
+
mode: 'explicit',
|
|
40
|
+
mass: 1,
|
|
41
|
+
centerOfMass: [0, 0, 0],
|
|
42
|
+
principalInertia: [1, 0, 1],
|
|
43
|
+
});
|
|
44
|
+
expect(mass.ok).toBe(false);
|
|
45
|
+
if (!mass.ok) expect(mass.error.code).toBe('derived-mass-invalid');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('requires seam origin/grid agreement while accepting quaternion sign equivalence', () => {
|
|
49
|
+
const aligned = cloneDerivedPhysicsInput({
|
|
50
|
+
entity: 7,
|
|
51
|
+
revision: 1,
|
|
52
|
+
sourceKey: 'body:7',
|
|
53
|
+
shapes: [
|
|
54
|
+
{ ...shape('left'), origin: [0, 0, 0], rotation: [0, 0, 0, 1] },
|
|
55
|
+
{ ...shape('right'), origin: [2, 0, 0], rotation: [0, 0, 0, -1] },
|
|
56
|
+
],
|
|
57
|
+
seams: [{ shapeA: 'left', shapeB: 'right', offset: [2, 0, 0] }],
|
|
58
|
+
});
|
|
59
|
+
expect(aligned.ok).toBe(true);
|
|
60
|
+
|
|
61
|
+
const offsetMismatch = cloneDerivedPhysicsInput({
|
|
62
|
+
entity: 7,
|
|
63
|
+
revision: 1,
|
|
64
|
+
sourceKey: 'body:7',
|
|
65
|
+
shapes: [
|
|
66
|
+
{ ...shape('left'), origin: [0, 0, 0] },
|
|
67
|
+
{ ...shape('right'), origin: [2, 0, 0] },
|
|
68
|
+
],
|
|
69
|
+
seams: [{ shapeA: 'left', shapeB: 'right', offset: [1, 0, 0] }],
|
|
70
|
+
});
|
|
71
|
+
expect(offsetMismatch.ok).toBe(false);
|
|
72
|
+
if (!offsetMismatch.ok) expect(offsetMismatch.error.code).toBe('derived-seam-invalid');
|
|
73
|
+
|
|
74
|
+
const rotationMismatch = cloneDerivedPhysicsInput({
|
|
75
|
+
entity: 7,
|
|
76
|
+
revision: 1,
|
|
77
|
+
sourceKey: 'body:7',
|
|
78
|
+
shapes: [
|
|
79
|
+
{ ...shape('left'), origin: [0, 0, 0] },
|
|
80
|
+
{ ...shape('right'), origin: [2, 0, 0], rotation: [0, Math.SQRT1_2, 0, Math.SQRT1_2] },
|
|
81
|
+
],
|
|
82
|
+
seams: [{ shapeA: 'left', shapeB: 'right', offset: [2, 0, 0] }],
|
|
83
|
+
});
|
|
84
|
+
expect(rotationMismatch.ok).toBe(false);
|
|
85
|
+
if (!rotationMismatch.ok) expect(rotationMismatch.error.code).toBe('derived-seam-invalid');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('checks seam offsets after rotating the local delta into the shared grid frame', () => {
|
|
89
|
+
const quarterTurnZ: readonly [number, number, number, number] = [
|
|
90
|
+
0,
|
|
91
|
+
0,
|
|
92
|
+
Math.SQRT1_2,
|
|
93
|
+
Math.SQRT1_2,
|
|
94
|
+
];
|
|
95
|
+
const aligned = cloneDerivedPhysicsInput({
|
|
96
|
+
entity: 7,
|
|
97
|
+
revision: 1,
|
|
98
|
+
sourceKey: 'body:7',
|
|
99
|
+
shapes: [
|
|
100
|
+
{ ...shape('left'), origin: [0, 0, 0], rotation: quarterTurnZ },
|
|
101
|
+
{ ...shape('right'), origin: [0, 1, 0], rotation: quarterTurnZ },
|
|
102
|
+
],
|
|
103
|
+
seams: [{ shapeA: 'left', shapeB: 'right', offset: [1, 0, 0] }],
|
|
104
|
+
});
|
|
105
|
+
expect(aligned.ok).toBe(true);
|
|
106
|
+
|
|
107
|
+
const misaligned = cloneDerivedPhysicsInput({
|
|
108
|
+
entity: 7,
|
|
109
|
+
revision: 1,
|
|
110
|
+
sourceKey: 'body:7',
|
|
111
|
+
shapes: [
|
|
112
|
+
{ ...shape('left'), origin: [0, 0, 0], rotation: quarterTurnZ },
|
|
113
|
+
{ ...shape('right'), origin: [1, 0, 0], rotation: quarterTurnZ },
|
|
114
|
+
],
|
|
115
|
+
seams: [{ shapeA: 'left', shapeB: 'right', offset: [1, 0, 0] }],
|
|
116
|
+
});
|
|
117
|
+
expect(misaligned.ok).toBe(false);
|
|
118
|
+
if (!misaligned.ok) expect(misaligned.error.code).toBe('derived-seam-invalid');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('bounds candidate counts and estimated staged bytes', () => {
|
|
122
|
+
const tooManyShapes = cloneDerivedPhysicsInput({
|
|
123
|
+
entity: 7,
|
|
124
|
+
revision: 1,
|
|
125
|
+
sourceKey: 'body:7',
|
|
126
|
+
shapes: Array.from({ length: 65 }, (_, index) => shape(`shape-${index}`)),
|
|
127
|
+
});
|
|
128
|
+
expect(tooManyShapes.ok).toBe(false);
|
|
129
|
+
if (!tooManyShapes.ok) {
|
|
130
|
+
expect(tooManyShapes.error.code).toBe('derived-candidate-budget-exceeded');
|
|
131
|
+
expect(tooManyShapes.error.detail.actual).toBe(65);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const tooManyBytes = cloneDerivedPhysicsInput({
|
|
135
|
+
entity: 7,
|
|
136
|
+
revision: 1,
|
|
137
|
+
sourceKey: 'body:7',
|
|
138
|
+
shapes: [
|
|
139
|
+
{
|
|
140
|
+
id: 'large',
|
|
141
|
+
revision: 1,
|
|
142
|
+
cells: new Int32Array(262_144 * 3),
|
|
143
|
+
voxelSize: [1, 1, 1],
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
expect(tooManyBytes.ok).toBe(false);
|
|
148
|
+
if (!tooManyBytes.ok) expect(tooManyBytes.error.code).toBe('derived-candidate-budget-exceeded');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('preserves linear velocity at a moved center of mass and leaves angular velocity intact', () => {
|
|
152
|
+
expect(preserveCenterOfMassVelocity([1, 2, 3], [0, 0, 2], [0, 0, 0], [0, 1, 0])).toEqual([
|
|
153
|
+
-1, 2, 3,
|
|
154
|
+
]);
|
|
155
|
+
});
|
|
156
|
+
});
|