From 30d029d0fe3e8548fee50f233697c91b305a200f Mon Sep 17 00:00:00 2001 From: Francois Date: Sat, 18 Oct 2025 15:15:01 +0900 Subject: [PATCH] feat(physics): add pin barrier --- ROADMAP.md | 6 +-- docs/index.d.ts | 14 +++++++ examples/foldPinBarrier.ts | 23 +++++++++++ src/index.ts | 3 ++ src/physics/fold/index.ts | 1 + src/physics/fold/pinBarrier.ts | 70 ++++++++++++++++++++++++++++++++++ tests/index.test.ts | 2 + tests/pinBarrier.test.ts | 44 +++++++++++++++++++++ 8 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 examples/foldPinBarrier.ts create mode 100644 src/physics/fold/pinBarrier.ts create mode 100644 tests/pinBarrier.test.ts diff --git a/ROADMAP.md b/ROADMAP.md index a2403c3..59a3000 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -119,9 +119,9 @@ - [x] Align Fold barrier scope with the paper and define shared constraint interfaces in `src/physics/fold` - **Barrier primitives** (each item: runtime module + `docs/index.d.ts` entry + Vitest coverage + runnable example when feasible) - [x] Cubic barrier potential (energy, gradient, Hessian evaluation) - - [ ] Stiffness design principle for frozen barrier stiffness - - [ ] Contact barrier with extended direction handling - - [ ] Pin constraint barrier using cubic barrier formulation + - [x] Stiffness design principle for frozen barrier stiffness + - [x] Contact barrier with extended direction handling + - [x] Pin constraint barrier using cubic barrier formulation - [ ] Wall constraint barrier for plane collisions - [ ] Triangle strain-limiting barrier driven by deformation singular values - **Integrator and solver** diff --git a/docs/index.d.ts b/docs/index.d.ts index 581de62..bb4e115 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -120,6 +120,7 @@ export const examples: { readonly createCubicBarrier: 'examples/foldCubicBarrier.ts'; readonly computeFrozenStiffness: 'examples/foldStiffness.ts'; readonly createContactBarrier: 'examples/foldContactBarrier.ts'; + readonly createPinBarrier: 'examples/foldPinBarrier.ts'; }; readonly performance: { readonly debounce: 'examples/requestDedup.ts'; @@ -3347,6 +3348,19 @@ export interface ContactBarrierOptions { } export function createContactBarrier(options?: ContactBarrierOptions): FoldConstraint; +/** + * Pin constraint barrier using cubic barrier formulation. + * Use for: soft positional pinning with Fold barrier guarantees. + * Import: physics/fold/pinBarrier.ts + */ +export interface PinBarrierOptions { + id?: string; + stiffnessOverride?: number; + maxGap?: number; + direction?: Vector3D; +} +export function createPinBarrier(options?: PinBarrierOptions): FoldConstraint; + export type FoldConstraintType = | 'cubic-barrier' | 'contact-barrier' diff --git a/examples/foldPinBarrier.ts b/examples/foldPinBarrier.ts new file mode 100644 index 0000000..f9b1b01 --- /dev/null +++ b/examples/foldPinBarrier.ts @@ -0,0 +1,23 @@ +import { createPinBarrier } from '../src/index.js'; + +const barrier = createPinBarrier(); + +const evaluation = barrier.evaluate( + { + gap: -0.015, + maxGap: 0, + stiffness: 0, + direction: { x: 1, y: 0, z: 0 }, + effectiveMass: 0.3, + metadata: { + hessian: [ + [2, 0, 0], + [0, 2, 0], + [0, 0, 2], + ], + }, + }, + { deltaTime: 1 / 60 } +); + +console.log('pin energy', evaluation.energy); diff --git a/src/index.ts b/src/index.ts index 8bccccb..c6faa32 100644 --- a/src/index.ts +++ b/src/index.ts @@ -118,6 +118,7 @@ export const examples = { createCubicBarrier: 'examples/foldCubicBarrier.ts', computeFrozenStiffness: 'examples/foldStiffness.ts', createContactBarrier: 'examples/foldContactBarrier.ts', + createPinBarrier: 'examples/foldPinBarrier.ts', }, performance: { debounce: 'examples/requestDedup.ts', @@ -1195,6 +1196,7 @@ export { createCubicBarrier, computeFrozenStiffness, createContactBarrier, + createPinBarrier, } from './physics/fold/index.js'; export type { @@ -1211,6 +1213,7 @@ export type { StiffnessDesignInput, StiffnessDesignOptions, ContactBarrierOptions, + PinBarrierOptions, } from './physics/fold/index.js'; // ============================================================================ diff --git a/src/physics/fold/index.ts b/src/physics/fold/index.ts index 2224dc5..1bc6760 100644 --- a/src/physics/fold/index.ts +++ b/src/physics/fold/index.ts @@ -2,3 +2,4 @@ export * from './types.js'; export * from './cubicBarrier.js'; export * from './stiffness.js'; export * from './contactBarrier.js'; +export * from './pinBarrier.js'; diff --git a/src/physics/fold/pinBarrier.ts b/src/physics/fold/pinBarrier.ts new file mode 100644 index 0000000..4ed456e --- /dev/null +++ b/src/physics/fold/pinBarrier.ts @@ -0,0 +1,70 @@ +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 PinBarrierOptions { + id?: string; + stiffnessOverride?: number; + maxGap?: number; + direction?: Vector3D; +} + +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 createPinBarrier(options: PinBarrierOptions = {}): FoldConstraint { + const baseBarrier = createCubicBarrier({ + id: options.id, + maxGap: options.maxGap, + direction: options.direction, + }); + + return { + type: 'pin-barrier', + id: options.id, + enabled: true, + evaluate(state: FoldConstraintState, context: FoldComputationContext): FoldConstraintEvaluation { + const direction = options.direction ?? state.direction; + if (!direction) { + return { energy: 0, gradient: ZERO_GRADIENT, hessian: ZERO_HESSIAN }; + } + + const stiffness = options.stiffnessOverride ?? + computeFrozenStiffness( + { + gap: state.gap, + effectiveMass: state.effectiveMass ?? 0, + direction, + hessian: (state.metadata?.hessian as Matrix3x3 | undefined) ?? ZERO_HESSIAN, + }, + { min: 0 } + ); + + if (stiffness <= 0) { + return { energy: 0, gradient: ZERO_GRADIENT, hessian: ZERO_HESSIAN }; + } + + const evaluation = baseBarrier.evaluate( + { + ...state, + stiffness, + direction, + maxGap: options.maxGap ?? state.maxGap, + }, + context + ); + + return evaluation; + }, + }; +} diff --git a/tests/index.test.ts b/tests/index.test.ts index 22e4fd1..8dd9fe1 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -61,6 +61,7 @@ describe('package entry point', () => { expect(examples.physics.createCubicBarrier).toBe('examples/foldCubicBarrier.ts'); expect(examples.physics.computeFrozenStiffness).toBe('examples/foldStiffness.ts'); expect(examples.physics.createContactBarrier).toBe('examples/foldContactBarrier.ts'); + expect(examples.physics.createPinBarrier).toBe('examples/foldPinBarrier.ts'); }); it('provides strong typing for example categories and names', () => { @@ -205,6 +206,7 @@ describe('package entry point', () => { | 'createCubicBarrier' | 'computeFrozenStiffness' | 'createContactBarrier' + | 'createPinBarrier' >(); expectTypeOf>().toEqualTypeOf< diff --git a/tests/pinBarrier.test.ts b/tests/pinBarrier.test.ts new file mode 100644 index 0000000..fff7344 --- /dev/null +++ b/tests/pinBarrier.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; + +import { createPinBarrier } from '../src/physics/fold/pinBarrier.js'; + +describe('pin barrier', () => { + it('returns zero energy when stiffness is zero', () => { + const barrier = createPinBarrier({ stiffnessOverride: 0 }); + const evaluation = barrier.evaluate( + { + gap: -0.05, + maxGap: 0, + stiffness: 0, + direction: { x: 1, y: 0, z: 0 }, + }, + { deltaTime: 1 } + ); + + expect(evaluation.energy).toBe(0); + }); + + it('derives stiffness from design principle', () => { + const barrier = createPinBarrier(); + const evaluation = barrier.evaluate( + { + gap: -0.02, + maxGap: 0, + stiffness: 0, + direction: { x: 0, y: 0, z: 1 }, + effectiveMass: 0.4, + metadata: { + hessian: [ + [4, 0, 0], + [0, 4, 0], + [0, 0, 4], + ], + }, + }, + { deltaTime: 1 / 120 } + ); + + expect(evaluation.energy).toBeGreaterThan(0); + expect(evaluation.gradient.z).toBeGreaterThan(0); + }); +});