diff --git a/docs/index.d.ts b/docs/index.d.ts index 35d9433..581de62 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -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'; @@ -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' diff --git a/examples/foldContactBarrier.ts b/examples/foldContactBarrier.ts new file mode 100644 index 0000000..a780824 --- /dev/null +++ b/examples/foldContactBarrier.ts @@ -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); diff --git a/examples/foldCubicBarrier.ts b/examples/foldCubicBarrier.ts index 4e73e64..7a48d6e 100644 --- a/examples/foldCubicBarrier.ts +++ b/examples/foldCubicBarrier.ts @@ -1,6 +1,6 @@ import { createCubicBarrier } from '../src/index.js'; -const cubicBarrier = createCubicBarrier({ stiffness: 50 }); +const cubicBarrier = createCubicBarrier({ stiffnessOverride: 50 }); const evaluation = cubicBarrier.evaluate( { diff --git a/src/index.ts b/src/index.ts index 7e8d1ac..8bccccb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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', @@ -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, @@ -1204,6 +1210,7 @@ export type { FoldSystemState, StiffnessDesignInput, StiffnessDesignOptions, + ContactBarrierOptions, } from './physics/fold/index.js'; // ============================================================================ diff --git a/src/physics/fold/contactBarrier.ts b/src/physics/fold/contactBarrier.ts new file mode 100644 index 0000000..8e9fd22 --- /dev/null +++ b/src/physics/fold/contactBarrier.ts @@ -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, + }; +} diff --git a/src/physics/fold/cubicBarrier.ts b/src/physics/fold/cubicBarrier.ts index fefb11f..3f2a05a 100644 --- a/src/physics/fold/cubicBarrier.ts +++ b/src/physics/fold/cubicBarrier.ts @@ -8,7 +8,7 @@ import type { export interface CubicBarrierOptions { id?: string; - stiffness?: number; + stiffnessOverride?: number; maxGap?: number; direction?: Vector3D; } @@ -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) { diff --git a/src/physics/fold/index.ts b/src/physics/fold/index.ts index 18315f0..2224dc5 100644 --- a/src/physics/fold/index.ts +++ b/src/physics/fold/index.ts @@ -1,3 +1,4 @@ export * from './types.js'; export * from './cubicBarrier.js'; export * from './stiffness.js'; +export * from './contactBarrier.js'; diff --git a/tests/contactBarrier.test.ts b/tests/contactBarrier.test.ts new file mode 100644 index 0000000..7534ace --- /dev/null +++ b/tests/contactBarrier.test.ts @@ -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); + }); +}); diff --git a/tests/cubicBarrier.test.ts b/tests/cubicBarrier.test.ts index 3b4eaba..81a0fe3 100644 --- a/tests/cubicBarrier.test.ts +++ b/tests/cubicBarrier.test.ts @@ -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, diff --git a/tests/index.test.ts b/tests/index.test.ts index e827470..22e4fd1 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -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', () => { @@ -203,6 +204,7 @@ describe('package entry point', () => { | 'createFoldConstraintRegistry' | 'createCubicBarrier' | 'computeFrozenStiffness' + | 'createContactBarrier' >(); expectTypeOf>().toEqualTypeOf<