From 39ac65f3bb08f0a353ba3d5440caa2eddff5364c Mon Sep 17 00:00:00 2001 From: Francois Date: Sat, 18 Oct 2025 15:27:38 +0900 Subject: [PATCH] feat(physics): add wall barrier --- ROADMAP.md | 2 +- docs/index.d.ts | 15 +++++ examples/foldWallBarrier.ts | 27 ++++++++ src/index.ts | 3 + src/physics/fold/index.ts | 1 + src/physics/fold/wallBarrier.ts | 112 ++++++++++++++++++++++++++++++++ tests/index.test.ts | 2 + tests/wallBarrier.test.ts | 55 ++++++++++++++++ 8 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 examples/foldWallBarrier.ts create mode 100644 src/physics/fold/wallBarrier.ts create mode 100644 tests/wallBarrier.test.ts diff --git a/ROADMAP.md b/ROADMAP.md index 59a3000..d3d86cc 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -122,7 +122,7 @@ - [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 + - [x] Wall constraint barrier for plane collisions - [ ] Triangle strain-limiting barrier driven by deformation singular values - **Integrator and solver** - [ ] Inexact Newton integrator with beta accumulation diff --git a/docs/index.d.ts b/docs/index.d.ts index bb4e115..4bee9cc 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -121,6 +121,7 @@ export const examples: { readonly computeFrozenStiffness: 'examples/foldStiffness.ts'; readonly createContactBarrier: 'examples/foldContactBarrier.ts'; readonly createPinBarrier: 'examples/foldPinBarrier.ts'; + readonly createWallBarrier: 'examples/foldWallBarrier.ts'; }; readonly performance: { readonly debounce: 'examples/requestDedup.ts'; @@ -3361,6 +3362,20 @@ export interface PinBarrierOptions { } export function createPinBarrier(options?: PinBarrierOptions): FoldConstraint; +/** + * Wall constraint barrier for plane contacts. + * Use for: enforcing collision against static planes with Fold guarantees. + * Import: physics/fold/wallBarrier.ts + */ +export interface WallBarrierOptions { + id?: string; + stiffnessOverride?: number; + maxGap?: number; + normal?: Vector3D; + planePoint?: Vector3D; +} +export function createWallBarrier(options?: WallBarrierOptions): FoldConstraint; + export type FoldConstraintType = | 'cubic-barrier' | 'contact-barrier' diff --git a/examples/foldWallBarrier.ts b/examples/foldWallBarrier.ts new file mode 100644 index 0000000..7fc2939 --- /dev/null +++ b/examples/foldWallBarrier.ts @@ -0,0 +1,27 @@ +import { createWallBarrier } from '../src/index.js'; + +const barrier = createWallBarrier({ + planePoint: { x: 0, y: 0, z: 0 }, + normal: { x: 0, y: 1, z: 0 }, +}); + +const evaluation = barrier.evaluate( + { + gap: -0.03, + maxGap: 0, + stiffness: 0, + direction: { x: 0, y: 1, z: 0 }, + effectiveMass: 0.25, + metadata: { + position: { x: 0, y: -0.03, z: 0 }, + hessian: [ + [3, 0, 0], + [0, 3, 0], + [0, 0, 3], + ], + }, + }, + { deltaTime: 1 / 90 } +); + +console.log('wall energy', evaluation.energy); diff --git a/src/index.ts b/src/index.ts index c6faa32..5a66690 100644 --- a/src/index.ts +++ b/src/index.ts @@ -119,6 +119,7 @@ export const examples = { computeFrozenStiffness: 'examples/foldStiffness.ts', createContactBarrier: 'examples/foldContactBarrier.ts', createPinBarrier: 'examples/foldPinBarrier.ts', + createWallBarrier: 'examples/foldWallBarrier.ts', }, performance: { debounce: 'examples/requestDedup.ts', @@ -1197,6 +1198,7 @@ export { computeFrozenStiffness, createContactBarrier, createPinBarrier, + createWallBarrier, } from './physics/fold/index.js'; export type { @@ -1214,6 +1216,7 @@ export type { StiffnessDesignOptions, ContactBarrierOptions, PinBarrierOptions, + WallBarrierOptions, } from './physics/fold/index.js'; // ============================================================================ diff --git a/src/physics/fold/index.ts b/src/physics/fold/index.ts index 1bc6760..35ca6b6 100644 --- a/src/physics/fold/index.ts +++ b/src/physics/fold/index.ts @@ -3,3 +3,4 @@ export * from './cubicBarrier.js'; export * from './stiffness.js'; export * from './contactBarrier.js'; export * from './pinBarrier.js'; +export * from './wallBarrier.js'; diff --git a/src/physics/fold/wallBarrier.ts b/src/physics/fold/wallBarrier.ts new file mode 100644 index 0000000..bdeddfa --- /dev/null +++ b/src/physics/fold/wallBarrier.ts @@ -0,0 +1,112 @@ +import type { Vector3D, Matrix3x3 } from '../../types.js'; +import type { + FoldComputationContext, + FoldConstraint, + FoldConstraintEvaluation, + FoldConstraintState, +} from './types.js'; +import { createCubicBarrier } from './cubicBarrier.js'; +import { computeFrozenStiffness } from './stiffness.js'; + +export interface WallBarrierOptions { + id?: string; + stiffnessOverride?: number; + maxGap?: number; + normal?: Vector3D; + planePoint?: 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 createWallBarrier(options: WallBarrierOptions = {}): FoldConstraint { + const baseBarrier = createCubicBarrier({ + id: options.id, + maxGap: options.maxGap, + direction: options.normal, + }); + + return { + type: 'wall-barrier', + id: options.id, + enabled: true, + evaluate(state: FoldConstraintState, context: FoldComputationContext): FoldConstraintEvaluation { + const wallNormal = normalise(options.normal ?? state.direction); + if (!wallNormal) { + return { energy: 0, gradient: ZERO_GRADIENT, hessian: ZERO_HESSIAN }; + } + + const adjustedGap = adjustGap(state, wallNormal, options.planePoint); + + const stiffness = options.stiffnessOverride ?? + computeFrozenStiffness( + { + gap: adjustedGap, + effectiveMass: state.effectiveMass ?? 0, + direction: wallNormal, + 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, + gap: adjustedGap, + direction: wallNormal, + stiffness, + maxGap: options.maxGap ?? state.maxGap, + }, + context + ); + + return evaluation; + }, + }; +} + +function adjustGap( + state: FoldConstraintState, + normal: Vector3D, + planePoint: Vector3D | undefined +): number { + if (!planePoint) { + return state.gap; + } + + const position = state.metadata?.position as Vector3D | undefined; + if (!position) { + return state.gap; + } + + const offsetVector = { + x: position.x - planePoint.x, + y: position.y - planePoint.y, + z: position.z - planePoint.z, + }; + const signedDistance = dot(offsetVector, normal); + return Math.min(state.gap, signedDistance); +} + +function normalise(vector: Vector3D | undefined): Vector3D | null { + if (!vector) return 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, + }; +} + +function dot(a: Vector3D, b: Vector3D): number { + return a.x * b.x + a.y * b.y + a.z * b.z; +} diff --git a/tests/index.test.ts b/tests/index.test.ts index 8dd9fe1..5782fe5 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -62,6 +62,7 @@ describe('package entry point', () => { expect(examples.physics.computeFrozenStiffness).toBe('examples/foldStiffness.ts'); expect(examples.physics.createContactBarrier).toBe('examples/foldContactBarrier.ts'); expect(examples.physics.createPinBarrier).toBe('examples/foldPinBarrier.ts'); + expect(examples.physics.createWallBarrier).toBe('examples/foldWallBarrier.ts'); }); it('provides strong typing for example categories and names', () => { @@ -207,6 +208,7 @@ describe('package entry point', () => { | 'computeFrozenStiffness' | 'createContactBarrier' | 'createPinBarrier' + | 'createWallBarrier' >(); expectTypeOf>().toEqualTypeOf< diff --git a/tests/wallBarrier.test.ts b/tests/wallBarrier.test.ts new file mode 100644 index 0000000..94dfd41 --- /dev/null +++ b/tests/wallBarrier.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; + +import { createWallBarrier } from '../src/physics/fold/wallBarrier.js'; + +describe('wall barrier', () => { + it('returns zero when outside penetration region', () => { + const barrier = createWallBarrier({ + normal: { x: 0, y: 1, z: 0 }, + planePoint: { x: 0, y: 0, z: 0 }, + }); + + const evaluation = barrier.evaluate( + { + gap: 0.1, + maxGap: 0, + stiffness: 0, + direction: { x: 0, y: 1, z: 0 }, + effectiveMass: 0.2, + metadata: { position: { x: 0, y: 0.5, z: 0 } }, + }, + { deltaTime: 1 } + ); + + expect(evaluation.energy).toBe(0); + }); + + it('computes energy when penetrating the wall', () => { + const barrier = createWallBarrier({ + normal: { x: 0, y: 1, z: 0 }, + planePoint: { x: 0, y: 0, z: 0 }, + }); + + const evaluation = barrier.evaluate( + { + gap: -0.05, + maxGap: 0, + stiffness: 0, + direction: { x: 0, y: 1, z: 0 }, + effectiveMass: 0.3, + metadata: { + position: { x: 0, y: -0.05, z: 0 }, + hessian: [ + [5, 0, 0], + [0, 5, 0], + [0, 0, 5], + ], + }, + }, + { deltaTime: 1 / 60 } + ); + + expect(evaluation.energy).toBeGreaterThan(0); + expect(evaluation.gradient.y).toBeGreaterThan(0); + }); +});