Skip to content

Commit 39ac65f

Browse files
committed
feat(physics): add wall barrier
1 parent 8cc3c24 commit 39ac65f

8 files changed

Lines changed: 216 additions & 1 deletion

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@
122122
- [x] Stiffness design principle for frozen barrier stiffness
123123
- [x] Contact barrier with extended direction handling
124124
- [x] Pin constraint barrier using cubic barrier formulation
125-
- [ ] Wall constraint barrier for plane collisions
125+
- [x] Wall constraint barrier for plane collisions
126126
- [ ] Triangle strain-limiting barrier driven by deformation singular values
127127
- **Integrator and solver**
128128
- [ ] Inexact Newton integrator with beta accumulation

docs/index.d.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export const examples: {
121121
readonly computeFrozenStiffness: 'examples/foldStiffness.ts';
122122
readonly createContactBarrier: 'examples/foldContactBarrier.ts';
123123
readonly createPinBarrier: 'examples/foldPinBarrier.ts';
124+
readonly createWallBarrier: 'examples/foldWallBarrier.ts';
124125
};
125126
readonly performance: {
126127
readonly debounce: 'examples/requestDedup.ts';
@@ -3361,6 +3362,20 @@ export interface PinBarrierOptions {
33613362
}
33623363
export function createPinBarrier(options?: PinBarrierOptions): FoldConstraint;
33633364

3365+
/**
3366+
* Wall constraint barrier for plane contacts.
3367+
* Use for: enforcing collision against static planes with Fold guarantees.
3368+
* Import: physics/fold/wallBarrier.ts
3369+
*/
3370+
export interface WallBarrierOptions {
3371+
id?: string;
3372+
stiffnessOverride?: number;
3373+
maxGap?: number;
3374+
normal?: Vector3D;
3375+
planePoint?: Vector3D;
3376+
}
3377+
export function createWallBarrier(options?: WallBarrierOptions): FoldConstraint;
3378+
33643379
export type FoldConstraintType =
33653380
| 'cubic-barrier'
33663381
| 'contact-barrier'

examples/foldWallBarrier.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { createWallBarrier } from '../src/index.js';
2+
3+
const barrier = createWallBarrier({
4+
planePoint: { x: 0, y: 0, z: 0 },
5+
normal: { x: 0, y: 1, z: 0 },
6+
});
7+
8+
const evaluation = barrier.evaluate(
9+
{
10+
gap: -0.03,
11+
maxGap: 0,
12+
stiffness: 0,
13+
direction: { x: 0, y: 1, z: 0 },
14+
effectiveMass: 0.25,
15+
metadata: {
16+
position: { x: 0, y: -0.03, z: 0 },
17+
hessian: [
18+
[3, 0, 0],
19+
[0, 3, 0],
20+
[0, 0, 3],
21+
],
22+
},
23+
},
24+
{ deltaTime: 1 / 90 }
25+
);
26+
27+
console.log('wall energy', evaluation.energy);

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export const examples = {
119119
computeFrozenStiffness: 'examples/foldStiffness.ts',
120120
createContactBarrier: 'examples/foldContactBarrier.ts',
121121
createPinBarrier: 'examples/foldPinBarrier.ts',
122+
createWallBarrier: 'examples/foldWallBarrier.ts',
122123
},
123124
performance: {
124125
debounce: 'examples/requestDedup.ts',
@@ -1197,6 +1198,7 @@ export {
11971198
computeFrozenStiffness,
11981199
createContactBarrier,
11991200
createPinBarrier,
1201+
createWallBarrier,
12001202
} from './physics/fold/index.js';
12011203

12021204
export type {
@@ -1214,6 +1216,7 @@ export type {
12141216
StiffnessDesignOptions,
12151217
ContactBarrierOptions,
12161218
PinBarrierOptions,
1219+
WallBarrierOptions,
12171220
} from './physics/fold/index.js';
12181221

12191222
// ============================================================================

src/physics/fold/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from './cubicBarrier.js';
33
export * from './stiffness.js';
44
export * from './contactBarrier.js';
55
export * from './pinBarrier.js';
6+
export * from './wallBarrier.js';

src/physics/fold/wallBarrier.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { Vector3D, Matrix3x3 } from '../../types.js';
2+
import type {
3+
FoldComputationContext,
4+
FoldConstraint,
5+
FoldConstraintEvaluation,
6+
FoldConstraintState,
7+
} from './types.js';
8+
import { createCubicBarrier } from './cubicBarrier.js';
9+
import { computeFrozenStiffness } from './stiffness.js';
10+
11+
export interface WallBarrierOptions {
12+
id?: string;
13+
stiffnessOverride?: number;
14+
maxGap?: number;
15+
normal?: Vector3D;
16+
planePoint?: Vector3D;
17+
}
18+
19+
const ZERO_GRADIENT: Vector3D = { x: 0, y: 0, z: 0 };
20+
const ZERO_HESSIAN: Matrix3x3 = [
21+
[0, 0, 0],
22+
[0, 0, 0],
23+
[0, 0, 0],
24+
];
25+
26+
export function createWallBarrier(options: WallBarrierOptions = {}): FoldConstraint {
27+
const baseBarrier = createCubicBarrier({
28+
id: options.id,
29+
maxGap: options.maxGap,
30+
direction: options.normal,
31+
});
32+
33+
return {
34+
type: 'wall-barrier',
35+
id: options.id,
36+
enabled: true,
37+
evaluate(state: FoldConstraintState, context: FoldComputationContext): FoldConstraintEvaluation {
38+
const wallNormal = normalise(options.normal ?? state.direction);
39+
if (!wallNormal) {
40+
return { energy: 0, gradient: ZERO_GRADIENT, hessian: ZERO_HESSIAN };
41+
}
42+
43+
const adjustedGap = adjustGap(state, wallNormal, options.planePoint);
44+
45+
const stiffness = options.stiffnessOverride ??
46+
computeFrozenStiffness(
47+
{
48+
gap: adjustedGap,
49+
effectiveMass: state.effectiveMass ?? 0,
50+
direction: wallNormal,
51+
hessian: (state.metadata?.hessian as Matrix3x3 | undefined) ?? ZERO_HESSIAN,
52+
},
53+
{ min: 0 }
54+
);
55+
56+
if (stiffness <= 0) {
57+
return { energy: 0, gradient: ZERO_GRADIENT, hessian: ZERO_HESSIAN };
58+
}
59+
60+
const evaluation = baseBarrier.evaluate(
61+
{
62+
...state,
63+
gap: adjustedGap,
64+
direction: wallNormal,
65+
stiffness,
66+
maxGap: options.maxGap ?? state.maxGap,
67+
},
68+
context
69+
);
70+
71+
return evaluation;
72+
},
73+
};
74+
}
75+
76+
function adjustGap(
77+
state: FoldConstraintState,
78+
normal: Vector3D,
79+
planePoint: Vector3D | undefined
80+
): number {
81+
if (!planePoint) {
82+
return state.gap;
83+
}
84+
85+
const position = state.metadata?.position as Vector3D | undefined;
86+
if (!position) {
87+
return state.gap;
88+
}
89+
90+
const offsetVector = {
91+
x: position.x - planePoint.x,
92+
y: position.y - planePoint.y,
93+
z: position.z - planePoint.z,
94+
};
95+
const signedDistance = dot(offsetVector, normal);
96+
return Math.min(state.gap, signedDistance);
97+
}
98+
99+
function normalise(vector: Vector3D | undefined): Vector3D | null {
100+
if (!vector) return null;
101+
const length = Math.hypot(vector.x, vector.y, vector.z);
102+
if (length === 0) return null;
103+
return {
104+
x: vector.x / length,
105+
y: vector.y / length,
106+
z: vector.z / length,
107+
};
108+
}
109+
110+
function dot(a: Vector3D, b: Vector3D): number {
111+
return a.x * b.x + a.y * b.y + a.z * b.z;
112+
}

tests/index.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ describe('package entry point', () => {
6262
expect(examples.physics.computeFrozenStiffness).toBe('examples/foldStiffness.ts');
6363
expect(examples.physics.createContactBarrier).toBe('examples/foldContactBarrier.ts');
6464
expect(examples.physics.createPinBarrier).toBe('examples/foldPinBarrier.ts');
65+
expect(examples.physics.createWallBarrier).toBe('examples/foldWallBarrier.ts');
6566
});
6667

6768
it('provides strong typing for example categories and names', () => {
@@ -207,6 +208,7 @@ describe('package entry point', () => {
207208
| 'computeFrozenStiffness'
208209
| 'createContactBarrier'
209210
| 'createPinBarrier'
211+
| 'createWallBarrier'
210212
>();
211213

212214
expectTypeOf<ExampleName<'ai'>>().toEqualTypeOf<

tests/wallBarrier.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { createWallBarrier } from '../src/physics/fold/wallBarrier.js';
4+
5+
describe('wall barrier', () => {
6+
it('returns zero when outside penetration region', () => {
7+
const barrier = createWallBarrier({
8+
normal: { x: 0, y: 1, z: 0 },
9+
planePoint: { x: 0, y: 0, z: 0 },
10+
});
11+
12+
const evaluation = barrier.evaluate(
13+
{
14+
gap: 0.1,
15+
maxGap: 0,
16+
stiffness: 0,
17+
direction: { x: 0, y: 1, z: 0 },
18+
effectiveMass: 0.2,
19+
metadata: { position: { x: 0, y: 0.5, z: 0 } },
20+
},
21+
{ deltaTime: 1 }
22+
);
23+
24+
expect(evaluation.energy).toBe(0);
25+
});
26+
27+
it('computes energy when penetrating the wall', () => {
28+
const barrier = createWallBarrier({
29+
normal: { x: 0, y: 1, z: 0 },
30+
planePoint: { x: 0, y: 0, z: 0 },
31+
});
32+
33+
const evaluation = barrier.evaluate(
34+
{
35+
gap: -0.05,
36+
maxGap: 0,
37+
stiffness: 0,
38+
direction: { x: 0, y: 1, z: 0 },
39+
effectiveMass: 0.3,
40+
metadata: {
41+
position: { x: 0, y: -0.05, z: 0 },
42+
hessian: [
43+
[5, 0, 0],
44+
[0, 5, 0],
45+
[0, 0, 5],
46+
],
47+
},
48+
},
49+
{ deltaTime: 1 / 60 }
50+
);
51+
52+
expect(evaluation.energy).toBeGreaterThan(0);
53+
expect(evaluation.gradient.y).toBeGreaterThan(0);
54+
});
55+
});

0 commit comments

Comments
 (0)