Skip to content

Commit e8c91f7

Browse files
committed
feat(physics): add friction potential
1 parent 7403eec commit e8c91f7

8 files changed

Lines changed: 175 additions & 1 deletion

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@
131131
- [ ] Error-reduction pass leveraging beta-delta time refinement
132132
- [ ] Linear solver pipeline (PCG with 3x3 block-Jacobi preconditioner)
133133
- **Contact and friction infrastructure**
134-
- [ ] Friction potential tied to contact force magnitude
134+
- [x] Friction potential tied to contact force magnitude
135135
- [ ] Matrix assembly with cached contact index tables
136136
- [ ] Gap evaluators for point/triangle, edge/edge, and wall constraints
137137
- [ ] SPD enforcement pass for elasticity Hessian blocks

docs/index.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export const examples: {
123123
readonly createPinBarrier: 'examples/foldPinBarrier.ts';
124124
readonly createWallBarrier: 'examples/foldWallBarrier.ts';
125125
readonly createStrainBarrier: 'examples/foldStrainBarrier.ts';
126+
readonly createFrictionPotential: 'examples/foldFriction.ts';
126127
};
127128
readonly performance: {
128129
readonly debounce: 'examples/requestDedup.ts';
@@ -3391,6 +3392,18 @@ export interface StrainBarrierOptions {
33913392
}
33923393
export function createStrainBarrier(options?: StrainBarrierOptions): FoldConstraint;
33933394

3395+
/**
3396+
* Friction potential tied to contact force magnitude.
3397+
* Use for: tangential friction response in Fold contact solver.
3398+
* Import: physics/fold/frictionPotential.ts
3399+
*/
3400+
export interface FrictionOptions {
3401+
id?: string;
3402+
coefficient?: number;
3403+
epsilon?: number;
3404+
}
3405+
export function createFrictionPotential(options?: FrictionOptions): FoldConstraint;
3406+
33943407
export type FoldConstraintType =
33953408
| 'cubic-barrier'
33963409
| 'contact-barrier'

examples/foldFriction.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createFrictionPotential } from '../src/index.js';
2+
3+
const friction = createFrictionPotential({ coefficient: 0.6 });
4+
5+
const evaluation = friction.evaluate(
6+
{
7+
gap: 0,
8+
maxGap: 0,
9+
stiffness: 0,
10+
direction: { x: 0, y: 0, z: 1 },
11+
effectiveMass: 0.1,
12+
metadata: {
13+
contactForce: 5,
14+
tangentDisplacement: { x: 0.02, y: 0.01, z: 0 },
15+
},
16+
},
17+
{ deltaTime: 1 / 120 }
18+
);
19+
20+
console.log('friction energy', evaluation.energy);

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export const examples = {
121121
createPinBarrier: 'examples/foldPinBarrier.ts',
122122
createWallBarrier: 'examples/foldWallBarrier.ts',
123123
createStrainBarrier: 'examples/foldStrainBarrier.ts',
124+
createFrictionPotential: 'examples/foldFriction.ts',
124125
},
125126
performance: {
126127
debounce: 'examples/requestDedup.ts',
@@ -1201,6 +1202,7 @@ export {
12011202
createPinBarrier,
12021203
createWallBarrier,
12031204
createStrainBarrier,
1205+
createFrictionPotential,
12041206
} from './physics/fold/index.js';
12051207

12061208
export type {
@@ -1220,6 +1222,7 @@ export type {
12201222
PinBarrierOptions,
12211223
WallBarrierOptions,
12221224
StrainBarrierOptions,
1225+
FrictionOptions,
12231226
} from './physics/fold/index.js';
12241227

12251228
// ============================================================================
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { Matrix3x3, Vector3D } from '../../types.js';
2+
import type {
3+
FoldComputationContext,
4+
FoldConstraint,
5+
FoldConstraintEvaluation,
6+
FoldConstraintState,
7+
} from './types.js';
8+
9+
export interface FrictionOptions {
10+
id?: string;
11+
coefficient?: number;
12+
epsilon?: number;
13+
}
14+
15+
const ZERO_GRADIENT: Vector3D = { x: 0, y: 0, z: 0 };
16+
const ZERO_HESSIAN: Matrix3x3 = [
17+
[0, 0, 0],
18+
[0, 0, 0],
19+
[0, 0, 0],
20+
];
21+
22+
export function createFrictionPotential(options: FrictionOptions = {}): FoldConstraint {
23+
const coefficient = options.coefficient ?? 0.5;
24+
const epsilon = options.epsilon ?? 1e-6;
25+
26+
return {
27+
type: 'friction',
28+
id: options.id,
29+
enabled: true,
30+
evaluate(state: FoldConstraintState, context: FoldComputationContext): FoldConstraintEvaluation {
31+
void context;
32+
const contactForce = getContactForce(state.metadata?.contactForce);
33+
const tangent = getVector(state.metadata?.tangentDisplacement);
34+
35+
if (!tangent || contactForce <= 0 || coefficient <= 0) {
36+
return { energy: 0, gradient: ZERO_GRADIENT, hessian: ZERO_HESSIAN };
37+
}
38+
39+
const rawMagnitude = Math.hypot(tangent.x, tangent.y, tangent.z);
40+
if (rawMagnitude <= epsilon) {
41+
return { energy: 0, gradient: ZERO_GRADIENT, hessian: ZERO_HESSIAN };
42+
}
43+
44+
const tangentMagnitude = rawMagnitude;
45+
const normalizedTangent = {
46+
x: tangent.x / tangentMagnitude,
47+
y: tangent.y / tangentMagnitude,
48+
z: tangent.z / tangentMagnitude,
49+
};
50+
51+
const stiffness = coefficient * contactForce / tangentMagnitude;
52+
const energy = 0.5 * stiffness * tangentMagnitude * tangentMagnitude;
53+
const gradient = scaleVector(normalizedTangent, stiffness * tangentMagnitude);
54+
const hessian = outerProduct(normalizedTangent, stiffness);
55+
56+
return { energy, gradient, hessian };
57+
},
58+
};
59+
}
60+
61+
function getContactForce(value: unknown): number {
62+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
63+
}
64+
65+
function getVector(value: unknown): Vector3D | null {
66+
if (!value) return null;
67+
const vector = value as Partial<Vector3D>;
68+
if (
69+
typeof vector.x === 'number' && Number.isFinite(vector.x) &&
70+
typeof vector.y === 'number' && Number.isFinite(vector.y) &&
71+
typeof vector.z === 'number' && Number.isFinite(vector.z)
72+
) {
73+
return { x: vector.x, y: vector.y, z: vector.z };
74+
}
75+
return null;
76+
}
77+
78+
function scaleVector(vector: Vector3D, scalar: number): Vector3D {
79+
return {
80+
x: vector.x * scalar,
81+
y: vector.y * scalar,
82+
z: vector.z * scalar,
83+
};
84+
}
85+
86+
function outerProduct(vector: Vector3D, scalar: number): Matrix3x3 {
87+
return [
88+
[vector.x * vector.x * scalar, vector.x * vector.y * scalar, vector.x * vector.z * scalar],
89+
[vector.y * vector.x * scalar, vector.y * vector.y * scalar, vector.y * vector.z * scalar],
90+
[vector.z * vector.x * scalar, vector.z * vector.y * scalar, vector.z * vector.z * scalar],
91+
];
92+
}

src/physics/fold/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './contactBarrier.js';
55
export * from './pinBarrier.js';
66
export * from './wallBarrier.js';
77
export * from './strainBarrier.js';
8+
export * from './frictionPotential.js';

tests/frictionPotential.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { createFrictionPotential } from '../src/physics/fold/frictionPotential.js';
4+
5+
describe('friction potential', () => {
6+
it('returns zero when no tangential displacement', () => {
7+
const friction = createFrictionPotential({ coefficient: 0.5 });
8+
const evaluation = friction.evaluate(
9+
{
10+
gap: 0,
11+
maxGap: 0,
12+
stiffness: 0,
13+
direction: { x: 0, y: 0, z: 1 },
14+
effectiveMass: 0.1,
15+
metadata: { contactForce: 2, tangentDisplacement: { x: 0, y: 0, z: 0 } },
16+
},
17+
{ deltaTime: 1 }
18+
);
19+
20+
expect(evaluation.energy).toBe(0);
21+
});
22+
23+
it('produces energy proportional to tangent displacement and force', () => {
24+
const friction = createFrictionPotential({ coefficient: 0.8 });
25+
const evaluation = friction.evaluate(
26+
{
27+
gap: 0,
28+
maxGap: 0,
29+
stiffness: 0,
30+
direction: { x: 0, y: 0, z: 1 },
31+
effectiveMass: 0.1,
32+
metadata: {
33+
contactForce: 5,
34+
tangentDisplacement: { x: 0.02, y: 0.01, z: 0 },
35+
},
36+
},
37+
{ deltaTime: 1 / 60 }
38+
);
39+
40+
expect(evaluation.energy).toBeGreaterThan(0);
41+
expect(evaluation.gradient.x ** 2 + evaluation.gradient.y ** 2).toBeGreaterThan(0);
42+
});
43+
});

tests/index.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ describe('package entry point', () => {
6464
expect(examples.physics.createPinBarrier).toBe('examples/foldPinBarrier.ts');
6565
expect(examples.physics.createWallBarrier).toBe('examples/foldWallBarrier.ts');
6666
expect(examples.physics.createStrainBarrier).toBe('examples/foldStrainBarrier.ts');
67+
expect(examples.physics.createFrictionPotential).toBe('examples/foldFriction.ts');
6768
});
6869

6970
it('provides strong typing for example categories and names', () => {
@@ -211,6 +212,7 @@ describe('package entry point', () => {
211212
| 'createPinBarrier'
212213
| 'createWallBarrier'
213214
| 'createStrainBarrier'
215+
| 'createFrictionPotential'
214216
>();
215217

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

0 commit comments

Comments
 (0)