Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const examples: {
readonly createFoldConstraintRegistry: 'examples/foldSetup.ts';
readonly createCubicBarrier: 'examples/foldCubicBarrier.ts';
readonly computeFrozenStiffness: 'examples/foldStiffness.ts';
readonly createContactBarrier: 'examples/foldContactBarrier.ts';
};
readonly performance: {
readonly debounce: 'examples/requestDedup.ts';
Expand Down Expand Up @@ -3332,6 +3333,20 @@ export interface StiffnessDesignOptions {
}
export function computeFrozenStiffness(input: StiffnessDesignInput, options?: StiffnessDesignOptions): number;

/**
* Contact barrier leveraging Fold extended direction scaling.
* Use for: point-triangle and edge-edge contact inequality enforcement.
* Import: physics/fold/contactBarrier.ts
*/
export interface ContactBarrierOptions {
id?: string;
stiffnessOverride?: number;
maxGap?: number;
direction?: Vector3D;
extendedDirectionScale?: number;
}
export function createContactBarrier(options?: ContactBarrierOptions): FoldConstraint;

export type FoldConstraintType =
| 'cubic-barrier'
| 'contact-barrier'
Expand Down
24 changes: 24 additions & 0 deletions examples/foldContactBarrier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createContactBarrier } from '../src/index.js';

const barrier = createContactBarrier({ extendedDirectionScale: 1.25 });

const evaluation = barrier.evaluate(
{
gap: -0.01,
maxGap: 0,
stiffness: 0,
direction: { x: 0, y: 0, z: 1 },
extendedDirection: { x: 0, y: 1, z: 0 },
effectiveMass: 0.2,
metadata: {
hessian: [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
],
},
},
{ deltaTime: 1 / 120 }
);

console.log('contact energy', evaluation.energy);
2 changes: 1 addition & 1 deletion examples/foldCubicBarrier.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createCubicBarrier } from '../src/index.js';

const cubicBarrier = createCubicBarrier({ stiffness: 50 });
const cubicBarrier = createCubicBarrier({ stiffnessOverride: 50 });

