Skip to content

Commit 1984fb8

Browse files
authored
feat(physics): add contact barrier (#72)
1 parent 2038371 commit 1984fb8

10 files changed

Lines changed: 195 additions & 5 deletions

File tree

docs/index.d.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export const examples: {
119119
readonly createFoldConstraintRegistry: 'examples/foldSetup.ts';
120120
readonly createCubicBarrier: 'examples/foldCubicBarrier.ts';
121121
readonly computeFrozenStiffness: 'examples/foldStiffness.ts';
122+
readonly createContactBarrier: 'examples/foldContactBarrier.ts';
122123
};
123124
readonly performance: {
124125
readonly debounce: 'examples/requestDedup.ts';
@@ -3332,6 +3333,20 @@ export interface StiffnessDesignOptions {
33323333
}
33333334
export function computeFrozenStiffness(input: StiffnessDesignInput, options?: StiffnessDesignOptions): number;
33343335

3336+
/**
3337+
* Contact barrier leveraging Fold extended direction scaling.
3338+
* Use for: point-triangle and edge-edge contact inequality enforcement.
3339+
* Import: physics/fold/contactBarrier.ts
3340+
*/
3341+
export interface ContactBarrierOptions {
3342+
id?: string;
3343+
stiffnessOverride?: number;
3344+
maxGap?: number;
3345+
direction?: Vector3D;
3346+
extendedDirectionScale?: number;
3347+
}
3348+
export function createContactBarrier(options?: ContactBarrierOptions): FoldConstraint;
3349+
33353350
export type FoldConstraintType =
33363351
| 'cubic-barrier'
33373352
| 'contact-barrier'

examples/foldContactBarrier.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { createContactBarrier } from '../src/index.js';
2+
3+
const barrier = createContactBarrier({ extendedDirectionScale: 1.25 });
4+
5+
const evaluation = barrier.evaluate(
6+
{
7+
gap: -0.01,
8+
maxGap: 0,
9+
stiffness: 0,
10+
direction: { x: 0, y: 0, z: 1 },
11+
extendedDirection: { x: 0, y: 1, z: 0 },
12+
effectiveMass: 0.2,
13+
metadata: {
14+
hessian: [
15+
[1, 0, 0],
16+
[0, 1, 0],
17+
[0, 0, 1],
18+
],
19+
},
20+
},
21+
{ deltaTime: 1 / 120 }
22+
);
23+
24+
console.log('contact energy', evaluation.energy);

examples/foldCubicBarrier.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createCubicBarrier } from '../src/index.js';
22

3-
const cubicBarrier = createCubicBarrier({ stiffness: 50 });
3+
const cubicBarrier = createCubicBarrier({ stiffnessOverride: 50 });
44

55
const evaluation = cubicBarrier.evaluate(
66
{

src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export const examples = {
117117
createFoldConstraintRegistry: 'examples/foldSetup.ts',
118118
createCubicBarrier: 'examples/foldCubicBarrier.ts',
119119
computeFrozenStiffness: 'examples/foldStiffness.ts',
120+
createContactBarrier: 'examples/foldContactBarrier.ts',
120121
},
121122
performance: {
122123
debounce: 'examples/requestDedup.ts',
@@ -1189,7 +1190,12 @@ export type { ClosestPairResult } from './geometry/closestPair.js';
11891190
// ⚙️ PHYSICS & FOLD BARRIERS
11901191
// ============================================================================
11911192

1192-
export { createFoldConstraintRegistry, createCubicBarrier, computeFrozenStiffness } from './physics/fold/index.js';
1193+
export {
1194+
createFoldConstraintRegistry,
1195+
createCubicBarrier,
1196+
computeFrozenStiffness,
1197+
createContactBarrier,
1198+
} from './physics/fold/index.js';
11931199

11941200
export type {
11951201
CubicBarrierOptions,
@@ -1204,6 +1210,7 @@ export type {
12041210
FoldSystemState,
12051211
StiffnessDesignInput,
12061212
StiffnessDesignOptions,
1213+
ContactBarrierOptions,
12071214
} from './physics/fold/index.js';
12081215

12091216
// ============================================================================

src/physics/fold/contactBarrier.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import type { Matrix3x3, Vector3D } from '../../types.js';
2+
import type {
3+
FoldComputationContext,
4+
FoldConstraint,
5+
FoldConstraintEvaluation,
6+
FoldConstraintState,
7+
} from './types.js';
8+
import { computeFrozenStiffness } from './stiffness.js';
9+
import { createCubicBarrier } from './cubicBarrier.js';
10+
11+
export interface ContactBarrierOptions {
12+
id?: string;
13+
stiffnessOverride?: number;
14+
maxGap?: number;
15+
direction?: Vector3D;
16+
extendedDirectionScale?: number;
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 createContactBarrier(options: ContactBarrierOptions = {}): FoldConstraint {
27+
const extendedScale = options.extendedDirectionScale ?? 1.25;
28+
const baseBarrier = createCubicBarrier({
29+
id: options.id,
30+
stiffnessOverride: options.stiffnessOverride,
31+
maxGap: options.maxGap,
32+
direction: options.direction,
33+
});
34+
35+
return {
36+
type: 'contact-barrier',
37+
id: options.id,
38+
enabled: true,
39+
evaluate(state: FoldConstraintState, context: FoldComputationContext): FoldConstraintEvaluation {
40+
const baseDirection = options.direction ?? state.direction;
41+
const contactDirection = state.extendedDirection ?? baseDirection ?? state.direction;
42+
const stiffness = options.stiffnessOverride ??
43+
computeFrozenStiffness(
44+
{
45+
gap: state.gap,
46+
effectiveMass: state.effectiveMass ?? 0,
47+
direction: contactDirection ?? baseDirection ?? state.direction,
48+
hessian: (state.metadata?.hessian as Matrix3x3 | undefined) ?? ZERO_HESSIAN,
49+
},
50+
{ min: 0 }
51+
);
52+
53+
const maxGap = options.maxGap ?? state.maxGap;
54+
const direction = contactDirection ?? baseDirection ?? state.direction;
55+
56+
if (!direction) {
57+
return { energy: 0, gradient: ZERO_GRADIENT, hessian: ZERO_HESSIAN };
58+
}
59+
60+
const violation = Math.max(0, maxGap - state.gap * extendedScale);
61+
if (violation <= 0 || stiffness <= 0) {
62+
return { energy: 0, gradient: ZERO_GRADIENT, hessian: ZERO_HESSIAN };
63+
}
64+
65+
const unit = normalise(direction);
66+
if (!unit) {
67+
return { energy: 0, gradient: ZERO_GRADIENT, hessian: ZERO_HESSIAN };
68+
}
69+
70+
const cubicEvaluation = baseBarrier.evaluate(
71+
{
72+
...state,
73+
maxGap,
74+
direction,
75+
stiffness,
76+
},
77+
context
78+
);
79+
80+
return cubicEvaluation;
81+
},
82+
};
83+
}
84+
85+
function normalise(vector: Vector3D): Vector3D | null {
86+
const length = Math.hypot(vector.x, vector.y, vector.z);
87+
if (length <= 0) {
88+
return null;
89+
}
90+
return {
91+
x: vector.x / length,
92+
y: vector.y / length,
93+
z: vector.z / length,
94+
};
95+
}

src/physics/fold/cubicBarrier.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88

99
export interface CubicBarrierOptions {
1010
id?: string;
11-
stiffness?: number;
11+
stiffnessOverride?: number;
1212
maxGap?: number;
1313
direction?: Vector3D;
1414
}
@@ -28,7 +28,7 @@ export function createCubicBarrier(options: CubicBarrierOptions = {}): FoldConst
2828
evaluate(state: FoldConstraintState, context: FoldComputationContext): FoldConstraintEvaluation {
2929
void context;
3030
const maxGap = options.maxGap ?? state.maxGap;
31-
const stiffness = options.stiffness ?? state.stiffness;
31+
const stiffness = options.stiffnessOverride ?? state.stiffness;
3232
const direction = options.direction ?? state.direction;
3333

3434
if (stiffness <= 0) {

src/physics/fold/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './types.js';
22
export * from './cubicBarrier.js';
33
export * from './stiffness.js';
4+
export * from './contactBarrier.js';

tests/contactBarrier.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { createContactBarrier } from '../src/physics/fold/contactBarrier.js';
4+
5+
describe('contact barrier', () => {
6+
it('falls back to cubic barrier when no violation occurs', () => {
7+
const barrier = createContactBarrier();
8+
const evaluation = barrier.evaluate(
9+
{
10+
gap: 0.1,
11+
maxGap: 0,
12+
stiffness: 10,
13+
direction: { x: 0, y: 0, z: 1 },
14+
effectiveMass: 1,
15+
},
16+
{ deltaTime: 1 / 60 }
17+
);
18+
19+
expect(evaluation.energy).toBe(0);
20+
});
21+
22+
it('uses extended direction and stiffness design', () => {
23+
const barrier = createContactBarrier({ extendedDirectionScale: 1.25 });
24+
const evaluation = barrier.evaluate(
25+
{
26+
gap: -0.02,
27+
maxGap: 0,
28+
stiffness: 0,
29+
direction: { x: 0, y: 0, z: 1 },
30+
extendedDirection: { x: 0, y: 1, z: 0 },
31+
effectiveMass: 0.5,
32+
metadata: {
33+
hessian: [
34+
[2, 0, 0],
35+
[0, 4, 0],
36+
[0, 0, 6],
37+
],
38+
},
39+
},
40+
{ deltaTime: 1 / 60 }
41+
);
42+
43+
expect(evaluation.energy).toBeGreaterThan(0);
44+
expect(evaluation.gradient.y).toBeGreaterThan(0);
45+
});
46+
});

tests/cubicBarrier.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { createCubicBarrier } from '../src/physics/fold/cubicBarrier.js';
44

55
describe('cubic barrier potential', () => {
66
it('returns zero output when constraint is satisfied', () => {
7-
const barrier = createCubicBarrier({ stiffness: 10 });
7+
const barrier = createCubicBarrier({ stiffnessOverride: 10 });
88
const evaluation = barrier.evaluate(
99
{
1010
gap: 0.5,

tests/index.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ describe('package entry point', () => {
6060
expect(examples.physics.createFoldConstraintRegistry).toBe('examples/foldSetup.ts');
6161
expect(examples.physics.createCubicBarrier).toBe('examples/foldCubicBarrier.ts');
6262
expect(examples.physics.computeFrozenStiffness).toBe('examples/foldStiffness.ts');
63+
expect(examples.physics.createContactBarrier).toBe('examples/foldContactBarrier.ts');
6364
});
6465

6566
it('provides strong typing for example categories and names', () => {
@@ -203,6 +204,7 @@ describe('package entry point', () => {
203204
| 'createFoldConstraintRegistry'
204205
| 'createCubicBarrier'
205206
| 'computeFrozenStiffness'
207+
| 'createContactBarrier'
206208
>();
207209

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

0 commit comments

Comments
 (0)