const evaluation = cubicBarrier.evaluate(
{
Expand Down
9 changes: 8 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export const examples = {
createFoldConstraintRegistry: 'examples/foldSetup.ts',
createCubicBarrier: 'examples/foldCubicBarrier.ts',
computeFrozenStiffness: 'examples/foldStiffness.ts',
createContactBarrier: 'examples/foldContactBarrier.ts',
},
performance: {
debounce: 'examples/requestDedup.ts',
Expand Down Expand Up @@ -1189,7 +1190,12 @@ export type { ClosestPairResult } from './geometry/closestPair.js';
// ⚙️ PHYSICS & FOLD BARRIERS
// ============================================================================

export { createFoldConstraintRegistry, createCubicBarrier, computeFrozenStiffness } from './physics/fold/index.js';
export {
createFoldConstraintRegistry,
createCubicBarrier,
computeFrozenStiffness,
createContactBarrier,
} from './physics/fold/index.js';

export type {
CubicBarrierOptions,
Expand All @@ -1204,6 +1210,7 @@ export type {
FoldSystemState,
StiffnessDesignInput,
StiffnessDesignOptions,
ContactBarrierOptions,
} from './physics/fold/index.js';

// ============================================================================
Expand Down
95 changes: 95 additions & 0 deletions src/physics/fold/contactBarrier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { Matrix3x3, Vector3D } from '../../types.js';
import type {
FoldComputationContext,
FoldConstraint,
FoldConstraintEvaluation,
FoldConstraintState,
} from './types.js';
import { computeFrozenStiffness } from './stiffness.js';
import { createCubicBarrier } from './cubicBarrier.js';

export interface ContactBarrierOptions {
id?: string;
stiffnessOverride?: number;
maxGap?: number;
direction?: Vector3D;
extendedDirectionScale?: number;
}

const ZERO_GRADIENT: Vector3D = { x: 0, y: 0, z: 0 };
const ZERO_HESSIAN: Matrix3x3 = [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
];

export function createContactBarrier(options: ContactBarrierOptions = {}): FoldConstraint {
const extendedScale = options.extendedDirectionScale ?? 1.25;
const baseBarrier = createCubicBarrier({
id: options.id,
stiffnessOverride: options.stiffnessOverride,
maxGap: options.maxGap,
direction: options.direction,
});

return {
type: 'contact-barrier',
id: options.id,
enabled: true,
evaluate(state: FoldConstraintState, context: FoldComputationContext): FoldConstraintEvaluation {
const baseDirection = options.direction ?? state.direction;
const contactDirection = state.extendedDirection ?? baseDirection ?? state.direction;
const stiffness = options.stiffnessOverride ??
computeFrozenStiffness(
{
gap: state.gap,
effectiveMass: state.effectiveMass ?? 0,
direction: contactDirection ?? baseDirection ?? state.direction,
hessian: (state.metadata?.hessian as Matrix3x3 | undefined) ?? ZERO_HESSIAN,
},
{ min: 0 }
);

const maxGap = options.maxGap ?? state.maxGap;
const direction = contactDirection ?? baseDirection ?? state.direction;

if (!direction) {
return { energy: 0, gradient: ZERO_GRADIENT, hessian: ZERO_HESSIAN };
}

const violation = Math.max(0, maxGap - state.gap * extendedScale);
if (violation <= 0 || stiffness <= 0) {
return { energy: 0, gradient: ZERO_GRADIENT, hessian: ZERO_HESSIAN };
}

const unit = normalise(direction);
if (!unit) {
return { energy: 0, gradient: ZERO_GRADIENT, hessian: ZERO_HESSIAN };
}

const cubicEvaluation = baseBarrier.evaluate(
{
...state,
maxGap,
direction,
stiffness,
},
context
);

return cubicEvaluation;
},
};
}

function normalise(vector: Vector3D): Vector3D | null {
const length = Math.hypot(vector.x, vector.y, vector.z);
if (length <= 0) {
return null;
}
return {
x: vector.x / length,
y: vector.y / length,
z: vector.z / length,
};
}
4 changes: 2 additions & 2 deletions src/physics/fold/cubicBarrier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {

export interface CubicBarrierOptions {
id?: string;
stiffness?: number;
stiffnessOverride?: number;
maxGap?: number;
direction?: Vector3D;
}
Expand All @@ -28,7 +28,7 @@ export function createCubicBarrier(options: CubicBarrierOptions = {}): FoldConst
evaluate(state: FoldConstraintState, context: FoldComputationContext): FoldConstraintEvaluation {
void context;
const maxGap = options.maxGap ?? state.maxGap;
const stiffness = options.stiffness ?? state.stiffness;
const stiffness = options.stiffnessOverride ?? state.stiffness;
const direction = options.direction ?? state.direction;

if (stiffness <= 0) {
Expand Down
1 change: 1 addition & 0 deletions src/physics/fold/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './types.js';
export * from './cubicBarrier.js';
export * from './stiffness.js';
export * from './contactBarrier.js';
46 changes: 46 additions & 0 deletions tests/contactBarrier.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, expect, it } from 'vitest';

import { createContactBarrier } from '../src/physics/fold/contactBarrier.js';

describe('contact barrier', () => {
it('falls back to cubic barrier when no violation occurs', () => {
const barrier = createContactBarrier();
const evaluation = barrier.evaluate(
{
gap: 0.1,
maxGap: 0,
stiffness: 10,
direction: { x: 0, y: 0, z: 1 },
effectiveMass: 1,
},
{ deltaTime: 1 / 60 }
);

expect(evaluation.energy).toBe(0);
});

it('uses extended direction and stiffness design', () => {
const barrier = createContactBarrier({ extendedDirectionScale: 1.25 });
const evaluation = barrier.evaluate(
{
gap: -0.02,
maxGap: 0,
stiffness: 0,
direction: { x: 0, y: 0, z: 1 },
extendedDirection: { x: 0, y: 1, z: 0 },
effectiveMass: 0.5,
metadata: {
hessian: [
[2, 0, 0],
[0, 4, 0],
[0, 0, 6],
],
},
},
{ deltaTime: 1 / 60 }
);

expect(evaluation.energy).toBeGreaterThan(0);
expect(evaluation.gradient.y).toBeGreaterThan(0);
});
});
2 changes: 1 addition & 1 deletion tests/cubicBarrier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createCubicBarrier } from '../src/physics/fold/cubicBarrier.js';

describe('cubic barrier potential', () => {
it('returns zero output when constraint is satisfied', () => {
const barrier = createCubicBarrier({ stiffness: 10 });
const barrier = createCubicBarrier({ stiffnessOverride: 10 });
const evaluation = barrier.evaluate(
{
gap: 0.5,
Expand Down
2 changes: 2 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ describe('package entry point', () => {
expect(examples.physics.createFoldConstraintRegistry).toBe('examples/foldSetup.ts');
expect(examples.physics.createCubicBarrier).toBe('examples/foldCubicBarrier.ts');
expect(examples.physics.computeFrozenStiffness).toBe('examples/foldStiffness.ts');
expect(examples.physics.createContactBarrier).toBe('examples/foldContactBarrier.ts');
});

it('provides strong typing for example categories and names', () => {
Expand Down Expand Up @@ -203,6 +204,7 @@ describe('package entry point', () => {
| 'createFoldConstraintRegistry'
| 'createCubicBarrier'
| 'computeFrozenStiffness'
| 'createContactBarrier'
>();

expectTypeOf<ExampleName<'ai'>>().toEqualTypeOf<
Expand Down